/* ============================================================================= * formula_doc_copy.ts * 수식 문서 사본 몫(PLAN 802-4b-2 · 요구 655-2 · 656 · 659-3) — M02 단가산출(SK) 수식 탭 머리 * 「M01 새 판 있음」 배지(값 줄 · 부름 문서가 지금 M01 과 다름) · [단가 갱신](자원 단가만 · 식 그대로) · * [재생성](지금 M01 문서로 다시 찍음 · 버리는 것 알림) — 누를 때만(자동 갱신 없음) * 흐름 = 미리 셈(`{길}/preview` {할일}) → 확인 창(계 전 → 후 · 바뀐 자원 · 버리는 것) → 저장(`{길}/save` {할일, 판}) * 층 쓰기 권한 없으면 「읽기만」 배지 * (821-3) 값 줄만 바뀜(M01 단가 · 내 기계 고침) = 「새 값 있음」(같은 자리 · 같은 꼴) · 지운 키(M01 · 내 기계) = 「지워짐 n」 · * [단가 갱신] 확인 창 = 바뀐 자원 줄(이름 · 바뀐 칸 전 → 후 · 지워짐 = 마지막 값 유지) * (822-1) 값 사본에 없는 셈 줄(옛 저장본 빈틈) = 「사본 없는 줄 n」(지금 값으로 셈 · 저장하면 굳음) * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; import { createButton } from "@ui/ui_template_button"; import { eqText } from "@ui/ui_template_equation"; import { showConfirmDialog } from "@ui/ui_template_elements"; import type { Basis } from "../common_util/common_util_formula_doc_import"; import { ft } from "./formula_doc_text"; export type CopyJob = "단가갱신" | "재생성"; /** 사본 표지(서버 `M02_Doc_Copy.state` + 시점 · 따로) */ export interface CopyMark { M01새판?: boolean; 지운키?: string[]; 단가새값?: boolean; /** 값 사본에 없는 셈 줄 수(822-1) — 지금 값으로 셈 · 저장하면 굳음 */ 사본없음?: number; 문서새판?: string[]; 따로?: boolean; /** 가져온 식(엑셀 불러오기 · 807-1) 단가기준 — 엑셀 = 엑셀 값 기억 · 우리 = 연결 값 */ 단가기준?: Basis; } export interface CopyOptions { /** SK 키 · 꼬리(`""` · `/preview` · `/table?key=…`) → 길(`/m02/layers/{층}/sangeun/{키}/doc…` · 주인 물음) */ route: (key: string, path: string) => string; /** 저장 뒤 그 SK 새 판(페이지 [저장] 409 막음) */ onSaved?: (version: string) => void; } type Money = Record | null; export interface JobReply { 전?: Money; 후?: Money; 바뀐자원?: unknown[]; 버림?: { 문서고침?: boolean; 끊음?: string[] }; } /** 「M01 새 판 있음」 — 값 줄 · 단가 · 부름 문서 어느 하나라도 지금 M01 과 다름 */ export function isStale(m: CopyMark | undefined): boolean { return !!m && (!!m.M01새판 || !!m.단가새값 || !!m.지운키?.length || !!m.문서새판?.length); } export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[] { const out: HTMLElement[] = []; const doc = !!m?.M01새판 || !!m?.문서새판?.length; if (doc || m?.단가새값) { const b = createBadge(ft(doc ? "New_M01" : "New_Value"), "danger"); b.dataset.copyStale = doc ? "doc" : "value"; out.push(b); } if (m?.지운키?.length) { const b = createBadge(`${ft("Gone")} ${m.지운키.length}`, "warning"); b.title = `${m.지운키.join(" · ")} — ${ft("Gone_Note")}`; b.dataset.copyGone = m.지운키.join(","); out.push(b); } if (m?.사본없음) { const b = createBadge(`${ft("Gap")} ${m.사본없음}`, "warning"); b.title = ft("Gap_Note"); b.dataset.copyGap = String(m.사본없음); out.push(b); } if (!edit) out.push(createBadge(ft("Read_Only"), "neutral")); return out; } export function copyTools(edit: boolean, run: (job: CopyJob) => void): HTMLElement[] { if (!edit) return []; return (["단가갱신", "재생성"] as const).map((job) => { const b = createButton({ label: job === "단가갱신" ? ft("Price") : ft("Regen"), variant: "ghost", size: 28, onClick: () => run(job), }); b.dataset.copyJob = job; return b; }); } const sum = (m: Money | undefined): string => typeof m?.계 === "number" ? `${eqText(m.계)} 원` : "—"; /** 바뀐 자원 한 줄 — 「이름 칸 전 → 후」(값 묶음에서 다른 칸만) · 지워짐 = 마지막 값 유지 */ export function changedLine(x: unknown): string { const r = (x ?? {}) as { 키?: string; 이름?: string; 전?: unknown; 후?: unknown; 지워짐?: boolean; }; const name = r.이름 || r.키 || ""; if (r.지워짐) return `${name} ${ft("Gone")}(${ft("Gone_Note")})`; const show = (v: unknown): string => (typeof v === "number" ? eqText(v) : String(v ?? "—")); const a = r.전 && typeof r.전 === "object" ? (r.전 as Record) : null; const b = r.후 && typeof r.후 === "object" ? (r.후 as Record) : null; if (!a || !b) return `${name} ${show(r.전)} → ${show(r.후)}`; const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].filter( (k) => JSON.stringify(a[k]) !== JSON.stringify(b[k]), ); return `${name} ${keys.map((k) => `${k} ${show(a[k])} → ${show(b[k])}`).join(", ")}`; } /** 할일 알림 — 계 전 → 후 · 바뀐 자원 수 · 재생성이 버리는 것(이름) → 확인이면 true */ export function askJob(job: CopyJob, got: JobReply, names: Map): Promise { const bits = [ job === "단가갱신" ? ft("Price_Ask") : ft("Regen_Ask"), `${ft("Before_After")} ${sum(got.전)} → ${sum(got.후)}`, ]; const lines = job === "단가갱신" ? (got.바뀐자원 ?? []).slice(0, 8).map(changedLine) : []; // 821-3 if (job === "단가갱신") bits.push(`${ft("Changed")} ${got.바뀐자원?.length ?? 0}`); const lost = [ ...(got.버림?.문서고침 ? [ft("Regen_Edit")] : []), ...(got.버림?.끊음 ?? []).map((k) => `${ft("Cut")} ${names.get(k) || k}`), ]; if (job === "재생성" && lost.length) bits.push(`${ft("Regen_Lost")} — ${lost.join(" · ")}`); const text = [bits.join(" · "), ...lines].join("\n"); return showConfirmDialog(text, job === "단가갱신" ? ft("Price") : ft("Regen")); }