/* ============================================================================= * B07_DesignDetail_UI_Settings_Scale.ts * ⚙ 모달 X · Y 축척 칸(485-2 · 요구 280-5) — 종단 · 횡단 · 토적 · 전개도만. * 종단 = 셋째 칸 「평면도」(521-2 · 우측 상단 평면 위치도) — 칸은 서버 기본값 열쇠 그대로. * * 칸 = 분모 정수(늘 지정값 · 자동 없음 523-1) · 범위 밖 = 칸 붉게 + 모달 [확인] 꺼짐 * 저장 = 모달 [확인](`_UI_Settings`)이 `save` 를 부름 — 그 종류 저장본이 있으면 n 장 물음(취소 = 그대로) → PUT * [기본값] = 칸만 기본으로 채움 — 저장은 [확인] * `settingsGroup` = 모달 안 그룹 상자(523-1) — 횡단 배치 · 표도 같은 상자 * ========================================================================== */ import { createInputField, hideLoadingOverlay, showConfirmDialog, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { type DrawingScaleAxes, type DrawingScaleKind, type DrawingScales, type DrawingScaleSend, fetchDrawingScales, saveDrawingScales, } from "./B07_DesignDetail_Api_Fetch"; import type { DrawingSettingsDeps } from "./B07_DesignDetail_UI_Settings"; import { readScaleAxis, scaleToSend } from "./logic/B07_DesignDetail_Scale_Check"; const LABEL: Record = { longitudinal: "종단도", cross: "횡단도", mass_haul: "토적도", development: "전개도", }; type Axis = keyof DrawingScaleAxes; const AXIS_LABEL: Record = { x: "X(가로) 1/", y: "Y(세로) 1/", plan: "평면도 1/" }; const isScaleKind = (kind: string): kind is DrawingScaleKind => kind in LABEL; /** 모달 안 그룹 상자 — 제목(legend) + 칸. */ export function settingsGroup(legend: string, ...children: HTMLElement[]): HTMLFieldSetElement { const box = document.createElement("fieldset"); box.className = "b07-settings__group"; const title = document.createElement("legend"); title.textContent = legend; box.append(title, ...children); return box; } /** 축척 칸 한 벌 — 모달 [확인] · [기본값] 이 씀. */ export interface ScaleFields { /** null = 그대로(못 받음 포함) · "invalid" = 붉은 칸 있음 · 그 밖 = 보낼 값 */ read: () => DrawingScaleSend | null | "invalid"; reset: () => void; /** 저장본 물음 → PUT — 취소 · 실패면 거짓(모달 머묾) */ save: (next: DrawingScaleSend) => Promise; } /** ⚙ 모달 몸에 축척 그룹을 붙임 — 축척 없는 종류는 undefined · `onCheck` = 칸 바뀜(모달 [확인] 켜고 끔). */ export function mountDrawingScale( body: HTMLElement, kind: string, deps: DrawingSettingsDeps, onCheck: () => void, ): ScaleFields | undefined { if (!isScaleKind(kind) || !deps.plan) return undefined; const { projectId } = deps.plan; const status = document.createElement("p"); status.className = "b07-settings__note"; status.textContent = "축척 받는 중…"; const section = settingsGroup( kind === "cross" ? "축척 — 횡단도 장(표준 횡단면도 뺌)" : `축척 — ${LABEL[kind]}`, status, ); section.classList.add("b07-scale"); body.append(section); let built: Omit | undefined; let data: DrawingScales | undefined; void fetchDrawingScales(projectId) .then((got) => { data = got; status.remove(); built = buildScaleFields(section, kind, got, onCheck); onCheck(); }) .catch(() => { status.textContent = "축척을 받지 못했습니다."; }); return { read: () => built?.read() ?? null, reset: () => built?.reset(), save: async (next) => { const saved = data?.saved[kind] ?? 0; const ask = `${LABEL[kind]} 축척을 바꾸면 저장본 ${saved}장이 풀립니다(원본에서 다시 그림). 바꿀까요?`; if (saved > 0 && !(await showConfirmDialog(ask, "바꿈"))) return false; showLoadingOverlay(); try { await saveDrawingScales(projectId, { [kind]: next }); showToast(`${LABEL[kind]} 축척을 바꿨습니다.`, "success"); return true; } catch (error) { showToast(error instanceof Error ? error.message : "축척을 저장하지 못했습니다.", "error"); return false; } finally { hideLoadingOverlay(); } }, }; } function buildScaleFields( section: HTMLElement, kind: DrawingScaleKind, data: DrawingScales, onCheck: () => void, ): Omit { const axes = Object.keys(data.defaults[kind]) as Axis[]; const fields = axes.map((axis) => { const fallback = data.defaults[kind][axis] as number; const range = data.ranges[kind][axis] ?? [1, 1]; const [min, max] = range; const field = createInputField({ label: AXIS_LABEL[axis], type: "number", compact: true, min, max, }); field.root.classList.add("b07-scale__field"); const set = (value: number): void => { field.input.value = String(value); }; set(data.scales[kind][axis] ?? fallback); const read = () => readScaleAxis(field.input.value, range); return { axis, field, fallback, set, read }; }); // 칸 밑 오류 글은 숨기고(틀린 칸만 길어짐) 줄 아래 한 줄로 알림 const alertLine = document.createElement("p"); alertLine.className = "b07-scale__error"; alertLine.setAttribute("role", "alert"); alertLine.hidden = true; const check = (): void => { const errors = new Map(); for (const f of fields) { const result = f.read(); const message = typeof result === "string" ? result : undefined; f.field.setError(message); const name = f.axis === "plan" ? "평면도" : f.axis.toUpperCase(); if (message) errors.set(message, [...(errors.get(message) ?? []), name]); } alertLine.textContent = [...errors].map(([m, axes]) => `${axes.join(" · ")}: ${m}`).join(" / "); alertLine.hidden = errors.size === 0; onCheck(); }; for (const f of fields) f.field.input.addEventListener("input", check); const row = document.createElement("div"); row.className = "b07-scale__row"; row.append(...fields.map((f) => f.field.root)); section.append(row, alertLine); return { read: () => { const read = fields.map((f) => [f, f.read()] as const); if (read.some(([, value]) => typeof value === "string")) return "invalid"; const now = data.scales[kind]; if (read.every(([f, value]) => value === (now[f.axis] ?? f.fallback))) return null; return Object.fromEntries( read.map(([f, v]) => [f.axis, scaleToSend(v as number, f.fallback)]), ) as DrawingScaleSend; }, reset: () => { fields.forEach((f) => f.set(f.fallback)); check(); }, }; }