/* ============================================================================= * M02_MasterTemplete_Labor_Paste.ts * 엑셀 붙여넣기(PLAN 705-2) — 일위대가 격자 Ctrl+V → 붙인 글 그대로 서버(`/hopyo-paste` · 숫자는 서버가 글에서 읽음). * 열 짝 = 처음 한 번 묻고 기억(이 브라우저 `localStorage` · 사용자 설정) · 엑셀 머리 글이 붙어 오면 묻지 않음 * 답 「줄」(호표 머리 없음) = 열린 문서에 고정 줄로 더함 → [저장] · 알림을 누르면 「이 공사 단가로 다시 잇기」 * 답 「만듦」(호표 머리 있음) = 호표마다 사전조합(실무자료 「붙여넣기 …」) · 결과 창 [이 공사 단가로 다시 잇기] * `askCols` · `withCols` = B09 펼친 일위대가 붙여넣기도 씀 * ========================================================================== */ import { createButton, el, showToast } from "@ui/ui_template_elements"; import { createSelectField } from "@ui/ui_template_input"; import { openModal } from "@ui/ui_template_modal"; import { createDataTable } from "@ui/ui_template_data_table"; import { createText } from "@ui/ui_template_text"; import { hideLoadingOverlay, showLoadingOverlay } from "@ui/ui_template_toast"; import { HOPYO_BUILT } from "./M02_MasterTemplete_Hopyo_Check"; import { pageScope, pasteHopyo, type HopyoBuiltAll, type PasteCols, } from "./M02_MasterTemplete_Hopyo_Api"; import { openRelink, relinkDoc, relinkKeys } from "./M02_MasterTemplete_Hopyo_Relink"; import type { LaborDoc } from "./M02_MasterTemplete_Labor_Api"; import { lineId } from "./M02_MasterTemplete_Labor_Calc"; const COLS_KEY = "aislo:hopyo-paste-cols"; const FIELDS = ["품명", "규격", "수량", "단위", "금액", "노", "재", "경"] as const; const LABEL: Record<(typeof FIELDS)[number], string> = { 품명: "품명", 규격: "규격", 수량: "수량", 단위: "단위", 금액: "합계 금액", 노: "노무비 단가", 재: "재료비 단가", 경: "경비 단가", }; const NEED = new Set(["품명", "수량", "단위"]); const letter = (n: number): string => n < 26 ? String.fromCharCode(65 + n) : `${letter(Math.floor(n / 26) - 1)}${letter(n % 26)}`; const won = (v: number | null | undefined): string => v == null ? "-" : Math.round(v).toLocaleString("ko-KR"); export function rememberedCols(): PasteCols | null { try { const got = JSON.parse(localStorage.getItem(COLS_KEY) ?? "null") as PasteCols | null; return got && typeof got === "object" ? got : null; } catch { return null; } } /** 열 짝 묻기 — 짐작 값이 미리 골라짐 · 보기 = 붙인 첫 줄들(칸 머리 A · B · C …) · 취소 = null */ export function askCols(guess: PasteCols, look: string[][]): Promise { return new Promise((done) => { const width = Math.max(0, ...look.map((r) => r.length)); const options = [ { value: "", text: "없음" }, ...Array.from({ length: width }, (_, n) => ({ value: String(n), text: `${letter(n)} 칸 · ${look.find((r) => r[n])?.[n] ?? ""}`.slice(0, 40), })), ]; const picks = FIELDS.map((f) => createSelectField({ label: `${LABEL[f]}${NEED.has(f) ? " *" : ""}`, options, value: guess[f] != null ? String(guess[f]) : "", compact: true, }), ); const table = createDataTable({ columns: Array.from({ length: width }, (_, n) => ({ key: `c${n}`, label: letter(n) })), rows: look.map((r, i) => ({ key: String(i), ...Object.fromEntries(r.map((c, n) => [`c${n}`, c])), })), empty: "", }); let answered = false; const { close } = openModal({ title: "붙인 엑셀 열 짝 — 한 번만 묻고 기억", closeLabel: "취소", width: "960px", beforeClose: () => { if (!answered) done(null); return true; }, mount: (body, _close, actions) => { const ok = createButton({ label: "확인", onClick: () => { const cols: PasteCols = {}; FIELDS.forEach((f, i) => { const v = picks[i].select.value; if (v !== "") cols[f] = Number(v); }); if ([...NEED].some((f) => cols[f as keyof PasteCols] == null)) return showToast("품명 · 수량 · 단위 칸은 꼭 고름", "warning"); localStorage.setItem(COLS_KEY, JSON.stringify(cols)); answered = true; done(cols); close(); }, }); actions.append(ok); body.append( createText( "엑셀 머리 글 없이 붙여 열 짝을 모름 — 칸마다 무엇인지 고름(짐작 값이 골라져 있음) · 다음부터 묻지 않음", "muted", ), el("div", { className: "ui-toolbar", children: picks.map((p) => p.root) }), table.root, ); }, }); }); } /** 열 짝 고리 — 기억한 짝으로 부름 · 「물음」이면 묻고 다시 부름 · 취소 = null */ export async function withCols( run: (cols: PasteCols | null) => Promise, ): Promise { let got = await run(rememberedCols()); if (got.갈래 === "물음") { const ask = got as { 열: PasteCols; 보기: string[][] }; hideLoadingOverlay(); const cols = await askCols(ask.열, ask.보기); if (!cols) return null; showLoadingOverlay(); got = await run(cols); } return got as T; } /** 만든 결과 창 — 엑셀 합계 = 우리 합계 · 다음 걸음 [이 공사 단가로 다시 잇기] */ export function builtNotice(got: HopyoBuiltAll, keys: string[]): void { const text = `사전조합 ${got.만듦?.length ?? 0} 만듦(호표 ${got.호표} · 줄 ${got.줄} · 고정 줄 ${got.고정} · ` + `하위 호표 참조 ${got.호표참조} · 빠짐 ${got.빠짐}) · 엑셀 합계 = 우리 합계 ${got.같음}/${got.호표}` + ` · 저절로 이음 ${got.이음} · 이음 후보 ${got.이음후보}`; const rows = got.다름.map((x) => ({ key: String(x.번), name: `제${x.번}호표 ${x.이름}`, excel: x.엑셀, ours: x.우리, gap: x.차이, })); openModal({ title: "엑셀 붙여넣기 — 만듦", closeLabel: "닫기", mount: (body, close, actions) => { actions.append( createButton({ label: "다음 — 이 공사 단가로 다시 잇기", onClick: () => { close(); void openRelink(relinkKeys(keys)); }, }), ); body.append(createText(text, got.빠짐 || got.실패.length ? "error" : "muted")); if (rows.length) body.append( createDataTable({ columns: [ { key: "name", label: "합계 다른 호표", width: 260 }, { key: "excel", label: "엑셀", kind: "number" }, { key: "ours", label: "우리", kind: "number" }, { key: "gap", label: "차이", kind: "number", decimals: 1 }, ], rows, }).root, ); }, }); } /** 격자 Ctrl+V(M02 일위대가 상세 · 사전조합) — 줄 = 문서에 더함(`changed`) · 만듦 = 사전조합 여럿 */ export async function pasteInto(doc: LaborDoc, text: string, changed: () => void): Promise { const scope = pageScope(); const have = doc.담은로직.map((r) => lineId(r) ?? "").filter((id) => id.startsWith("고정|")); showLoadingOverlay(); try { const got = await withCols((cols) => pasteHopyo(scope, text, cols, { 있음: have })); if (!got || got.갈래 === "물음") return; if (got.갈래 === "만듦") { document.dispatchEvent(new Event(HOPYO_BUILT)); builtNotice( got, (got.만듦 ?? []).map((x) => x.키), ); return; } for (const one of got.줄) doc.담은로직.push({ 로직: "", 고정: one.고정, 별칭: one.별칭, 메모: one.메모, 입력값: {}, 수량: one.수량.손 == null ? null : { 손: one.수량.손 } }); // prettier-ignore changed(); showToast( `고정 줄 ${got.줄.length} 붙임 · 엑셀 합계 ${won(got.엑셀)} · 이음 ${got.이음} · 후보 ${got.이음후보}` + " — [저장]으로 남김 · 이 알림을 누르면 이 공사 단가로 다시 잇기", "success", 9000, () => void openRelink(relinkDoc(doc, changed)), ); } catch (error) { showToast(`붙여넣기 실패 — ${error instanceof Error ? error.message : String(error)}`, "error"); } finally { hideLoadingOverlay(); } }