Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts
T
eomsangdonandClaude Opus 5 77f833bb25 feat(B05): 보호공 개편·종단 기준 기본 접힘·700줄 파일 분리 3건
지시 1 — 사이드 「종단 설계 기준」 컨테이너를 기본 접힘으로. section()에
collapsed 인자를 더해 초기 is-collapsed만 붙이고 토글은 전역 collapsible이
그대로 맡는다(B06 buildGroup과 같은 패턴).

지시 4 — 배수관 유입·유출 바닥 보호를 "보호공" 한 축으로 개편:
- 구 돌붙임 있음/없음 + 표면처리(찰/메) 두 축 → 돌붙임(찰)/돌붙임(메)/도수로.
  도수로·산비탈수로(B4)는 유출부에서 돌붙임 대신 쓰는 공종이라 별도 구조물이
  아니라 이 선택지로 받는다(별표2 유출구~원지반 도달 의무)
- "없음" 삭제 — 물이 흐르는 자리라 보호공은 필수(사용자 확정). 기본값은
  유입 돌붙임(찰)·유출 돌붙임(메)
- 치수는 배타 — 돌붙임이면 면적(㎡, 기본 10), 도수로면 폭(m, 기본 1.0)
- 저장 키 개명 *_pitching → *_protection, *_pitching_finish 삭제,
  *_protection_width_m 신설. 독립 기슭막이(D4)도 같은 축
- 구 저장분은 parse_pipe_points가 읽는 순간 새 키로 옮긴다("없음"은 부위
  기본값으로 승격). 폼 write()도 구 키를 폴백으로 읽어 structures.json 몫을 덮는다
- revetment.pitching_area_m2가 기본값과 required를 동시에 갖던 모순도 정리

지시 5 — 700줄 초과 파일 분리(순수 이동 + import 정리, 공개 인터페이스 불변):
- Drainage_Facility 724 → 401 (+_Facility_Fields 354)
- Structures_Panel 753 → 695 (+_Structures_List 113)
- Drainage_Panel 822 → 693 (+_Drainage_Chrome 150, +_Drainage_Interact 159)
- Page 1031 → 911 (+_Page_Helpers 144) — 아직 초과
- Profile_Panel 1152 — 미착수. 두 파일은 가변 클로저 상태를 곳곳에서 직접
  읽고 써 상태 승격이 선행돼야 하며, 프론트 테스트가 없어 순수 이동으로는
  떼어낼 수 없다. 분리안은 PLAN.md 3-3에 기록

검증: pytest tmp/tests 107 passed·7 skipped(신규 보호공 마이그레이션 6건 포함),
npm run typecheck 무오류, npm run build 성공(374 modules).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:19:46 +09:00

