/* ============================================================================= * M01_MasterData_UI_LogicLab_Revise.ts * 로직 정본 개정 확인 창(PLAN 713-2 · 요구서 내역12 4장 10) — `/save` 409 `{개정, 까닭}` 를 받으면 * 바뀌는 정본 줄(파일 · 로직 · 바뀜)을 보이고 「원문 개정 반영」 확인을 받음. * 확인 = 부르는 쪽이 같은 몸에 `개정확인: true` 로 다시 보냄 · 취소 = 아무것도 안 바뀜(캐시 그대로) * 옛 409(`stale` — 그 사이 파일 바뀜)는 이 창 대상 밖 · `reviseHits` 가 null * ========================================================================== */ import { currentLanguageIndex } from "@ui/ui_template_locale"; import { createButton, el, hideLoadingOverlay, showLoadingOverlay } from "@ui/ui_template_elements"; import { openModal } from "@ui/ui_template_modal"; import { createListRow } from "@ui/ui_template_list_row"; import { createBadge, type BadgeTone } from "@ui/ui_template_badge"; import { ApiError } from "./M01_MasterData_UI_Logic_Api"; /** 서버 `Store_Revise.changed` 한 줄 */ export interface ReviseHit { file: string; key: string | null; op: "edit" | "add" | "delete"; 이름?: string | null; } const TEXT = { Title: ["원문 개정 반영", "Apply source revision"], Lead: [ "로직 정본(품셈 원문) 줄을 바꿈 — 원문이 개정된 때만 저장 · 아래 {n} 줄", "This changes canonical logic rows (source text) — save only for a source revision · {n} rows below", ], Ok: ["원문 개정 반영", "Apply revision"], Cancel: ["취소", "Cancel"], New: ["새 줄", "New row"], edit: ["고침", "Edit"], add: ["더함", "Add"], delete: ["지움", "Delete"], } as const satisfies Record; const t = (key: keyof typeof TEXT, n = 0): string => (TEXT[key][currentLanguageIndex as 0 | 1] ?? TEXT[key][0]).replace("{n}", String(n)); const TONE: Record = { edit: "warning", add: "info", delete: "danger" }; /** 개정 409 면 바뀌는 줄 · 아니면 null */ export function reviseHits(error: unknown): ReviseHit[] | null { if (!(error instanceof ApiError) || error.status !== 409) return null; const hits = (error.detail as { 개정?: ReviseHit[] } | null)?.개정; return Array.isArray(hits) && hits.length ? hits : null; } /** 확인 창 — 「원문 개정 반영」이면 true · 취소 · 닫기 · Esc · 바깥 누름이면 false */ export function askRevise(hits: ReviseHit[]): Promise { return new Promise((resolve) => { openModal({ title: t("Title"), closeLabel: t("Cancel"), width: "40rem", // [취소] · Esc · 바깥 누름 = 닫기 길(묻는 길) · 확인은 `shut` 이라 이 길을 안 거침 beforeClose: () => { resolve(false); return true; }, mount: (body, shut, actions) => { body.append( el("p", { text: t("Lead", hits.length) }), ...hits.map((h) => createListRow({ label: `${h.key ?? t("New")} ${h.이름 ?? ""}`.trim(), sub: h.file, trailing: createBadge(t(h.op), TONE[h.op]), size: 36, }), ), ); actions.append( createButton({ label: t("Ok"), variant: "danger", onClick: () => { shut(); resolve(true); }, }), ); }, }); }); } /** 저장 한 번 — 개정 409 면 창을 띄워 확인 뒤 `개정확인: true` 로 다시 보냄 · 취소면 null(캐시 그대로) */ export async function saveRevised( save: (confirmed: boolean) => Promise, ): Promise<{ added?: { file: string; key: string }[] } | null> { try { return (await save(false)) as { added?: { file: string; key: string }[] }; } catch (error) { const hits = reviseHits(error); if (!hits) throw error; hideLoadingOverlay(); // 창이 가림막 아래로 숨지 않게 const ok = await askRevise(hits); showLoadingOverlay(); return ok ? ((await save(true)) as { added?: { file: string; key: string }[] }) : null; } }