diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index de00f3749..2e7cc7526 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -42,8 +42,8 @@ import { bindDrainageInteractions } from "./B05_Profile_UI_Drainage_Interact"; import type { RouteSpanBand } from "./B05_Profile_UI_Drainage_Spans"; import { drawDrainageScene } from "./B05_Profile_UI_Drainage_Render"; import { computeZoomOutLimit } from "./B05_Profile_UI_Drainage_ZoomRange"; +import { mountDrainageShow } from "./B05_Profile_UI_Drainage_Show"; import { - mountDrainageToggles, DRAINAGE_LAYERS, fetchDrainageLayers, createProgressReporter, @@ -159,21 +159,17 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra // 평균 흐름 화살표 — B04가 계산해 둔 것을 그대로 받아 그린다(여기서 계산하지 않는다). let flowArrows: FlowArrow[] = []; let arrowSpacingM = 0; - let showArrows = true; // 유입 집중점 — 관 자리를 판단하는 근거. 기본 꺼짐(마커가 관 마커와 겹쳐 읽기 어렵다). let hotspots: Array<{ chainage: number; area: number }> = []; let maxHotspotArea = 0; - let showHotspots = false; /** 종단 Z 출처 — 세부유역이 갈리는 근거라 화면에서 확인 가능해야 한다. */ let zSource = ""; // 유역 안쪽 상류 세류망 — B04가 채택한 기준선을 그대로 받아 강조만 한다. let upstreamLines: Array> = []; - let showUpstream = true; // 계획선 위 유입 강도 색칠 — B04 지도와 같은 색띠·같은 값(2026-08-01 사용자 지시). let strengthSamples: RoutePoint[] = []; let strength: Float64Array = new Float64Array(0); let maxStrength = 0; - let showStrength = true; let scale = 1; let offsetX = 0; let offsetY = 0; @@ -187,42 +183,12 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra let canvasHeight = 0; let canvasDpr = 0; - // 위성사진은 기본 꺼짐(2026-08-18 사용자 지시) — 어두운 사진이 유역 채움색을 묻는다. - backgroundImage.hidden = true; - mountDrainageToggles(layerButtons, { - initial: { - satellite: false, - arrows: showArrows, - strength: showStrength, - hotspots: showHotspots, - upstream: showUpstream, - }, - onSatellite: (next) => { - backgroundImage.hidden = !next; - scheduleDraw(); - }, - onSheetLayer: (layer, next) => { - if (next) activeLayers.add(layer); - else activeLayers.delete(layer); - scheduleDraw(); - }, - onArrows: (next) => { - showArrows = next; - scheduleDraw(); - }, - onStrength: (next) => { - showStrength = next; - legend.update(maxStrength, showStrength); - scheduleDraw(); - }, - onHotspots: (next) => { - showHotspots = next; - scheduleDraw(); - }, - onUpstream: (next) => { - showUpstream = next; - scheduleDraw(); - }, + // 표시 토글(위성 · 도엽 · 화살표 · 강도 · 집중점 · 상류) — 상태는 `_Drainage_Show`. + const show = mountDrainageShow(layerButtons, { + backgroundImage, + activeLayers, + onStrength: (next) => legend.update(maxStrength, next), + redraw: () => scheduleDraw(), }); function updateImageTransform(): void { @@ -260,17 +226,17 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra activeLayers, routeLayer, upstreamLines, - showUpstream, + showUpstream: show.upstream, strengthSamples, strength, maxStrength, - showStrength, + showStrength: show.strength, flowArrows, arrowSpacingM, - showArrows, + showArrows: show.arrows, hotspots, maxHotspotArea, - showHotspots, + showHotspots: show.hotspots, pipeEditor, pipeColor, markedChainage, @@ -352,7 +318,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra ); strength = built.strength; maxStrength = built.maximum; - legend.update(maxStrength, showStrength); + legend.update(maxStrength, show.strength); maxHotspotArea = hotspots.reduce((max, spot) => (spot.area > max ? spot.area : max), 0); // 실제 사용된 배관 목록으로 마커를 동기화한다(유역 없는 관 포함). pipeEditor.setPipes( diff --git a/B05_Profile/B05_Profile_UI_Drainage_Show.ts b/B05_Profile/B05_Profile_UI_Drainage_Show.ts new file mode 100644 index 000000000..68666ccaf --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Drainage_Show.ts @@ -0,0 +1,62 @@ +/* ============================================================================= + * B05_Profile_UI_Drainage_Show.ts + * 배수유역 판 표시 토글 상태 — 위성 · 도엽 · 화살표 · 강도 · 집중점 · 상류 세류 + * (`B05_Profile_UI_Drainage_Panel.ts` 에서 나눔 · 472-5 · 동작 같음). + * ========================================================================== */ + +import { mountDrainageToggles, type DrainageLayer } from "./B05_Profile_UI_Drainage_Parts"; + +/** 지금 켜진 표시 — 그리기가 읽음(토글이 고침). */ +export interface DrainageShow { + arrows: boolean; + /** 유입 강도 색칠 — B04 지도와 같은 색띠·같은 값(2026-08-01 사용자 지시). */ + strength: boolean; + /** 유입 집중점 — 기본 꺼짐(마커가 관 마커와 겹쳐 읽기 어렵다). */ + hotspots: boolean; + /** 유역 안쪽 상류 세류망 — B04 기준선 강조. */ + upstream: boolean; +} + +/** 토글 줄을 붙이고 상태를 돌려줌 — 위성사진은 기본 꺼짐(2026-08-18 사용자 지시). */ +export function mountDrainageShow( + layerButtons: HTMLElement, + deps: { + backgroundImage: HTMLImageElement; + activeLayers: Set; + onStrength: (show: boolean) => void; + redraw: () => void; + }, +): DrainageShow { + const show: DrainageShow = { arrows: true, strength: true, hotspots: false, upstream: true }; + deps.backgroundImage.hidden = true; + mountDrainageToggles(layerButtons, { + initial: { satellite: false, ...show }, + onSatellite: (next) => { + deps.backgroundImage.hidden = !next; + deps.redraw(); + }, + onSheetLayer: (layer, next) => { + if (next) deps.activeLayers.add(layer); + else deps.activeLayers.delete(layer); + deps.redraw(); + }, + onArrows: (next) => { + show.arrows = next; + deps.redraw(); + }, + onStrength: (next) => { + show.strength = next; + deps.onStrength(next); + deps.redraw(); + }, + onHotspots: (next) => { + show.hotspots = next; + deps.redraw(); + }, + onUpstream: (next) => { + show.upstream = next; + deps.redraw(); + }, + }); + return show; +} diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel.ts b/B05_Profile/B05_Profile_UI_Structures_Panel.ts index 436a16adc..207781293 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Panel.ts @@ -48,6 +48,7 @@ import { hiddenOptionKeys } from "./B05_Profile_UI_Structures_Define_Form"; import { stationLabel as formatStation } from "../common_util/common_util_station"; import { PIPE_MATCH_M, resolveSelection } from "../A00_Common/design/design_selection"; import { movedStructure, snapOldStations, snapStationFor } from "./B05_Profile_UI_Structures_Snap"; +import { followPipeList } from "./B05_Profile_UI_Structures_Panel_Pipes"; /** 공개 타입·표시 상수는 700줄 제한으로 `_Structures_Panel_Types` 로 옮겼다 — * 옛 임포트 경로가 그대로 동작하도록 여기서 다시 내보낸다(2026-09-04). */ @@ -573,36 +574,7 @@ export function createStructuresSection( getStructures: () => [...structures], setPipeFacilities(pipes) { pipeFacilities = pipes.map((pipe) => ({ ...pipe })); - if (tempPipeChainage !== null) { - // 스냅·재계산으로 좌표가 조금 옮겨진 임시 관을 계속 따라간다. - const hit = pipeFacilities.find( - (pipe) => Math.abs(pipe.chainage_m - tempPipeChainage!) < PIPE_MATCH_M, - ); - if (hit) { - tempPipeChainage = hit.chainage_m; - if (selectedPipeChainage !== null) selectedPipeChainage = hit.chainage_m; - // 재계산이 끝나 도착한 담당 유역 설계유량을 입력 중인 폼에 반영한다 — - // 물넘이·세월교 월류 높이가 이 값으로 계산된다(2026-08-18 진단: 재계산 - // 후 선택 통지가 조용히 끝나 폼이 유량을 못 받던 문제). - facilityOptions.setDesignFlow(hit.design_flow_m3s ?? null); - } else { - // 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다). - tempPipeChainage = null; - syncButtons(); - } - } - const kept = pipeFacilities.find( - (pipe) => Math.abs(pipe.chainage_m - (selectedPipeChainage ?? NaN)) < PIPE_MATCH_M, - ); - if (kept) - selectedPipeChainage = kept.chainage_m; // 조금 옮겨진 관도 고름 따라감(233-3) - else if (selectedPipeChainage !== null) { - // 고르고 있던 관이 사라졌다(삭제·재계산 이동) — 폼도 함께 닫는다. - selectedPipeChainage = null; - facilityOptions.setFacility(null); - syncButtons(); - } - renderList(); + followPipeList(panelContext, pipeFacilities); }, selectPipeByChainage(chainageM) { if (chainageM !== null) callbacks.onReveal?.(); diff --git a/B05_Profile/B05_Profile_UI_Structures_Panel_Pipes.ts b/B05_Profile/B05_Profile_UI_Structures_Panel_Pipes.ts new file mode 100644 index 000000000..046466007 --- /dev/null +++ b/B05_Profile/B05_Profile_UI_Structures_Panel_Pipes.ts @@ -0,0 +1,42 @@ +/* ============================================================================= + * B05_Profile_UI_Structures_Panel_Pipes.ts + * 「구조물 배치」 폼이 관 목록을 받는 몫 — 임시 관 · 고른 관이 조금 옮겨져도 따라감 + * (`B05_Profile_UI_Structures_Panel.ts` setPipeFacilities 에서 나눔 · 472-5 · 동작 같음). + * ========================================================================== */ + +import { PIPE_MATCH_M } from "../A00_Common/design/design_selection"; +import type { StructuresEventContext } from "./B05_Profile_UI_Structures_Panel_Events"; +import type { PipeFacilityItem } from "./B05_Profile_UI_Structures_Panel_Types"; + +/** 새 관 목록(`list` = 이미 갈아 낀 폼 목록) — 임시 · 고른 관 누가거리 따라감 · 사라지면 폼 닫음. */ +export function followPipeList(ctx: StructuresEventContext, list: PipeFacilityItem[]): void { + const temp = ctx.tempPipeChainage(); + if (temp !== null) { + // 스냅·재계산으로 좌표가 조금 옮겨진 임시 관을 계속 따라간다. + const hit = list.find((pipe) => Math.abs(pipe.chainage_m - temp) < PIPE_MATCH_M); + if (hit) { + ctx.setTempPipeChainage(hit.chainage_m); + if (ctx.selectedPipeChainage() !== null) ctx.setSelectedPipeChainage(hit.chainage_m); + // 재계산이 끝나 도착한 담당 유역 설계유량을 입력 중인 폼에 반영한다 — + // 물넘이·세월교 월류 높이가 이 값으로 계산된다(2026-08-18 진단: 재계산 + // 후 선택 통지가 조용히 끝나 폼이 유량을 못 받던 문제). + ctx.facilityOptions.setDesignFlow(hit.design_flow_m3s ?? null); + } else { + // 임시 배치 관이 재계산에서 사라졌다 — 임시 상태만 정리(관은 이미 없다). + ctx.setTempPipeChainage(null); + ctx.syncButtons(); + } + } + const kept = list.find( + (pipe) => Math.abs(pipe.chainage_m - (ctx.selectedPipeChainage() ?? NaN)) < PIPE_MATCH_M, + ); + if (kept) + ctx.setSelectedPipeChainage(kept.chainage_m); // 조금 옮겨진 관도 고름 따라감(233-3) + else if (ctx.selectedPipeChainage() !== null) { + // 고르고 있던 관이 사라졌다(삭제·재계산 이동) — 폼도 함께 닫는다. + ctx.setSelectedPipeChainage(null); + ctx.facilityOptions.setFacility(null); + ctx.syncButtons(); + } + ctx.renderList(); +} diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 1c9a1a4cd..bde0bd1e8 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -13,11 +13,7 @@ * ========================================================================== */ import type { PipeFacilityItem } from "../A00_Common/b_structures_section"; -import { - bindDesignView, - sharedStructures, - type DesignPageShared, -} from "../A00_Common/design/design_shared"; +import { bindDesignView, sharedStructures } from "../A00_Common/design/design_shared"; import { fetchStructureTypes, staleDefinitionNotice, @@ -58,10 +54,17 @@ import { resetAdjustDock, setAdjustSideSlots, } from "./B06_Section_UI_Adjust_Dock"; -import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import { withSpecDefaults } from "./B06_Section_UI_Page_Structures_Spec"; import { listPick } from "../common_util/common_util_structure_catalog"; import { followMovedPipe, pipeIdAt } from "../A00_Common/design/design_pipe_ids"; +import type { + B06StructuresPanel, + B06StructuresPanelDeps, +} from "./B06_Section_UI_Page_Structures_Panel_Types"; +export type { + B06StructuresPanel, + B06StructuresPanelDeps, +} from "./B06_Section_UI_Page_Structures_Panel_Types"; const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요."; const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다."; @@ -74,96 +77,6 @@ const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세 const PIPE_MOVE_NOTICE = "기준 측점을 옮겼습니다 — 횡단도는 바로 보이고, 세부 배수유역은 뒤에서 다시 나눕니다."; -export interface B06StructuresPanelDeps { - projectId: string | null; - /** 한 페이지 한 벌 — 「구조물 배치」 폼은 종단 보기와 같은 노드다(계획서 24-3b). */ - shared: DesignPageShared; - /** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다 - * (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */ - detail?: () => SectionDetailResponse | null; - /** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본 - * 높이도 이 값을 쓴다(조정창 표시값과 같다). */ - wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null; - /** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */ - wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null; - /** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */ - wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; - /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ - stationInterval: () => number; - /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지도 스크롤도 않음 · 측점 없는 구조물 · 307). */ - focusChainage: (chainageM: number, revealOnly?: boolean) => void; - /** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */ - releaseCard?: () => void; - /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */ - markStructure?: (structureId: string | null, span?: boolean) => void; - /** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */ - /** `pipes` = 관 지점(구간 포함) — 종단 띠 측점 계획(322-3). */ - onMarks?: ( - structures: StructureInstance[], - types: StructureType[], - pipes: ReadonlyArray, - ) => void; - /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — - * 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */ - applyPipeOptions?: (chainageM: number, patch: Record) => void; - /** 관 목록 초안을 고쳤다 — 한 벌 배수유역도가 그 초안으로 다시 선다. */ - onPipesWritten?: () => void; -} - -export interface B06StructuresPanel { - /** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */ - root: HTMLElement; - /** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */ - listRoot: HTMLElement; - /** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */ - load: () => Promise; - /** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */ - listStale: () => boolean; - /** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인 - * 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */ - overrideOptions: (chainageM: number, patch: Record) => void; - /** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고, - * 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). 횡단도의 카드 · - * 벽 · 구체 어디를 눌러도 이 길 하나다. 폼이 연 것을 고름 값으로 돌려준다. */ - showAtChainage: (chainageM: number | null) => DesignSelection; - /** 횡단도에서 벽·구체 선택을 풀 때 — 관 계열 폼만 비운다. 구조물 폼은 그대로 둔다(카드를 - * 갈아 선택하며 옛 카드의 벽 선택이 풀리는 순간 새 카드의 폼을 지우면 안 된다). */ - releasePipe: () => void; - /** 그 측점에서 폼이 열 항목을 **열지 않고** 고름 값으로 풀어 준다(세션에 적는 데 쓴다). */ - selectionAt: (chainageM: number | null) => DesignSelection; - /** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */ - prefer: (structureId: string | null) => void; - /** 지금 카드 고름이 먼저 열 구조물(목록 · 알약 · 넘김) — 없으면 null(카드만 고름 · 189-1). */ - preferred: () => string | null; - /** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */ - selectStructure: (structureId: string) => void; - /** 측점 없는 관 하나를 폼에 올림 — 카드 안 고름 · 스크롤 없음 · 알약 · 고름 값에 알약 id(요구 178 · 307). */ - selectPipeAt: (chainageM: number) => void; - /** 넘김값(B05 알약 · 3D · 지도)의 카드 고름 — `focus` 가 그 카드를 고름 · 넘긴 구조물이 먼저(52-15) · - * 폼은 id 로 직접 엶(53-1) · 측점 없는 구조물은 카드 고름 · 스크롤 없이 폼만(요구 178 · 307). */ - focusPicked: (structureId: string | null, focus: () => void) => void; - /** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */ - adoptFordHeights: () => void; - /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ - getStructures: () => StructureInstance[]; - /** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */ - currentChainage: () => number | null; - /** 종단 그래프 우클릭이 쓰는 길 — 좌측 폼·목록과 **같은 함수**를 탄다(2026-09-12). */ - addStructureAt: (chainageM: number, typeId: string) => void; - removeStructureById: (structureId: string) => boolean; - addPipeAt: (chainageM: number) => void; - removePipeAt: (chainageM: number) => void; - movePipeTo: (fromChainageM: number, toChainageM: number) => void; - moveStructureTo: (structureId: string, toChainageM: number) => boolean; - /** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */ - facility: { - setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void; - onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void; - /** 횡단 단 수가 바뀜 — 배관 흙막이 컨테이너 수를 맞춤(54-1). */ - recount: () => void; - }; -} - type PlaceOf = { chainage_m?: number | null; start_m?: number | null; end_m?: number | null }; /** 목록 자리 지문 — 누가거리 · 구간만(옵션 고침은 안 셈 · 440). */ const placeKey = (list: ReadonlyArray): string => diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel_Types.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel_Types.ts new file mode 100644 index 000000000..1908219c9 --- /dev/null +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel_Types.ts @@ -0,0 +1,101 @@ +/* ============================================================================= + * B06_Section_UI_Page_Structures_Panel_Types.ts + * 횡단 보기 「구조물 배치」 몫의 타입 — `B06_Section_UI_Page_Structures_Panel.ts` 에서 나눔 + * (472-5 · 700줄 · 동작 같음 · 그쪽이 다시 내보냄). + * ========================================================================== */ + +import type { PipeFacilityItem } from "../A00_Common/b_structures_section"; +import type { DesignPageShared } from "../A00_Common/design/design_shared"; +import type { DesignSelection } from "../A00_Common/design/design_selection"; +import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures"; +import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; + +export interface B06StructuresPanelDeps { + projectId: string | null; + /** 한 페이지 한 벌 — 「구조물 배치」 폼은 종단 보기와 같은 노드다(계획서 24-3b). */ + shared: DesignPageShared; + /** 횡단 캐시 — 폼 기본값을 조정창과 **같은 원천**(culvert 스펙)에서 읽는다 + * (2026-08-29 사용자: 예전 조정창이 보여주던 값이 기본값이다). */ + detail?: () => SectionDetailResponse | null; + /** 지금 그려진 벽 높이(m) — 배관 벽은 스펙이 아니라 기하가 정하므로 폼 기본 + * 높이도 이 값을 쓴다(조정창 표시값과 같다). */ + wallHeight?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => number | null; + /** 지금 그려진 벽 형태 — 조정값(m)이 있으면 그쪽이 정본이라 폼도 이 값을 보인다. */ + wallForm?: (chainageM: number, role: "inlet" | "outlet" | `extra${number}`) => string | null; + /** 그 측점에서 유입·유출 벽이 각각 좌·우 어느 쪽인가(독립 기슭막이 칸 이름표). */ + wallSideLabels?: (chainageM: number) => { inlet: "좌" | "우"; outlet: "좌" | "우" } | null; + /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ + stationInterval: () => number; + /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다(revealOnly = 고르지도 스크롤도 않음 · 측점 없는 구조물 · 307). */ + focusChainage: (chainageM: number, revealOnly?: boolean) => void; + /** 목록에서 고른 줄을 다시 눌러 풂 — 횡단 카드 선택도 끔(54-8). */ + releaseCard?: () => void; + /** 종단 알약 강조를 목록에서 고른 것에 맞춘다(null = 관 · 해제 — 측점선 짝을 따름 · span 끔 = 범위 테두리 없이 · 137-4). */ + markStructure?: (structureId: string | null, span?: boolean) => void; + /** 목록이 바뀔 때마다 종단 알약 레인에 같은 목록을 넘긴다(2026-09-07 표시 통일). */ + /** `pipes` = 관 지점(구간 포함) — 종단 띠 측점 계획(322-3). */ + onMarks?: ( + structures: StructureInstance[], + types: StructureType[], + pipes: ReadonlyArray, + ) => void; + /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — + * 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */ + applyPipeOptions?: (chainageM: number, patch: Record) => void; + /** 관 목록 초안을 고쳤다 — 한 벌 배수유역도가 그 초안으로 다시 선다. */ + onPipesWritten?: () => void; +} + +export interface B06StructuresPanel { + /** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */ + root: HTMLElement; + /** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */ + listRoot: HTMLElement; + /** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */ + load: () => Promise; + /** 가게(초안 · 없으면 저장분) 관 · 구조물 자리가 목록과 다른가 — 다시 실을 때(노선 초안 따라감 · 평면 마커 옮김 · 440). */ + listStale: () => boolean; + /** 기하가 한계로 자른 실제 값을 폼에 되돌린다 — 입력 숫자만 커지고 그림은 그대로인 + * 상태를 막는다(2026-08-29 사용자: 재질 높이 한계에 걸린 값). */ + overrideOptions: (chainageM: number, patch: Record) => void; + /** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고, + * 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). 횡단도의 카드 · + * 벽 · 구체 어디를 눌러도 이 길 하나다. 폼이 연 것을 고름 값으로 돌려준다. */ + showAtChainage: (chainageM: number | null) => DesignSelection; + /** 횡단도에서 벽·구체 선택을 풀 때 — 관 계열 폼만 비운다. 구조물 폼은 그대로 둔다(카드를 + * 갈아 선택하며 옛 카드의 벽 선택이 풀리는 순간 새 카드의 폼을 지우면 안 된다). */ + releasePipe: () => void; + /** 그 측점에서 폼이 열 항목을 **열지 않고** 고름 값으로 풀어 준다(세션에 적는 데 쓴다). */ + selectionAt: (chainageM: number | null) => DesignSelection; + /** 다음 카드 고름이 측점보다 먼저 열 구조물 — 알약 · 넘김값이 누른 것(null = 측점 규칙대로). */ + prefer: (structureId: string | null) => void; + /** 지금 카드 고름이 먼저 열 구조물(목록 · 알약 · 넘김) — 없으면 null(카드만 고름 · 189-1). */ + preferred: () => string | null; + /** 구조물 하나를 폼에 올리고 그 카드로 간다 — 알약을 누른 길(목록 클릭과 같은 흐름). */ + selectStructure: (structureId: string) => void; + /** 측점 없는 관 하나를 폼에 올림 — 카드 안 고름 · 스크롤 없음 · 알약 · 고름 값에 알약 id(요구 178 · 307). */ + selectPipeAt: (chainageM: number) => void; + /** 넘김값(B05 알약 · 3D · 지도)의 카드 고름 — `focus` 가 그 카드를 고름 · 넘긴 구조물이 먼저(52-15) · + * 폼은 id 로 직접 엶(53-1) · 측점 없는 구조물은 카드 고름 · 스크롤 없이 폼만(요구 178 · 307). */ + focusPicked: (structureId: string | null, focus: () => void) => void; + /** 배수유역도가 관 초안에 채운 빈 월류 높이(126-1)를 목록 · 폼에 받는다(298-1). */ + adoptFordHeights: () => void; + /** 지금 목록의 구조물 정본(미저장분 포함) — 카드 찾기에 쓴다. */ + getStructures: () => StructureInstance[]; + /** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */ + currentChainage: () => number | null; + /** 종단 그래프 우클릭이 쓰는 길 — 좌측 폼·목록과 **같은 함수**를 탄다(2026-09-12). */ + addStructureAt: (chainageM: number, typeId: string) => void; + removeStructureById: (structureId: string) => boolean; + addPipeAt: (chainageM: number) => void; + removePipeAt: (chainageM: number) => void; + movePipeTo: (fromChainageM: number, toChainageM: number) => void; + moveStructureTo: (structureId: string, toChainageM: number) => boolean; + /** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */ + facility: { + setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void; + onInletStructureChange: (handler: (value: "auto" | "revet" | "I" | "L" | "U") => void) => void; + /** 횡단 단 수가 바뀜 — 배관 흙막이 컨테이너 수를 맞춤(54-1). */ + recount: () => void; + }; +} diff --git a/resources/tester/test_233_3_release.py b/resources/tester/test_233_3_release.py index 4af479f86..90dde8257 100644 --- a/resources/tester/test_233_3_release.py +++ b/resources/tester/test_233_3_release.py @@ -99,7 +99,9 @@ def test_배선() -> None: listing = _read("B05_Profile", "B05_Profile_UI_Structures_List.ts") assert "< PIPE_MATCH_M" in listing and "< 0.05" not in listing structures = _read("B05_Profile", "B05_Profile_UI_Structures_Panel.ts") - assert "selectedPipeChainage = kept.chainage_m;" in structures + # 관 목록 받기는 472-5 에서 `_Structures_Panel_Pipes` 로 나눔(동작 같음) + pipes = _read("B05_Profile", "B05_Profile_UI_Structures_Panel_Pipes.ts") + assert "ctx.setSelectedPipeChainage(kept.chainage_m);" in pipes # 저장 · 새로 읽기 뒤 목록 주입이 관 폼을 닫지 않음(닫히며 종단 선까지 꺼졌음) assert "if (!applyingLive && selectedPipeChainage === null)" in structures assert len(structures.splitlines()) <= 700