Files
Aislo/resources/tester/helper_165_pavement_flow.cjs
T
eomsangdonandClaude Opus 5.5 9e69bec512 fix(B06·B05): 노면공 고름 세 갈래 · 형상 누름 = 목록 고름 · 카드 하나 = 그 측점(189-1)
- ② 카드 안 노면공 포장층 누름 = 목록 고름과 같은 길(범위 카드 전부 · 포장층 강조) — 카드 고름보다 먼저 가로챔
- ③ 카드 하나 누름 = 연 노면공 · 범위 강조 풂 · 그 카드 하나(165-1 노면공 지킴 · 167-3 재누름 그대로 없앰)
- 카드 고름 세션 = 그 측점(구조물 id 는 목록 · 알약 · 넘김이 고른 것만)
- 종단: 구조물 넘김이면 측점선 · 3D 띠 안 켬
- 새 시험 test_189_1_pave_pick · test_165 · test_188_1 기대값 41번 규칙으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
2026-10-10 13:21:23 +09:00

388 lines
14 KiB
JavaScript

/* 165 노면공 고름 일관성 헬퍼 — 진짜 `createB06StructuresPanel`(SPN)에 폼 · 횡단 카드 · 종단 알약을 흉내 내 붙이고
* 조사 K 재현 차례 6단계를 돌려 단계마다 {폼 · 알약 · 포장층 · 고른 카드 · 세션 sid} 를 표준출력 JSON 한 벌로.
* 카드 고름 리스너는 design_view_cross.ts 의 `setStationSelectListener` 본문을, 카드 · 알약 흉내는 B06 Section_View ·
* Section_View_Profile 의 `selectStation` · `focusStation` · `selectMark` · `showCard` 를 그대로 옮김(TS 는 그 자리 트랜스파일).
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
function loadTs(file, stubs = {}) {
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
const localRequire = (request) => {
if (stubs[request]) return stubs[request];
throw new Error(`stub 없음: ${request}`);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
return box.exports;
}
// 세션(structure-pick) — 메모리 한 벌
const store = new Map();
const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), {
"../b_page_state": {
readState: (key, pid) => store.get(`${key}:${pid}`) ?? null,
writeState: (key, value, pid) => store.set(`${key}:${pid}`, value),
},
});
const PID = "p165";
const TYPES = [
{ type_id: "pavement_concrete", group: "G" },
{ type_id: "hazard_sign", group: "E" },
{ type_id: "pipe", group: "A", managed_by: "pipe_points" },
];
const PAVE = {
structure_id: "pave",
type_id: "pavement_concrete",
placement: "interval",
chainage_m: 30,
start_m: 30,
end_m: 250,
};
const PAVE2 = {
structure_id: "pave2",
type_id: "pavement_concrete",
placement: "interval",
chainage_m: 100,
start_m: 100,
end_m: 200,
};
const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }];
// 카드 = 20 m 측점 + 관 측점(비정규)
const CARDS = [...Array.from({ length: 16 }, (_, i) => i * 20), 80.17]
.sort((a, b) => a - b)
.map((at) => ({ station_id: `s${at}`, chainage_m: at }));
const cardAt = (at) => CARDS.find((card) => Math.abs(card.chainage_m - at) < 0.01);
/* ── 폼 흉내(B05 패널 · loadForm / loadPipeForm / resetForm 의 알림 차례) ── */
let binding = null;
let structures = [];
const form = { id: null, pipe: null };
const loadForm = (target) => {
form.id = target?.structure_id ?? null;
form.pipe = null;
binding.onSelect(target);
};
const loadPipeForm = (chainageM) => {
const hadStructure = form.id !== null;
form.id = null;
form.pipe = chainageM;
if (hadStructure) binding.onSelect(null);
};
const resetForm = () => {
const hadPipe = form.pipe !== null;
form.pipe = null;
loadForm(null);
if (hadPipe) binding.onPipeSelect(null);
};
const node = { addEventListener() {}, querySelector: () => ({ append() {} }) };
const section = {
root: node,
listRoot: node,
facility: { root: node, setRevetSideLabels() {}, recount() {} },
setTypes() {},
setStructures(next) {
structures = next;
},
setPipeFacilities() {},
hasSelection: () => form.id !== null || form.pipe !== null,
selectById(id) {
if (id === null) {
if (form.id !== null || form.pipe !== null) resetForm();
return;
}
loadForm(structures.find((entry) => entry.structure_id === id) ?? null);
},
selectPipeByChainage(chainageM) {
if (chainageM === null) {
if (form.pipe !== null) resetForm();
return;
}
loadPipeForm(chainageM);
},
};
const panelModule = loadTs(
path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structures_Panel.ts"),
{
"../A00_Common/design/design_shared": {
bindDesignView: (_shared, _view, parts) => {
binding = parts.structures;
},
sharedStructures: () => section,
},
"../B05_Profile/B05_Profile_Api_Structures": {
fetchStructureTypes: async () => TYPES,
fetchStructures: async () => ({ structures: [] }),
readPendingStructures: () => current.structures,
pipesToStructureMarks: (pipes) =>
pipes.map((pipe) => ({
structure_id: `pipe-${pipe.chainage_m.toFixed(2)}`,
type_id: pipe.facility,
chainage_m: pipe.chainage_m,
start_m: null,
end_m: null,
})),
structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0,
writePendingStructures() {},
},
"../B05_Profile/B05_Profile_Api_Pipes_Draft": {
readPendingPipes: () => null,
writePendingPipes() {},
},
"../B05_Profile/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x },
"../B04_PreProcess/B04_PreProcess_Api_Fetch": {
fetchDetailPipePoints: async () => ({ pipe_points: PIPES, basins: [] }),
},
"../common_util/common_util_summary_item": {
linkFields: () => ({}),
readGroupItems: (x) => x ?? {},
readSummaryItem: (x) => x ?? null,
},
"../B05_Profile/B05_Profile_UI_Structure_Pick_Session": { readStructurePick: () => null },
"../A00_Common/design/design_selection": selection,
"@ui/ui_template_elements": { showToast() {} },
"./B06_Section_UI_Adjust_Dock": {
adjustDockRoot: () => ({}),
refreshAdjustSlots() {},
resetAdjustDock() {},
setAdjustSideSlots() {},
},
"./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} },
},
);
/* ── 종단 알약 흉내(Section_View_Profile) ── */
let marks = [];
const profile = {
markedId: null,
pill: null,
line: null, // 종단 측점선(design_profile selectedStationId)
selectMark(id) {
this.markedId = id;
this.pill = id;
// setSelectedStructure(id, keepStation) — 선 있는 구조물(관)만 그 선 · 아니면 카드 선 지킴(177-3)
const pipe = PIPES.find((entry) => `pipe-${entry.chainage_m.toFixed(2)}` === id);
if (pipe) this.line = cardAt(pipe.chainage_m).station_id;
},
showCard(card) {
this.line = card ? card.station_id : null;
if (!card) return;
const pipe = PIPES.find((entry) => Math.abs(entry.chainage_m - card.chainage_m) < 0.51);
// 관 측점 = 그 측점선 짝 알약 · 일반 측점 = 꺼짐 — 고른 구조물이 덮으면 지킴(53-1)
this.pill = pipe ? `pipe-${pipe.chainage_m.toFixed(2)}` : null;
const marked = marks.find((entry) => entry.structure_id === this.markedId);
if (!pipe && marked && selection.coversChainage(marked, card.chainage_m))
this.pill = this.markedId;
},
};
/* ── 횡단 카드 흉내(Section_View) ── */
let markTypes = TYPES;
const view = {
selected: null,
spanMarkId: null,
listener: null,
isPavement(id) {
const mark = marks.find((entry) => entry.structure_id === id);
return !!mark && markTypes.find((type) => type.type_id === mark.type_id)?.group === "G";
},
selectStation(id) {
// 카드 하나 고름 = 그 측점 · 같은 카드 재누름 = 풂(189-1 · Section_View `selectStation`)
const previous = this.selected;
this.selected = this.selected === id ? null : id;
profile.showCard(CARDS.find((card) => card.station_id === this.selected) ?? null);
if (this.selected !== previous) this.listener(this.selected);
},
revealed: null,
revealStation(id) {
// 노면공 목록 고름 = 카드 하나 안 고름 · 리스너 없이 풂 · 스크롤만(177-4 · Section_View `revealStation`)
this.selected = null;
profile.showCard(null);
this.revealed = id;
},
focusStation(id) {
if (id === null) this.selected = null;
else if (this.selected !== id) this.selectStation(id);
else profile.showCard(CARDS.find((card) => card.station_id === id));
},
selectMark(id, span = true) {
this.spanMarkId = span || this.isPavement(id) ? id : null;
profile.selectMark(id);
},
};
let wallSelecting = false;
const focusStationAt = (at, revealOnly = false) => {
const nearest = CARDS.reduce((best, card) =>
Math.abs(card.chainage_m - at) < Math.abs(best.chainage_m - at) ? card : best,
);
if (revealOnly) view.revealStation(nearest.station_id);
else view.focusStation(nearest.station_id);
};
const current = { structures: [PAVE] };
let panel = null;
/** design_view_cross.ts 카드 고름 리스너 본문(벽 → 소유 측점) — 그대로 옮김. */
const ownerOf = (card) => (wallOwner && card.chainage_m === wallOwner.card ? wallOwner : null);
let wallOwner = null;
view.listener = (stationId) => {
const target = stationId ? CARDS.find((card) => card.station_id === stationId) : null;
if (!target) {
panel.showAtChainage(null);
selection.selectDesign(PID, selection.NO_SELECTION, { from: "b06" });
return;
}
const owner = wallSelecting ? ownerOf(target) : null;
const preferred = panel.preferred() !== null;
const opened = panel.showAtChainage(owner?.chainage_m ?? target.chainage_m);
// 카드 하나 고름 = 그 측점(구조물 id 는 목록 · 알약 · 넘김이 고른 것만 · 189-1)
const picked = preferred ? opened : { chainageM: opened.chainageM, structureId: null };
if (!wallSelecting) selection.selectDesign(PID, picked, { from: "b06" });
const pipeAt = panel.currentChainage();
const mark = opened.structureId ?? (pipeAt === null ? null : `pipe-${pipeAt.toFixed(2)}`);
view.selectMark(mark, false);
};
async function build(list) {
current.structures = list;
store.clear();
form.id = null;
form.pipe = null;
view.selected = null;
view.revealed = null;
view.spanMarkId = null;
profile.markedId = null;
profile.pill = null;
profile.line = null;
panel = panelModule.createB06StructuresPanel({
projectId: PID,
shared: { view: () => "cross" },
detail: () => ({ cross_sections: CARDS }),
stationInterval: () => 20,
focusChainage: (at, revealOnly) => {
if (!wallSelecting) focusStationAt(at, revealOnly);
},
markStructure: (id) => view.selectMark(id),
onMarks: (next, types) => {
marks = next;
markTypes = types;
},
});
await panel.load();
}
/** 지금 상태 한 줄 — 포장층 = 고른 노면공 범위 안 카드 수(포장층 is-active 와 같은 판정). */
function snap() {
const mark = marks.find((entry) => entry.structure_id === view.spanMarkId);
const pave = mark && view.isPavement(mark.structure_id);
const inSpan = (at) =>
at >= Math.min(mark.start_m, mark.end_m) - 0.01 &&
at <= Math.max(mark.start_m, mark.end_m) + 0.01;
const card = CARDS.find((entry) => entry.station_id === view.selected);
return {
form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`),
pill: profile.pill,
paveLayer: pave ? mark.structure_id : null,
paveCards: pave ? CARDS.filter((entry) => inSpan(entry.chainage_m)).length : 0,
card: card ? card.chainage_m : null,
sid: selection.readSelection(PID)?.structureId ?? null,
line: profile.line,
};
}
/** 좌측 목록 / 종단 알약 = 폼에 그 구조물(B05 `loadForm` · SVP `edit.selectStructure`). */
const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id));
const pillPick = (id) => {
profile.pill = id; // 종단 그림이 먼저 그 알약을 켬
panel.selectStructure(id);
};
const cardPick = (at) => view.selectStation(cardAt(at).station_id);
/** 카드 안 노면공 형상 누름 — Section_View 가 카드 고름 전에 가로채 design_view_cross 가 목록 길로(189-1). */
const shapePick = (id) => panel.selectStructure(id);
/** 벽 누름 — SPS `revetOffset.select` 감싸개(소유 측점 폼) 뒤 카드 고름(벽 고르는 턴). */
const wallPick = (cardAtM, ownerM) => {
wallOwner = { card: cardAtM, chainage_m: ownerM };
wallSelecting = true;
selection.selectDesign(PID, panel.selectionAt(ownerM), { partKey: "inlet", from: "b06" });
panel.showAtChainage(ownerM);
if (view.selected !== cardAt(cardAtM).station_id) cardPick(cardAtM);
wallSelecting = false;
};
(async () => {
const out = {};
await build([PAVE, { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }]);
listPick("pave");
out.step1 = snap();
out.reveal1 = view.revealed; // 고르지 않고 스크롤만 한 카드(177-4)
out.pick1 = selection.readSelection(PID);
cardPick(40); // 스크롤만 한 범위 첫 카드 누름
out.step2first = snap();
cardPick(40); // 고른 범위 카드 재누름(167-3 · 브레인 ORCA ②)
out.step2re = snap();
cardPick(140);
out.step2 = snap();
cardPick(100);
out.step2b = snap();
wallPick(100, 80.17); // 고른 카드의 벽 = 카드 고름 리스너 안 돎
out.step3 = snap();
pillPick("pave");
out.step4 = snap();
out.reveal4 = view.revealed;
cardPick(80); // 관 자리 카드 = 관(지금처럼)
out.pipeCard = snap();
pillPick("pave");
cardPick(280); // 범위 밖 = 모두 풂
out.outside = snap();
cardPick(280); // 범위 밖 카드 재누름 = 카드 풂(옛 규칙 그대로)
out.outsideRe = snap();
// 189-1 세 갈래 — ① 목록 · ② 형상 · ③ 카드 하나 · 종단 측점선 · 세션
await build([PAVE, { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }]);
listPick("pave");
out.r1 = snap();
cardPick(140); // ③ 범위 안 카드 = 목록 고름 · 범위 강조 풂 · 그 카드 하나
out.r3 = snap();
shapePick("pave"); // ② 고른 카드 안 형상 = ① 과 같음(옛 카드 풂)
out.r2 = snap();
out.r2reveal = view.revealed;
cardPick(100); // ③ 다시
out.r3b = snap();
cardPick(100); // 같은 카드 재누름 = 풂(옛 167-3 「그대로」 없앰)
out.r3re = snap();
await build([PAVE]);
shapePick("pave"); // ② 처음부터 형상 누름
out.r2first = snap();
cardPick(80); // ③ 관 자리 카드 = 관
out.r3pipe = snap();
// 5. B05 알약 → B06 복귀 — 넘김값 {범위 첫 카드 · sid} · `restoreStructurePick` 의 focusStation 차례
await build([PAVE]);
selection.selectDesign(PID, { chainageM: 40, structureId: "pave" }, { from: "b05" });
panel.prefer("pave");
view.focusStation(cardAt(40).station_id);
panel.prefer(null);
panel.selectStructure("pave");
out.step5 = snap();
// 6. 같은 측점에 노면공 둘 — 목록 둘째 → 그 층만 · 범위 안 카드 눌러도 둘째 그대로
await build([PAVE, PAVE2]);
listPick("pave2");
out.step6 = snap();
out.reveal6 = view.revealed;
cardPick(160);
out.step6b = snap();
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
process.exit(1);
});