- 새 M01_MasterData_UI_LogicLab_Revise.ts(reviseHits · askRevise · saveRevised) · 부품 상자 모달 · 목록 줄 · 배지 - LogicLab [저장](흐름 저장 · 지우기 · 더하기 같은 길)이 saveRevised 로 · 줄 수 그대로(627) - Logic_Api saveFiles 에 개정확인 인자(기본 false) - 옛 409 stale 은 토스트 그대로 - 새 지킴 시험 test_713_2_revise_ui Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.1 KiB
TypeScript
104 lines
4.1 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, readonly [string, string]>;
|
|
|
|
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<ReviseHit["op"], BadgeTone> = { 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<boolean> {
|
|
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<unknown>,
|
|
): 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;
|
|
}
|
|
}
|