/* 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 CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) 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 (CATALOG.isCatalogRequest(request)) return CATALOG; 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() {}, dropStaleStructureTypes: async () => {}, // 정의 판 확인(322-0) staleDefinitionNotice: () => null, // 판 알림(322-2) }, "../B05_Profile/B05_Profile_Api_Pipes_Draft": { readPendingPipes: () => null, writePendingPipes() {}, }, "../B05_Profile/logic/B05_Profile_UI_Drainage_Facility_Merge": { mergeFacilityOptions: (x) => x, }, "../B05_Profile/B05_Profile_UI_Drainage_Ford_Fill": { fordWaitsOf: () => new Set(), waitsFordHeight: () => false, }, "../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() {} }, // 관 기준 측점 정수 맞춤(234-1) — 일반 측점 없으면 정수 m 만 "../common_util/common_util_structure_station": { snapStructureStation: (value) => Math.floor(value + 0.5), }, "./B06_Section_UI_Adjust_Dock": { adjustDockRoot: () => ({}), refreshAdjustSlots() {}, resetAdjustDock() {}, setAdjustSideSlots() {}, }, "./B06_Section_UI_Page_Structures_Spec": { withSpecDefaults: (o) => o ?? {} }, // 옛 소수 관 맞춤(322-3 고침) · 관 세션 id(323-5) — 프로젝트 없음 = 옛 자리 이름 "../B05_Profile/B05_Profile_UI_Structures_Snap": { snapNotice() {}, snapPlan: () => ({ structures: [], pipes: [], skipped: 0 }), }, "../A00_Common/design/design_pipe_ids": { pipeIdAt: (at) => `pipe-${at.toFixed(2)}`, followMovedPipe() {}, }, // 요구 178 — 알약 측점 짝(진짜 `plantsStation` · 272 측점 심기 한 벌) "../B05_Profile/B05_Profile_UI_Profile_Structures": loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_Profile_Structures.ts"), { "../A00_Common/touch_bridge": { pickPx: (px) => px }, // 기기대응 4-4 — 마우스 반경 그대로 "@ui/ui_template_context_menu": { createMapContextMenu() {} }, "./B05_Profile_UI_IrregularStations": { irregularStationId: (id) => `irregular:${id}`, isPipeStation: () => false, }, "./B05_Profile_Api_Structures": { structureAnchorM: (s) => s.chainage_m ?? s.start_m ?? 0, }, "../A00_Common/design/design_selection": selection, "../common_util/common_util_structure_station": loadTs( path.join(ROOT, "common_util", "common_util_structure_station.ts"), ), }, ), }, ); /* ── 종단 알약 흉내(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); });