152 lines
6.2 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Drainage_Chrome.ts
* 배수유역 패널의 DOM 뼈대 — 헤더·버튼·뷰포트·범례·유역 목록·폭 조절 손잡이.
*
* 패널 본체(B05_Profile_UI_Drainage_Panel)가 700줄 한계에 닿아 분리했다.
* 여기에는 상태도 이벤트 배선도 없다 — 요소를 만들어 붙이고 그대로 넘긴다.
* 배선(토글·포인터·그리기)은 전부 본체가 맡는다.
* ========================================================================== */
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import { createPanelResizer } from "@ui/ui_template_resizer";
import { createProgressCircle } from "@ui/ui_template_progress";
import { createMapContextMenu } from "@ui/ui_template_context_menu";
import { createFlowLegend } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
export interface DrainageChrome {
root: HTMLElement;
panelHandle: ReturnType<typeof createWorkflowPanelHandle>;
layerButtons: HTMLElement;
analyzeButton: HTMLButtonElement;
deleteButton: HTMLButtonElement;
autoButton: HTMLButtonElement;
viewport: HTMLElement;
backgroundImage: HTMLImageElement;
canvas: HTMLCanvasElement;
status: HTMLElement;
progress: ReturnType<typeof createProgressCircle>;
contextMenu: ReturnType<typeof createMapContextMenu>;
legend: ReturnType<typeof createFlowLegend>;
summary: HTMLElement;
basinList: HTMLElement;
/** 폭 조절 손잡이 — 패널을 접을 때 본체가 dispose 한다. */
widthResizer: ReturnType<typeof createPanelResizer>;
}
export interface DrainageChromeLabels {
title: string;
analyze: string;
analyzeTip: string;
deleteSelected: string;
auto: string;
autoTip: string;
imageAlt: string;
statusNeedRoute: string;
}
export interface DrainageChromeMetrics {
minWidth: number;
maxWidthRatio: number;
widthStorageKey: string;
}
export function createDrainageChrome(
labels: DrainageChromeLabels,
metrics: DrainageChromeMetrics,
): DrainageChrome {
const root = document.createElement("aside");
root.className = "b05-drainage";
// 이 패널은 하단 패널의 **오른쪽**에 도킹해 오른쪽으로 접힌다(제목 카드와 반대).
// 방향을 넘기지 않으면 화살표가 정확히 거꾸로 나온다.
// 이름표 "배수유역" — 세로(90°)로 화살표 위에 얹힌다(2026-08-04 사용자 지시).
const panelHandle = createWorkflowPanelHandle("side", "right", "배수유역");
const header = document.createElement("div");
header.className = "b05-drainage__header";
const title = document.createElement("h3");
title.textContent = labels.title;
const layerButtons = document.createElement("div");
layerButtons.className = "b05-drainage__layers";
header.append(title, layerButtons);
// 세부유역 산정 — B04가 미리 분석해 둔 결과를 읽어 관을 보충하고 세부유역만 나눈다.
// 격자 해석은 하지 않으므로 즉시 끝난다.
const analyzeButton = document.createElement("button");
analyzeButton.type = "button";
analyzeButton.className = "b05-drainage__analyze";
analyzeButton.textContent = labels.analyze;
analyzeButton.title = labels.analyzeTip;
// 선택된 배관 삭제 — 편집 모드에서 마커를 선택해야 활성화된다.
const deleteButton = document.createElement("button");
deleteButton.type = "button";
deleteButton.className = "b05-drainage__analyze b05-drainage__tool";
deleteButton.textContent = labels.deleteSelected;
deleteButton.disabled = true;
// 자동 제안으로 되돌리기 — 편집한 배관 배치를 버리고 백엔드 자동 제안으로 재산정.
const autoButton = document.createElement("button");
autoButton.type = "button";
autoButton.className = "b05-drainage__analyze b05-drainage__tool";
autoButton.textContent = labels.auto;
autoButton.title = labels.autoTip;
header.append(analyzeButton, deleteButton, autoButton);
const viewport = document.createElement("div");
viewport.className = "b05-drainage__viewport";
const backgroundImage = document.createElement("img");
backgroundImage.className = "b05-drainage__image";
backgroundImage.alt = labels.imageAlt;
backgroundImage.draggable = false;
const canvas = document.createElement("canvas");
canvas.className = "b05-drainage__canvas";
const status = document.createElement("span");
status.className = "b05-drainage__status";
status.textContent = labels.statusNeedRoute;
// 지도 정중앙 로딩 서클 — 배경도·도엽 레이어·유역 산정이 끝날 때까지 화면이 비어 보인다.
const progress = createProgressCircle({ overlay: true });
progress.root.hidden = true;
// 배관 추가·삭제 우클릭 메뉴 — 편집 토글을 없앤 대신 이쪽으로 옮겼다(B04 지도와 같은 조작).
const contextMenu = createMapContextMenu("b05-drainage");
// 유입 강도 색띠 범례 — CSS가 지도 우측 세로로 세운다.
const legend = createFlowLegend();
viewport.append(backgroundImage, canvas, status, progress.root, contextMenu.element, legend.root);
const summary = document.createElement("div");
summary.className = "b05-drainage__summary";
summary.hidden = true;
const basinList = document.createElement("div");
basinList.className = "b05-drainage__basins";
basinList.hidden = true;
// 왼쪽 경계를 끌어 폭을 조절한다. 상한은 하단 패널 폭의 70%,
// 세로는 하단 패널에 그대로 딸려 간다(따로 조절하지 않는다 — 사용자 지시).
const widthResizer = createPanelResizer({
axis: "horizontal",
target: root,
cssVar: "--b05-drainage-width",
direction: -1,
min: metrics.minWidth,
max: () => (root.parentElement?.clientWidth ?? window.innerWidth) * metrics.maxWidthRatio,
storageKey: metrics.widthStorageKey,
});
root.append(panelHandle.root, widthResizer.root, header, viewport, summary, basinList);
return {
root,
panelHandle,
layerButtons,
analyzeButton,
deleteButton,
autoButton,
viewport,
backgroundImage,
canvas,
status,
progress,
contextMenu,
legend,
summary,
basinList,
widthResizer,
};
}