From a6e7a14217ea8a25eee81b5283d447bfa8464f2b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 19:01:18 +0900 Subject: [PATCH] =?UTF-8?q?refactor(b06):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=ED=8C=A8=EB=84=90=20=EC=84=A0=ED=83=9D=20=EB=B0=B0=EC=84=A0=20?= =?UTF-8?q?=EB=96=BC=EA=B8=B0=20=E2=80=94=20wireStructureSelection=20?= =?UTF-8?q?=EC=83=88=20=ED=8C=8C=EC=9D=BC(152-1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B06_Section_UI_Page_Structures_Panel.ts 699 → 540줄 · 동작 그대로 - 선택 배선 = B06_Section_UI_Page_Structures_Selection.ts - 문자열 시험 3개 새 파일 읽기로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P --- A00_Common/design/design_view_cross.ts | 4 +- .../B06_Section_UI_Page_Structures_Panel.ts | 159 ----------------- ...06_Section_UI_Page_Structures_Selection.ts | 168 ++++++++++++++++++ resources/tester/test_118_pipe_pick.py | 2 +- .../tester/test_54_29_pick_same_panel.py | 2 +- .../tester/test_b05_b06_selection_single.py | 5 +- 6 files changed, 176 insertions(+), 164 deletions(-) create mode 100644 B06_Section/B06_Section_UI_Page_Structures_Selection.ts diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index d84a5f93b..37bcf7a04 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -53,11 +53,11 @@ import { rememberRockBoundaryDefault, type StandardPanelController, } from "../../B06_Section/B06_Section_UI_Standard_Panel"; +import { createB06StructuresPanel } from "../../B06_Section/B06_Section_UI_Page_Structures_Panel"; import { - createB06StructuresPanel, isWallSelecting, wireStructureSelection, -} from "../../B06_Section/B06_Section_UI_Page_Structures_Panel"; +} from "../../B06_Section/B06_Section_UI_Page_Structures_Selection"; import "../../B06_Section/B06_Section_UI_Style.css"; import "../../B06_Section/B06_Section_UI_Style_Cross.css"; import "../../B06_Section/B06_Section_UI_Style_Cross_Controls.css"; diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 73332a1c9..f5901eb1d 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -54,8 +54,6 @@ import { setAdjustSideSlots, } from "./B06_Section_UI_Adjust_Dock"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; -import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; -import type { RevetPick } from "./B06_Section_UI_Cross_Culvert"; import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec"; const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요."; @@ -540,160 +538,3 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc }, }; } - -/** - * 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그 - * 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계). - * 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다. - */ -/** 지금 처리 중인 선택이 **도면에서 벽·구체를 찍은 것**인가 — 카드 선택 리스너가 - * 이 값으로 "카드만 고른 경우"를 가려낸다(2026-08-29 사용자: 측점 선택 때 구조물이 - * 자동으로 골라지면 안 된다). 같은 턴에서만 참이다. */ -let wallSelecting = false; - -export function isWallSelecting(): boolean { - return wallSelecting; -} - -function markWallSelecting(): void { - wallSelecting = true; - queueMicrotask(() => { - wallSelecting = false; - }); -} - -export function wireStructureSelection( - stationControls: StationControls, - detail: () => SectionDetailResponse | null, - panel: B06StructuresPanel, - /** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가 - * 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */ - remember?: (chainageM: number, key: string) => void, - /** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */ - refreshCard?: (chainageM: number) => void, - /** 폼에 올린 시설로 보기를 옮김 — 목록에서 고른 길(54-47). */ - reveal?: (chainageM: number) => void, -): { syncInletStructure: () => void } { - const ownerChainageOf = (chainageM: number): number => { - const sectionAt = detail()?.cross_sections.find( - (entry) => Math.abs(entry.chainage_m - chainageM) < 0.01, - ); - return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM; - }; - - // 유입구 "구조" ↔ B06 유입측 형식 — 폼에서 고르면 조정값에 반영하고, 시설을 폼에 - // 올릴 때는 조정값을 폼으로 되읽는다(2026-08-29 지시 5 병합). - const sectionAt = (chainageM: number) => - detail()?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01); - panel.facility.onInletStructureChange((value) => { - const chainageM = panel.currentChainage(); - if (chainageM === null) return; - stationControls.inletStructure.set(chainageM, value); - }); - /** 폼에 올라온 시설의 유입측 형식을 폼 드롭다운에 되싣는다. */ - const syncInletStructure = (): void => { - const chainageM = panel.currentChainage(); - const target = chainageM === null ? undefined : sectionAt(chainageM); - if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target)); - }; - - const origRevet = stationControls.revetOffset.select; - stationControls.revetOffset.select = (chainageM, key) => { - // 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1) - const nested = wallSelecting; - // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) - if (key) markWallSelecting(); - origRevet(chainageM, key); - if (key) remember?.(ownerChainageOf(chainageM), key); - if (key) panel.showAtChainage(ownerChainageOf(chainageM)); - else if (!nested) { - remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림 - panel.releasePipe(); - } - if (key) syncInletStructure(); - // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다. - refreshAdjustSlots(); - }; - - // 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30) - const keyOf = (box: HTMLElement): RevetPick | null => { - const id = box.dataset.specContainer; - if (id === "inlet" || id === "outlet" || id === "pipe") return id; - return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null; - }; - const pickBox = (box: HTMLElement): void => { - const chainageM = panel.currentChainage(); - const target = chainageM === null ? undefined : sectionAt(chainageM); - const key = keyOf(box); - if (!key || !target?.culvert || stationControls.revetOffset.highlightFor(target) === key) - return; - stationControls.revetOffset.select(target.chainage_m, key); - refreshCard?.(target.chainage_m); - }; - // 그 측점 카드 · 알약으로 감(목록에서 고른 길 · 54-47) — 같은 칸을 잇달아 고치면 한 번만(다시 그려도 칸 이름으로) - const fieldOf = (el: HTMLElement): string => { - const box = el.closest("fieldset[data-spec-container]"); - const item = el.closest("[data-spec-item]")?.dataset.specItem; - return `${box?.dataset.specContainer}:${box?.dataset.specIndex}:${item}`; - }; - let revealed: string | null = null; - const revealAt = (chainageM: number, field: string | null): void => { - if (field && field === revealed) return; - revealed = field; - reveal?.(chainageM); - }; - panel.root.addEventListener("focusin", (event) => { - if (fieldOf(event.target as HTMLElement) !== revealed) revealed = null; - }); - panel.root.addEventListener("change", ({ target: el }) => { - const box = (el as HTMLElement).closest("fieldset[data-spec-container]"); - const chainageM = panel.currentChainage(); - if (!box || chainageM === null) return; - revealAt(chainageM, fieldOf(el as HTMLElement)); - // 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지) — 새 단은 다시 그린 뒤 아래에서 고름 - if (!(el as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) pickBox(box); - }); - // 상자가 새로 서면(단 늘림 · [추가]) 그 상자를 고름 — 같은 시설을 다시 그린 때만 - let painted: { chainageM: number | null; keys: Set } | null = null; - panel.root.addEventListener("specpaint", () => { - const chainageM = panel.currentChainage(); - const boxes = [...panel.root.querySelectorAll("fieldset[data-spec-container]")]; - const before = painted?.chainageM === chainageM ? painted.keys : null; - painted = { chainageM, keys: new Set(boxes.map(fieldOf)) }; - const born = before ? boxes.filter((box) => !before.has(fieldOf(box))).at(-1) : undefined; - if (!born || chainageM === null) return; - revealAt(chainageM, null); - pickBox(born); - }); - - const origFord = stationControls.ford.select; - stationControls.ford.select = (chainageM, role) => { - const nested = wallSelecting; - if (role) markWallSelecting(); - origFord(chainageM, role); - if (role) remember?.(chainageM, role); - if (role) panel.showAtChainage(chainageM); - else if (!nested) { - remember?.(chainageM, ""); - panel.releasePipe(); - } - // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다 - // (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1). - refreshAdjustSlots(); - }; - const origBox = stationControls.box.select; - stationControls.box.select = (chainageM, role) => { - const nested = wallSelecting; - if (role) markWallSelecting(); - origBox(chainageM, role); - if (role) remember?.(chainageM, role); - if (role) panel.showAtChainage(chainageM); - else if (!nested) { - remember?.(chainageM, ""); - panel.releasePipe(); - } - // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다. - refreshAdjustSlots(); - }; - return { syncInletStructure }; -} diff --git a/B06_Section/B06_Section_UI_Page_Structures_Selection.ts b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts new file mode 100644 index 000000000..bfdb42d82 --- /dev/null +++ b/B06_Section/B06_Section_UI_Page_Structures_Selection.ts @@ -0,0 +1,168 @@ +/* ============================================================================= + * B06_Section_UI_Page_Structures_Selection.ts + * 횡단도 선택 → 좌측 폼 연동 배선(`wireStructureSelection`) — `_Page_Structures_Panel.ts` + * 에서 떼어 냄(700줄). 폼 · 목록 패널 자체는 그 파일 몫. + * ========================================================================== */ + +import { refreshAdjustSlots } from "./B06_Section_UI_Adjust_Dock"; +import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; +import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; +import type { RevetPick } from "./B06_Section_UI_Cross_Culvert"; +import type { B06StructuresPanel } from "./B06_Section_UI_Page_Structures_Panel"; + +/** + * 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그 + * 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계). + * 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다. + */ +/** 지금 처리 중인 선택이 **도면에서 벽·구체를 찍은 것**인가 — 카드 선택 리스너가 + * 이 값으로 "카드만 고른 경우"를 가려낸다(2026-08-29 사용자: 측점 선택 때 구조물이 + * 자동으로 골라지면 안 된다). 같은 턴에서만 참이다. */ +let wallSelecting = false; + +export function isWallSelecting(): boolean { + return wallSelecting; +} + +function markWallSelecting(): void { + wallSelecting = true; + queueMicrotask(() => { + wallSelecting = false; + }); +} + +export function wireStructureSelection( + stationControls: StationControls, + detail: () => SectionDetailResponse | null, + panel: B06StructuresPanel, + /** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가 + * 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */ + remember?: (chainageM: number, key: string) => void, + /** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */ + refreshCard?: (chainageM: number) => void, + /** 폼에 올린 시설로 보기를 옮김 — 목록에서 고른 길(54-47). */ + reveal?: (chainageM: number) => void, +): { syncInletStructure: () => void } { + const ownerChainageOf = (chainageM: number): number => { + const sectionAt = detail()?.cross_sections.find( + (entry) => Math.abs(entry.chainage_m - chainageM) < 0.01, + ); + return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM; + }; + + // 유입구 "구조" ↔ B06 유입측 형식 — 폼에서 고르면 조정값에 반영하고, 시설을 폼에 + // 올릴 때는 조정값을 폼으로 되읽는다(2026-08-29 지시 5 병합). + const sectionAt = (chainageM: number) => + detail()?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01); + panel.facility.onInletStructureChange((value) => { + const chainageM = panel.currentChainage(); + if (chainageM === null) return; + stationControls.inletStructure.set(chainageM, value); + }); + /** 폼에 올라온 시설의 유입측 형식을 폼 드롭다운에 되싣는다. */ + const syncInletStructure = (): void => { + const chainageM = panel.currentChainage(); + const target = chainageM === null ? undefined : sectionAt(chainageM); + if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target)); + }; + + const origRevet = stationControls.revetOffset.select; + stationControls.revetOffset.select = (chainageM, key) => { + // 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1) + const nested = wallSelecting; + // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) + if (key) markWallSelecting(); + origRevet(chainageM, key); + if (key) remember?.(ownerChainageOf(chainageM), key); + if (key) panel.showAtChainage(ownerChainageOf(chainageM)); + else if (!nested) { + remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림 + panel.releasePipe(); + } + if (key) syncInletStructure(); + // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다. + refreshAdjustSlots(); + }; + + // 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30) + const keyOf = (box: HTMLElement): RevetPick | null => { + const id = box.dataset.specContainer; + if (id === "inlet" || id === "outlet" || id === "pipe") return id; + return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null; + }; + const pickBox = (box: HTMLElement): void => { + const chainageM = panel.currentChainage(); + const target = chainageM === null ? undefined : sectionAt(chainageM); + const key = keyOf(box); + if (!key || !target?.culvert || stationControls.revetOffset.highlightFor(target) === key) + return; + stationControls.revetOffset.select(target.chainage_m, key); + refreshCard?.(target.chainage_m); + }; + // 그 측점 카드 · 알약으로 감(목록에서 고른 길 · 54-47) — 같은 칸을 잇달아 고치면 한 번만(다시 그려도 칸 이름으로) + const fieldOf = (el: HTMLElement): string => { + const box = el.closest("fieldset[data-spec-container]"); + const item = el.closest("[data-spec-item]")?.dataset.specItem; + return `${box?.dataset.specContainer}:${box?.dataset.specIndex}:${item}`; + }; + let revealed: string | null = null; + const revealAt = (chainageM: number, field: string | null): void => { + if (field && field === revealed) return; + revealed = field; + reveal?.(chainageM); + }; + panel.root.addEventListener("focusin", (event) => { + if (fieldOf(event.target as HTMLElement) !== revealed) revealed = null; + }); + panel.root.addEventListener("change", ({ target: el }) => { + const box = (el as HTMLElement).closest("fieldset[data-spec-container]"); + const chainageM = panel.currentChainage(); + if (!box || chainageM === null) return; + revealAt(chainageM, fieldOf(el as HTMLElement)); + // 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지) — 새 단은 다시 그린 뒤 아래에서 고름 + if (!(el as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) pickBox(box); + }); + // 상자가 새로 서면(단 늘림 · [추가]) 그 상자를 고름 — 같은 시설을 다시 그린 때만 + let painted: { chainageM: number | null; keys: Set } | null = null; + panel.root.addEventListener("specpaint", () => { + const chainageM = panel.currentChainage(); + const boxes = [...panel.root.querySelectorAll("fieldset[data-spec-container]")]; + const before = painted?.chainageM === chainageM ? painted.keys : null; + painted = { chainageM, keys: new Set(boxes.map(fieldOf)) }; + const born = before ? boxes.filter((box) => !before.has(fieldOf(box))).at(-1) : undefined; + if (!born || chainageM === null) return; + revealAt(chainageM, null); + pickBox(born); + }); + + const origFord = stationControls.ford.select; + stationControls.ford.select = (chainageM, role) => { + const nested = wallSelecting; + if (role) markWallSelecting(); + origFord(chainageM, role); + if (role) remember?.(chainageM, role); + if (role) panel.showAtChainage(chainageM); + else if (!nested) { + remember?.(chainageM, ""); + panel.releasePipe(); + } + // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다 + // (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1). + refreshAdjustSlots(); + }; + const origBox = stationControls.box.select; + stationControls.box.select = (chainageM, role) => { + const nested = wallSelecting; + if (role) markWallSelecting(); + origBox(chainageM, role); + if (role) remember?.(chainageM, role); + if (role) panel.showAtChainage(chainageM); + else if (!nested) { + remember?.(chainageM, ""); + panel.releasePipe(); + } + // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다. + refreshAdjustSlots(); + }; + return { syncInletStructure }; +} diff --git a/resources/tester/test_118_pipe_pick.py b/resources/tester/test_118_pipe_pick.py index 161d0e4f3..0bf896f9a 100644 --- a/resources/tester/test_118_pipe_pick.py +++ b/resources/tester/test_118_pipe_pick.py @@ -97,7 +97,7 @@ def test_좌측_관_상자_강조_스크롤() -> None: assert 'body.classList.add("is-active");' in dock assert 'if (scroll) body.scrollIntoView({ block: "nearest" });' in dock # 관 상자 칸을 고치면 관을 고름 — 횡단에서 누른 것과 같은 길 - page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts") + page = _src("B06_Section/B06_Section_UI_Page_Structures_Selection.ts") assert 'if (id === "inlet" || id === "outlet" || id === "pipe") return id;' in page css = _src("B06_Section/B06_Section_UI_Style_Cross_Areas.css") assert ".b06-chart__culvert-pipe.is-active {" in css diff --git a/resources/tester/test_54_29_pick_same_panel.py b/resources/tester/test_54_29_pick_same_panel.py index 00ffa046a..7622d62af 100644 --- a/resources/tester/test_54_29_pick_same_panel.py +++ b/resources/tester/test_54_29_pick_same_panel.py @@ -49,7 +49,7 @@ def test_폼을_다시_그리면_강조_다시() -> None: def test_상자_칸을_고치면_그_부분을_고름() -> None: """54-30 — 칸 고침 = 횡단에서 그 벽을 누른 것과 같은 길 · 단 수 칸은 고른 단을 지킴.""" - page = _src("B06_Section/B06_Section_UI_Page_Structures_Panel.ts") + page = _src("B06_Section/B06_Section_UI_Page_Structures_Selection.ts") assert '[data-spec-item="outlet.tiers"]' in page assert "`extra${Number(box.dataset.specIndex)}`" in page assert "refreshCard?.(target.chainage_m);" in page diff --git a/resources/tester/test_b05_b06_selection_single.py b/resources/tester/test_b05_b06_selection_single.py index 33b63cf3e..e095972c5 100644 --- a/resources/tester/test_b05_b06_selection_single.py +++ b/resources/tester/test_b05_b06_selection_single.py @@ -61,6 +61,7 @@ def test_거리_정의는_한_곳뿐이고_옛_0_05는_없다(): ("design_view_cross.ts", "A00_Common/design"), ("B05_Profile_UI_Selection.ts", "B05_Profile"), ("B06_Section_UI_Page_Structures_Panel.ts", "B06_Section"), + ("B06_Section_UI_Page_Structures_Selection.ts", "B06_Section"), ("B06_Section_UI_Page_Structure_Pick.ts", "B06_Section"), ("B05_Profile_UI_Structure_Pick_Session.ts", "B05_Profile"), ): @@ -187,7 +188,9 @@ def test_B05_되살리기_같은_측점_관보다_구조물_id가_먼저(out): # ── 배선 ─────────────────────────────────────────────────────────────── def test_횡단도_벽_구체_눌러도_폼은_같은_길_하나(): - panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + panel = _read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts") + _read( + "B06_Section", "B06_Section_UI_Page_Structures_Selection.ts" + ) assert "showPipeAt" not in panel # 관 전용 길을 없앰 assert panel.count("panel.showAtChainage(") == 3 # 기슭막이 · 세월교 · BOX assert panel.count("panel.releasePipe()") == 3 # 벽 선택을 풀 때는 관 폼만 비움