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
This commit is contained in:
2026-09-29 20:17:35 +09:00
co-authored by Claude Sonnet 5.5
parent 439f605ba6
commit 2c520537b6
13 changed files with 970 additions and 669 deletions
+37 -68
View File
@@ -4,11 +4,10 @@ import { CURRENT_PROJECT_ID_KEY, ROUTES } from "@config/config_frontend";
import { leaveForDashboard } from "../A00_Common/b_missing_data_guard";
import { readByKey, stateKey, writeByKey } from "../A00_Common/b_page_state";
import { navigateTo } from "../A00_Common/router";
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
import { showToast } from "@ui/ui_template_elements";
import { mountMissingStationNotice } from "./B06_Section_UI_Missing_Stations";
import type { StructureInstance, StructureType } from "../B05_Profile/B05_Profile_Api_Structures";
import { createWorkflowLayout } from "@ui/ui_template_workflow_layout";
import { attachCollapsible } from "@ui/ui_template_collapsible";
import { workflowSteps } from "../A00_Common/b_page_scaffold";
import {
fetchWorkflowState,
@@ -60,7 +59,10 @@ import "./B06_Section_UI_Style_Cross_Controls.css";
import "./B06_Section_UI_Style_Cross_Areas.css";
import { loadSectionDetail } from "./B06_Section_Section_Store";
import { applyStructureAreaRows, structureAreaRows } from "./B06_Section_Structure_Layouts";
import { buildGroup, createSampleWidener, L } from "./B06_Section_UI_Page_Common";
import { createSampleWidener, L } from "./B06_Section_UI_Page_Common";
import { createCrossViewPart, createStandardSlot } from "./B06_Section_UI_Page_Panel";
import { createDesignPanel } from "../A00_Common/design/design_panel";
import { resetDesign } from "../A00_Common/design/design_reset";
import "@util/common_util_mass_haul.css";
export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
@@ -70,65 +72,18 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
let stationInterval: number | undefined;
// 표준 횡단면 설정 패널 자리. context 로드 후 config 기본값으로 채운다.
// 표준 횡단면 설정은 기본 접힘(2026-08-23 사용자 지시) — 자주 여는 칸이 아니다.
const standardGroup = buildGroup(L("B06_Std_Title"), true);
const standardPanelSlot = document.createElement("div");
standardGroup.append(standardPanelSlot);
const standardPanelSlot = createStandardSlot();
let standardPanel: StandardPanelController | null = null;
// 높이 배율 옵션은 폐지 — 항상 1배율(2026-08-06 사용자 지시).
// 횡단 반폭은 **자체 컨테이너 + 자체 [보기 반폭 적용] 버튼**이다(2026-08-23 사용자 지시)
// — 표준 횡단면 설정의 [전체 측점 반영]에 물려 있어 반폭만 바꿔도 전 측점 설계
// 재계산을 기다려야 했다. 요청 반폭이 보유 샘플 폭(기본 20m, config
// SECTION_CROSS_HALF_WIDTH_M) 이하면 표시만 바꾸므로 즉시 끝나고, 넘을 때만 재생성한다.
const crossHalfWidthField = createInputField({
label: L("B05_Route_Field_CrossHalfWidth"),
type: "number",
});
crossHalfWidthField.input.min = "0.1";
crossHalfWidthField.input.step = "0.1";
const applyWidthButton = createButton({
label: L("B06_View_Apply"),
variant: "filled",
onClick: () => void applyCrossHalfWidth(),
});
applyWidthButton.title = L("B06_View_Apply_Tip");
const viewGroup = buildGroup(L("B06_View_Title"));
// 반폭 입력 + [적용]은 한 줄(2026-08-23 사용자 지시).
const viewRow = document.createElement("div");
viewRow.className = "b06-profile__field-row";
viewRow.append(crossHalfWidthField.root, applyWidthButton);
viewGroup.append(viewRow);
// 「횡단 보기 설정」 칸 재료(반폭 입력 + [보기 반폭 적용])는 `_Page_Panel` 이 만든다.
const crossView = createCrossViewPart(() => void applyCrossHalfWidth());
const crossHalfWidthField = crossView.field;
/** 횡단면도 **표시** 반폭 기본값(m) — 2026-08-19 사용자 지시로 20 → 12. 계산·보유
* 샘플 폭은 백엔드 기본(SECTION_CROSS_HALF_WIDTH_M = 20m) 그대로라 12는 표시
* 범위만 좁힌다(12 ≤ 20이므로 재계산 없음). 계산 범위와 혼동 금지. */
const DISPLAY_HALF_WIDTH_DEFAULT_M = 12;
// 하단 액션 [종단 이동][임시저장][확정] — 종단 이동은 저장 없이 B05로 페이지 이동만,
// 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 상세설계로 넘어간다
// (2026-08-08 워크플로우 재정의).
const goProfileButton = createButton({
label: L("B06_Profile_Btn_GoProfile"),
variant: "ghost",
onClick: () => navigateTo(ROUTES.B05_PROFILE),
});
const saveButton = createButton({
label: L("B06_Profile_Btn_Save"),
variant: "ghost",
onClick: () => void saveCurrentSections(persistContext),
});
saveButton.title = L("B06_Profile_Btn_Save_Tip");
saveButton.disabled = true;
const confirmButton = createButton({
label: L("B06_Profile_Btn_Confirm"),
variant: "filled",
onClick: () => void confirmCurrentSections(persistContext),
});
confirmButton.disabled = true;
const actionRow = document.createElement("div");
actionRow.className = "b06-profile__actions";
actionRow.append(goProfileButton, saveButton, confirmButton);
/** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤 — 가장 가까운 카드로 간다. */
const focusStationAt = (chainageM: number): void => {
const sections = sectionDetail?.cross_sections;
@@ -202,18 +157,29 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
applyPipeOptionsToCache(pipeOptionsContext, chainageM, patch),
});
const dockDivider = document.createElement("hr");
dockDivider.className = "b05-structure__divider";
const actionDock = document.createElement("div");
actionDock.className = "b05-route__dock ui-sidebar-actions";
actionDock.append(structuresPanel.listRoot, dockDivider, actionRow);
const leftForm = document.createElement("div");
leftForm.className = "b06-profile__form";
// 순서: 구조물 배치(최상단 — 2026-08-29 사용자 지시) → 횡단 보기 설정 → 표준.
leftForm.append(structuresPanel.root, viewGroup, standardGroup, actionDock);
// 그룹 제목 행 클릭 시 접기/펼치기(N-4-1). 액션 버튼 행은 collapsible 아님.
attachCollapsible(leftForm);
// 좌측 패널 = 등록부가 칸을 차례대로 세우고 아래 고정 영역(단추 넷)을 붙인다. 순서: 구조물 배치
// (최상단 — 2026-08-29 사용자 지시) → 횡단 보기 설정 → 표준. 자료가 오기 전에는 [저장]·[확정]을
// 잠근다(`updateActionState`). 확정은 임시저장과 같은 내용 저장 후 stage 2·3을 닫고 B07 상세설계로
// 넘어간다(2026-08-08 워크플로우 재정의).
const design = createDesignPanel(
{
projectId,
parts: {
structures: structuresPanel,
crossView,
standardSlot: standardPanelSlot,
},
dock: {
reset: { run: () => resetDesign(projectId ?? "", currentRouteId) },
save: { run: () => saveCurrentSections(persistContext), enabled: false },
confirm: { run: () => confirmCurrentSections(persistContext), enabled: false },
switchView: { run: () => navigateTo(ROUTES.B05_PROFILE) },
},
},
"cross",
);
const leftForm = design.root;
const saveTip = L("B06_Profile_Btn_Save_Tip");
// 설계 선택 반영·재계산·소단 동기화는 따로 뗀 모듈이 맡는다(2026-09-13 분리).
const {
@@ -451,8 +417,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
}
function updateActionState(): void {
saveButton.disabled = sectionDetail === null;
confirmButton.disabled = sectionDetail === null;
const ready = sectionDetail !== null;
design.dock.setActions({
save: { run: () => saveCurrentSections(persistContext), enabled: ready, title: saveTip },
confirm: { run: () => confirmCurrentSections(persistContext), enabled: ready },
});
}
/** 세션에 남은 선택을 이어받는다(2026-09-04 — 두 화면이 한 페이지처럼).
-10
View File
@@ -11,16 +11,6 @@ export function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
export function buildGroup(legend: string, collapsed = false): HTMLElement {
const group = document.createElement("section");
group.className = `b06-profile__group ui-collapsible ui-sidebar-section${collapsed ? " is-collapsed" : ""}`;
const title = document.createElement("h3");
title.className = "b06-profile__group-legend ui-collapsible__title";
title.textContent = legend;
group.append(title);
return group;
}
/** 반폭 확대(백엔드 재생성)에 필요한 페이지 상태 — 클로저 대신 함수로 받아 결합을 끊는다. */
export interface SampleWidenerDeps {
/** 현재 대상 경로. 없으면 확대 불가. */
+70
View File
@@ -0,0 +1,70 @@
/* =============================================================================
* B06_Section_UI_Page_Panel.ts
* 횡단 보기 좌측 패널의 **칸 정의**(등록부 `registerPanelSection`)와 그 재료 — 횡단 보기 설정 ·
* 표준 횡단면 설정. 「구조물 배치」는 두 보기 공용이라 `design_sections.ts` 에 있다.
*
* 칸 차례(2026-08-29 사용자 지시): 구조물 배치(최상단) > 횡단 보기 설정 > 표준 횡단면 설정 >
* 아래 고정 영역. 페이지(`B06_Section_UI_Page.ts`)는 칸도 단추도 직접 만들지 않고 여기 재료와
* `createDesignPanel` 만 쓴다(계획서 24-2b · 시험 `test_design_view_parity.py`).
* ========================================================================== */
import "../A00_Common/design/design_sections";
import { createSectionFrame, part, registerPanelSection } from "../A00_Common/design/design_panel";
import { createButton, createInputField } from "@ui/ui_template_elements";
import { L } from "./B06_Section_UI_Page_Common";
/** 「횡단 보기 설정」 재료 — 반폭 입력 + 자체 [보기 반폭 적용] 한 줄.
*
* 횡단 반폭은 **자체 컨테이너 + 자체 [보기 반폭 적용] 버튼**이다(2026-08-23 사용자 지시) — 표준
* 횡단면 설정의 [전체 측점 반영]에 물려 있어 반폭만 바꿔도 전 측점 설계 재계산을 기다려야 했다. */
export function createCrossViewPart(onApply: () => void) {
const field = createInputField({
label: L("B05_Route_Field_CrossHalfWidth"),
type: "number",
});
field.input.min = "0.1";
field.input.step = "0.1";
const applyButton = createButton({
label: L("B06_View_Apply"),
variant: "filled",
onClick: onApply,
});
applyButton.title = L("B06_View_Apply_Tip");
// 반폭 입력 + [적용]은 한 줄(2026-08-23 사용자 지시).
const row = document.createElement("div");
row.className = "b06-profile__field-row";
row.append(field.root, applyButton);
return { field, row };
}
export type CrossViewPart = ReturnType<typeof createCrossViewPart>;
/** 「표준 횡단면 설정」 패널이 들어갈 자리 — context 를 받은 뒤 패널을 채운다. */
export function createStandardSlot(): HTMLElement {
return document.createElement("div");
}
registerPanelSection({
id: "cross-view",
title: L("B06_View_Title"),
views: ["cross"],
order: 20,
build: (ctx, view) => {
const frame = createSectionFrame(view, L("B06_View_Title"));
frame.body.append(part<CrossViewPart>(ctx, "crossView").row);
return frame.root;
},
});
registerPanelSection({
id: "standard-cross",
title: L("B06_Std_Title"),
views: ["cross"],
order: 30,
build: (ctx, view) => {
// 표준 횡단면 설정은 기본 접힘(2026-08-23 사용자 지시) — 자주 여는 칸이 아니다.
const frame = createSectionFrame(view, L("B06_Std_Title"), true);
frame.body.append(part<HTMLElement>(ctx, "standardSlot"));
return frame.root;
},
});