/* ============================================================================= * 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; /** 축척 고정으로 장이 나뉘는 도면 — `plan_route`, `plan_route_2` … 를 한 묶음으로 본다. */ idPrefix?: string; }[] = [ { label: "표지", kind: "cover" }, { label: "계획평면도(지형)", idPrefix: "plan_terrain" }, { label: "계획평면도(노선배치도)", idPrefix: "plan_route" }, { label: "계획평면도(배치도)", idPrefix: "plan_layout" }, { label: "계획평면도(라이다)", idPrefix: "plan_lidar" }, { label: "종단면도", kind: "longitudinal" }, { label: "표준 횡단면도", kind: "cross_standard" }, { label: "횡단면도", kind: "cross" }, { label: "토적도(유토곡선)", kind: "mass_haul" }, { label: "유역도(배수 유역도)", kind: "watershed" }, { label: "용지도", idPrefix: "landuse" }, ]; /** B06 확정 산출물 기반 도면 목록 패널. */ export function buildDrawingSidePanel( drawings: DesignDrawingItem[], onSelect: (drawing: DesignDrawingItem) => void, errorMessage?: string, devBypass = false, ): 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); // 확정을 건너뛴 상태라는 것을 **조용히 두지 않는다** — 그냥 열면 다음 사람이 // 「왜 값이 없나」로 헤맨다(2026-09-09). 되돌리기는 B08 좌측 [확정 없이 다음으로] 옆. if (devBypass) { const notice = document.createElement("p"); notice.className = "b07-drawing-list__bypass"; notice.textContent = "확정을 건너뛴 상태입니다 — 개발 전용. 값이 비어 보일 수 있습니다."; panel.append(notice); } 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) : group.idPrefix ? drawings.filter( (item) => item.id === group.idPrefix || item.id.startsWith(`${group.idPrefix}_`), ) : 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분할로 렌더한다 (B06 정본 값 · 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; }