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:
@@ -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 — 두 화면이 한 페이지처럼).
|
||||
|
||||
@@ -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 {
|
||||
/** 현재 대상 경로. 없으면 확대 불가. */
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user