/* ============================================================================= * B07_DesignDetail_UI_PlanManage.ts * [평면도 관리] 모달(PLAN 130-1) — 좌측 패널이 좁아 종류 · 대피소 자리 · 장 보기를 한 창에 모음. * * 왼쪽 = [종류 ▼] 상세 폼(추가 · 업데이트 · 지우기) · 평면도 장(종류마다 한 줄 [≡] 이름 [1][2] · * 줄 끌어 놓기 = 종류 순서 · 놓는 즉시 저장 140-2) · 대피소 · 사토장 자리 * 오른쪽 = 고른 장 미리보기(보기 전용 CAD · 캔버스만 `bare`) · 확정 배지 · [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 { fetchDesignDrawing, fetchDesignDrawingList, fetchPlanKinds, reorderPlanKinds, type CadDrawing, type DesignDrawingItem, type PlanKindsState, } from "./B07_DesignDetail_Api_Fetch"; import { groupPlanSheets, kindForm, marksForm, run, type KindForm, 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; /** 드롭다운 · 줄 강조의 종류 — 장 단추를 누르면 그 장의 종류로 따라감. */ let pickedKind = ""; let form: KindForm | undefined; let sheets: DesignDrawingItem[] = []; 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 kindSheets = (kindId: string) => groupPlanSheets(sheets.filter((item) => item.plan_kind === kindId))[0]?.sheets ?? []; /** 장 단추 → 미리보기 + 드롭다운이 그 종류로. */ const pickSheet = (drawing: DesignDrawingItem, label: string): void => { pickedKind = drawing.plan_kind ?? ""; form?.pick(pickedKind); void show(drawing, label); }; /** 「평면도 장」 한 블록 — 종류마다 한 줄 [≡] 이름 [1][2] · 줄 끌어 놓기 = 종류 순서(놓는 즉시). */ const sheetBlock = (state: PlanKindsState): HTMLElement => { const list = el("div", "b07-plan-tools__block b07-plan-manage__sheets"); list.append(el("strong", "", "평면도 장")); const ids = state.kinds.map((kind) => kind.id); let dragId: string | null = null; for (const kind of state.kinds) { const row = el("div", "b07-plan-kind__head b07-plan-manage__row"); row.dataset.planKind = kind.id; row.draggable = true; const grip = el("span", "b07-plan-manage__grip", "≡"); grip.title = "끌어 놓아 종류 순서 바꾸기"; const group = kindSheets(kind.id); // 한 장이면 이름이 곧 장 단추 · 여러 장이면 이름 + 작은 장 단추 const name = group.length === 1 ? button("b07-plan-manage__sheet b07-plan-kind__name", kind.name, () => pickSheet(group[0].drawing, group[0].label), ) : el("span", "b07-plan-kind__name", kind.name); if (group.length === 1) { name.dataset.drawingId = group[0].drawing.id; name.dataset.confirmed = String(group[0].drawing.confirmed); } row.append(grip, name); if (group.length > 1) group.forEach(({ drawing, label }, index) => { const item = button("b07-plan-manage__sheet", String(index + 1), () => pickSheet(drawing, label), ); item.title = label; item.dataset.drawingId = drawing.id; item.dataset.confirmed = String(drawing.confirmed); row.append(item); }); // 끌기 꼴 = 공용 탭 줄(`cad_host_tabs.ts`) — 세로라 위 · 아래 반으로 앞 · 뒤 row.addEventListener("dragstart", (event) => { dragId = kind.id; event.dataTransfer?.setData("text/plain", kind.id); if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; }); row.addEventListener("dragend", () => (dragId = null)); row.addEventListener("dragover", (event) => { if (!dragId) return; event.preventDefault(); const box = row.getBoundingClientRect(); row.dataset.drop = event.clientY > box.top + box.height / 2 ? "after" : "before"; }); row.addEventListener("dragleave", () => delete row.dataset.drop); row.addEventListener("drop", (event) => { event.preventDefault(); const after = row.dataset.drop === "after"; delete row.dataset.drop; if (!dragId || dragId === kind.id) return; const order = ids.filter((id) => id !== dragId); order.splice(order.indexOf(kind.id) + (after ? 1 : 0), 0, dragId); dragId = null; if (order.join() !== ids.join()) void run(() => reorderPlanKinds(projectId, order), edits); }); list.append(row); } return list; }; const markActive = (): void => { side .querySelectorAll(".b07-plan-manage__sheet") .forEach((item) => (item.dataset.active = String(item.dataset.drawingId === picked))); side .querySelectorAll(".b07-plan-manage__row") .forEach((row) => (row.dataset.active = String(row.dataset.planKind === pickedKind))); }; /** 장 하나를 미리보기에 — 보기 전용 · 확정 풀기는 메인 [수정](140-1). */ 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); 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, bare: 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; sheets = list.drawings.filter((item) => item.kind === "plan"); const all = groupPlanSheets(sheets).flatMap((group) => group.sheets); const keep = all.find((sheet) => sheet.drawing.id === picked) ?? all[0]; if (keep) pickedKind = keep.drawing.plan_kind ?? ""; // 드롭다운으로 고른 종류 → 그 종류 첫 장 미리보기 + 줄 강조 form = kindForm(state, edits, pickedKind, (kindId) => { pickedKind = kindId; const first = kindSheets(kindId)[0]; if (first) void show(first.drawing, first.label); else markActive(); }); side.replaceChildren(form.root, sheetBlock(state), marksForm(state, edits)); 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(); }, }); }