/* ============================================================================= * ui_template_item_picker_edit.ts * 항목 고르기 모달의 **편집 화면**(계획서 25-2d) — 카드 [수정] 이 모달 안에서 이 화면으로 바뀜. * 도면 = M02 CAD 부품 · 면적산출 · 재료산출 = M02 스프레드시트 · 선택 입력 = 값 받기 카드. * 고침은 **프로젝트 복사본**에만 씀(`ProjectItems.write`) — 마스터 · 회사 · 개인 원본은 안 바뀜. * [뒤로] = 선택지(나무 + 카드)로 · 저장 안 한 고침이 있으면 버릴지 물음. * 편집 부품은 이 화면을 열 때만 `import()`(B05 · B06 번들에 안 섞임). * ========================================================================== */ import { createButton, el, showConfirmDialog, showToast } from "./ui_template_elements"; import type { SpreadsheetHandle, Workbook } from "../A00_Common/spreadsheet/spreadsheet"; import type { LinkOptions } from "../A00_Common/spreadsheet/spreadsheet_links"; import type { DrawingTemplateHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Drawing"; import type { InputsValueHandle } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs"; import { pillsOf, type ValueDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Inputs_Model"; import type { InputLinks, StructureDoc } from "../M02_MasterTemplete/M02_MasterTemplete_Api_Fetch"; import type { EditRequest } from "./ui_template_item_picker_detail"; import { itemName } from "./ui_template_item_picker_data"; import { loadInputs, PART_OF } from "./ui_template_item_picker_project"; export interface EditView { root: HTMLElement; /** 붙인 뒤 부름 — 편집 부품을 받아 세움 */ start: () => void; isDirty: () => boolean; /** 저장 안 한 고침을 버릴지 묻고, 괜찮으면 부품을 치움 — false 면 그대로 */ leave: () => Promise; destroy: () => void; } const TEXT = { back: "← 뒤로", save: "저장", saved: "프로젝트 복사본에 저장했음", saveFailed: "저장하지 못함 — {value}", loadFailed: "복사본을 못 읽음 — {value}", discard: "저장 안 한 고침이 있음 — 버리고 돌아갈까요?", outOfRange: "범위 밖 값이 있어 저장하지 못함", noAreaPill: "면적산출 알약 없음 — 선택 입력에서 자리를 면적산출로 이으면 여기 뜸(마스터 · 회사 · 개인 층에서)", drawing: "도면", area: "면적산출", material: "재료산출", inputs: "선택 입력", suffix: "{value} 수정", }; const why = (error: unknown): string => (error instanceof Error ? error.message : String(error)); const fill = (text: string, value: string): string => text.replace("{value}", value); export function createEditView(req: EditRequest, onBack: (saved: boolean) => void): EditView { const { kind, row, info, items } = req; let dirty = false; let saving = false; let saved = false; // 한 번이라도 저장했나 — 돌아갈 때 「고침 있음」 표시 let doc: unknown = null; let drawing: DrawingTemplateHandle | null = null; let sheet: SpreadsheetHandle | null = null; let card: InputsValueHandle | null = null; const word = { structure: TEXT.drawing, area: TEXT.area, material: TEXT.material, inputs: TEXT.inputs, }[kind]; const back = createButton({ label: TEXT.back, variant: "ghost", size: "sm" }); const save = createButton({ label: TEXT.save, variant: "filled", size: "sm" }); save.disabled = true; const note = el("span", { className: "ui-item-picker__edit-note" }); const body = el("div", { className: "ui-item-picker__edit-body" }); const root = el("div", { className: "ui-item-picker__edit", children: [ el("div", { className: "ui-item-picker__edit-top", children: [ back, el("h4", { className: "ui-item-picker__edit-title", text: `${itemName(row)} — ${fill(TEXT.suffix, word)}`, }), note, save, ], }), body, ], }); const setDirty = (next: boolean): void => { dirty = next; save.disabled = !next || saving; }; const fail = (text: string): void => { body.replaceChildren(el("p", { className: "ui-item-picker__hint", text })); }; /** 복사본의 항목 연결 — 자리가 면적 · 재료인 입력이 그 시트 알약(25-1b · 키 모양은 `pillsOf`) */ const linkOf = async (): Promise => (await items.read(info.폴더, "입력연결")) as InputLinks | null; async function sheetLinks(kindOf: "area" | "material"): Promise { const [list, link, { searchMaterials }] = await Promise.all([ loadInputs(row), linkOf(), import("../M01_MasterData/M01_MasterData_Api_Fetch"), ]); const 항목 = pillsOf(list, link, kindOf === "area" ? "면적" : "재료"); if (kindOf === "area") { if (!항목.length) note.textContent = TEXT.noAreaPill; return { 항목, 모드: "입력" }; } return { 항목, 모드: "자재", 자재찾기: searchMaterials }; } async function mountSheet(kindOf: "area" | "material"): Promise { const [{ createSpreadsheet }, links] = await Promise.all([ import("../A00_Common/spreadsheet/spreadsheet"), sheetLinks(kindOf), ]); const host = el("div", { className: "ui-item-picker__edit-sheet" }); body.replaceChildren(host); sheet = createSpreadsheet(host, doc as Workbook, { readOnly: false, tabs: false, onChange: () => setDirty(true), links, }); } async function mountDrawing(): Promise { const { mountDrawingTemplate } = await import("../M02_MasterTemplete/M02_MasterTemplete_Drawing"); const host = el("div", { className: "ui-item-picker__edit-cad" }); body.replaceChildren(host); drawing = mountDrawingTemplate(host, (doc as StructureDoc).도면 as never, { onSave: () => void doSave(), readOnly: false, name: `item:${info.폴더}`, layer: "project", projectId: req.projectId, onChanged: (changed) => setDirty(changed), originMarker: true, noArea: true, }); } async function mountInputs(): Promise { const [{ createInputsValueCard }, list, link] = await Promise.all([ import("../M02_MasterTemplete/M02_MasterTemplete_Inputs"), loadInputs(row), linkOf(), ]); const values = ((await items.read(info.폴더, "입력값")) as ValueDoc | null) ?? {}; card = createInputsValueCard({ mode: "value", onChange: () => setDirty(true) }); card.set({ list, link, values }); body.replaceChildren( el("div", { className: "ui-item-picker__edit-inputs", children: [card.root] }), ); } async function start(): Promise { try { if (kind !== "inputs") { doc = await items.read(info.폴더, PART_OF[kind]); if (doc == null) return fail(fill(TEXT.loadFailed, "복사본에 없음")); } if (kind === "structure") await mountDrawing(); else if (kind === "inputs") await mountInputs(); else await mountSheet(kind); } catch (error) { fail(fill(TEXT.loadFailed, why(error))); } } async function doSave(): Promise { if (saving) return; saving = true; save.disabled = true; try { if (kind === "inputs") { if (!card?.isValid()) return void showToast(TEXT.outOfRange, "error"); await items.write(info.폴더, "입력값", card.getValues()); } else if (kind === "structure") { const edited = await drawing?.getDoc(); await items.write(info.폴더, "도면", { ...(doc as StructureDoc), 도면: edited }); } else if (sheet) { // 번호 칸은 스프레드시트가 모름 — 읽은 문서 위에 편집본을 얹음(M02 저장과 같은 규칙) const merged = { ...(doc as Record), ...sheet.getDoc() }; await items.write(info.폴더, PART_OF[kind], merged); doc = merged; } setDirty(false); saved = true; showToast(TEXT.saved, "success"); } catch (error) { showToast(fill(TEXT.saveFailed, why(error)), "error"); } finally { saving = false; save.disabled = !dirty; } } const destroy = (): void => { drawing?.destroy(); sheet?.destroy(); drawing = null; sheet = null; root.remove(); }; const leave = async (): Promise => !dirty || showConfirmDialog(TEXT.discard); save.addEventListener("click", () => void doSave()); back.addEventListener("click", async () => { if (await leave()) onBack(saved); }); return { root, start: () => void start(), isDirty: () => dirty, leave, destroy }; }