From 907cfc144bd506b716c7fe9507a8812de6d7a9e8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 11:33:51 +0900 Subject: [PATCH] =?UTF-8?q?refactor(B07):=20=EC=83=81=EC=84=B8=20=EC=84=A4?= =?UTF-8?q?=EA=B3=84=20=ED=99=94=EB=A9=B4=20700=EC=A4=84=20=EC=B4=88?= =?UTF-8?q?=EA=B3=BC=20=EB=B6=84=EB=A6=AC=20=E2=80=94=20=EC=A2=8C=EC=B8=A1?= =?UTF-8?q?=20=ED=8C=A8=EB=84=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 732줄 한 파일을 둘로 나눔 (동작 불변, 순수 이동). - `B07_DesignDetail_UI_Page.ts` 517줄 — CAD 임베드·postMessage 교신·저장/확정 흐름 - `B07_DesignDetail_UI_Panels.ts` 234줄 — 도면 목록 12분류(`DRAWING_GROUPS`)와 측점 설계 정보 카드. DOM 만 만들고 고르기 동작은 콜백으로 받음 검증: 공용 브라우저 B07 화면 실측 — 도면 목록 패널 1개·항목 54개(「설계 도면 49건」), CAD iframe 1개 로드, 콘솔 오류 0. `tsc --noEmit` 통과, tmp/tests 378 passed Co-Authored-By: Claude Opus 5 (1M context) --- B07_DesignDetail/B07_DesignDetail_UI_Page.ts | 229 +---------------- .../B07_DesignDetail_UI_Panels.ts | 234 ++++++++++++++++++ 2 files changed, 241 insertions(+), 222 deletions(-) create mode 100644 B07_DesignDetail/B07_DesignDetail_UI_Panels.ts diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts index 8fd49e2a..1615d984 100644 --- a/B07_DesignDetail/B07_DesignDetail_UI_Page.ts +++ b/B07_DesignDetail/B07_DesignDetail_UI_Page.ts @@ -11,7 +11,6 @@ * ========================================================================== */ import "./B07_DesignDetail_UI_Style.css"; -import { attachCollapsible } from "@ui/ui_template_collapsible"; import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, @@ -21,6 +20,12 @@ import { } from "@ui/ui_template_elements"; import { createWorkflowLayout } from "@ui/ui_template_workflow_layout"; import { CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; +// 좌측 패널(도면 목록·설계 정보)은 파일이 700줄을 넘어 떼어냈다(2026-09-04). +import { + buildDesignInfoPanel, + buildDrawingSidePanel, + isCrossSheet, +} from "./B07_DesignDetail_UI_Panels"; import { workflowSteps } from "../A00_Common/b_page_scaffold"; import { fetchWorkflowState, @@ -34,7 +39,6 @@ import { fetchDesignDrawingList, invalidateDesignDrawing, type CadDrawing, - type CrossDesignInfo, type DesignDrawingItem, type DesignDrawingResponse, type QuantityTable, @@ -83,227 +87,8 @@ const CAD_NAVIGATE_MESSAGE = "aislo:b08:navigate"; const CAD_TOAST_MESSAGE = "aislo:b08:toast"; const CAD_TOAST_ACTION_MESSAGE = "aislo:b08:toast-action"; -/** - * 도면 구성 12분류 (2026-08-29 사용자 확정 순서). - * - * 아직 내용을 만들지 않은 도면은 `blankId`로 서버의 빈 도각 도면에 물린다 - * (2026-09-01 사용자 지시) — 눌리지 않는 회색 버튼으로 두면 고장난 것처럼 보인다. - */ -const DRAWING_GROUPS: readonly { - label: string; - kind?: DesignDrawingItem["kind"]; - blankId?: string; -}[] = [ - { label: "표지", kind: "cover" }, - { label: "계획평면도(지형)", blankId: "blank_plan_terrain" }, - { label: "계획평면도(노선배치도)", blankId: "blank_plan_route" }, - { label: "계획평면도(배치도)", blankId: "blank_plan_layout" }, - { label: "계획평면도(라이다)", blankId: "blank_plan_lidar" }, - { label: "종단면도", kind: "longitudinal" }, - { label: "표준 횡단면도", blankId: "blank_cross_standard" }, - { label: "횡단면도", kind: "cross" }, - { label: "토적도(유토곡선)", kind: "mass_haul" }, - { label: "유역도(배수 유역도)", kind: "watershed" }, - { label: "표준도", blankId: "blank_standard" }, - { label: "용지도", blankId: "blank_landuse" }, -]; - -/** B06 확정 산출물 기반 도면 목록 패널. */ -function buildDrawingSidePanel( - drawings: DesignDrawingItem[], - onSelect: (drawing: DesignDrawingItem) => void, - errorMessage?: string, -): HTMLDivElement { - const panel = document.createElement("div"); - panel.className = "b07-drawing-list"; - const heading = document.createElement("div"); - heading.className = "b07-drawing-list__heading"; - const title = document.createElement("strong"); - title.textContent = "설계 도면"; - const count = document.createElement("span"); - count.textContent = `${drawings.length}건`; - heading.append(title, count); - panel.append(heading); - - if (errorMessage || drawings.length === 0) { - const empty = document.createElement("p"); - empty.className = "b07-drawing-list__empty"; - empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다."; - panel.append(empty); - return panel; - } - - const drawingButton = (drawing: DesignDrawingItem, label: string): HTMLButtonElement => { - const button = document.createElement("button"); - button.type = "button"; - button.className = "b07-drawing-button"; - button.dataset.drawingId = drawing.id; - button.dataset.confirmed = String(drawing.confirmed); - const name = document.createElement("span"); - name.className = "b07-drawing-button__name"; - name.textContent = label; - button.append(name); - button.addEventListener("click", () => onSelect(drawing)); - return button; - }; - - for (const group of DRAWING_GROUPS) { - const items = group.kind - ? drawings.filter((item) => item.kind === group.kind) - : drawings.filter((item) => item.id === group.blankId); - // 한 장짜리(와 아직 내용이 없는 도면)는 컨테이너 없이 버튼 하나로 둔다. - if (items.length <= 1) { - const [drawing] = items; - // 서버가 빈 도각을 내주지 못한 경우에만 회색 버튼으로 남는다. - if (!drawing) { - const placeholder = document.createElement("button"); - placeholder.type = "button"; - placeholder.className = "b07-drawing-button"; - placeholder.disabled = true; - placeholder.title = "준비 중"; - placeholder.dataset.pending = "true"; - const name = document.createElement("span"); - name.className = "b07-drawing-button__name"; - name.textContent = group.label; - placeholder.append(name); - panel.append(placeholder); - continue; - } - const button = drawingButton(drawing, group.label); - // 도각만 있는 도면은 그렇다고 알린다 — 빈 화면을 보고 오류로 오해하지 않게. - button.dataset.pending = String(drawing.kind === "blank"); - if (drawing.kind === "blank") button.title = "준비 중 — 도각만 표시합니다"; - panel.append(button); - continue; - } - const section = document.createElement("section"); - // ui-sidebar-section: 사이드 컨테이너 공통 외곽선(B04~B06과 통일, 2026-08-06 사용자 지시). - section.className = "b07-drawing-group ui-collapsible ui-sidebar-section"; - if (group.kind) section.dataset.kind = group.kind; - const sectionTitle = document.createElement("h3"); - sectionTitle.className = "ui-collapsible__title"; - sectionTitle.textContent = `${group.label} ${items.length}`; - section.append(sectionTitle); - for (const drawing of items) { - // 횡단면도는 장 단위(여러 측점)라 서버가 준 "N장 (구간)" 라벨을 그대로 쓴다. - section.append(drawingButton(drawing, drawing.label)); - } - panel.append(section); - } - attachCollapsible(panel); - return panel; -} - -const GROUND_TYPE_LABEL: Record = { - soil: "B06_Design_Ground_Soil", - ripping_rock: "B06_Design_Ground_Ripping", - blasting_rock: "B06_Design_Ground_Blasting", -}; - -/** 단면유형에서 절토측 표기를 유도한다. */ -function cutSideLabel(mode: CrossDesignInfo["section_mode"]): string { - if (mode === "left_cut") return L("B06_Design_Ditch_Left"); - if (mode === "right_cut") return L("B06_Design_Ditch_Right"); - if (mode === "both_cut") return L("B06_Design_Mode_BothCut"); - return L("B06_Design_Mode_BothFill"); -} - -/** 장(여러 측점을 담은 횡단 도면)인가 — id 가 `cross_s…` 면 장이다. */ -function isCrossSheet(drawing: DesignDrawingItem): boolean { - return drawing.kind === "cross" && drawing.id.startsWith("cross_s"); -} - -/** 측구 규격 표시 문자열 (design 신구조: 형식별 ditch spec, F-2 호환). */ -function ditchLabel(design: CrossDesignInfo): string { - const ditch = design.ditch; - if (!ditch || ditch.type === "none" || design.ditch_enabled === false) return "없음"; - if (ditch.type === "l_type") - return `L형 ${ditch.width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`; - return `${ditch.top_width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`; -} - -function infoRow(label: string, value: string): HTMLElement { - const row = document.createElement("div"); - row.className = "b07-info__row"; - const key = document.createElement("span"); - key.className = "b07-info__key"; - key.textContent = label; - const val = document.createElement("span"); - val.className = "b07-info__val"; - val.textContent = value; - row.append(key, val); - return row; -} - -/** - * 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산). - * - * **장(여러 측점을 담은 도면)에는 측점 단위 값이 없다** — 서버가 `design` 을 넘기지 - * 않는데도 제목만 「측점 …」으로 달려 어느 측점 값인지 오해됐다(2026-09-03 정리). - * 장이면 제목을 「장」으로 바꾸고 어디서 봐야 하는지 한 줄로 알린다. - */ -function buildDesignInfoPanel( - title: string, - design: CrossDesignInfo | null, - scope: "station" | "sheet" = "station", -): HTMLElement { - const panel = document.createElement("div"); - panel.className = "b07-info"; - const heading = document.createElement("div"); - heading.className = "b07-info__heading"; - const stationName = document.createElement("strong"); - const scopeLabel = scope === "sheet" ? L("B07_Info_Sheet") : L("B07_Info_Station"); - stationName.textContent = `${scopeLabel} ${title}`; - const confirmed = design?.status === "confirmed"; - const badge = document.createElement("span"); - badge.className = `b07-info__badge${confirmed ? " b07-info__badge--confirmed" : ""}`; - badge.textContent = confirmed ? L("B07_Info_Confirmed") : L("B07_Info_Provisional"); - heading.append(stationName, badge); - panel.append(heading); - - if (!design) { - const empty = document.createElement("p"); - empty.className = "b07-info__empty"; - empty.textContent = scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign"); - panel.append(empty); - return panel; - } - - const ground = document.createElement("section"); - ground.className = "b07-info__block"; - const groundTitle = document.createElement("h4"); - groundTitle.textContent = L("B07_Info_Ground_Title"); - ground.append( - groundTitle, - infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])), - infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)), - infoRow( - L("B07_Info_DitchSide"), - design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"), - ), - ); - - const plan = document.createElement("section"); - plan.className = "b07-info__block"; - const planTitle = document.createElement("h4"); - planTitle.textContent = L("B07_Info_Plan_Title"); - plan.append( - planTitle, - infoRow(L("B07_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`), - infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`), - infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`), - infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`), - infoRow(L("B07_Info_Ditch"), ditchLabel(design)), - infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`), - infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`), - ); - - panel.append(ground, plan); - return panel; -} - /* ----------------------------------------------------------------------------- - * 페이지 진입점 + * 화면 조립 * -------------------------------------------------------------------------- */ export async function renderB07DesignDetail(root: HTMLElement): Promise { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); diff --git a/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts new file mode 100644 index 00000000..bf045511 --- /dev/null +++ b/B07_DesignDetail/B07_DesignDetail_UI_Panels.ts @@ -0,0 +1,234 @@ +/* ============================================================================= + * B07_DesignDetail_UI_Panels.ts + * 상세 설계 화면의 좌측 패널 — 도면 목록 12분류, 측점 설계 정보 카드. + * + * 화면 조립(`B07_DesignDetail_UI_Page.ts`)이 700줄을 넘어 떼어냈다(2026-09-04). + * DOM 만 만들고 상태는 갖지 않는다 — 고르기 동작은 인자로 받은 콜백에 넘긴다. + * ========================================================================== */ + +import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; +import type { CrossDesignInfo, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch"; + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +/** + * 도면 구성 12분류 (2026-08-29 사용자 확정 순서). + * + * 아직 내용을 만들지 않은 도면은 `blankId`로 서버의 빈 도각 도면에 물린다 + * (2026-09-01 사용자 지시) — 눌리지 않는 회색 버튼으로 두면 고장난 것처럼 보인다. + */ +export const DRAWING_GROUPS: readonly { + label: string; + kind?: DesignDrawingItem["kind"]; + blankId?: string; +}[] = [ + { label: "표지", kind: "cover" }, + { label: "계획평면도(지형)", blankId: "blank_plan_terrain" }, + { label: "계획평면도(노선배치도)", blankId: "blank_plan_route" }, + { label: "계획평면도(배치도)", blankId: "blank_plan_layout" }, + { label: "계획평면도(라이다)", blankId: "blank_plan_lidar" }, + { label: "종단면도", kind: "longitudinal" }, + { label: "표준 횡단면도", blankId: "blank_cross_standard" }, + { label: "횡단면도", kind: "cross" }, + { label: "토적도(유토곡선)", kind: "mass_haul" }, + { label: "유역도(배수 유역도)", kind: "watershed" }, + { label: "표준도", blankId: "blank_standard" }, + { label: "용지도", blankId: "blank_landuse" }, +]; + +/** B06 확정 산출물 기반 도면 목록 패널. */ +export function buildDrawingSidePanel( + drawings: DesignDrawingItem[], + onSelect: (drawing: DesignDrawingItem) => void, + errorMessage?: string, +): HTMLDivElement { + const panel = document.createElement("div"); + panel.className = "b07-drawing-list"; + const heading = document.createElement("div"); + heading.className = "b07-drawing-list__heading"; + const title = document.createElement("strong"); + title.textContent = "설계 도면"; + const count = document.createElement("span"); + count.textContent = `${drawings.length}건`; + heading.append(title, count); + panel.append(heading); + + if (errorMessage || drawings.length === 0) { + const empty = document.createElement("p"); + empty.className = "b07-drawing-list__empty"; + empty.textContent = errorMessage ?? "확정된 종·횡단 도면이 없습니다."; + panel.append(empty); + return panel; + } + + const drawingButton = (drawing: DesignDrawingItem, label: string): HTMLButtonElement => { + const button = document.createElement("button"); + button.type = "button"; + button.className = "b07-drawing-button"; + button.dataset.drawingId = drawing.id; + button.dataset.confirmed = String(drawing.confirmed); + const name = document.createElement("span"); + name.className = "b07-drawing-button__name"; + name.textContent = label; + button.append(name); + button.addEventListener("click", () => onSelect(drawing)); + return button; + }; + + for (const group of DRAWING_GROUPS) { + const items = group.kind + ? drawings.filter((item) => item.kind === group.kind) + : drawings.filter((item) => item.id === group.blankId); + // 한 장짜리(와 아직 내용이 없는 도면)는 컨테이너 없이 버튼 하나로 둔다. + if (items.length <= 1) { + const [drawing] = items; + // 서버가 빈 도각을 내주지 못한 경우에만 회색 버튼으로 남는다. + if (!drawing) { + const placeholder = document.createElement("button"); + placeholder.type = "button"; + placeholder.className = "b07-drawing-button"; + placeholder.disabled = true; + placeholder.title = "준비 중"; + placeholder.dataset.pending = "true"; + const name = document.createElement("span"); + name.className = "b07-drawing-button__name"; + name.textContent = group.label; + placeholder.append(name); + panel.append(placeholder); + continue; + } + const button = drawingButton(drawing, group.label); + // 도각만 있는 도면은 그렇다고 알린다 — 빈 화면을 보고 오류로 오해하지 않게. + button.dataset.pending = String(drawing.kind === "blank"); + if (drawing.kind === "blank") button.title = "준비 중 — 도각만 표시합니다"; + panel.append(button); + continue; + } + const section = document.createElement("section"); + // ui-sidebar-section: 사이드 컨테이너 공통 외곽선(B04~B06과 통일, 2026-08-06 사용자 지시). + section.className = "b07-drawing-group ui-collapsible ui-sidebar-section"; + if (group.kind) section.dataset.kind = group.kind; + const sectionTitle = document.createElement("h3"); + sectionTitle.className = "ui-collapsible__title"; + sectionTitle.textContent = `${group.label} ${items.length}`; + section.append(sectionTitle); + for (const drawing of items) { + // 횡단면도는 장 단위(여러 측점)라 서버가 준 "N장 (구간)" 라벨을 그대로 쓴다. + section.append(drawingButton(drawing, drawing.label)); + } + panel.append(section); + } + attachCollapsible(panel); + return panel; +} + +const GROUND_TYPE_LABEL: Record = { + soil: "B06_Design_Ground_Soil", + ripping_rock: "B06_Design_Ground_Ripping", + blasting_rock: "B06_Design_Ground_Blasting", +}; + +/** 단면유형에서 절토측 표기를 유도한다. */ +function cutSideLabel(mode: CrossDesignInfo["section_mode"]): string { + if (mode === "left_cut") return L("B06_Design_Ditch_Left"); + if (mode === "right_cut") return L("B06_Design_Ditch_Right"); + if (mode === "both_cut") return L("B06_Design_Mode_BothCut"); + return L("B06_Design_Mode_BothFill"); +} + +/** 장(여러 측점을 담은 횡단 도면)인가 — id 가 `cross_s…` 면 장이다. */ +export function isCrossSheet(drawing: DesignDrawingItem): boolean { + return drawing.kind === "cross" && drawing.id.startsWith("cross_s"); +} + +/** 측구 규격 표시 문자열 (design 신구조: 형식별 ditch spec, F-2 호환). */ +function ditchLabel(design: CrossDesignInfo): string { + const ditch = design.ditch; + if (!ditch || ditch.type === "none" || design.ditch_enabled === false) return "없음"; + if (ditch.type === "l_type") + return `L형 ${ditch.width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`; + return `${ditch.top_width_m.toFixed(2)}×${ditch.depth_m.toFixed(2)}m`; +} + +function infoRow(label: string, value: string): HTMLElement { + const row = document.createElement("div"); + row.className = "b07-info__row"; + const key = document.createElement("span"); + key.className = "b07-info__key"; + key.textContent = label; + const val = document.createElement("span"); + val.className = "b07-info__val"; + val.textContent = value; + row.append(key, val); + return row; +} + +/** + * 선택 횡단도의 지반정보/계획정보를 2분할로 렌더한다 (잠정치, B07 확정 시 재계산). + * + * **장(여러 측점을 담은 도면)에는 측점 단위 값이 없다** — 서버가 `design` 을 넘기지 + * 않는데도 제목만 「측점 …」으로 달려 어느 측점 값인지 오해됐다(2026-09-03 정리). + * 장이면 제목을 「장」으로 바꾸고 어디서 봐야 하는지 한 줄로 알린다. + */ +export function buildDesignInfoPanel( + title: string, + design: CrossDesignInfo | null, + scope: "station" | "sheet" = "station", +): HTMLElement { + const panel = document.createElement("div"); + panel.className = "b07-info"; + const heading = document.createElement("div"); + heading.className = "b07-info__heading"; + const stationName = document.createElement("strong"); + const scopeLabel = scope === "sheet" ? L("B07_Info_Sheet") : L("B07_Info_Station"); + stationName.textContent = `${scopeLabel} ${title}`; + const confirmed = design?.status === "confirmed"; + const badge = document.createElement("span"); + badge.className = `b07-info__badge${confirmed ? " b07-info__badge--confirmed" : ""}`; + badge.textContent = confirmed ? L("B07_Info_Confirmed") : L("B07_Info_Provisional"); + heading.append(stationName, badge); + panel.append(heading); + + if (!design) { + const empty = document.createElement("p"); + empty.className = "b07-info__empty"; + empty.textContent = scope === "sheet" ? L("B07_Info_SheetHint") : L("B07_Info_NoDesign"); + panel.append(empty); + return panel; + } + + const ground = document.createElement("section"); + ground.className = "b07-info__block"; + const groundTitle = document.createElement("h4"); + groundTitle.textContent = L("B07_Info_Ground_Title"); + ground.append( + groundTitle, + infoRow(L("B07_Info_GroundType"), L(GROUND_TYPE_LABEL[design.ground_type])), + infoRow(L("B07_Info_CutSide"), cutSideLabel(design.section_mode)), + infoRow( + L("B07_Info_DitchSide"), + design.ditch_side === "left" ? L("B06_Design_Ditch_Left") : L("B06_Design_Ditch_Right"), + ), + ); + + const plan = document.createElement("section"); + plan.className = "b07-info__block"; + const planTitle = document.createElement("h4"); + planTitle.textContent = L("B07_Info_Plan_Title"); + plan.append( + planTitle, + infoRow(L("B07_Info_DesignElevation"), `${design.design_elevation_m.toFixed(2)}m`), + infoRow(L("B07_Info_CutSlope"), `1:${design.cut_slope_ratio}`), + infoRow(L("B07_Info_FillSlope"), `1:${design.fill_slope_ratio}`), + infoRow(L("B07_Info_RoadWidth"), `${design.roadbed_width_m.toFixed(2)}m`), + infoRow(L("B07_Info_Ditch"), ditchLabel(design)), + infoRow(L("B07_Info_CutArea"), `${design.cut_area_m2.toFixed(2)}㎡`), + infoRow(L("B07_Info_FillArea"), `${design.fill_area_m2.toFixed(2)}㎡`), + ); + + panel.append(ground, plan); + return panel; +}