/* ============================================================================= * B09_Estimation_UI_Tabs_Bill_Link.ts * B09 설계내역서 연결 고르기(166-2 · 184-3) — 있는 호표(일위대가목록표 dg · 단가산출근거목록표 sg) · * 서고(마스터 · 회사 · 개인) 구조물조합 · 일위대가 사전조합(204-2 — 서버가 프로젝트 사본으로 들여와 새 * 대가 호표 · 같은 것 = 있는 사본) · M01 로직(새 산근 = 서버 셈 · [저장] 뒤 단가) · 로직 입력값 고치기 · * 연결 풀기. * 251 단가연결 = 한 모달(`openPriceLink` · 종류 고르기 + 검색) — B09 설계내역서 · B08 거리이정표 같이 씀 · * 단가산출 = 옵션 값 창(`askInputs` · 160) * * 줄에 적는 일 = 부르는 쪽 `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 { loadRows } from "@ui/ui_template_item_picker"; import { itemName, type PickerLayer } from "@ui/ui_template_item_picker_data"; 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]; const LAYER_TEXT: Record = { system: "마스터", company: "회사", personal: "개인", }; /** M01 로직 → 입력값 창 → 연결(새 산근 · 단가는 [저장] 뒤 서버 셈) */ export 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( o: { apply: ApplyRef; onPayload?: (payload: Row) => void }, pick: { kind: "combo" | "precombo"; layer: string; owner: string | null; key: string }, ): Promise { const apply = o.apply; 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; o.onPayload?.(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"); } } /** 항목 줄 단가연결 메뉴(251-6) — [단가연결 모달, 그 밖(로직 입력값 · 단가연결 풀기)] · 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 open: Leaf = [ "단가연결", () => openPriceLink({ apply, hopyo: { dg: ctx.sheets.daega || [], sg: ctx.sheets.sg_list || [] }, onPayload: (p) => Object.assign(ctx.payload, p), }), ]; return [[open], more]; } /** 단가연결 모달(251 · 159 · 160) — 한 목록에 단가산출 로직 · 일위대가 사전조합 · 구조물 조합(마스터 · 회사 · * 개인 층 단추) · 호표(B09 · 있을 때) · 종류 고르기 + 검색 + 구분 · 상세구분 거름 · * 단가산출 = 옵션 값 창(`askInputs` · 160) · 사전조합 · 구조물 조합 = 프로젝트 사본 들여옴 → `{tab: dg, key, lib}` */ export interface PriceLinkOpts { apply: ApplyRef; /** B09 호표 목록(일위대가 · 단가산출) — 없으면 그 종류 없음(B08) */ hopyo?: { dg: Row[]; sg: Row[] }; /** 서고 사본 들여온 뒤 받은 원가 자료(B09 = 화면 자료에 끼움) */ onPayload?: (payload: Row) => void; } export async function openPriceLink(o: PriceLinkOpts): Promise { const hop = (tab: "dg" | "sg") => ((tab === "dg" ? o.hopyo?.dg : o.hopyo?.sg) ?? []).map((r) => ({ kind: tab, key: String(r.key), tag: tab === "dg" ? `대가${r.no}` : `산근 ${r.no}호표`, name: String(r.name || ""), sub: String(r.spec || ""), unit: String(r.unit || ""), onPick: () => o.apply({ tab, key: r.key }, r), })); // 구조물 조합 = 마스터 · 회사 · 개인 한 목록(B08 [선택] 나무와 같은 원천) const combos = (await loadRows().catch(() => [])).map((r) => ({ kind: "combo", key: `${r.layer}:${r.owner ?? ""}:${r.key}`, tag: "구조물 조합", name: itemName(r), sub: `${LAYER_TEXT[r.layer ?? "system"]} · ${r.key}`, unit: String(r.단위 || ""), onPick: () => void importLib(o, { kind: "combo", layer: r.layer ?? "system", owner: r.owner ?? null, key: r.key, }), })); openLogicPicker((logic) => askInputs(o.apply, logic.키, logic.이름), { layer: "personal", owner: null, title: "단가연결", extra: [...combos, ...hop("dg"), ...hop("sg")], kindFilter: [ { value: "logic", text: "단가산출" }, { value: "precombo", text: "일위대가 사전조합" }, { value: "combo", text: "구조물 조합" }, ...(o.hopyo ? [ { value: "dg", text: "일위대가 호표" }, { value: "sg", text: "단가산출 호표" }, ] : []), ], pick: (ref) => void importLib(o, { kind: "precombo", layer: ref.층, owner: ref.주인, key: ref.키 }), }); }