From 42673505e7b2781fc54e84b27d3418db6a69b694 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 10 Oct 2026 17:49:12 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=EC=B8=A1=EC=A0=90=EB=A7=8C=20?= =?UTF-8?q?=EB=82=A8=EC=9D=80=20=EA=B3=A0=EB=A6=84=EC=9D=98=20=EB=85=B8?= =?UTF-8?q?=EB=A9=B4=EA=B3=B5=20=EB=B2=94=EC=9C=84=20=EA=B0=95=EC=A1=B0=20?= =?UTF-8?q?=EB=A7=89=EC=9D=8C=20=C2=B7=20B05=203D=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=EA=B5=AC=EB=A9=8D=20=C2=B7=20=EC=B9=B4=EB=93=9C=20=EC=98=AE?= =?UTF-8?q?=EA=B2=A8=20=EA=B0=90=20=C2=B7=20=EA=B4=80=20=EB=84=98=EA=B9=80?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C(231-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B06 폼 다시 세움(load) 의 selectAt 에 노면공 거름(coverable) · 측점 고름이 연 폼은 카드 그대로 · 범위 테두리 없음 - B05 3D 부재 고름 resolveAt · selectAt 에 같은 거름 — 노면공 id 를 고름 한 칸에 안 적음 - 넘김 되얹기 카드 = 찾는 거리 안 가장 가까운 카드(관 80.17 이 카드 80 으로 안 감) - 새 시험 test_231_1 · test_106 배선 문자열 맞춤 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- A00_Common/design/design_view_cross.ts | 2 +- A00_Common/design/design_view_profile.ts | 9 +- .../B05_Profile_UI_Structure_Pick_Session.ts | 7 +- .../B05_Profile_UI_Structures_Panel.ts | 3 +- .../B05_Profile_UI_Structures_Panel_Types.ts | 7 +- .../B06_Section_UI_Page_Structure_Pick.ts | 7 +- .../B06_Section_UI_Page_Structures_Panel.ts | 32 +- resources/tester/helper_231_1_pave_reload.cjs | 385 ++++++++++++++++++ .../tester/test_106_span_highlight_wording.py | 5 +- resources/tester/test_231_1_pave_reload.py | 115 ++++++ 10 files changed, 552 insertions(+), 20 deletions(-) create mode 100644 resources/tester/helper_231_1_pave_reload.cjs create mode 100644 resources/tester/test_231_1_pave_reload.py diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index 21bb3380c..985c16f40 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -191,7 +191,7 @@ export async function mountCrossView( if (!isWallSelecting()) focusStationAt(chainageM, revealOnly); }, releaseCard: () => releaseCross(), - markStructure: (structureId) => sectionView.selectMark(structureId), + markStructure: (structureId, span) => sectionView.selectMark(structureId, span), applyPipeOptions: (chainageM, patch) => applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch), }); diff --git a/A00_Common/design/design_view_profile.ts b/A00_Common/design/design_view_profile.ts index 9a1fe7824..013ccdf01 100644 --- a/A00_Common/design/design_view_profile.ts +++ b/A00_Common/design/design_view_profile.ts @@ -356,7 +356,14 @@ export async function mountProfileView( shared, ); - wireStructurePick(viewer.structurePick, activeProjectId, panel.structures, selectStationOfPipe); + // 3D 부재 고름도 측점 고름과 같은 거름 — 노면공 id 를 고름 한 칸에 적지 않음(231-1) + wireStructurePick( + viewer.structurePick, + activeProjectId, + panel.structures, + selectStationOfPipe, + (entry) => coversAtStation(bridge.typeGroup(entry.type_id)), + ); /** 입력·마커 변경 시 측점 라인만 다시 그린다. 지형 구분·등급·기울기 세부 입력은 종단 그래프의 * 위반 판정 기준까지 **즉시** 갈아 끼운다(2026-08-19 사용자 지시 6). */ diff --git a/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts b/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts index 585175a53..84ce34729 100644 --- a/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts +++ b/B05_Profile/B05_Profile_UI_Structure_Pick_Session.ts @@ -17,6 +17,7 @@ import { } from "../A00_Common/design/design_selection"; import type { StructurePick, StructurePickControls } from "./B05_Profile_UI_Viewer_Structure_Pick"; import type { StructuresSection } from "./B05_Profile_UI_Structures_Panel_Types"; +import type { StructureInstance } from "./B05_Profile_Api_Structures"; /** B06으로 넘기는 선택 — B06이 쓰는 `{ 측점, 부재키 }` 한 쌍에 구조물 id 가 붙는다. */ export interface StructurePickHandoff { @@ -53,6 +54,8 @@ export function wireStructurePick( structures: StructuresSection, /** 기존 선택 동기화(`selectStationOfPipe`) — 누가거리 하나로 전 화면을 맞춘다. */ selectStation: (chainageM: number | null) => void, + /** 측점이 덮어 열어도 되는 구조물 — 노면공은 뺌(측점선 · 카드 고름과 같은 거름 · 231-1). */ + coverable?: (entry: StructureInstance) => boolean, ): void { controls.onPick = (pick: StructurePick | null): void => { if (!pick) { @@ -61,10 +64,10 @@ export function wireStructurePick( return; } const selection: DesignSelection = { chainageM: pick.chainageM, structureId: null }; - const { structureId } = structures.resolveAt(selection); + const { structureId } = structures.resolveAt(selection, coverable); selectDesign(projectId, { ...selection, structureId }, { partKey: pick.key, from: "b05" }); selectStation(pick.chainageM); - if (!structures.hasSelection()) structures.selectAt({ ...selection, structureId }); + if (!structures.hasSelection()) structures.selectAt({ ...selection, structureId }, coverable); }; } diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 2b7e97e5e..526d27113 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -616,7 +616,8 @@ export function createStructuresSection( if (!pipe) return; loadPipeForm(pipe); }, - resolveAt: (selection) => resolveSelection(selection, pipeFacilities, structures), + resolveAt: (selection, coverable) => + resolveSelection(selection, pipeFacilities, structures, coverable), selectAt(selection, coverable) { const hit = resolveSelection(selection, pipeFacilities, structures, coverable); if (hit.pipeChainageM !== null) this.selectPipeByChainage(hit.pipeChainageM); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts index 589623338..676d199a2 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Types.ts @@ -171,8 +171,11 @@ export interface StructuresSection { hasSelection: () => boolean; /** 종단도·3D에서 고른 구조물을 폼에 올린다. null이면 선택 해제. */ selectById: (structureId: string | null) => void; - /** 측점 · 구조물 고름 한 값을 폼이 열 항목으로 푼다 — 열지는 않는다(관 계열이 먼저). */ - resolveAt: (selection: DesignSelection) => ResolvedSelection; + /** 측점 · 구조물 고름 한 값을 폼이 열 항목으로 푼다 — 열지는 않는다(관 계열이 먼저). `coverable` = selectAt 과 같음. */ + resolveAt: ( + selection: DesignSelection, + coverable?: (entry: StructureInstance) => boolean, + ) => ResolvedSelection; /** 고름 한 값으로 폼을 연다(관 → 구조물 순) — 어느 화면에서 골라도 같은 거리 · 같은 순서다. * 고를 항목이 없으면 폼을 해제한다. 푼 결과를 돌려준다. `coverable` = 덮어 열어도 되는 구조물(측점 고름 · 216-1). */ selectAt: ( diff --git a/B06_Section/B06_Section_UI_Page_Structure_Pick.ts b/B06_Section/B06_Section_UI_Page_Structure_Pick.ts index 4ec8b2bb4..28a495bf7 100644 --- a/B06_Section/B06_Section_UI_Page_Structure_Pick.ts +++ b/B06_Section/B06_Section_UI_Page_Structure_Pick.ts @@ -19,7 +19,7 @@ import type { RevetPick } from "./B06_Section_UI_Cross_Culvert"; import type { StructurePickHandoff } from "../B05_Profile/B05_Profile_UI_Structure_Pick_Session"; import { coversChainage, - isNear, + nearestWithin, type SelectableStructure, } from "../A00_Common/design/design_selection"; @@ -57,7 +57,8 @@ export function applyStructurePick( const covering = structures.find((entry) => coversChainage(entry, handoff.at)); const target = (byId && cardOfStructure(sections, byId, handoff.at)) || - sections.find((section) => isNear(section.chainage_m, handoff.at)) || + // 찾는 거리 안 가장 가까운 카드 — 첫 카드면 관 80.17 이 이웃 측점 80 카드로 감(231-1) + nearestWithin(sections, handoff.at, (section) => section.chainage_m) || (covering && cardOfStructure(sections, covering, handoff.at)); if (!target) return false; // 조정창은 앞칸(측 이름)만 본다 — 뒤칸은 3D 강조를 부재 하나로 좁히는 몫이다. @@ -83,7 +84,7 @@ export function applyStructurePick( type Sections = SectionDetailResponse["cross_sections"]; /** 구조물이 자리한 카드 — 구간형이면 그 안의 첫 카드, 점형이면 가장 가까운 카드. 옹벽 · 기슭막이는 - * 횡단 제원이라 시작 측점 카드가 주인이다(관은 `isNear` 로 카드를 바로 찾는다). */ + * 횡단 제원이라 시작 측점 카드가 주인이다(관은 `nearestWithin` 으로 카드를 바로 찾는다). */ function cardOfStructure( sections: Sections, structure: SelectableStructure, diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index e3c76208c..6fdb971ce 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -89,8 +89,8 @@ export interface B06StructuresPanelDeps { focusChainage: (chainageM: number, revealOnly?: boolean) => void; /** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */ releaseCard?: () => void; - /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름). */ - markStructure?: (structureId: string | null) => void; + /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */ + markStructure?: (structureId: string | null, span?: boolean) => void; /** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */ onMarks?: (structures: StructureInstance[], types: StructureType[]) => void; /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — @@ -167,6 +167,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc coversAtStation(markTypes.find((type) => type.type_id === entry.type_id)?.group); /** 다음 카드 고름이 열 구조물 — 같은 측점에 관이 있어도 누른 것이 먼저다(52-15). */ let preferredId: string | null = null; + /** 측점 고름(카드 하나 · 측점만 남은 고름 다시 세움)이 폼을 여는 중 — 폼 알림이 카드를 옮기지 않게(231-1). */ + let openingAtStation = false; /** 그 구조물로 갈 카드 측점 — 구간형은 범위 안 카드 중 기준점에 가까운 것(경계 기준점이 범위 밖 카드로 안 감 · 165-2). */ const focusOf = (structure: StructureInstance): number => { const anchor = structureAnchorM(structure); @@ -220,10 +222,11 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc const pave = !coverable(structure); const id = structure.structure_id ?? null; preferredId = id; - deps.focusChainage(focusOf(structure), pave); + // 측점 고름(카드 하나 · 다시 세움)이 연 폼 = 누른 카드 그대로 — 구조물 기준 카드로 옮겨 가지 않음(189-1 · 231-1) + if (!openingAtStation) deps.focusChainage(focusOf(structure), pave); preferredId = null; // 알약은 카드 뒤에 — 카드 측점이 알약을 갈아 끼우거나 끄지 않게(53-2) - deps.markStructure?.(id); + deps.markStructure?.(id, !openingAtStation); // 측점 고름이 연 것 = 카드 하나 · 범위 테두리 없음(231-1) // 카드 고름 리스너를 안 거치니 세션 고름은 여기서(B05 로 가도 그 노면공) const at = { chainageM: focusOf(structure), structureId: id }; if (pave) selectDesign(deps.projectId, at, { from: "b06" }); @@ -406,11 +409,15 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc const pick = readStructurePick(deps.projectId); const picked = currentChainageM ?? pick?.at ?? null; // 관이면 그 관, 옹벽 · 기슭막이 같은 구조물이면 저장된 구조물 id 로 폼을 세운다. + // 측점만 남은 고름은 노면공을 덮어 열지 않음 — 카드 고름과 같은 거름(152-5 · 231-1) if (picked !== null) { - section.selectAt({ - chainageM: picked, - structureId: currentChainageM === null ? (pick?.structureId ?? null) : null, - }); + const structureId = currentChainageM === null ? (pick?.structureId ?? null) : null; + openingAtStation = structureId === null; + try { + section.selectAt({ chainageM: picked, structureId }, coverable); + } finally { + openingAtStation = false; + } } } catch (error) { showToast( @@ -567,7 +574,14 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc section.selectPipeByChainage(hit.pipeChainageM); // 알약은 폼 뒤 — 폼이 연 구조물을 닫으며 알약을 끄므로(165-2 · 측점선 없는 물넘이포장도 53-2) deps.markStructure?.(`pipe-${hit.pipeChainageM.toFixed(2)}`); - } else section.selectById(hit.structureId); + } else { + openingAtStation = preferredId === null; + try { + section.selectById(hit.structureId); + } finally { + openingAtStation = false; + } + } return { chainageM, structureId: hit.structureId }; }, }; diff --git a/resources/tester/helper_231_1_pave_reload.cjs b/resources/tester/helper_231_1_pave_reload.cjs new file mode 100644 index 000000000..3319d3183 --- /dev/null +++ b/resources/tester/helper_231_1_pave_reload.cjs @@ -0,0 +1,385 @@ +/* 231-1 노면공 범위 고름 — 「측점만 남은 고름」이 B05↔B06 오감 · 다시 세움에서 노면공 범위를 켜지 않는지. + * 진짜 `createB06StructuresPanel` · `applyStructurePick` · `wireStructurePick` · `readStructurePick` 를 Node 로 돌리고 + * 폼 · 카드 · 알약 흉내는 helper_165 와 같은 꼴(공용 폼 `selectAt` = B05 패널 본문 그대로). 표준출력 JSON 한 벌. + */ +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; +} + +global.requestAnimationFrame = (fn) => fn(); + +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 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" }, + { 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() {}, + }, + "../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": pickSession, + "../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 ?? {} }, + }, +); + +/* ── 종단 알약 · 횡단 카드 흉내(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); +}; + +async function build(list) { + current.structures = list; + 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" }, + 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(); +} + +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 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(); + process.stdout.write(JSON.stringify(out)); +})().catch((error) => { + process.stderr.write(String(error?.stack ?? error)); + process.exit(1); +}); diff --git a/resources/tester/test_106_span_highlight_wording.py b/resources/tester/test_106_span_highlight_wording.py index e0b5f9c97..c3275bb7a 100644 --- a/resources/tester/test_106_span_highlight_wording.py +++ b/resources/tester/test_106_span_highlight_wording.py @@ -40,7 +40,10 @@ def test_측점_고름은_카드_하나_목록_고름은_여럿(): """137-4 — 측점 고름 리스너는 span 끔 · 목록(노면공) 길 · 알약은 기본(여럿) 그대로.""" cross = _read("A00_Common", "design", "design_view_cross.ts") assert "sectionView.selectMark(mark, false);" in cross # 152-5 덮는 것 없으면 null = 풂 - assert "markStructure: (structureId) => sectionView.selectMark(structureId)," in cross + # 목록 길 = 기본(여럿) · 측점 고름이 연 폼만 span 끔(231-1) + assert ( + "markStructure: (structureId, span) => sectionView.selectMark(structureId, span)," in cross + ) types = _read("B06_Section", "B06_Section_UI_Section_View_Types.ts") assert "selectMark: (structureId: string | null, span?: boolean) => void;" in types diff --git a/resources/tester/test_231_1_pave_reload.py b/resources/tester/test_231_1_pave_reload.py new file mode 100644 index 000000000..7363f9b3e --- /dev/null +++ b/resources/tester/test_231_1_pave_reload.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""231-1 노면공 범위 고름 — 측점만 남은 고름은 노면공 범위 강조를 켜지 않음(조사 AD 1안 + 같은 구멍). + +까닭: B06 폼 다시 세움(`load()` — B05↔B06 오감 · 새로 고침)이 측점만 남은 고름을 거름 없이 `selectAt` 으로 +열어 「그 측점을 덮는 첫 구조물」= 노면공이 열리고 범위 강조 · 고름 기록까지 노면공으로 바뀜. +B05 3D 부재 고름(`wireStructurePick`)도 거름 없이 노면공 id 를 고름 한 칸에 적었음. +고침: 측점 고름이 여는 모든 길에 `coversAtStation`(노면공 뺌 · 152-5) 한 벌 · 노면공은 목록 · 알약 · 형상 누름(id)만. +곁: 측점 고름이 연 폼은 카드를 구조물 기준 카드로 옮기지 않음 · 넘김 카드 = 찾는 거리 안 가장 가까운 카드. +진짜 TS 를 `helper_231_1_pave_reload.cjs` 가 Node 로 돌린다. +""" + +from __future__ import annotations + +import json +import os +import subprocess + +import pytest + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) + +pytestmark = pytest.mark.skipif( + not os.path.isdir(os.path.join(ROOT, "config", "node_modules")), + reason="프론트엔드 의존성 미설치", +) + + +def _read(*parts: str) -> str: + with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle: + return handle.read() + + +@pytest.fixture(scope="module") +def out() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_231_1_pave_reload.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +CARD_120 = {"form": None, "paveLayer": None, "span": None, "card": 120, "sid": None} +PAVE = {"form": "pave", "paveLayer": "pave", "span": "pave", "card": None, "sid": "pave"} + + +def test_범위_안_카드_고름_오감해도_노면공_안_켬_폼_그대로(out) -> None: + assert out["cardBefore"] == CARD_120 + assert out["cardAfter"] == CARD_120 + assert out["cardAfter2"] == CARD_120 # 두 번 오가도 + + +def test_같은_카드_목록_고름_뒤_오감도_노면공_안_켬(out) -> None: + assert out["signPick"]["form"] == "sign" + after = out["signAfter"] + assert after["form"] != "pave" + assert after["paveLayer"] is None + assert after["span"] is None + assert after["card"] == 120 + + +def test_목록_노면공은_오감해도_범위_강조_그대로(out) -> None: + assert out["paveBefore"] == PAVE + assert out["paveAfter"] == PAVE + + +def test_옹벽_범위_안_카드는_그_카드_그대로(out) -> None: + wall = {"form": "wall", "paveLayer": None, "span": None, "card": 120, "sid": None} + assert out["wallCard"] == wall # 기준 카드 100 으로 안 옮김 + assert out["wallAfter"] == wall # 오감 뒤도 · 범위 테두리 없음(137-4) + # 목록 고름은 그대로 기준 카드 · 범위 테두리 · id 기록 + assert out["wallList"] == { + "form": "wall", + "paveLayer": None, + "span": "wall", + "card": 100, + "sid": "wall", + } + + +def test_관_넘김은_가장_가까운_카드(out) -> None: + assert out["pipeHandoff"]["card"] == 80.17 # 이웃 측점 80 아님 + assert out["pipeHandoff"]["form"] == "pipe-80.17" + + +def test_B05_3D_고름은_노면공_id_를_안_적음(out) -> None: + assert out["pick3dWall"] == {"sid": "wall", "form": "wall"} # 범위 안 옹벽 = 옹벽 + assert out["pick3dBare"] == {"sid": None, "form": None} # 노면공만 덮음 = 안 엶 + after = out["pick3dAfter"] + assert after["paveLayer"] is None + assert after["sid"] is None + assert after["form"] is None + + +def test_배선() -> None: + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + assert "section.selectAt({ chainageM: picked, structureId }, coverable);" in panel + assert "if (!openingAtStation) deps.focusChainage(focusOf(structure), pave);" in panel + assert "deps.markStructure?.(id, !openingAtStation);" in panel + cross = _read("A00_Common", "design", "design_view_cross.ts") + assert ( + "markStructure: (structureId, span) => sectionView.selectMark(structureId, span)," in cross + ) + pick = _read("B05_Profile", "B05_Profile_UI_Structure_Pick_Session.ts") + assert "structures.resolveAt(selection, coverable)" in pick + assert "structures.selectAt({ ...selection, structureId }, coverable)" in pick + profile = _read("A00_Common", "design", "design_view_profile.ts") + at = profile.index("wireStructurePick(") + assert "coversAtStation(bridge.typeGroup(entry.type_id))" in profile[at : at + 300] + handoff = _read("B06_Section", "B06_Section_UI_Page_Structure_Pick.ts") + assert "nearestWithin(sections, handoff.at, (section) => section.chainage_m)" in handoff