Files
Aislo/resources/tester/helper_150_detail_pick.cjs
T
eomsangdonandClaude Opus 5.5 b400993b82 feat(b06): [상세] 그림 휠 줌 · 가운데 단추 팬 · 표 강조 붉은 한 벌(164-1 · 164-2)
- 복제 자식을 g 하나로 감싸 공용 b_svg_zoom_pan(wheelZoom · 커서 고정 · 상한 16 · 1배 팬) · [+ − ⤢] · 칸 비율 = viewBox
- 강조 띠 · 판정 선 · 십자 · 사토장 모두 --color-danger · 고른 칸 붉은 테두리 · 줄 표시 · hover 옅게
- 그림에 대상 없는 칸 data-empty 회색 · 누름 없음(presentKeys) · 이름 칸도 그 줄 첫 키로 눌림
- 시험 test_164_detail_zoom 새로 · test_150_detail_pick presentKeys 사례

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-10 00:01:27 +09:00

143 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* PLAN 150-4 단위검증 헬퍼 — [상세] 수량표 칸 → 그림 복제 강조(`B06_Section_UI_Cross_Detail_Pick.ts`).
* 화면 TS 를 그 자리에서 트랜스파일해 Node 로 돌림 · 결과 = 표준출력 JSON 한 벌. 짝 = `test_150_detail_pick.py`.
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
// 조정창 공용 부품(Design_Pick → Cross_Panel_Base · 151-3)이 불러올 때 문서에 귀를 닮
globalThis.document = { addEventListener: () => {}, removeEventListener: () => {} };
const resolve = (base) => (fs.existsSync(`${base}.ts`) ? `${base}.ts` : base);
function loadTs(full) {
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (request.endsWith(".css")) return {};
if (request.startsWith("@util/"))
return loadTs(resolve(path.join(ROOT, "common_util", request.slice(6))));
return loadTs(resolve(path.join(path.dirname(full), request)));
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const pick = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Cross_Detail_Pick.ts"));
// ── 그림 복제 흉내 — 평평한 요소 목록 · 「tag.class」 · 「.class」 · 「g.class line」 만 ──
function node(tag, classes, attrs = {}, parent = null) {
const set = new Set(classes);
return {
tag,
parent,
set,
getAttribute: (name) => attrs[name] ?? null,
classList: {
add: (c) => set.add(c),
remove: (c) => set.delete(c),
contains: (c) => set.has(c),
},
};
}
const one = (el, part) => {
const [tag, ...cls] = part.split(".");
return (!tag || el.tag === tag) && cls.every((c) => el.set.has(c));
};
const matches = (el, selector) => {
const parts = selector.split(" ");
if (!one(el, parts.at(-1))) return false;
return parts.length === 1 || (el.parent !== null && one(el.parent, parts[0]));
};
function fakeSvg(ppm, x0) {
const center = node("g", ["b06-chart__center"]);
const els = [
center,
node("line", [], { x1: String(x0) }, center),
node("g", ["b06-chart__area", "b06-chart__area--cut_soil"]),
node("g", ["b06-chart__area", "b06-chart__area--cut_rock", "b06-chart__area--cut_rr"]),
node("g", ["b06-chart__area", "b06-chart__area--fill", "is-active"]), // 카드에서 묻어 온 강조
node("polyline", ["b06-chart__spoil-fill"]),
];
// 판정 선: offset 구간(m) → 화면 x = x0 − offset × ppm
const hit = (a, b) =>
node("polyline", ["b06-chart__design-hit"], {
points: `${x0 - a * ppm},10 ${x0 - b * ppm},20`,
});
const hits = {
road: hit(-2, 2),
ditch: hit(2.5, 3.0),
cutSlope: hit(3.0, 6.0),
fillSlope: hit(-2.5, -6.0),
};
els.push(...Object.values(hits));
return {
els,
hits,
getAttribute: (name) => (name === "data-px-per-m" ? String(ppm) : null),
querySelectorAll: (sel) =>
els.filter((el) => sel.split(",").some((s) => matches(el, s.trim()))),
querySelector: (sel) => els.find((el) => matches(el, sel)) ?? null,
};
}
const design = {
road_edges: {
left: { offset_m: 2.5, elevation_m: 99.95 },
right: { offset_m: -2.5, elevation_m: 99.95 },
},
ditch: { type: "standard", top_width_m: 0.5, bottom_width_m: 0.2, depth_m: 0.3 },
ditch_side: "left",
ditch_enabled: true,
section_mode: "left_cut",
};
const activeOf = (svg) => {
const names = [];
for (const [name, el] of Object.entries(svg.hits)) if (el.set.has("is-active")) names.push(name);
for (const el of svg.els)
if (el.set.has("is-active") && !el.set.has("b06-chart__design-hit"))
names.push(
[...el.set].find(
(c) => c.includes("--") || c === "b06-chart__center" || c === "b06-chart__spoil-fill",
),
);
return names;
};
const out = { targets: Object.keys(pick.DETAIL_TARGETS), zone: {}, active: {} };
out.zone = {
road: pick.lineZone(design, 0),
ditch: pick.lineZone(design, 2.8),
cutSlope: pick.lineZone(design, 4),
fillSlope: pick.lineZone(design, -4),
};
for (const key of [
null,
"cut_soil",
"cut_rock",
"embankment",
"spoil_fill",
"ground",
"ditch_soil",
"grading_cut",
"grading_fill",
"road_compaction",
"fence",
]) {
const svg = fakeSvg(20, 300);
pick.highlightDetail(svg, design, key);
out.active[String(key)] = activeOf(svg);
}
// 164-2 — 그 카드에 대상이 있는 키(암석 띠 · 사토장 없는 카드 = 그 칸 빠짐)
out.present = [...pick.presentKeys(fakeSvg(20, 300), design)].sort();
const bare = fakeSvg(20, 300);
const kept = bare.els.filter(
(el) => !el.set.has("b06-chart__area--cut_rock") && !el.set.has("b06-chart__spoil-fill"),
);
bare.els.splice(0, bare.els.length, ...kept);
out.presentBare = [...pick.presentKeys(bare, design)].sort();
process.stdout.write(JSON.stringify(out));