/* ============================================================================= * B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts * B09 설계내역서 펼친 일위대가 우클릭(PLAN 703-2) — 복사해 새 일위대가 · 내 자료집에 저장 · 어디 쓰였나. * * 복사 = 이름 · 규격 묻기 → 덮개 `K:…` 에 `@copy` {from · name · spec} + 원본 덮개째(캐시 · 셈 짝이 새 호표 · * 번호 끝) → 「이 줄 단가연결을 새 호표로」 물음 · 복사본 지우기 = 그 덮개 통째(쓰는 줄 있으면 안 지움) * 내 자료집 = POST …/estimation/daega/personal(화면 캐시 덮개째 · 서버가 개인 층 사전조합) · * 같은 이름 409 → 「덮어쓰기 · 새 이름」 · 담음 · 안 담음(품 · 단가 덮개) 수 알림 * 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 그 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로 * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; import { askFields } from "@ui/ui_template_item_picker_ask"; import { createListRow } from "@ui/ui_template_list_row"; import type { MapContextMenuItem } from "@ui/ui_template_context_menu"; import { askConfirm, askText, openModal } from "@ui/ui_template_modal"; import type { DataRow } from "@ui/ui_template_data_table"; import { createText } from "@ui/ui_template_text"; import { currentProjectId } from "../A00_Common/b_project_switch"; import type { Row } from "./B09_Estimation_Calc_Base"; import { COPY } from "./B09_Estimation_Calc_Labor"; import type { TabContext } from "./B09_Estimation_UI_Tabs"; import { editCell } from "./B09_Estimation_UI_Tabs_Bill_Edit"; /** 어디 쓰였나 — 같은 품목으로 찾는 줄 */ const GOODS = ["labor", "material", "machine"]; /** 부른 대가 따라가는 깊이 */ const DEPTH = 6; const covers = (ctx: TabContext): Row => JSON.parse(JSON.stringify(ctx.user.daega_cover || {})); const newKey = (): string => `K:${Math.random().toString(16).slice(2, 10).padEnd(8, "0")}`; /** 복사해 새 일위대가 — 덮개째 · 번호 끝 · 이 줄 연결 바꿀지 물음 */ async function copyBlock(ctx: TabContext, raw: DataRow[], row: Row, block: Row): Promise { const got = await askFields({ title: "복사해 새 일위대가", note: `「${block.name}」 을 이 공사 안 새 호표로 — 고친 칸까지 복사`, fields: [ { key: "name", label: "이름", value: `${block.name} 복사`, required: true }, { key: "spec", label: "규격", value: block.spec || "" }, ], okLabel: "만들기", closeLabel: "닫기", emptyText: "{label} 이 빔", }); if (!got) return; const key = newKey(); const all = covers(ctx); all[key] = { ...(all[block.cover] || {}), [COPY]: { from: block.cover, ...got } }; // 덮개째 const relink = await askConfirm({ title: "단가연결", message: `「${row.name || ""}」 줄의 단가연결을 새 호표 「${got.name}」 로 바꿀지`, okLabel: "바꿈", cancelLabel: "그대로", }); const next = { ...ctx, user: { ...ctx.user, daega_cover: all } }; const i = raw.findIndex((r) => r.key === row.key); if (relink && i >= 0) editCell(next, raw, i, "ref", { tab: "dg", key: `DG:${key}` }); else ctx.setUser(next.user); showToast(`새 호표 「${got.name}」 — 일위대가목록표 끝 · [저장] 때 서버가 다시 셈`, "success"); } /** 복사본 지우기 — 그 호표를 잇는 내역 줄이 있으면 막음 */ function removeCopy(ctx: TabContext, raw: DataRow[], block: Row): void { const used = raw.filter((r) => (r.ref as Row | undefined)?.key === block.id); if (used.length) { showToast(`잇는 내역 줄 ${used.length} — 단가연결을 먼저 바꿈`, "error"); return; } const all = covers(ctx); delete all[block.cover]; ctx.setUser({ ...ctx.user, daega_cover: all }); } async function postPersonal(body: Row): Promise<{ ok: boolean; status: number; got: Row }> { const response = await fetch( `${API_BASE_URL}/projects/${currentProjectId()}/estimation/daega/personal`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }, ); const got = ((await response.json().catch(() => null)) as Row | null) ?? {}; return { ok: response.ok, status: response.status, got }; } /** 내 자료집에 저장 — 개인 층 사전조합(덮개 · 넣을 값 · 고른 자재) · 같은 이름 물음 */ async function savePersonal(ctx: TabContext, block: Row): Promise { let name = await askText({ title: "내 자료집에 저장", message: "개인 층 일위대가 사전조합으로 — 넣을 값 · 고른 자재 · 줄 수량 고친 칸까지", value: block.name || "", okLabel: "저장", }); let overwrite: string | null = null; while (name !== null && name.trim()) { const body = { cover: block.cover, name: name.trim(), overwrite, user: ctx.user }; const { ok, status, got } = await postPersonal(body); const same = got.detail?.같은이름; if (ok) { const skip: Row = got.안담음 || {}; const left = (skip.품 || 0) + (skip.단가 || 0); const tail = left ? ` · 안 담은 고친 칸 ${left}(품 ${skip.품} · 단가 ${skip.단가}) — 품은 고정 줄(PLAN 705) 뒤 담김` : ""; showToast(`내 자료집에 저장 「${got.이름}」(${got.키})${tail}`, "success", 6000); return; } if (status !== 409 || !same) { const detail = got.detail; showToast(`저장 못 함 — ${typeof detail === "string" ? detail : status}`, "error"); return; } const over = await askConfirm({ title: "같은 이름", message: `내 자료집에 「${name.trim()}」(${same}) 이 있음 — 덮어쓰기 · 새 이름`, okLabel: "덮어쓰기", cancelLabel: "새 이름", }); if (over) overwrite = same; else { overwrite = null; name = await askText({ title: "새 이름", message: "내 자료집 이름", value: `${name} 2` }); } } } /** 블록 나무에 그 품목(종류 · 키) · 그 호표(id)가 있나 */ function holds(blocks: Map, bid: string, hit: (ln: Row) => boolean): boolean { const seen = new Set(); const walk = (id: string, depth: number): boolean => { const b = blocks.get(id); if (!b || seen.has(id) || depth > DEPTH) return false; seen.add(id); return (b.lines || []).some( (ln: Row) => hit(ln) || (ln.kind === "daega" && walk(String(ln.key), depth + 1)), ); }; return walk(bid, 0); } /** 어디 쓰였나 — 그 품목 · 호표를 쓰는 내역 줄 → 누르면 그 줄로 */ function whereUsed( ctx: TabContext, raw: DataRow[], block: Row, line: Row, go: (key: string) => void, ): void { const blocks = new Map( (ctx.sheets.daega_blocks || []).map((b: Row) => [String(b.id), b]), ); const goods = GOODS.includes(line.kind); const id = line.kind === "daega" ? String(line.key) : String(block.id); const label = goods ? `${line.name || line.key}` : `${blocks.get(id)?.name || ""}`; const hit = goods ? (ln: Row) => ln.kind === line.kind && ln.key === line.key : (ln: Row) => ln.kind === "daega" && ln.key === id; const used = raw.filter((r) => { const ref = r.ref as Row | undefined; if (ref?.tab !== "dg") return false; return (!goods && String(ref.key) === id) || holds(blocks, String(ref.key), hit); }); openModal({ title: `어디 쓰였나 — 「${label}」`, closeLabel: "닫기", width: "40rem", mount: (body, close) => { body.append( createText(`쓰는 내역 줄 ${used.length} · 누르면 그 줄로`, "muted"), ...used.map((r) => createListRow({ label: String(r.name || ""), sub: [r.spec, r.qty != null ? `${r.qty} ${r.unit || ""}` : ""] .filter(Boolean) .join(" · "), onClick: () => { close(); go(String(r.key)); }, }), ), ); }, }); } /** 펼친 줄 우클릭 덧메뉴 — 복사 · 내 자료집 · 어디 쓰였나(+ 복사본 지우기) */ export function foldMenu( ctx: TabContext, raw: DataRow[], row: Row, block: Row, line: Row | undefined, go: (key: string) => void, ): MapContextMenuItem[] { return [ ["복사해 새 일위대가", () => void copyBlock(ctx, raw, row, block)], ["내 자료집에 저장", () => void savePersonal(ctx, block)], ["어디 쓰였나", () => whereUsed(ctx, raw, block, line || {}, go)], ...(String(block.cover).startsWith("K:") ? [["이 복사본 지우기", () => removeCopy(ctx, raw, block)] as MapContextMenuItem] : []), ]; }