Files
Aislo/resources/tester/helper_152_4_chute_pick.cjs
T
eomsangdonandClaude Opus 5.5 c924ee2042 feat(b06): 도수로 · 돌붙임 띠 고름 · 강조 — 관 고름 꼴(152-4)
- 그린 띠마다 투명 판정면(벽 · 관 판정면 아래 · 보라 선 위) · 고름 키 chute · 강조 setter
- 좌측 「도수로/돌붙임」 상자 강조 · 스크롤 · 상자 고치면 띠 고름 · 자기 측점 폼으로
- 조정창 없음 · B05 3D chute 키 → B06 고름 · 새 시험 test_152_4_chute_pick
- 받는 줄 문자열 시험 54-29 고침(152-2 OWN_HEIGHT_ROW)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
2026-10-09 19:17:41 +09:00

279 lines
9.6 KiB
JavaScript

/* PLAN 152-4 단위검증 헬퍼 — B06 도수로 · 돌붙임 띠 고름 · 강조(118-1 관 고름 꼴) · 조정창 숨김 · 3D chute 키.
* 화면 TS 를 그 자리에서 트랜스파일해 흉내 DOM 에서 돌림 · 결과 = 표준출력 JSON 한 벌.
* 입력 = `fixtures/b07_cross_overlay_sections.json` 배관 측점(도수로 상자를 붙인 사본). 짝 = `test_152_4_chute_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();
let STUBS = {
"@ui/ui_template_toast": { showToast: () => undefined },
"@ui/ui_template_elements": { showToast: () => undefined },
};
// ── 화면 그릇 흉내 — 오버레이 · 조정창이 쓰는 만큼만 ─────────────────────────
class FakeNode {
constructor(tag) {
this.tagName = String(tag).toUpperCase();
this.children = [];
this.attrs = {};
this.dataset = {};
this.listeners = {};
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("");
}
append(...kids) {
kids.forEach((k) => this.children.push(k));
}
insertBefore(kid, ref) {
const at = ref ? this.children.indexOf(ref) : -1;
at < 0 ? this.children.push(kid) : this.children.splice(at, 0, kid);
return kid;
}
appendChild(kid) {
this.children.push(kid);
return kid;
}
replaceChildren(...kids) {
this.children = kids;
}
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);
}
addEventListener(type, fn) {
(this.listeners[type] ??= []).push(fn);
}
removeEventListener() {}
click() {
let stopped = false;
const event = { stopPropagation: () => (stopped = true), preventDefault: () => {} };
(this.listeners.click ?? []).forEach((fn) => fn(event));
return stopped;
}
querySelectorAll(selector) {
const found = [];
const walk = (node) =>
node.children.forEach((kid) => {
if (kid.classList && kid.classList.contains(selector.slice(1))) found.push(kid);
walk(kid);
});
walk(this);
return found;
}
querySelector(selector) {
return this.querySelectorAll(selector)[0] ?? null;
}
getClientRects() {
return [];
}
}
globalThis.document = {
createElement: (tag) => new FakeNode(tag),
createElementNS: (_ns, tag) => new FakeNode(tag),
createComment: () => new FakeNode("#comment"),
addEventListener: () => {},
removeEventListener: () => {},
};
globalThis.window = { addEventListener: () => {}, removeEventListener: () => {} };
globalThis.requestAnimationFrame = (fn) => fn();
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];
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 { computeStoredLayouts } = loadTs(B06("B06_Section_Structure_Layouts"));
const { appendCulvertOverlay } = loadTs(B06("B06_Section_UI_Cross_Culvert"));
const { attachChuteBandPick } = loadTs(B06("B06_Section_UI_Cross_Wall_Hatch"));
const { applyStructurePick } = loadTs(B06("B06_Section_UI_Page_Structure_Pick"));
const fixture = JSON.parse(
fs.readFileSync(path.join(__dirname, "fixtures", "b07_cross_overlay_sections.json"), "utf8"),
);
const sections = fixture.cross_sections;
const plain = sections.find((s) => s.culvert && !s.culvert.hidden_pipe);
const pipeSection = JSON.parse(JSON.stringify(plain));
pipeSection.culvert.chute = { structure: "돌붙임", thickness_m: 0.3 };
const listWith = sections.map((s) => (s === plain ? pipeSection : s));
const out = {};
const cls = (n) => n.className;
// ① 오버레이 + 띠 고름 — 판정면 차례 · 누름 · 강조
function draw(section, list, onPick) {
const layer = new FakeNode("g");
const layout = computeStoredLayouts(section, list).culvert;
const setter = appendCulvertOverlay(
layer,
layout,
(o) => -o * 10,
(e) => -e * 10,
() => {},
false,
);
const picked = [];
const setChute = attachChuteBandPick(layer, onPick ? () => picked.push("chute") : undefined);
return { layer, setter, setChute, picked };
}
const drawn = draw(pipeSection, listWith, true);
const kids = drawn.layer.children;
const bands = kids.filter((n) => n.classList.contains("b06-chart__culvert-pitching"));
const hits = kids.filter((n) => n.classList.contains("b06-chart__culvert-revet-hit"));
const chuteHits = hits.filter((n) => n.classList.contains("b06-chart__chute-hit"));
const stopped = chuteHits[0].click();
drawn.setChute(true);
const on = bands.map((b) => b.classList.contains("is-active"));
drawn.setChute(false);
const off = bands.map((b) => b.classList.contains("is-active"));
out.overlay = {
bands: bands.length,
chuteHits: chuteHits.length,
// 판정면 차례 — 띠 판정면이 관 · 벽 판정면보다 먼저(아래)
hitOrder: hits.map((n) => (n.classList.contains("b06-chart__chute-hit") ? "chute" : "other")),
sameShape: chuteHits.every((h, i) => h.attrs.points === bands[i].attrs.points),
picked: drawn.picked,
stopped,
on,
off,
};
// 고름 배선 없음(조작 없는 카드) — 판정면 없이 강조만 · 띠 없으면 null
const bare = draw(pipeSection, listWith, false);
out.bare = {
hits: bare.layer.children.filter((n) => n.classList.contains("b06-chart__chute-hit")).length,
setter: typeof bare.setChute,
};
out.noChute = draw(plain, sections, true).setChute === null;
// ② B05 3D 띠 고름(chute 키) → B06 고름 길로
const calls = [];
applyStructurePick(
{ at: pipeSection.chainage_m, key: "chute" },
{ cross_sections: [pipeSection] },
{
focusStation: () => {},
refreshCard: () => {},
revetSelect: (c, k) => calls.push(["revet", k]),
fordSelect: (c, k) => calls.push(["ford", k]),
boxSelect: (c, k) => calls.push(["box", k]),
},
);
out.pick = calls;
// ③ 조정창 — show("chute") = 숨김 · side "chute"
const base = loadTs(B06("B06_Section_UI_Cross_Panel_Base"));
STUBS = {
...STUBS,
"./B06_Section_UI_Section_Common": { L: (key) => key },
"./B06_Section_UI_Cross_Panel_Base": {
...base,
bindDragHandle: () => ({ attach: () => {}, detach: () => {} }),
},
};
const { buildStructurePanel } = loadTs(B06("B06_Section_UI_Cross_Structure_Panel"));
const touched = [];
const spy = (name, value) => () => {
touched.push(name);
return value;
};
const panel = buildStructurePanel({
scrollKey: "t152",
adjustFor: spy("adjustFor", { x: 0, d: null, h: null, m: null }),
nudge: spy("nudge"),
nudgeSlope: spy("nudgeSlope"),
nudgeHeight: spy("nudgeHeight"),
heightFor: spy("heightFor", 2),
formFor: spy("formFor", "돌쌓기(메)"),
setForm: spy("setForm"),
reset: spy("reset"),
close: () => {},
structureFor: () => "revet",
basinAdjustFor: () => ({ innerWidthM: 1, innerHeightM: 1.2, lateralM: 0, slopeM: 0 }),
updateBasin: () => {},
nudgeBasinLateral: () => {},
resetBasin: () => {},
canNudge: spy("canNudge", true),
fineMove: spy("fineMove", false),
dragAxes: spy("dragAxes", null),
extraState: () => ({ canAdd: false, count: 0 }),
isLinkedCard: () => false,
basinExtraState: () => ({ canAdd: false, count: 0 }),
hiddenPipe: () => false,
bodyGuard: () => false,
setExtraCount: () => {},
setBasinExtraCount: () => {},
equalizeExtras: () => {},
spanFor: spy("spanFor", null),
setSpan: () => {},
linkState: () => null,
setLinked: () => {},
followGrade: () => true,
setFollowGrade: () => {},
});
panel.show("chute");
out.panel = {
hidden: panel.root.classList.contains("is-hidden"),
side: panel.root.dataset.side,
touched,
};
process.stdout.write(JSON.stringify(out));