/* ============================================================================= * B07_DesignDetail_UI_PlanManage.ts * [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음. * * 왼쪽 = 종류 목록(× 지우기) · 종류 추가 · 업데이트 폼 · 대피소 · 사토장 자리 · 평면도 장 목록 * 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 장 바꾸기) · [확정 풀기] · [CAD 에서 열기] * 고치기는 지금 서버 길 그대로(종류 파일 · 확정 풀기) — 고칠 때마다 모달 안을 다시 받고, * 닫을 때 `onClosed(changed)` 로 좌측 목록 · 도면을 다시 읽음. * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createCadHost } from "@ui/cad_host/cad_host"; import { deletePlanKind, fetchDesignDrawing, fetchDesignDrawingList, fetchPlanKinds, invalidateDesignDrawing, type CadDrawing, type DesignDrawingItem, type PlanKindsState, } from "./B07_DesignDetail_Api_Fetch"; import { groupPlanSheets, kindForm, marksForm, run, type PlanKindsControls, } from "./B07_DesignDetail_UI_PlanKinds"; export interface PlanManageControls { projectId: string; /** 모달을 닫음 — changed = 종류 · 자리 · 확정을 고쳤나 · openId = [CAD 에서 열기]로 고른 장. */ onClosed: (changed: boolean, openId?: string) => void; } function el( tag: K, className: string, text = "", ): HTMLElementTagNameMap[K] { const node = document.createElement(tag); node.className = className; if (text) node.textContent = text; return node; } function button(className: string, text: string, onClick: () => void): HTMLButtonElement { const node = el("button", className, text); node.type = "button"; node.addEventListener("click", onClick); return node; } export function openPlanManager(controls: PlanManageControls): void { const { projectId } = controls; let changed = false; let closed = false; /** 미리보기 중인 장 — 늦게 온 응답이 다른 장을 덮지 않게 이것과 맞춰 봄. */ let picked: string | undefined; const preview = createCadHost({ title: "평면도 미리보기" }); const side = el("div", "b07-plan-manage__side"); const head = el("div", "b07-plan-manage__head"); const view = el("div", "b07-plan-manage__view"); view.append(head, preview.element); // 종류 · 자리 폼은 좌측에 있던 것 그대로 — 고친 뒤 모달 안만 다시 세움(화면은 닫을 때). const edits: PlanKindsControls = { projectId, onChanged: () => { changed = true; void refresh(); }, }; const kindList = (state: PlanKindsState, sheets: DesignDrawingItem[]): HTMLElement => { const list = el("div", "b07-plan-tools__block b07-plan-manage__kinds"); list.append(el("strong", "", "종류 목록")); for (const kind of state.kinds) { const row = el("div", "b07-plan-kind__head"); row.dataset.planKind = kind.id; const count = sheets.filter((item) => item.plan_kind === kind.id).length; row.append(el("span", "b07-plan-kind__name", `${kind.name} · ${count}장`)); const remove = button("b07-plan-tools__remove", "×", () => { if ( !window.confirm(`「${kind.name}」 종류를 지웁니다. 다시 쓰려면 「새 종류」로 추가합니다.`) ) return; void run(() => deletePlanKind(projectId, kind.id), edits); }); remove.title = `「${kind.name}」 종류 지우기`; row.append(remove); list.append(row); } return list; }; const sheetList = (sheets: DesignDrawingItem[]): HTMLElement => { const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets"); list.append(el("strong", "", "평면도 장")); for (const group of groupPlanSheets(sheets)) { for (const { drawing, label } of group.sheets) { const item = button("b07-drawing-button", label, () => void show(drawing, label)); item.dataset.drawingId = drawing.id; item.dataset.confirmed = String(drawing.confirmed); list.append(item); } } return list; }; const markActive = (): void => side .querySelectorAll(".b07-plan-manage__sheets .b07-drawing-button") .forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked))); /** 장 하나를 미리보기에 — 보기 전용 · 확정이면 [확정 풀기](지금 [수정]과 같은 서버 길). */ const show = async (drawing: DesignDrawingItem, label: string): Promise => { picked = drawing.id; markActive(); head.replaceChildren(el("strong", "", label)); preview.beginLoading(); try { const response = await fetchDesignDrawing(projectId, drawing.id); if (closed || picked !== drawing.id) return; const badge = el("span", "b07-plan-manage__badge", response.confirmed ? "확정" : "확정 전"); badge.dataset.confirmed = String(response.confirmed); head.append(badge); if (response.confirmed) head.append( button("b07-plan-tools__button", "확정 풀기", () => { void run(() => invalidateDesignDrawing(projectId, drawing.id), edits); }), ); head.append(button("b07-plan-tools__button", "CAD 에서 열기", () => finish(drawing.id))); preview.load( response.drawing, null, false, {}, { recoveryScope: "b07-plan-preview", readOnly: true, viewOnly: true, hostTitle: "평면도 미리보기", ribbonCollapsed: true, panelsCollapsed: true, }, ); } catch (error) { if (closed || picked !== drawing.id) return; preview.fail(error instanceof Error ? error.message : "평면도를 불러오지 못했습니다."); } }; /** 종류 · 장 목록을 서버에서 다시 받아 왼쪽을 세우고 보던 장(없으면 첫 장)을 다시 봄. */ const refresh = async (): Promise => { try { const [state, list] = await Promise.all([ fetchPlanKinds(projectId), fetchDesignDrawingList(projectId), ]); if (closed) return; const sheets = list.drawings.filter((item) => item.kind === "plan"); side.replaceChildren( kindList(state, sheets), kindForm(state, edits), marksForm(state, edits), sheetList(sheets), ); const all = groupPlanSheets(sheets).flatMap((group) => group.sheets); const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0]; if (keep) void show(keep.drawing, keep.label); else { picked = undefined; head.replaceChildren(el("span", "b07-plan-kind__name", "평면도 장이 없습니다.")); } } catch (error) { showToast(error instanceof Error ? error.message : "평면도 목록을 받지 못했습니다.", "error"); } }; let closeModal = (): void => undefined; /** 한 번만 — 미리보기 듣기를 멈추고 페이지에 알림. */ const finish = (openId?: string): void => { if (closed) return; closed = true; preview.destroy(); closeModal(); controls.onClosed(changed, openId); }; openModal({ title: "평면도 관리", closeLabel: "닫기", dialogClass: "b07-plan-manage", beforeClose: () => { finish(); return true; }, mount: (body, close) => { closeModal = close; const grid = el("div", "b07-plan-manage__grid"); grid.append(side, view); body.append(grid); void refresh(); }, }); }