/* ============================================================================= * B07_DesignDetail_UI_Settings.ts * 좌측 도면 묶음 ⚙ 설정 모달(요구 280-5) — 공용 `openModal` · 머리 [원본에서 다시 그리기](281). * * 평면도 = 평면도 관리 모달 그대로(`openPlanManager`) * 그 밖 = 그룹 상자(523-1) + 맨 아래 단추 줄 하나 — [안내선 숨기기](횡단) · [기본값](축척) · [취소] · [확인] * 횡단도 = 「장 배치」(열 · 행 · 적용 이 장 / 모든 장) · 「표」(표 배율) · 축척 * 구조물 도면 = 「M02 그림 — 축척 없음」 한 줄 · 종단 · 횡단 · 토적 · 전개도 = X · Y 축척 칸(`_UI_Settings_Scale`) * [확인] = 바뀐 것만 저장(축척 → 배치) 뒤 닫고 다시 그림 · 바뀐 것 없으면 닫기만 · 붉은 칸 있으면 꺼짐 * [원본에서 다시 그리기] = 지금 연 도면이 그 묶음일 때만 켬 * ========================================================================== */ import { createButton, createInputField, createSelectField } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import type { CrossLayout, DesignDrawingItem } from "./B07_DesignDetail_Api_Fetch"; import { DRAWING_GROUPS, isCrossSheet } from "./B07_DesignDetail_UI_Panels"; import { openPlanManager, type PlanManageControls } from "./B07_DesignDetail_UI_PlanManage"; import { mountDrawingScale, settingsGroup } from "./B07_DesignDetail_UI_Settings_Scale"; type Kind = DesignDrawingItem["kind"]; export interface DrawingSettingsDeps { drawings: DesignDrawingItem[]; current: () => DesignDrawingItem | undefined; /** 지금 도면에 버릴 것(저장본 · 고친 것)이 있나 */ canRedraw: () => boolean; /** 원본에서 다시 그리기 — 해냈으면 참(모달 닫음) */ redraw: () => Promise; plan?: PlanManageControls; cross: { layout: () => Promise; /** 배치 저장 — 모달은 닫힌 뒤 · `sheet` = 지금 장 번호(장 아니면 0) */ save: (next: CrossLayout, sheet: number) => void; guideToggle: HTMLButtonElement; }; } /** ⚙ 묶음에 드는 도면 종류 — 횡단도 카드엔 표준 횡단면도 줄이 같이 있음. */ const memberOf = (kind: Kind): Kind[] => (kind === "cross" ? ["cross", "cross_standard"] : [kind]); /** 표 · 글 배율 범위(180-2) — 서버 `CROSS_SCALE_RANGE` 와 같음. */ const CROSS_SCALE_MIN = 0.5; const CROSS_SCALE_MAX = 2; const note = (text: string): HTMLElement => { const line = document.createElement("p"); line.className = "b07-settings__note"; line.textContent = text; return line; }; /** 묶음 `kind` 의 ⚙ 모달을 엶. */ export function openDrawingSettings(kind: Kind, deps: DrawingSettingsDeps): void { const current = deps.current(); const mine = !!current && memberOf(kind).includes(current.kind); let closeModal = (): void => undefined; const redraw = createButton({ label: "원본에서 다시 그리기", variant: "ghost", disabled: !(mine && deps.canRedraw()), onClick: () => { void deps.redraw().then((done) => done && closeModal()); }, }); redraw.title = !mine ? "이 묶음 도면을 연 뒤 쓸 수 있음" : redraw.disabled ? "이미 원본 그대로" : "저장본 · 고친 것을 버리고 B06 원본에서 새로 그림"; redraw.classList.add("b07-settings__redraw"); if (kind === "plan" && deps.plan) { const manager = openPlanManager({ ...deps.plan, drawings: deps.drawings, actions: [redraw] }); closeModal = manager.close; return; } const label = DRAWING_GROUPS.find((group) => group.kind === kind)?.label ?? "도면"; openModal({ title: `${label} 설정`, closeLabel: "닫기", dialogClass: "b07-settings", actions: [redraw], mount: (body, close) => { closeModal = close; if (mine) body.append(note(`지금 도면: ${current?.label}`)); if (kind === "structure") body.append(settingsGroup("축척", note("M02 그림 — 축척 없음"))); const confirm = createButton({ label: "확인", onClick: () => void apply() }); const refresh = (): void => { confirm.disabled = scale?.read() === "invalid" || cross?.read() === "invalid"; }; const cross = kind === "cross" ? mountCross(body, deps, refresh) : undefined; const scale = mountDrawingScale(body, kind, deps, refresh); const apply = async (): Promise => { const nextScale = scale?.read() ?? null; const nextLayout = cross?.read() ?? null; if (nextScale === "invalid" || nextLayout === "invalid") return; if (nextScale && !(await scale!.save(nextScale))) return; close(); if (nextLayout) deps.cross.save(nextLayout, cross!.sheet); else if (nextScale) deps.plan?.onClosed(true, deps.current()?.id); // 목록 · 도면 다시 받음 }; const cancel = createButton({ label: "취소", variant: "ghost", onClick: close }); cancel.classList.add("b07-settings__cancel"); const footer = document.createElement("div"); footer.className = "b07-settings__actions"; if (cross) footer.append(deps.cross.guideToggle); if (scale) { const reset = createButton({ label: "기본값", variant: "ghost", onClick: scale.reset }); reset.title = "축척 칸을 기본으로 채움 — [확인]으로 씀"; footer.append(reset); } footer.append(cancel, confirm); body.append(footer); }, }); } interface CrossFields { sheet: number; /** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 · 그 밖 = 저장할 배치 */ read: () => CrossLayout | null | "invalid"; } /** 횡단 「장 배치」 · 「표」 그룹 — 칸은 배치를 받은 뒤 채움. */ function mountCross( body: HTMLElement, deps: DrawingSettingsDeps, onCheck: () => void, ): CrossFields { const current = deps.current(); const sheet = current && isCrossSheet(current) ? deps.drawings.filter(isCrossSheet).findIndex((item) => item.id === current.id) + 1 : 0; const number = (label: string, min: number, max: number) => { const field = createInputField({ label, type: "number", min, max, compact: true }); field.input.disabled = true; // 받기 전 field.input.addEventListener("input", onCheck); return field; }; const cols = number("배치 열", 1, 10); const rows = number("행", 1, 10); const scope = createSelectField({ label: "적용", compact: true, options: [ ...(sheet ? [{ value: "sheet", text: "이 장만" }] : []), { value: "all", text: "모든 장" }, ], value: sheet ? "sheet" : "all", }); scope.select.title = "이 장만 = 지금 장 · 모든 장 = 모든 장을 이 배치로(장마다 값 지움)"; const table = number("표 배율", CROSS_SCALE_MIN, CROSS_SCALE_MAX); table.input.setAttribute("step", "0.1"); table.root.title = "모든 장의 표 · 표 위 글 크기(기본 1)"; const line = (...items: HTMLElement[]): HTMLElement => { const row = document.createElement("div"); row.className = "b07-cross-layout__row"; row.append(...items); return row; }; const layoutBox = settingsGroup("장 배치", line(cols.root, rows.root), line(scope.root)); const tableBox = settingsGroup("표", line(table.root)); layoutBox.classList.add("b07-cross-layout"); tableBox.classList.add("b07-cross-layout"); body.append(layoutBox, tableBox); let layout: CrossLayout | undefined; let shown: [number, number] = [0, 0]; void deps.cross .layout() .then((got) => { layout = got; shown = (sheet && got.sheets[String(sheet)]) || got.default; cols.input.value = String(shown[0]); rows.input.value = String(shown[1]); table.input.value = String(got.table_scale ?? 1); for (const field of [cols, rows, table]) field.input.disabled = false; }) .catch(() => layoutBox.append(note("배치를 받지 못했습니다."))); return { sheet, read: () => { if (!layout) return null; const grid: [number, number] = [Number(cols.input.value), Number(rows.input.value)]; const tableScale = Number(table.input.value); const badGrid = !grid.every((n) => Number.isInteger(n) && n >= 1 && n <= 10); const badTable = !(tableScale >= CROSS_SCALE_MIN && tableScale <= CROSS_SCALE_MAX); cols.setError(badGrid ? "1~10 정수" : undefined); rows.setError(badGrid ? "1~10 정수" : undefined); table.setError(badTable ? `${CROSS_SCALE_MIN}~${CROSS_SCALE_MAX}` : undefined); if (badGrid || badTable) return "invalid"; let next = layout; const touched = grid[0] !== shown[0] || grid[1] !== shown[1] || scope.select.value !== (sheet ? "sheet" : "all"); if (touched && scope.select.value === "sheet") { next = { ...next, sheets: { ...next.sheets, [String(sheet)]: grid } }; } else if (touched) { next = { ...next, default: grid, sheets: {} }; } if (tableScale !== (layout.table_scale ?? 1)) next = { ...next, table_scale: tableScale }; return next === layout ? null : next; }, }; }