/* ============================================================================= * M02_MasterTemplete_Hopyo_View.ts * 엑셀 불러오기 [수정] 모달(PLAN 809-1 · 요구 702 · 703) — 엑셀 보기 오른쪽 호표 목록 [수정](`Hopyo_Sheet`)이 엶. * 경쟁사 로직 인정 · 요소만 연결 — 몸 = 일위대가 컨테이너 부품(`Labor_Table` · 806-1 조합과 한 벌 · 읽기만) + 덧칸 둘: * · 표지 = 로직 줄 「단가산출 N」 · 「일위대가 N」 칩 / 요소 줄 「✓ 연결됨」 · 「후보 n」 · 「못 이음」(엑셀 값 그대로) * · 엑셀 금액 = 그 줄 엑셀 금액 계 * 줄 = 서버 `GET …/hopyo-view/{키}/{번}`(`M02_Hopyo_View` · 엑셀 숫자 그대로 고정 줄 · 저장 없음 · 금액 = 엑셀). * · 단가산출 줄 누름 = 그 블록 가져온 식 SK(서버 `…/sangeun/from-hopyo` · 만들 층 · 같은 내용 = 있는 것) → * 단가산출 「수식」 탭 모달(`Labor_SkEdit` 부르기만 · [엑셀 단가 그대로 | 우리 단가로]) * · 일위대가 줄 누름 = 그 하위 호표 [수정] 모달 · 후보 줄 누름 = 후보 목록(고르기 = [내 일위대가로] 뒤 일위대가 [연결]) * · 머리 = 표지 수 · 엑셀 단가 · [내 일위대가로](807-1 `Hopyo_Mine`) · [구조물 조합으로](만든 SC 있을 때 · `Hopyo_ToItem`) · [엑셀 보기] * 기초 판정 · 「우리 기초로 바꾸기」 없음(요구 703). * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton, el, showToast } from "@ui/ui_template_elements"; import { loadingError, showLoading } from "@ui/ui_template_loading"; import { currentLanguageIndex, t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; import { createTable } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; import { createToolbar } from "@ui/ui_template_toolbar"; import { formatPrice } from "../common_util/common_util_money"; import { call } from "./M02_MasterTemplete_Api_Fetch"; import { at, type HopyoBrief, type HopyoScope } from "./M02_MasterTemplete_Hopyo_Api"; import { mineButton } from "./M02_MasterTemplete_Hopyo_Mine"; import { toItem } from "./M02_MasterTemplete_Hopyo_ToItem"; import type { LaborDetailHandle } from "./M02_MasterTemplete_Labor_Detail"; import { openSkEdit } from "./M02_MasterTemplete_Labor_SkEdit"; import { mountLaborTable } from "./M02_MasterTemplete_Labor_Table"; const TEXT = { Title: ["제{n}호표 — 엑셀 불러오기", "Unit cost #{n} — Excel import"], Mark: ["표지", "Status"], Excel: ["엑셀 금액", "Excel amount"], Linked: ["✓ 연결됨", "✓ Linked"], Near: ["후보 {n}", "{n} candidates"], None: ["못 이음", "Not linked"], Logic: ["로직 {n}", "Logic {n}"], ExcelPrice: ["엑셀 단가", "Excel price"], Hint: [ "경쟁사 로직 그대로 · 요소만 연결 — 단가산출 줄을 누르면 수식 탭 · 금액 = 엑셀", "Competitor logic kept · elements linked — click a build-up line for its formula", ], Sheet: ["엑셀 보기", "Show in Excel"], NearTitle: ["이음 후보 — {name}", "Link candidates — {name}"], NearHint: [ "고르기 = [내 일위대가로] 뒤 내 일위대가의 [연결]", "Pick after [To my unit costs] with [Link]", ], Key: ["키", "Key"], Name: ["이름", "Name"], Close: ["닫기", "Close"], } as const satisfies Record; const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]; const why = (e: unknown): string => (e instanceof Error ? e.message : String(e)); const enc = encodeURIComponent; type Money = Record<"노무비" | "재료비" | "경비" | "계", number | null>; /** 줄 표지(서버 `M02_Hopyo_View.mark`) */ export interface ViewMark { 차례: number; 갈래: "로직" | "요소"; 표지: "단가산출" | "일위대가" | "연결됨" | "후보" | "못 이음"; 번?: number | null; 이음?: Record; 후보?: { 키: string; 이름: string }[]; 엑셀: Money; } export interface ViewReply { 번: number; 이름: string; 규격: string | null; 단위: string | null; 금액: Money | null; 문서: unknown; 줄: ViewMark[]; } export const readHopyoView = (file: HopyoScope, key: string, no: number): Promise => call(at(file, `/hopyo-view/${enc(key)}/${no}`)); /** 표지 칸 글 */ export function markText(m: ViewMark): string { if (m.갈래 === "로직") return `${m.표지}${m.번 ? ` ${m.번}` : ""}`; if (m.표지 === "연결됨") return t("Linked"); if (m.표지 === "후보") return t("Near").replace("{n}", String(m.후보?.length ?? 0)); return t("None"); } /** 표지 칸 꼴 — 로직 = 칩 · 연결됨 초록 · 후보 주황 · 못 이음 빨강 */ const markTone = (m: ViewMark): string => m.갈래 === "로직" ? "ui-hopyo-grid__tag is-pum" : m.표지 === "연결됨" ? "ui-text--success" : m.표지 === "후보" ? "ui-text--warning" : "ui-text--error"; export interface ViewOptions { /** 실무자료 파일 층 */ file: HopyoScope; key: string; /** 가져온 식 SK 를 만들 층 */ target: HopyoScope; no: number; /** 목록 한 줄([내 일위대가로] · 엑셀 단가) */ brief?: HopyoBrief; canWrite: boolean; /** 모달 닫고 엑셀 보기 그 호표 */ onSheet?: () => void; } /** 후보 줄 — 후보 목록 */ function showNear(name: string, m: ViewMark): void { openModal({ title: t("NearTitle").replace("{name}", name), closeLabel: t("Close"), dialogClass: "m02-modal", mount: (body) => { const rows = (m.후보 ?? []).map((c) => [c.키, c.이름]); body.append(createTable([{ label: t("Key") }, { label: t("Name") }], rows, { density: "data" })); // prettier-ignore body.append(createText(t("NearHint"), "hint")); }, }); } /** [구조물 조합으로] — 만든 SC(이 파일 층에서 옮긴 것)로 구조물 조합 만들기 */ const itemButton = (target: HopyoScope, sc: string, name: string, unit: string): HTMLElement => createButton({ label: L("M02_CheckToItem"), size: 28, variant: "ghost", onClick: () => toItem({ target, sc, name, unit }) }); // prettier-ignore /** [수정] 모달 */ export function openHopyoView(o: ViewOptions): void { const root = el("div", { className: "m02-hopyo", attrs: { "data-hopyo-view": `${o.key}|${o.no}` } }); // prettier-ignore const host = el("div", { attrs: { "data-hopyo-view-body": "" } }); let table: LaborDetailHandle | null = null; let close = (): void => {}; /** 단가산출 줄 — 가져온 식 SK(만들 층) → 수식 탭 모달 */ async function openSk(m: ViewMark, name: string, head: string): Promise { try { const made = await call<{ 이름: string }>(at(o.target, "/sangeun/from-hopyo"), { method: "POST", body: JSON.stringify({ 층: o.file.layer, ...(o.file.owner ? { 주인: o.file.owner } : {}), 키: o.key, 번: m.번 }), // prettier-ignore }); const ref = { 층: o.target.layer, 주인: o.target.owner, 키: made.이름 }; void openSkEdit({ ref, title: `${name} — ${made.이름}`, head, readOnly: !o.canWrite, closed: () => {} }); // prettier-ignore } catch (e) { showToast(why(e), "error"); } } function draw(r: ViewReply): void { const count = (k: ViewMark["표지"]): number => r.줄.filter((m) => m.표지 === k).length; const logic = r.줄.filter((m) => m.갈래 === "로직").length; const name = [r.이름, r.규격].filter(Boolean).join(" "); const head = [name, r.단위 ? `1 ${r.단위} 당` : ""].filter(Boolean).join(" · "); const badges = [ createBadge(t("Logic").replace("{n}", String(logic)), "accent"), createBadge(`${t("Linked")} ${count("연결됨")}`, "success"), createBadge(t("Near").replace("{n}", String(count("후보"))), "warning"), createBadge(`${t("None")} ${count("못 이음")}`, "danger"), ]; badges.forEach((b, i) => (b.dataset.viewCount = ["로직", "연결됨", "후보", "못 이음"][i])); const sheet = createButton({ label: t("Sheet"), size: 28, variant: "ghost", onClick: () => { close(); o.onSheet?.(); } }); // prettier-ignore const items: (HTMLElement | "divider")[] = [ el("strong", { text: `제${r.번}호표 ${name}` }), ...badges, "divider", createText(`${t("ExcelPrice")} ${r.금액?.계 == null ? "—" : formatPrice(r.금액.계)}`, "muted", 14), // prettier-ignore ...(o.brief ? [mineButton(o.file, o.key, o.brief)] : []), ...(o.brief?.만든 && o.canWrite ? [itemButton(o.target, o.brief.만든, name, r.단위 ?? "")] : []), ...(o.onSheet ? [sheet] : []), ]; root.replaceChildren(createToolbar({ items }), createText(t("Hint"), "hint"), host); table?.destroy(); table = mountLaborTable(host, r.문서, { layer: o.target.layer, owner: o.target.owner, column: `제${r.번}호표`, mode: "precombo", readOnly: true, onChanged: () => {}, view: { cols: [ { key: "x_mark", label: t("Mark"), width: 96 }, { key: "x_excel", label: t("Excel"), kind: "number", decimals: 1, trim: true, width: 96 }, ], cell: (n, key) => { const m = r.줄[n]; if (!m) return null; return key === "x_mark" ? markText(m) : m.엑셀.계; }, tone: (n, key) => (key === "x_mark" && r.줄[n] ? markTone(r.줄[n]) : null), onRow: (n) => { const m = r.줄[n]; const line = (r.문서 as { 담은로직?: { 별칭?: string | null }[] }).담은로직?.[n]; const label = line?.별칭 ?? name; if (m?.표지 === "단가산출" && m.번) return (void openSk(m, label, head), true); if (m?.표지 === "일위대가" && m.번) return void openHopyoView({ ...o, no: m.번, brief: undefined }), true; // prettier-ignore if (m?.표지 === "후보") return (void showNear(label, m), true); return true; // 요소 줄 = 엑셀 숫자 그대로(모달 없음) }, }, }); } async function load(): Promise { const stop = showLoading(root); try { draw(await readHopyoView(o.file, o.key, o.no)); } catch (e) { root.replaceChildren(loadingError(why(e), () => void load())); } finally { stop(); } } ({ close } = openModal({ title: t("Title").replace("{n}", String(o.no)), closeLabel: t("Close"), wide: true, width: "1100px", dialogClass: "m02-modal m02-hopyo__view-modal", beforeClose: () => { table?.destroy(); return true; }, mount: (body) => body.append(root), })); void load(); }