Files
Aislo/B05_Profile/B05_Profile_UI_Drainage_Chrome.ts
eomsangdonandClaude Fable 5 8812657022 feat(B05/B02): 표시 정보 정비 + 설계속도 축 도입
- 종단 상단 표시줄을 법정 판정 3항목으로 축소: 최대 기울기/상한(별표2 Ⅰ.2.라),
  절·성토 불균형(Ⅰ.1.나.(4)(다)), 종단곡선 필요 n곳(Ⅰ.2.마 — 대수차 5% 초과인데
  곡선이 빠진 변화점). 변화점·종단곡선 개수·기본 곡선길이 L·중복 경고칩은 제거.
- [초기선 복원] → [편집 되돌리기] — 실제 동작은 화면 편집 델타 삭제이지 저장 지점
  복귀가 아니다. 툴팁에 동작과 [초기화]와의 차이를 적었다.
- 3D 뷰포트 안내 라벨 삭제(로딩 완료 후 남던 조작법 문구).
- 배수유역 버튼 정비: 유역 다시 나누기 / 선택한 관 삭제 / 배관 배치 초기화 +
  각 툴팁에 실제 동작 명시(선택 삭제는 툴팁 자체가 없었다).
- 유토곡선 요약 12칩 → 5칩(절토·성토·잉여|부족·운반·검산). 토질 3종 내역·다짐
  환산·블록 수·장거리 운반·사토는 해당 칩 툴팁으로. B05·B06 공용 함수라 동시 반영.
- 설계속도 축 도입(임도는 속도를 낼 수 없는 노선 — 기본 20km/h):
  · 임도 종류는 프로젝트 등록값(projects.road_type)을 읽어 B05가 읽기 전용 표시
    (main→간선임도, fire→산불진화임도, 그 외→작업임도). 화면에서 고치지 않는다.
  · 설계속도 선택(간선·산불진화 20/30/40, 작업 20 고정) 신설 — 종단기울기 상한과
    종단곡선 반경이 등급이 아니라 이 값으로 정해진다(지식DB 설계제원_총괄 §6·§7).
  · B02 임도 종류 선택지를 현행 3종으로 정리(지선 폐지·계류보전은 사방 구분).
  · 백엔드: ROUTE_GRADE_CLASSES 3종+branch 호환, selectable_design_speeds 신설,
    resolve_design_speed 신설, legal_grade_limit_pct·resolve_grade_options에
    설계속도 인자, sections/context가 road_type 제공(B06도 저장값 승계).
  · 계획선 정책은 그릴 때마다 현재 기준으로 동기화 — 저장분의 옛 상한이 남지 않는다.
- 검증: pytest 125 통과(설계속도 12건 신규), tsc, 헤드 브라우저 — 진입 시 상한 9%,
  40km/h 전환 시 7% 즉시 반영, 원복 9%, 안내문 '간선임도 · 설계속도 20km/h ·
  일반지형', 유토곡선 5칩, 유역 버튼 새 이름·툴팁, 3D 라벨 빈 문자열 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 19:46:03 +09:00

170 lines
7.0 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;
deleteSelectedTip: 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;
// 표시 필터 버튼 묶음 — 헤더가 아니라 지도 좌측 상단에 얹는다(2026-08-17 사용자 지시).
// 지도 위에 있으니 버튼을 누른 포인터가 아래 지도까지 내려가면 안 된다 — 유역 고르기가
// 딸려 일어난다. 배선이 아니라 얹은 자리 때문에 생긴 것이라 여기서 막는다.
const layerButtons = document.createElement("div");
layerButtons.className = "b05-drainage__layers";
layerButtons.addEventListener("pointerdown", (event) => event.stopPropagation());
header.append(title);
// 세부유역 산정 — 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.title = labels.deleteSelectedTip;
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();
// 지도 좌상단 오버레이 — 필터 버튼 줄과 상태 문구를 세로로 쌓는다. 둘을 각각 지도에
// 절대배치하면 버튼이 두 줄로 접힐 때 문구가 그 아래에 겹친다.
const mapOverlay = document.createElement("div");
mapOverlay.className = "b05-drainage__map-overlay";
mapOverlay.append(layerButtons, status);
viewport.append(
backgroundImage,
canvas,
mapOverlay,
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,
};
}