/* ============================================================================= * B09_Estimation_UI_Tabs_Bill_Link.ts * B09 설계내역서 연결 고르기(166-2 · 184-3) — 있는 호표(일위대가목록표 dg · 단가산출근거목록표 sg) · * 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새 * 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 · * 연결 풀기. * * 줄에 적는 일 = 부르는 쪽 `apply(ref, from)`(`…_UI_Tabs_Bill_Edit` · 저장 꼴 모름) * 로직 · 사전조합 고르기 · 입력값 = M02 창 재사용 · 구조물조합 고르기 = B08 [선택] 나무(173) 재사용 · * 층 읽기 권한 = 서버(`M02_Layer_Access`) * ========================================================================== */ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { showToast } from "@ui/ui_template_elements"; import { createFindList } from "@ui/ui_template_find_list"; import { openItemPicker } from "@ui/ui_template_item_picker"; import { openModal } from "@ui/ui_template_modal"; import { fetchLogicInputs } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Api"; import { openLaborInput } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Input"; import { openLogicPicker } from "../M02_MasterTemplete/M02_MasterTemplete_Labor_Pick"; import type { Row } from "./B09_Estimation_Calc_Base"; import type { TabContext } from "./B09_Estimation_UI_Tabs"; /** 연결 적용 — ref = null 이면 풀기 · from = 고른 호표(명칭 · 규격 · 단위 채움) */ export type ApplyRef = (ref: Row | null, from?: Row) => void; export type Leaf = [string, () => void]; /** 있는 호표 고르기 창 — 일위대가목록표(dg) · 단가산출근거목록표(sg) */ function pickHopyo(ctx: TabContext, apply: ApplyRef, tab: "dg" | "sg"): void { const listing: Row[] = (tab === "dg" ? ctx.sheets.daega : ctx.sheets.sg_list) || []; if (!listing.length) return showToast("고를 호표 없음 — 연결된 구조물 조합 사본이 없음", "error"); const label = (r: Row): string => (tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`); openModal({ title: tab === "dg" ? "일위대가 호표 고르기" : "단가산출 호표 고르기", closeLabel: "닫기", mount: (body, close) => { const finder = createFindList({ placeholder: "호표 · 명칭 · 규격 찾기" }); const find = (): void => { const q = finder.search.value.trim().toLowerCase(); const found = listing.filter( (r) => !q || `${label(r)} ${r.name} ${r.spec}`.toLowerCase().includes(q), ); finder.hint.textContent = `${found.length} / ${listing.length}`; finder.setRows( found.map((r) => ({ key: String(r.key), tag: label(r), name: String(r.name || ""), sub: String(r.spec || ""), unit: String(r.unit || ""), onPick: () => { close(); apply({ tab, key: r.key }, r); }, })), ); }; finder.search.addEventListener("input", find); find(); body.append(finder.root); requestAnimationFrame(() => finder.search.focus()); }, }); } /** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */ function askInputs(apply: ApplyRef, code: string, name: string, old?: Row): void { fetchLogicInputs([code]).then( (got) => { const logic = got.로직[0] ?? null; openLaborInput({ title: `${code} ${name}`.trim(), logic, values: old?.given || {}, memo: null, rate: old?.rate ?? null, readOnly: false, okLabel: "연결", notice: "새 산근 = 단가산출근거 뒤 번호 · 단가는 [저장] 뒤 서버가 셈", done: (next) => { const unit = String(logic?.결과단위 || "") .replace(/^원\//, "") .replace(/^[\d.]+/, ""); const ref = { tab: "logic", code, name, given: next.입력값, rate: next.켜쌓기율 }; apply({ ...ref, picks: old?.picks ?? null }, { name, unit }); showToast("연결함 — 새 산근 단가는 [저장] 뒤에 채워짐", "success"); }, }); }, (error: unknown) => showToast(`로직 입력을 못 읽음 — ${String(error)}`, "error"), ); } /** 서고 것 → 프로젝트 사본 · 새 대가 호표(204-2) — 서버가 들여옴 → 받은 원가 자료(대가 블록 · 산근)만 * 바꿔 끼우고(사용자 캐시 그대로) 줄 연결 `{tab: dg, key: 블록 id, lib: 사본 폴더}` */ async function importLib( ctx: TabContext, apply: ApplyRef, pick: { kind: "combo" | "precombo"; layer: string; owner: string | null; key: string }, ): Promise { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); if (!projectId) return; showToast("서고에서 들여오는 중…", "info"); try { const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation/library`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json" }, body: JSON.stringify(pick), }); const got = (await response.json().catch(() => null)) as Row | null; if (!response.ok || !got) { const detail = got?.detail; throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`); } if (!got.key) throw new Error("사본은 들여왔으나 호표를 못 만듦"); const { user: _user, ...rest } = got.payload as Row; Object.assign(ctx.payload, rest); const from = ((rest.sheets?.daega as Row[]) || []).find((r) => r.key === got.key); apply({ tab: "dg", key: got.key, lib: got.폴더 }, from); showToast( got.만듦 ? "새 호표로 연결함 — [저장]해야 연결이 남음" : "들여온 사본 호표로 연결함", "success", ); } catch (error) { showToast(`서고에서 못 들여옴 — ${error instanceof Error ? error.message : error}`, "error"); } } /** 서고 구조물조합 — B08 [선택] 나무(마스터 · 회사 · 개인 · 보기만) */ function pickCombo(ctx: TabContext, apply: ApplyRef): void { void openItemPicker({ current: null, canCopy: false }).then((got) => { if (got.action !== "apply") return; const { layer, owner, key } = got.item; void importLib(ctx, apply, { kind: "combo", layer, owner, key }); }); } /** 서고 일위대가 사전조합 — M02 고르기 창 서고 줄만(층 단추 마스터 · 회사 · 개인) */ function pickPrecombo(ctx: TabContext, apply: ApplyRef): void { openLogicPicker(() => undefined, { layer: "personal", owner: null, libOnly: true, title: "일위대가 사전조합 고르기", pick: (ref) => void importLib(ctx, apply, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }), }); } /** 항목 줄 연결 메뉴 — [고르기 셋, 그 밖(로직 입력값 · 연결 풀기)] · ref = 지금 연결 */ export function linkItems(ctx: TabContext, ref: Row | null, apply: ApplyRef): [Leaf[], Leaf[]] { const more: Leaf[] = []; if (ref?.tab === "logic") more.push(["로직 입력값 고치기", () => askInputs(apply, ref.code, ref.name || "", ref)]); if (ref) more.push(["연결 풀기", () => apply(null)]); const picks: Leaf[] = [ ["일위대가 호표", () => pickHopyo(ctx, apply, "dg")], ["단가산출 호표", () => pickHopyo(ctx, apply, "sg")], ["서고 구조물조합(마스터 · 회사 · 개인)", () => pickCombo(ctx, apply)], ["서고 일위대가 사전조합(마스터 · 회사 · 개인)", () => pickPrecombo(ctx, apply)], [ "단가산출 로직(M01 · 새 산근)", () => openLogicPicker((logic) => askInputs(apply, logic.키, logic.이름)), ], ]; return [picks, more]; }