Files
Aislo/resources/tester/helper_b05_b06_selection.cjs
T
eomsangdonandClaude Sonnet 5.5 8a9456579a refactor(B05·B06): 측점 · 구조물 고름을 selection 한 곳으로 · 관 찾는 거리 PIPE_MATCH_M 한 값 · 횡단 카드 누름이 옹벽 · 세월교 · BOX · 기슭막이 폼을 엶 (24-2d)
- 목록 · 종단 알약 · 3D · 횡단 카드 · 벽 어디서 골라도 selectDesign 한 길로 세션에 적음(구조물 id 포함) — B05 목록 구조물 고름도 B06 으로 이어짐
- 폼이 열 항목 풀이 resolveSelection 한 곳(관 먼저 · 구조물 다음 · 가장 가까운 것) — 공용 폼의 0.05 없앰
- B06 벽 · 구체 누름은 showAtChainage 하나를 탐(옛 관 전용 showPipeAt 없앰) · 벽 누름 중 폼이 카드를 먼저 고르는 이중 토글 막음
- B06 진입 때 구조물 id 로 카드를 찾고, 못 찾으면 다시 부르게 함 · B05 진입 때 구조물 id 가 있을 때만 구조물로 연다
- 시험 test_b05_b06_selection_single 21

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ly2oHAaTDYRjcniAmHR81z
2026-09-29 21:02:55 +09:00

179 lines
7.6 KiB
JavaScript

