/* 231-1 노면공 범위 고름 — 「측점만 남은 고름」이 B05↔B06 오감 · 다시 세움에서 노면공 범위를 켜지 않는지. * 진짜 `createB06StructuresPanel` · `applyStructurePick` · `wireStructurePick` · `readStructurePick` 를 Node 로 돌리고 * 폼 · 카드 · 알약 흉내는 helper_165 와 같은 꼴(공용 폼 `selectAt` = B05 패널 본문 그대로). 표준출력 JSON 한 벌. */ const fs = require("fs"); const path = require("path"); const { makeLoader } = require("./helper_ts_stub_loader.cjs"); const CATALOG = require("./helper_322_catalog_module.cjs"); // 구조물 표(322-1) const ROOT = path.join(__dirname, "..", ".."); function loadTs(file, stubs = {}) { return makeLoader({ root: ROOT, cache: false, requireFor: (full) => (request) => { if (CATALOG.isCatalogRequest(request)) return CATALOG; if (stubs[request]) return stubs[request]; throw new Error(`stub 없음: ${request}`); }, }).loadTs(file); } global.requestAnimationFrame = (fn) => fn(); const store = new Map(); const selection = loadTs(path.join(ROOT, "A00_Common", "design", "design_selection.ts"), { "../../common_util/common_util_pipe_match": { PIPE_MATCH_M: 0.51 }, // 찾는 거리 정의 자리(472-6) "../b_page_state": { readState: (key, pid) => store.get(`${key}:${pid}`) ?? null, writeState: (key, value, pid) => store.set(`${key}:${pid}`, value), }, }); const pickSession = loadTs( path.join(ROOT, "B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts"), { "../A00_Common/design/design_selection": selection }, ); const structurePick = loadTs( path.join(ROOT, "B06_Section", "B06_Section_UI_Page_Structure_Pick.ts"), { "../A00_Common/design/design_selection": selection }, ); const PID = "p231"; const TYPES = [ { type_id: "pavement_concrete", group: "G" }, { type_id: "retaining_wall", group: "C", placement: "interval" }, // 실제 등록부 = C 구간형(측점 심음) { type_id: "hazard_sign", group: "E" }, { type_id: "pipe", group: "A", managed_by: "pipe_points" }, ]; const groupOf = (typeId) => TYPES.find((type) => type.type_id === typeId)?.group; const atStation = (entry) => selection.coversAtStation(groupOf(entry.type_id)); const PAVE = { structure_id: "pave", type_id: "pavement_concrete", placement: "interval", chainage_m: 30, start_m: 30, end_m: 250, }; const WALL = { structure_id: "wall", type_id: "retaining_wall", placement: "interval", chainage_m: 100, start_m: 100, end_m: 160, }; const SIGN = { structure_id: "sign", type_id: "hazard_sign", chainage_m: 127.86 }; const PIPES = [{ chainage_m: 80.17, facility: "pipe", source: "spacing", options: {} }]; 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 · selectAt 본문) ── */ 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); }, resolveAt: (sel, coverable) => selection.resolveSelection(sel, PIPES, structures, coverable), selectAt(sel, coverable) { const hit = selection.resolveSelection(sel, PIPES, structures, coverable); if (hit.pipeChainageM !== null) this.selectPipeByChainage(hit.pipeChainageM); else this.selectById(hit.structureId); return hit; }, }; const current = { structures: [] }; 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: [] }), }, // 페이지 자료 줄(386-2) — 저장분 한 번 "../A00_Common/design/design_page_data": { pageTypesChecked: async () => false, pageStructures: async () => ({ structures: [] }), pagePipes: 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": pickSession, "../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"), ), }, ), }, ); /* ── 종단 알약 · 횡단 카드 흉내(helper_165 와 같음) ── */ let marks = []; const profile = { markedId: null, pill: null, selectMark(id) { this.markedId = id; this.pill = id; }, showCard(card) { if (!card) return; const pipe = PIPES.find((entry) => Math.abs(entry.chainage_m - card.chainage_m) < 0.51); 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; }, }; const isPavement = (id) => { const mark = marks.find((entry) => entry.structure_id === id); return !!mark && groupOf(mark.type_id) === "G"; }; const view = { selected: null, spanMarkId: null, listener: null, selectStation(id) { 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); }, revealStation() { this.selected = null; profile.showCard(null); }, 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 || isPavement(id) ? id : null; profile.selectMark(id); }, }; 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); }; let panel = null; /** design_view_cross.ts 카드 고름 리스너 본문(벽 누름 없음). */ 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 preferred = panel.preferred() !== null; const opened = panel.showAtChainage(target.chainage_m); const picked = preferred ? opened : { chainageM: opened.chainageM, structureId: null }; 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); }; /** 새로 세움 — keep = 새로 고침(F5 · 세션은 남고 화면 · 폼은 처음부터). */ async function build(list, keep = false) { current.structures = list; if (!keep) store.clear(); form.id = null; form.pipe = null; view.selected = null; view.spanMarkId = null; profile.markedId = null; profile.pill = null; panel = panelModule.createB06StructuresPanel({ projectId: PID, shared: { view: () => "cross", lane: { set() {}, setTypes() {}, setStored() {} } }, // 시설 줄(462) detail: () => ({ cross_sections: CARDS }), stationInterval: () => 20, focusChainage: (at, revealOnly) => focusStationAt(at, revealOnly), markStructure: (id, span) => view.selectMark(id, span), onMarks: (next) => { marks = next; }, }); await panel.load(); } /** design_view_cross.ts `restoreStructurePick` 의 focusStation 차례 그대로. */ function restore() { const handoff = pickSession.readStructurePick(PID); structurePick.applyStructurePick( handoff, { cross_sections: CARDS }, { focusStation: (stationId) => { panel.prefer(handoff?.structureId ?? null); view.focusStation(stationId); panel.prefer(null); const sid = handoff?.structureId; if (sid && structures.some((entry) => entry.structure_id === sid)) panel.selectStructure(sid); }, refreshCard() {}, revetSelect() {}, fordSelect() {}, boxSelect() {}, }, structures, ); } /** B05 로 갔다 B06 으로 돌아옴 — B05 `restorePick`(폼 비었을 때만 · id 길) → B06 onShow(load → 되얹기). */ async function roundTrip() { const pick = pickSession.readStructurePick(PID); if (!section.hasSelection() && pick?.structureId) section.selectAt({ chainageM: pick.at, structureId: pick.structureId }); await panel.load(); restore(); } /** 새로 고침(F5) — 첫 마운트 `load().then(restoreStructurePick)` 차례(231-2). */ async function refresh() { await build(current.structures, true); restore(); } function snap() { const mark = marks.find((entry) => entry.structure_id === view.spanMarkId); const pave = mark && isPavement(mark.structure_id); const card = CARDS.find((entry) => entry.station_id === view.selected); return { form: form.id ?? (form.pipe === null ? null : `pipe-${form.pipe.toFixed(2)}`), paveLayer: pave ? mark.structure_id : null, span: view.spanMarkId, card: card ? card.chainage_m : null, sid: selection.readSelection(PID)?.structureId ?? null, }; } const cardPick = (at) => view.selectStation(cardAt(at).station_id); const listPick = (id) => loadForm(structures.find((entry) => entry.structure_id === id)); /** 좌측 목록 관 줄 — B05 패널 `onSelectPipe`(loadPipeForm → onPipeSelect). */ const listPipe = (at) => { loadPipeForm(at); binding.onPipeSelect(at); }; /** B05 3D 부재 고름 — 진짜 `wireStructurePick` 에 B05 와 같은 거름(design_view_profile). */ function pick3d(chainageM) { const controls = {}; pickSession.wireStructurePick(controls, PID, section, () => undefined, atStation); controls.onPick({ chainageM, kind: "revet", key: "own" }); } (async () => { const out = {}; // A. 노면공 범위 안 빈 카드 → 오감 await build([PAVE, SIGN]); cardPick(120); out.cardBefore = snap(); await roundTrip(); out.cardAfter = snap(); await roundTrip(); out.cardAfter2 = snap(); // B. 같은 카드에서 목록 표지 → 오감(세션 = 측점만 그대로) await build([PAVE, SIGN]); cardPick(120); listPick("sign"); out.signPick = snap(); await roundTrip(); out.signAfter = snap(); // C. 목록 노면공 → 오감 = 그대로 범위 강조(id 길) await build([PAVE, SIGN]); listPick("pave"); out.paveBefore = snap(); await roundTrip(); out.paveAfter = snap(); // D. 옹벽 범위 안 카드 = 그 카드 그대로(기준 카드 100 으로 안 옮김) · 오감 뒤도 await build([PAVE, WALL]); cardPick(120); out.wallCard = snap(); await roundTrip(); out.wallAfter = snap(); listPick("wall"); // 목록 고름은 기준 카드로 감(그대로) out.wallList = snap(); // E. 관 80.17 넘김(측점만) = 카드 80.17(이웃 80 아님) await build([PAVE]); selection.selectDesign(PID, { chainageM: 80.17, structureId: null }, { from: "b05" }); restore(); out.pipeHandoff = snap(); // F. B05 3D 부재 고름 — 노면공 범위 안이라도 노면공 id 를 적지 않음 await build([PAVE, WALL]); pick3d(120); out.pick3dWall = { sid: snap().sid, form: snap().form }; await build([PAVE]); pick3d(200); out.pick3dBare = { sid: snap().sid, form: snap().form }; await roundTrip(); out.pick3dAfter = snap(); // 231-2 목록 고름도 늘 고름 기록 — 카드 120 → 목록 관 · 표지 · 옹벽 → 오감 · 새로 고침 await build([PAVE, WALL, SIGN]); cardPick(120); listPipe(80.17); out.listPipe = snap(); await roundTrip(); out.listPipeTrip = snap(); await refresh(); out.listPipeF5 = snap(); await build([PAVE, SIGN]); cardPick(120); listPick("sign"); // 같은 카드 120 — 카드 리스너 안 돎 out.listSign = snap(); await roundTrip(); out.listSignTrip = snap(); await refresh(); out.listSignF5 = snap(); await build([PAVE, WALL]); cardPick(120); listPick("wall"); out.listWall = snap(); await roundTrip(); out.listWallTrip = snap(); await build([PAVE, SIGN]); cardPick(120); listPick("pave"); await refresh(); out.listPaveF5 = snap(); process.stdout.write(JSON.stringify(out)); })().catch((error) => { process.stderr.write(String(error?.stack ?? error)); process.exit(1); });