Files
Aislo/resources/tester/helper_356_center.cjs
T
eomsangdonandClaude Opus 5.5 8f5286ad79 feat(424 횡단 설계중심): 중심점 좌우 걷음 — ◀▶ 단추 · 가로 끌기 · 노선 좌우 배선 없음 · ▲▼ 종단 계획고 그대로 (요구 260)
- B06 중심점 칸 = 버튼 없음 · 「▲▼ = 종단 계획고 · 좌우 이동은 평면에서」 · ◀▶ 키 먹어 막음
- B06_Section_UI_Cross_Center = 판정 원 · 한 축 끌기(암 경계선)만 남김
- B05 노선 좌우 host(_RouteEdit_Shift) · 고름 계산(logic/B05_Profile_RouteShift) 지움 · 페이지 · 편집기 배선 한 줄씩 걷음
- 시험 356 = 가짜 DOM 으로 중심점 고름 · 버튼 없음 · 키 · 끌기 없음 확인 · 356_4 = 중심점 좌우 걸음 뺌

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
2026-10-18 15:37:36 +09:00

238 lines
8.1 KiB
JavaScript

/* PLAN 424 단위검증 헬퍼 — 횡단 카드 중심점 = ▲▼(종단 계획고)만 · 좌우는 평면에서만(요구 260).
* 화면 TS(`B06_Section_UI_Cross_Design_Pick` · `_Cross_Center`)를 트랜스파일해 Node 로 돌림(가짜 DOM =
* `helper_370_2_toe_stop.cjs` 꼴) · 결과 = 표준출력 JSON 한 벌. 입력 = argv[2] 표준단면 JSON. 짝 = `test_356_b06_card_center.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();
const STUBS = {
"@ui/ui_template_toast": { showToast: () => undefined },
"@ui/ui_template_elements": { showToast: () => undefined },
};
class FakeNode {
constructor(tag) {
this.tagName = String(tag);
this.children = [];
this.attrs = {};
this.listeners = {};
this.parent = null;
this.hidden = false;
this.title = "";
this._text = "";
this._classes = new Set();
const self = this;
this.classList = {
add: (...names) => names.forEach((n) => self._classes.add(n)),
remove: (...names) => names.forEach((n) => self._classes.delete(n)),
toggle: (name, force) => {
const on = force === undefined ? !self._classes.has(name) : !!force;
on ? self._classes.add(name) : self._classes.delete(name);
return on;
},
contains: (name) => self._classes.has(name),
};
this.style = { setProperty: () => {} };
}
set className(value) {
this._classes = new Set(String(value).split(/\s+/).filter(Boolean));
}
get className() {
return [...this._classes].join(" ");
}
set textContent(value) {
this._text = String(value);
this.children = [];
}
get textContent() {
return this._text + this.children.map((c) => c.textContent).join("");
}
get childElementCount() {
return this.children.length;
}
get isConnected() {
return true;
}
_detach(kid) {
if (kid.parent) kid.parent.children = kid.parent.children.filter((k) => k !== kid);
kid.parent = this;
}
append(...kids) {
kids.forEach((k) => {
if (typeof k === "string") return;
this._detach(k);
this.children.push(k);
});
}
prepend(...kids) {
kids.reverse().forEach((k) => {
this._detach(k);
this.children.unshift(k);
});
}
appendChild(kid) {
this.append(kid);
return kid;
}
insertBefore(kid, ref) {
this._detach(kid);
const at = ref ? this.children.indexOf(ref) : -1;
if (at < 0) this.children.push(kid);
else this.children.splice(at, 0, kid);
return kid;
}
remove() {
if (this.parent) this.parent.children = this.parent.children.filter((k) => k !== this);
}
get descendants() {
return this.children.flatMap((c) => [c, ...c.descendants]);
}
/** `.이름` 한 개만 — 방향키 듣개가 슬롯 단추를 찾는 꼴(◀▶ 단추가 있으면 누름). */
querySelector(sel) {
return this.descendants.find((n) => n.classList.contains(sel.slice(1))) ?? null;
}
getClientRects() {
return this.classList.contains("is-hidden") ? [] : [{}];
}
setAttribute(name, value) {
this.attrs[name] = String(value);
if (name === "class") this.className = value;
}
getAttribute(name) {
return name === "class" ? this.className : (this.attrs[name] ?? null);
}
hasAttribute(name) {
return name in this.attrs;
}
addEventListener(type, fn) {
(this.listeners[type] ??= []).push(fn);
}
click() {
const event = { stopPropagation: () => {}, preventDefault: () => {} };
(this.listeners.click ?? []).forEach((fn) => fn(event));
}
}
const docKeys = new Set(); // 방향키 듣개(379-1 · 정확히 0 자리)
globalThis.document = {
createElement: (tag) => new FakeNode(tag),
createElementNS: (_ns, tag) => new FakeNode(tag),
addEventListener: (type, fn) => type === "keydown" && docKeys.add(fn),
removeEventListener: (type, fn) => type === "keydown" && docKeys.delete(fn),
};
globalThis.window = { setTimeout, clearTimeout, addEventListener: () => {} };
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
if (full.endsWith(".json")) {
const data = JSON.parse(fs.readFileSync(full, "utf8"));
cache.set(full, data);
return data;
}
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
esModuleInterop: true,
},
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (STUBS[request]) return STUBS[request];
if (request.endsWith(".css")) return {};
const alias = { "@util/": "common_util", "@ui/": "ui_template", "@config/": "config" };
for (const [prefix, dir] of Object.entries(alias))
if (request.startsWith(prefix))
return loadTs(resolve(path.join(ROOT, dir, request.slice(prefix.length))));
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 resolve = (base) =>
[base, `${base}.ts`, path.join(base, "index.ts")].find(
(p) => fs.existsSync(p) && fs.statSync(p).isFile(),
) ?? `${base}.ts`;
const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`);
const pick = loadTs(B06("B06_Section_UI_Cross_Design_Pick"));
const centerMod = loadTs(B06("B06_Section_UI_Cross_Center"));
const { appendCrossDesignOverlay } = loadTs(B06("B06_Section_UI_Cross_Design"));
const { computeCrossDesign } = loadTs(
path.join(ROOT, "common_util", "common_util_cross_design.ts"),
);
const standard = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
// 평지보다 조금 높은 지반 = 양쪽 절토(교차점 손잡이 없음 · 중심점 원 하나)
const samples = Array.from({ length: 49 }, (_, i) => ({
offset_m: -12 + i * 0.5,
elevation_m: 100.3,
valid: true,
}));
const design = {
...computeCrossDesign(samples, 100, { standard, groundType: "soil", sectionMode: "both_cut" }),
shoulder_extend_left_m: 0,
shoulder_extend_right_m: 0,
};
const section = { station_id: "S1", chainage_m: 20, samples, design };
const x = (o) => 300 - o * 20;
const y = (e) => 400 - (e - 100) * 20;
const layer = new FakeNode("g");
appendCrossDesignOverlay(layer, design, x, y, samples, undefined);
layer.children.forEach((el) => el.setAttribute(pick.DESIGN_LINE_ATTR, ""));
const picks = [];
const changes = [];
const moves = pick.designLineMoves(section, (_c, change) => changes.push(change));
const handle = pick.attachDesignLinePick({
layer,
design,
x,
toDisplayY: y,
onPick: (i) => picks.push(i),
...moves,
});
const centers = layer.children.filter(
(k) =>
k.classList.contains("b06-chart__center-hit") && !k.classList.contains("b06-chart__toe-hit"),
);
const center = centers[0];
center.click();
handle.setActive(picks[0]);
const row = handle.cell.children[1];
const press = (key) => {
let prevented = false;
const event = { key, target: null, preventDefault: () => (prevented = true) };
[...docKeys].forEach((fn) => fn(event));
return prevented;
};
const out = {
moves: { center: moves.center },
count: centers.length,
cx: center ? Number(center.getAttribute("cx")) : null,
x0: x(0),
active: center.classList.contains("is-active"),
title: handle.cell.children[0].textContent,
value: row.children.at(-1).textContent,
// 보이는 버튼(숨은 버튼 = 슬롯 없음)
slots: row.children
.filter((k) => /cell-btn--/.test(k.className) && !k.hidden)
.map((k) => k.className.match(/cell-btn--(\w+)/)[1]),
panelOpen: !handle.cell.classList.contains("is-empty"),
// 방향키 — ◀▶ = 먹어 막음(아무것도 안 옮김) · ▲▼ = 안 막음(종단도 듣개 = 종단 계획고)
keys: {
left: press("ArrowLeft"),
right: press("ArrowRight"),
up: press("ArrowUp"),
down: press("ArrowDown"),
},
dragListeners: (center.listeners.pointerdown ?? []).length,
changes: changes.length,
exports: Object.keys(centerMod).sort(),
};
process.stdout.write(JSON.stringify(out));