diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts index 701cc8f0..52589985 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Panel.ts @@ -47,6 +47,7 @@ import { MAX_PANEL_WIDTH_RATIO, MIN_PANEL_WIDTH, renderBasinRows, + summaryText, basinIndexOfPipe, pipeMarkerColor, pointInRing, @@ -382,11 +383,7 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra selectBasin(null, false); renderBasinList(); syncPipeSelection(); - summary.textContent = L("B05_Drainage_Summary") - .replace("{pipes}", String(pipeEditor.pipes().length)) - .replace("{basins}", String(basins.length)) - .replace("{source}", zSource || "-"); - summary.hidden = basins.length === 0; + summaryText(summary, pipeEditor.pipes().length, basins.length, zSource); callbacks.onPipesChanged?.(pipeEditor.chainages()); } @@ -440,8 +437,18 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra * B05는 노선 주변 배수유역을 보는 화면이라 도로에 맞춰 확대한 상태로 연다 * (2026-08-01 사용자 지시). 배경 전체를 보려면 휠로 축소하면 된다. * 노선이 없으면 배경 전체를 그대로 보여준다. */ + /** 아직 레이아웃 전(폭·높이 0)이라 화면 맞춤을 미뤄 둔 상태. 첫 배치 때 다시 맞춘다. */ + let fitPending = false; + function fitToRoute(): void { const rect = viewport.getBoundingClientRect(); + // 대시보드에서 곧장 들어오면 패널이 아직 배치되기 전이라 0×0이다. 그 상태로 맞추면 + // 엉뚱한 배율이 굳어 지도가 보이지 않는다(새로고침하면 보이던 원인 — 2026-08-02 사용자 보고). + if (rect.width < 2 || rect.height < 2) { + fitPending = true; + return; + } + fitPending = false; const fitted = fitViewToRoute(meta, routePoints, rect.width, rect.height); scale = fitted.scale; offsetX = fitted.offsetX; @@ -600,7 +607,11 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra offsetX = next.offsetX; offsetY = next.offsetY; }, - redraw: scheduleDraw, + redraw: () => { + // 배치가 잡히면 미뤄 둔 화면 맞춤을 그때 수행한다. + if (fitPending) fitToRoute(); + scheduleDraw(); + }, }); function setCollapsed(collapsed: boolean): void { diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Parts.ts b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Parts.ts index 85f710d9..d9d39ce6 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Parts.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Drainage_Parts.ts @@ -504,3 +504,17 @@ export function basinIndexOfPipe( const basin = basins.find((item) => Math.abs(item.chainage_m - pipe.chainage_m) < 0.51); return basin ? basin.index : null; } + +/** 관 개수·세부유역 수·종단 Z 출처 한 줄. 유역이 없으면 줄 자체를 감춘다. */ +export function summaryText( + element: HTMLElement, + pipeCount: number, + basinCount: number, + zSource: string, +): void { + element.textContent = L("B05_Drainage_Summary") + .replace("{pipes}", String(pipeCount)) + .replace("{basins}", String(basinCount)) + .replace("{source}", zSource || "-"); + element.hidden = basinCount === 0; +} diff --git a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts index e75bf415..5683ee1d 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_IrregularStations.ts @@ -28,6 +28,12 @@ export interface IrregularStation { /** 관 매설 지점이 구조물 목록에 실체화될 때 쓰는 이름. 향후 드롭다운으로 바꾼다. */ export const PIPE_STRUCTURE_NAME = "배관"; +/** 배관으로 볼 항목인가. 손으로 이름을 "배관"이라 적은 것도 관 지점 정본을 따르게 한다 — + * 그렇지 않으면 [초기화]로 지워지지 않고 배수유역도와도 어긋난 채 남는다(2026-08-02 사용자 보고). */ +export function isPipeStation(station: { origin?: "user" | "pipe"; structure: string }): boolean { + return station.origin === "pipe" || station.structure.trim() === PIPE_STRUCTURE_NAME; +} + export interface IrregularStationsSection { root: HTMLElement; getStations: () => IrregularStation[]; @@ -289,7 +295,7 @@ export function createIrregularStationsSection( setPipeStations(chainages) { // 배관 항목은 통째로 갈아 끼운다 — 정본은 배수유역도의 관 목록이다. for (let index = stations.length - 1; index >= 0; index -= 1) { - if (stations[index].origin === "pipe") stations.splice(index, 1); + if (isPipeStation(stations[index])) stations.splice(index, 1); } chainages.forEach((chainage) => { const { station, remainder } = splitChainage(chainage); diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts index 3b902294..bbe7b025 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Page.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Page.ts @@ -39,6 +39,7 @@ import { createRouteViewer } from "./B05_wf2_Route_UI_Viewer"; import { irregularLabel, irregularStationId, + isPipeStation, type IrregularStation, } from "./B05_wf2_Route_UI_IrregularStations"; import { @@ -194,7 +195,7 @@ export async function renderB05Route(root: HTMLElement): Promise { // 정본은 배수유역도의 관 지점이며, 구조물 목록은 그것을 실체화한 것이다. onPipesChanged: (chainages) => panel.irregularStations.setPipeStations(chainages), onStructureMove: (from, to, station) => { - if (station.origin === "pipe") { + if (isPipeStation(station)) { // 배관은 관 지점 정본을 거쳐야 세부유역까지 함께 다시 나뉜다. profilePanel.drainage.movePipe(from, to); return; @@ -202,7 +203,7 @@ export async function renderB05Route(root: HTMLElement): Promise { panel.irregularStations.moveByChainage(from, to); }, onStructureRemove: (station) => { - if (station.origin === "pipe") { + if (isPipeStation(station)) { profilePanel.drainage.removePipe(station.chainage_m); return; } @@ -416,7 +417,7 @@ export async function renderB05Route(root: HTMLElement): Promise { // 좌측 구조물 폼에서 "배관" 항목을 고쳤거나 지웠으면 배수유역도까지 따라가야 한다. // 관 목록이 그대로면 배수유역도가 아무 일도 하지 않으므로 되먹임 고리는 여기서 끊긴다. profilePanel.drainage.setPipeChainages( - stations.filter((entry) => entry.origin === "pipe").map((entry) => entry.chainage_m), + stations.filter(isPipeStation).map((entry) => entry.chainage_m), ); } diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Structures.ts b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Structures.ts index 493db944..e53223ad 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Profile_Structures.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Profile_Structures.ts @@ -10,7 +10,7 @@ * ========================================================================== */ import { createMapContextMenu } from "@ui/ui_template_context_menu"; -import type { IrregularStation } from "./B05_wf2_Route_UI_IrregularStations"; +import { isPipeStation, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations"; /** 선을 잡았다고 볼 좌우 여유(px). 선 자체는 얇아 그대로는 집기 어렵다. */ const GRAB_SLACK_PX = 6; @@ -59,7 +59,7 @@ export function mountStructureMenu(host: HTMLElement, options: StructureLineOpti menu.open(localX, event.clientY - rect.top, [ near ? [ - near.station.origin === "pipe" ? "배관 삭제" : "구조물 삭제", + isPipeStation(near.station) ? "배관 삭제" : "구조물 삭제", () => options.onRemove(near.station), ] : ["배관 추가", () => options.onAddPipe(Number(chainage.toFixed(2)))], diff --git a/B05_wf2_Route/B05_wf2_Route_UI_Selection.ts b/B05_wf2_Route/B05_wf2_Route_UI_Selection.ts index 381393e1..d6c721ab 100644 --- a/B05_wf2_Route/B05_wf2_Route_UI_Selection.ts +++ b/B05_wf2_Route/B05_wf2_Route_UI_Selection.ts @@ -9,7 +9,11 @@ * 구조물(`origin: "pipe"`)일 때만 붙는다 — 다른 구조물에는 대응하는 유역이 없다. * ========================================================================== */ -import { irregularStationId, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations"; +import { + irregularStationId, + isPipeStation, + type IrregularStation, +} from "./B05_wf2_Route_UI_IrregularStations"; export interface SelectionSyncPorts { /** 현재 구조물 측점 목록(수동 + 배관 투영분). */ @@ -50,7 +54,7 @@ export function createSelectionSync(ports: SelectionSyncPorts): SelectionSync { function syncBasinHighlight(stationId: string | null): void { const station = irregularOf(stationId); - ports.selectBasin(station?.origin === "pipe" ? station.chainage_m : null); + ports.selectBasin(station && isPipeStation(station) ? station.chainage_m : null); } return { @@ -77,8 +81,7 @@ export function createSelectionSync(ports: SelectionSyncPorts): SelectionSync { .stations() .find( (entry) => - entry.origin === "pipe" && - Math.abs(entry.chainage_m - chainageM) < SAME_CHAINAGE_M, + isPipeStation(entry) && Math.abs(entry.chainage_m - chainageM) < SAME_CHAINAGE_M, ); const id = matched ? irregularStationId(matched.id) : null; ports.setSyncing(true);