/* 측점 · 구조물 고름 한 곳 단위검증 헬퍼(PLAN 24-2d) — TS 를 그 자리에서 트랜스파일해 Node 로 돌린다.
* 결과는 표준출력에 JSON 한 벌. 화면 그릇은 쓰지 않는다(세션 저장소와 즉시 실행 프레임만 흉내).
*
* ⚠ 확장자가 `.cjs` 인 까닭은 `helper_b05_structures_form_summary.cjs` 머리와 같다.
*/
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 session = new Map();
const fakeStorage = {
getItem: (k) => session.get(k) ?? null,
setItem: (k, v) => session.set(k, String(v)),
removeItem: (k) => session.delete(k),
};
globalThis.window = { sessionStorage: fakeStorage, localStorage: fakeStorage };
globalThis.sessionStorage = fakeStorage;
globalThis.localStorage = fakeStorage;
globalThis.requestAnimationFrame = (fn) => fn();
function loadTs(file) {
const full = path.resolve(file);
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) =>
request.startsWith(".")
? loadTs(path.join(path.dirname(full), `${request}.ts`))
: require(request);
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
const sel = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"));
const pickPage = loadTs(path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structure_Pick.ts"));
const pickSession = loadTs(
path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"),
);
const out = { matchM: sel.PIPE_MATCH_M };
const P = "proj-1";
// ── 풀기: 관 → 구조물 순 · 한 거리 ───────────────────────────────────────
// 옹벽(구간 200~210) · 독립 기슭막이(구간 80~90) · BOX/세월교/배수관은 관 정본
const structures = [
{ structure_id: "wall", placement: "interval", start_m: 200, end_m: 210, chainage_m: 205 },
{ structure_id: "revet", placement: "interval", start_m: 80, end_m: 90, chainage_m: 85 },
];
const pipes = [{ chainage_m: 173.09 }, { chainage_m: 205.2 }, { chainage_m: 300 }];
const at = (chainageM, structureId = null) =>
sel.resolveSelection({ chainageM, structureId }, pipes, structures);
out.card = {
// 횡단 카드 측점이 관 측점에서 0.05 ~ 0.51 m 어긋나도 그 관을 찾는다(옛 B05 폼은 0.05 라 놓쳤다)
pipeNear: at(173.4),
pipeFar: at(173.09 + 0.6),
wallStart: at(200),
wallMiddle: at(207),
wallEdge: at(210.4),
revet: at(85),
none: at(250),
cleared: at(null),
// 같은 측점에 관과 구조물이 겹치면 관이 먼저(횡단도에 그려지는 것)
pipeBeatsStructure: at(205.0),
// 구조물 id 가 주어지고 목록에 있으면 측점보다 id 가 이긴다 · 없는 id 는 측점으로 되돌아간다
byId: at(300, "revet"),
staleId: at(200, "gone"),
};
// ── 세션 값 + 알림 ─────────────────────────────────────────────────────
const heard = [];
const off = sel.subscribeSelection(P, (s, from) => heard.push({ ...s, from }));
sel.selectDesign(P, { chainageM: 205, structureId: "wall" }, { partKey: "own", from: "b06" });
out.store1 = sel.readSelection(P);
sel.selectDesign(P, { chainageM: 205, structureId: "wall" }, { from: "b06" });
out.sameQuiet = heard.length; // 같은 값 · 부재키 생략 → 알리지 않음
sel.selectDesign(P, { chainageM: 173.09, structureId: null }, { from: "b05" });
out.keyKept = sel.readSelection(P)?.partKey; // 부재키를 안 주면 지킨다(옛 규칙)
sel.selectDesign(P, sel.NO_SELECTION, { from: "b05" });
out.cleared = sel.readSelection(P);
out.heardFrom = heard.map((h) => [h.chainageM, h.structureId, h.from]);
off();
sel.selectDesign(P, { chainageM: 1, structureId: null });
out.afterOff = heard.length;
out.noProject = sel.readSelection(null);
// 알림 도중 다시 낸 값은 적기만 한다(되울림 방지)
let echoes = 0;
const off2 = sel.subscribeSelection(P, () => {
echoes += 1;
sel.selectDesign(P, { chainageM: 2, structureId: null });
});
sel.selectDesign(P, { chainageM: 3, structureId: null });
out.echoes = echoes;
out.lastWritten = sel.readSelection(P)?.chainageM;
off2();
sel.selectDesign(P, sel.NO_SELECTION);
// ── 넘김값 읽기 ────────────────────────────────────────────────────────
sel.selectDesign(P, { chainageM: 205, structureId: "wall" }, { partKey: "right" });
out.handoff = pickSession.readStructurePick(P);
sel.selectDesign(P, sel.NO_SELECTION);
out.handoffNone = pickSession.readStructurePick(P);
// ── B06 진입 때 카드 찾기 ──────────────────────────────────────────────
const cards = [80, 173.09, 200, 205.2, 220].map((c) => ({
chainage_m: c,
station_id: `s${c}`,
ford: null,
box: null,
}));
const detail = { cross_sections: cards };
const run = (handoff, list) => {
const calls = [];
calls.applied = pickPage.applyStructurePick(
handoff,
detail,
{
focusStation: (id) => calls.push(["focus", id]),
refreshCard: (c) => calls.push(["refresh", c]),
revetSelect: (c, k) => calls.push(["revet", c, k]),
fordSelect: (c, k) => calls.push(["ford", c, k]),
boxSelect: (c, k) => calls.push(["box", c, k]),
},
list,
);
return calls;
};
const midWall = { at: 207, structureId: "wall" }; // 구간 한가운데 — 카드 위치가 아님
out.retry = {
// 구조물 목록이 아직 안 실렸으면 카드를 못 찾고 false → 부르는 쪽이 나중에 다시 부른다
before: run({ at: 260 }, []).applied,
// 목록이 실린 뒤 같은 넘김값은 카드를 찾는다
after: run(midWall, structures).applied,
notFound: run({ at: 260 }, structures).applied,
emptyHandoff: run(null, structures).applied,
};
out.apply = {
pipeOffsetCard: run({ at: 173.4 }, structures),
wallByAnchor: run({ at: 205, structureId: "wall" }, structures),
wallNoId: run({ at: 207 }, structures),
revetCardFirst: run({ at: 85, structureId: "revet", key: "own" }, structures),
unknown: run({ at: 260 }, structures),
none: run(null, structures),
};
// ── B05 진입 때 되살리기 — 구조물 id 가 있을 때만 구조물로 연다 ─────────────
const restoreRun = (pickValue) => {
sel.selectDesign(P, sel.NO_SELECTION);
if (pickValue) sel.selectDesign(P, pickValue.selection, { partKey: pickValue.key });
const calls = [];
let opened = false;
const section = {
hasSelection: () => opened,
selectAt: (s) => {
calls.push(["selectAt", s.chainageM, s.structureId]);
opened = true;
},
};
const controls = { select: (p) => calls.push(["3d", p.chainageM, p.key ?? null]) };
const done = pickSession.restoreStructurePick(controls, P, section, (c) =>
calls.push(["station", c]),
);
return { calls, done };
};
out.restore = {
pipeOnly: restoreRun({ selection: { chainageM: 85.05, structureId: null } }),
structure: restoreRun({ selection: { chainageM: 205, structureId: "wall" }, key: "own" }),
nothing: restoreRun(null),
};
sel.selectDesign(P, sel.NO_SELECTION);
console.log(JSON.stringify(out));