fix(B05): 손으로 넣은 "배관" 처리 통일 + 첫 진입 시 배수유역도 공백

1) 사이드바 폼으로 이름을 "배관"이라 적어 넣은 항목은 origin이 "user"라 [초기화]로
   지워지지 않고 배수유역도와도 어긋난 채 남았다. isPipeStation()(origin이 pipe이거나
   이름이 "배관")으로 판정을 통일해 목록 교체·이동·삭제·선택 동기화가 같은 규칙을 쓴다.

2) 대시보드에서 곧장 B05로 들어오면 배수유역도가 비어 보이고 새로고침해야 나왔다.
   패널이 배치되기 전(0×0)에 fitToRoute()가 돌아 엉뚱한 배율이 굳은 것이다. 크기가
   2px 미만이면 맞춤을 미뤘다가 첫 배치 때 다시 맞춘다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-02 00:48:05 +09:00
co-authored by Claude Opus 5
parent a58fac58d0
commit d9e8125e4c
6 changed files with 51 additions and 16 deletions
@@ -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 {
@@ -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;
}
@@ -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);
+4 -3
View File
@@ -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<void> {
// 정본은 배수유역도의 관 지점이며, 구조물 목록은 그것을 실체화한 것이다.
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<void> {
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<void> {
// 좌측 구조물 폼에서 "배관" 항목을 고쳤거나 지웠으면 배수유역도까지 따라가야 한다.
// 관 목록이 그대로면 배수유역도가 아무 일도 하지 않으므로 되먹임 고리는 여기서 끊긴다.
profilePanel.drainage.setPipeChainages(
stations.filter((entry) => entry.origin === "pipe").map((entry) => entry.chainage_m),
stations.filter(isPipeStation).map((entry) => entry.chainage_m),
);
}
@@ -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)))],
+7 -4
View File
@@ -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);