/* ============================================================================= * M02_MasterTemplete_Hopyo_SheetMake.ts * 엑셀 보기 몸의 만들기 셋(PLAN 785-4 · `Hopyo_Sheet` 에서 옮김 — 줄 바꿈 없음) — * · [저장] = 체크한 호표 + 확인 안 된 하위 호표(777-1 · `Hopyo_Kids`)마다 체크 줄(확인 창 캐시 · 없으면 서버 제안)로 * 하위 호표부터 차례로 `/hopyo-build` — 물음 한 번 · 체크 줄 없는 고른 호표 건너뜀 · 하위 실패 = 그 하위 [고르기] · * 결과 표(사전조합 키 · 빠진 줄 · 값 차이) · `onImport` 가 있으면 호표마다 알림([로직 추가] 참조 줄) * · [모두 고정으로 들여오기](705-3) = 물음 한 번 → 서버 `/hopyo-build-all`(호표 모두 · 하위 먼저 · 엑셀 숫자 그대로 · * 이미 만든 호표 그대로) → 알림 「호표 n · 줄 m · 품셈 줄 a · 고정 줄 b · 빠짐 0 · 엑셀 합계 = 우리 합계 n/n」 + 다음 걸음 * ========================================================================== */ import { createButton, el, hideLoadingOverlay, showConfirmDialog, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { buildHopyo, buildHopyoAll, readHopyoOne, type HopyoBrief, type HopyoBuilt, type HopyoBuiltAll, type HopyoOne, type HopyoScope, } from "./M02_MasterTemplete_Hopyo_Api"; import { draftKey, hasDraft, HOPYO_BUILT, loadDraft, partsOf, } from "./M02_MasterTemplete_Hopyo_Check"; import { bodyPair, kidsText, leftAt, readyFor, unmadeKids } from "./M02_MasterTemplete_Hopyo_Kids"; import { builtWord } from "./M02_MasterTemplete_Hopyo_Choose"; import { leftButton } from "./M02_MasterTemplete_Hopyo_Left"; import { openRelink, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink"; import { formatPrice } from "../common_util/common_util_money"; export const why = (error: unknown): string => error instanceof Error ? error.message : String(error); export const won = (v: number | null | undefined): string => v === null || v === undefined ? "" : formatPrice(v); export const note = (text: string): HTMLElement => el("span", { className: "ui-text ui-text--muted", text, attrs: { "data-text": "14" } }); /** 만들 차례 — 하위 호표(호표 참조 줄이 가리키는 고른 호표)부터 · 순환은 남은 차례대로 */ export function buildOrder(ones: { 번: number; 아래: number[] }[]): number[] { const left = new Map(ones.map((x) => [x.번, x.아래.filter((n) => n !== x.번)])); const out: number[] = []; while (left.size) { const ready = [...left].find(([, kids]) => kids.every((n) => !left.has(n))); const no = ready ? ready[0] : [...left.keys()][0]; out.push(no); left.delete(no); } return out; } export interface MakeContext { scope: HopyoScope; key: string; 파일: string; /** 만들 층 */ target: HopyoScope; onImport?: (got: HopyoBuilt, hopyo: HopyoBrief) => void; checked: Set; briefs: () => HopyoBrief[]; closed: () => boolean; go: HTMLButtonElement; fixAll: HTMLButtonElement; result: HTMLElement; paintGo: () => void; } /** [저장] · [모두 고정으로 들여오기] 누름을 이음 */ export function wireMake(c: MakeContext): void { const { scope, key, target, go, fixAll, result } = c; const table = (heads: string[], rows: HTMLElement[]): HTMLElement => el("table", { className: "ui-general-block__table m02-hopyo__table", children: [ el("thead", { children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })], }), el("tbody", { children: rows }), ], }); const body = (one: HopyoOne) => bodyPair(scope, key, one); const headCell = (no: number, name: string): HTMLElement => el("td", { text: `제${no}호표 ${name}`.trim() }); /** [저장] — 물음 한 번 → 고른 호표마다 서버 보기 → 체크 줄 있는 것만 만들기(체크 줄 없으면 건너뜀) */ async function save(): Promise { const nos = [...c.checked].sort((a, b) => a - b); const fresh = nos.filter((no) => !hasDraft(draftKey(scope, key, no))).length; const ask = `고른 호표 ${nos.length}개를 체크한 줄로 ${L("M02_PrecomboTitle")} 생성` + (fresh ? ` — 확인 창에서 안 고친 ${fresh}개는 서버 제안 체크 줄` : ""); if (!(await showConfirmDialog(ask, "저장"))) return; go.disabled = true; const ones: HopyoOne[] = []; const skipped: HTMLElement[] = []; showLoadingOverlay(); try { for (const no of nos) { if (c.closed()) return; try { ones.push(await readHopyoOne(scope, key, no, target)); } catch (error) { skipped.push(el("tr", { children: [headCell(no, ""), el("td", { text: why(error) })] })); } } ones.push(...(await unmadeKids(scope, key, target, ones))); // 777-1 하위 함께 } finally { hideLoadingOverlay(); c.paintGo(); } const ready = ones.filter((one) => readyFor(scope, key, one, nos)); // 하위 함께 for (const one of ones) if (!ready.includes(one)) skipped.push( el("tr", { children: [ headCell(one.호표.번, one.호표.이름), el("td", { text: "건너뜀 — 체크 줄 없음" }), ], }), ); await makeAll(ready, skipped); } /** 하위 호표부터 차례로 만들기 — 하위 확인 전(409)은 안내 */ async function makeAll(ones: HopyoOne[], skipped: HTMLElement[]): Promise { const by = new Map(ones.map((one) => [one.호표.번, one])); const order = buildOrder( ones.map((one) => { const d = loadDraft(draftKey(scope, key, one.호표.번)); const kids = one.호표.줄.flatMap((l) => partsOf(l, d).flatMap((p) => (p.호표 !== undefined ? [p.호표] : [])), ); return { 번: one.호표.번, 아래: kids }; }), ); const rows: HTMLElement[] = []; let ok = 0; showLoadingOverlay(); try { for (const no of order) { const one = by.get(no)!; const h = c.briefs().find((x) => x.번 === no) ?? one.호표; const cell = headCell(no, h.이름); try { const from = { 층: scope.layer, 주인: scope.owner, 키: key, 번: no }; const got = await buildHopyo(target, from, ...body(one)); ok += 1; c.onImport?.(got, h); rows.push( el("tr", { attrs: { "data-built": got.참조.키 }, children: [ cell, el("td", { text: `${got.참조.키} · ${builtWord(got)}${kidsText(got)}` }), el("td", { text: String(got.빠진줄.length), className: "m02-hopyo__num" }), el("td", { text: won(got.비교.차이), className: "m02-hopyo__num" }), ], }), ); } catch (error) { const [err, kid] = leftAt(error, no); // 하위 실패 = 그 하위 남은 줄 const left = leftButton(err, { scope, target, key, 파일: c.파일, 번: kid }); // prettier-ignore const text = `실패 — ${why(error)}`; rows.push(el("tr", { children: [cell, el("td", { text }), ...(left ? [el("td", { children: [left] })] : [])] })); // prettier-ignore } } } finally { hideLoadingOverlay(); } if (ok) document.dispatchEvent(new Event(HOPYO_BUILT)); result.replaceChildren( table( ["호표", `${L("M02_PrecomboTitle")}`, "빠진 줄", "값 차이(참고)"], [...rows, ...skipped], ), ); const all = order.length + skipped.length; showToast(`만듦 ${ok} / ${all}`, ok === all ? "success" : "warning"); } go.addEventListener("click", () => void save()); /** 들여오기 알림 글(705-3) */ const allText = (got: HopyoBuiltAll): string => `호표 ${got.호표} · 줄 ${got.줄} · 품셈 줄 ${got.품셈} · 고정 줄 ${got.고정} · ` + `하위 호표 참조 ${got.호표참조} · 빠짐 ${got.빠짐} · 엑셀 합계 = 우리 합계 ${got.같음}/${got.호표}` + (got.그대로 ? ` · 이미 만든 호표 ${got.그대로} 그대로` : ""); /** [모두 고정으로 들여오기](705-3) — 물음 한 번 · 서버가 호표 모두를 하위부터(숫자 = 서버가 파일에서) */ async function importAll(): Promise { const ask = `호표 ${c.briefs().length}개를 엑셀 숫자 그대로(고정 줄) ${L("M02_PrecomboTitle")}으로 들여옴 — ` + "이미 만든 호표는 그대로 · 품셈 맞춤은 뒤에 호표 [수정]에서 [품셈으로 바꾸기]"; if (!(await showConfirmDialog(ask, "들여오기"))) return; fixAll.disabled = true; showLoadingOverlay(); try { const from = { 층: scope.layer, 주인: scope.owner, 키: key }; const got = await buildHopyoAll(target, from); if (got.호표 > got.그대로) document.dispatchEvent(new Event(HOPYO_BUILT)); const next = "다음 — 호표 [수정]에서 [품셈으로 바꾸기]" + (got.이음후보 ? ` · 이음 후보 ${got.이음후보} 줄(이름은 같고 단가가 다름)은 엑셀 단가 그대로 — 아래 [다음 — 이 공사 단가로 다시 잇기]에서 바꿈` : ""); const rows = [ ...got.다름.map((x) => el("tr", { children: [ el("td", { text: `제${x.번}호표 ${x.이름}` }), el("td", { text: `엑셀 ${won(x.엑셀)} · 우리 ${won(x.우리)}`, className: "m02-hopyo__num", }), el("td", { text: won(x.차이), className: "m02-hopyo__num" }), ], }), ), ...got.실패.map((x) => { const left = leftButton({ detail: x }, { scope, target, key, 파일: c.파일, 번: x.번 }); // prettier-ignore return el("tr", { children: [el("td", { text: `제${x.번}호표` }), el("td", { text: x.까닭 }), ...(left ? [el("td", { children: [left] })] : [])] }); // prettier-ignore }), ]; const relink = createButton({ label: "다음 — 이 공사 단가로 다시 잇기", size: "sm", onClick: () => void openRelink(relinkKeys((got.만듦 ?? []).map((x) => x.키))), }); // 705-2 result.replaceChildren( note(allText(got)), note(next), relink, ...(rows.length ? [table(["호표", "합계 다름 · 실패", "차이"], rows)] : []), ); showToast(allText(got), got.빠짐 || got.실패.length ? "warning" : "success"); } catch (error) { showToast(`실패 — ${why(error)}`, "error"); } finally { hideLoadingOverlay(); fixAll.disabled = false; } } fixAll.addEventListener("click", () => void importAll()); }