/* ============================================================================= * 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_Labor_Values"; import type { ValueDoc } from "../M02_MasterTemplete/M02_Labor_Values_Model"; import type { LaborPill, 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 { laborValueData, 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: "면적산출", 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, 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 })); }; /** 복사본 일위대가의 알약(가져온 때 그대로) — 로직 물량 「이름|단위」 + 자재 「자재|MT키」 · 다 고정 */ async function sheetLinks(): Promise { const labor = (await items.read(info.폴더, "일위대가")) as { 알약?: LaborPill[] } | null; const 항목 = (labor?.알약 ?? []).map((p) => ({ 키: p.키, 이름: p.이름, 단위: p.단위, ...(p.자재 ? { 자재: p.자재 } : {}), 고정: true, })); if (!항목.length) note.textContent = TEXT.noAreaPill; return { 항목, 모드: "입력" }; } async function mountSheet(): Promise { const [{ createSpreadsheet }, links] = await Promise.all([ import("../A00_Common/spreadsheet/spreadsheet"), sheetLinks(), ]); 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, }); } /** 넣을 값 — 복사본 일위대가 담은 로직 입력 전부(39-1c) */ async function mountInputs(): Promise { const [{ createInputsValueCard }, labor, values] = await Promise.all([ import("../M02_MasterTemplete/M02_Labor_Values"), items.read(info.폴더, "일위대가") as Promise<{ 줄?: { 담은로직?: { 로직?: string }[] }; } | null>, items.read(info.폴더, "입력값") as Promise, ]); const logics = (labor?.줄?.담은로직 ?? []).map((one) => String(one.로직 ?? "")).filter(Boolean); card = createInputsValueCard({ mode: "value", onChange: () => setDirty(true) }); card.set(await laborValueData(logics, 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(); } 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 }; }