Files
Aislo/B05_Profile/B05_Profile_UI_Panel.ts
T
eomsangdonandClaude Sonnet 5.5 2c520537b6 refactor(B05·B06): 좌측 칸을 등록부로 · 아래 단추 넷 · B05 Panel 나눔(24-2b)
- B05 좌측 패널(714줄)을 Fields · Settings · Route · View · Sections 로 나누고 createRoutePanel 은 등록부로 세움
- B06 좌측 칸(횡단 보기 설정 · 표준 횡단면 설정)을 등록부로 · 페이지는 createDesignPanel 만 부름
- 「구조물 배치」는 design_sections 한 정의로 두 보기
- 아래 단추 [초기화][저장][확정][보기 전환] — B05 [확정]은 저장 흐름 이음 전까지 꺼 둠
- [초기화] 는 design_reset 한 함수를 두 보기가 부름
- 칸 DOM 은 고치기 전과 같음(전후 DOM 대조)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
2026-09-29 20:17:35 +09:00

245 lines
12 KiB
TypeScript

/* =============================================================================
* B05_Profile_UI_Panel.ts
* 종단 보기(B05) 좌측 패널 — 칸 재료를 모아 **공용 등록부**(`design_panel`)에 넘겨 세운다.
*
* 칸 정의는 `_Panel_Sections`(종단 보기 전용) · `design_sections`(두 보기 공용), 재료는
* `_Panel_Settings` · `_Panel_Route` · `_Panel_View`, 값 · 상수 · 입력 칸 만들기는 `_Panel_Fields`
* 에 있다(700줄 제한으로 나눔, 계획서 24-2b). 값을 읽고 되살리는 창구(`values` · `restore`)만
* 여기 남는다. 아래 고정 영역(단추 넷)은 `design_dock` 이 만든다.
* ========================================================================== */
import type { PlacedRoutePoint } from "./B05_Profile_UI_Markers";
import type { StructureInstance } from "./B05_Profile_Api_Structures";
import type { FacilityAttributes } from "./B05_Profile_UI_Drainage_Facility";
import { createStructuresSection, type StructuresSection } from "./B05_Profile_UI_Structures_Panel";
import { createDesignPanel } from "../A00_Common/design/design_panel";
import "./B05_Profile_UI_Panel_Sections";
import {
STATION_INTERVAL_M,
parseOptional,
type RoutePanelValues,
} from "./B05_Profile_UI_Panel_Fields";
import { createRoutePart } from "./B05_Profile_UI_Panel_Route";
import { createSettingsPart } from "./B05_Profile_UI_Panel_Settings";
import { createViewControls } from "./B05_Profile_UI_Panel_View";
export type { RoutePanelValues } from "./B05_Profile_UI_Panel_Fields";
interface PanelCallbacks {
onSolve: () => void;
/** [계획노선 편집] — 큰 모달을 열어 노선을 고친다(계산은 모달 [확인]에서만 돈다). */
onEditPlannedRoute: () => void;
/** [임시저장] — 현재 편집(계획선 델타·관로·비정규 측점·상단측)을 확정 전이 없이 저장. */
onTempSave: () => void;
/** [횡단 이동] — 저장 없이 B06 횡단 페이지로 이동만. */
onGoCross: () => void;
/** [초기화] — 사용자 편집을 버리고 초기 자동 계산 상태로 롤백. */
onReset: () => void;
onContourApply: (interval: number) => void | Promise<void>;
onSurfaceVisible: (visible: boolean) => void;
/** [예상형상] — 계획 코리도 서피스 + 클리핑 지형(공사 후) ↔ 원지반 완전체 전환. */
onCorridorVisible: (visible: boolean) => void;
onContoursVisible: (visible: boolean) => void;
onAxesVisible: (visible: boolean) => void;
onStationLinesVisible: (visible: boolean) => void;
/** 측점 번호·이름 라벨 표시 토글(기본 켜짐 — BP·EP·5측점 배수·구조물). */
onStationLabelsVisible: (visible: boolean) => void;
/** 지표면 흑백 표시 토글(기본 꺼짐 — 무지개 고도색). */
onSurfaceGrayscale: (grayscale: boolean) => void;
onView: (view: "iso" | "top" | "front" | "side") => void;
/** 직교/원근 전환 — 탑뷰에서 크기를 정밀 대조할 때만 직교로 본다(2026-09-04 사용자 지시). */
onProjection: (kind: "perspective" | "ortho") => void;
onResetView: () => void;
/** [3D 업데이트] — 계획선 편집을 3D 예상형상·측점선에 한 번에 반영(2026-09-01 사용자
* 지시). 편집마다 따라오던 자동 갱신을 없애고 이 버튼으로만 돌린다. */
onCorridorRefresh: () => void | Promise<void>;
onMovePoint: () => void;
onDeletePoint: () => void;
onRadiusChange: (radius: number) => void;
onInputChange: () => void;
/** 구조물 목록(구조물군 B~G)이 바뀔 때 — 정본 저장·그래프 반영은 Page가 맡는다. */
onStructuresChange: (structures: StructureInstance[]) => void;
/** 구조물을 목록에서 선택/해제할 때 해당 구조물(또는 null). */
onStructureSelect: (structure: StructureInstance | null) => void;
/** 바깥(3D·종단·유역도)에서 고른 것이 폼에 실릴 때 — 접힌 좌측 패널을 펼친다. */
onStructureReveal?: () => void;
/** 계곡 통과 시설 추가·수정·삭제·선택 — 관 지점 정본(배수유역 패널) 경유(2026-08-17 통합). */
onPipeFacilityAdd: (chainageM: number, attributes: FacilityAttributes) => void;
onPipeFacilityUpdate: (
fromChainageM: number,
toChainageM: number,
attributes: FacilityAttributes,
) => void;
onPipeFacilityRemove: (chainageM: number) => void;
/** null = 사이드 목록 재클릭 해제 — 전 화면 선택도 함께 푼다(2026-08-18). */
onPipeFacilitySelect: (chainageM: number | null) => void;
/** 이어 공사 시작 기준(시작 측점·누가거리 시작)이 바뀔 때. */
onStationDisplayChange: (offset: { station: number; cumulative: number }) => void;
}
export function createRoutePanel(callbacks: PanelCallbacks) {
const { viewControls, setCorridorPending } = createViewControls(callbacks);
const settings = createSettingsPart(callbacks);
const route = createRoutePart(callbacks);
const { fields: routeFields } = route;
const {
contourInterval,
gradeClass,
designSpeed,
terrainType,
crossSampleInterval,
longSampleInterval,
maxGradePct,
minVerticalRadius,
minTangentLength,
startElevationOffset,
endElevationOffset,
enforcePipeClearance,
} = settings.fields;
const {
algorithm,
paved,
avoidPass,
minCurveRadius,
maxUphillGrade,
maxDownhillGrade,
minUphillGrade,
minDownhillGrade,
} = routeFields;
// 「구조물 배치」 — 구 비정규 측점 섹션을 흡수한 단일 섹션(2026-08-17 컨테이너 병합).
// 타입 목록과 옵션 칸은 서버 레지스트리에서 받아 그리고, 계곡 통과 시설(A군)은 관
// 지점 정본과 연동한다. 위치 입출력은 측점번호+잔여거리 두 칸이다.
const structures = createStructuresSection({
onChange: callbacks.onStructuresChange,
onSelect: callbacks.onStructureSelect,
getInterval: () => STATION_INTERVAL_M,
onPipeAdd: callbacks.onPipeFacilityAdd,
onPipeUpdate: callbacks.onPipeFacilityUpdate,
onPipeRemove: callbacks.onPipeFacilityRemove,
onPipeSelect: callbacks.onPipeFacilitySelect,
onReveal: () => callbacks.onStructureReveal?.(),
});
const inputElements = [
algorithm,
gradeClass,
paved,
avoidPass,
minCurveRadius,
maxUphillGrade,
maxDownhillGrade,
minUphillGrade,
minDownhillGrade,
crossSampleInterval,
longSampleInterval,
designSpeed,
terrainType,
maxGradePct,
minVerticalRadius,
minTangentLength,
startElevationOffset,
endElevationOffset,
enforcePipeClearance,
];
inputElements.forEach((input) => input.addEventListener("change", callbacks.onInputChange));
// 좌측 패널 = 등록부가 칸을 차례대로 세우고 아래 고정 영역(단추 넷)을 붙인다. 하단 단추의
// [확정]은 저장 흐름(`design_save`)이 이어질 때까지 꺼 둔다 — 종·횡 통합 확정은 횡단 보기에서
// 한다(2026-08-08 워크플로우 재정의).
const design = createDesignPanel(
{
projectId: null,
parts: { structures, settings, route },
dock: {
reset: { run: callbacks.onReset },
save: { run: callbacks.onTempSave },
confirm: {
run: () => undefined,
enabled: false,
title: "확정은 횡단 보기에서 합니다(저장 한 줄기가 이어지면 이 자리에서 켜집니다).",
},
switchView: { run: callbacks.onGoCross },
},
},
"profile",
);
return {
root: design.root,
viewControls,
/** 「구조물 배치」 섹션 API(타입 주입·목록 교체·계곡 시설 병합·선택·추가/이동/삭제). */
structures: structures as StructuresSection,
/** 이어 공사 시작 기준(측점번호·누가거리 오프셋) 현재값. */
stationDisplayOffset: settings.stationDisplayOffset,
/** 지금 적용되는 종단기울기 상한(%) — 그래프 위반 판정 즉시 반영용(2026-08-19). */
gradeLimitPct: settings.gradeLimitPct,
/** [3D 업데이트] 대기 표시 — 편집이 3D에 아직 안 들어갔음을 버튼에 알린다. */
setCorridorPending,
values(): RoutePanelValues {
return {
contourInterval: Number(contourInterval.value) || 1,
algorithm: algorithm.value as RoutePanelValues["algorithm"],
gradeClass: gradeClass.value as RoutePanelValues["gradeClass"],
paved: paved.checked,
minCurveRadius: parseOptional(minCurveRadius),
maxUphillGrade: parseOptional(maxUphillGrade),
maxDownhillGrade: parseOptional(maxDownhillGrade),
minUphillGrade: parseOptional(minUphillGrade),
minDownhillGrade: parseOptional(minDownhillGrade),
allowAvoidPassThrough: avoidPass.checked,
stationInterval: STATION_INTERVAL_M,
crossSampleInterval: parseOptional(crossSampleInterval),
longSampleInterval: parseOptional(longSampleInterval),
designSpeed: settings.currentSpeed(),
terrainType: terrainType.value as RoutePanelValues["terrainType"],
maxGradePct: parseOptional(maxGradePct),
minVerticalRadius: parseOptional(minVerticalRadius),
minTangentLength: parseOptional(minTangentLength),
startElevationOffset: parseOptional(startElevationOffset),
endElevationOffset: parseOptional(endElevationOffset),
enforcePipeClearance: enforcePipeClearance.checked,
};
},
restore(values: Partial<RoutePanelValues>) {
if (values.contourInterval != null) contourInterval.value = String(values.contourInterval);
if (values.algorithm) algorithm.value = values.algorithm;
if (values.gradeClass) gradeClass.value = values.gradeClass;
if (values.paved != null) paved.checked = values.paved;
if (values.minCurveRadius != null) minCurveRadius.value = String(values.minCurveRadius);
if (values.maxUphillGrade != null) maxUphillGrade.value = String(values.maxUphillGrade);
if (values.maxDownhillGrade != null) maxDownhillGrade.value = String(values.maxDownhillGrade);
if (values.minUphillGrade != null) minUphillGrade.value = String(values.minUphillGrade);
if (values.minDownhillGrade != null) minDownhillGrade.value = String(values.minDownhillGrade);
if (values.allowAvoidPassThrough != null) avoidPass.checked = values.allowAvoidPassThrough;
// stationInterval은 복원하지 않는다 — 20m 고정(2026-08-18).
if (values.crossSampleInterval != null)
crossSampleInterval.value = String(values.crossSampleInterval);
if (values.longSampleInterval != null)
longSampleInterval.value = String(values.longSampleInterval);
if (values.gradeClass) {
gradeClass.value = values.gradeClass;
settings.syncSpeedChoices();
}
if (values.designSpeed) designSpeed.value = String(values.designSpeed);
if (values.terrainType) terrainType.value = values.terrainType;
if (values.maxGradePct != null) maxGradePct.value = String(values.maxGradePct);
if (values.minVerticalRadius != null)
minVerticalRadius.value = String(values.minVerticalRadius);
if (values.minTangentLength != null) minTangentLength.value = String(values.minTangentLength);
if (values.startElevationOffset != null)
startElevationOffset.value = String(values.startElevationOffset);
if (values.endElevationOffset != null)
endElevationOffset.value = String(values.endElevationOffset);
if (values.enforcePipeClearance != null)
enforcePipeClearance.checked = values.enforcePipeClearance;
settings.syncCriteria();
},
setSelected(point: PlacedRoutePoint | null) {
route.setSelected(point);
},
};
}
export type RoutePanel = ReturnType<typeof createRoutePanel>;