feat(713-2): M01 로직 정본 개정 확인 창 — /save 개정 409 받으면 바뀌는 정본 줄(파일 · 로직 · 바뀜) 창 · 「원문 개정 반영」 확인 뒤 같은 몸에 개정확인 true 다시 보냄 · 취소 = 캐시 그대로

- 새 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>
This commit is contained in:
2026-11-01 15:42:00 +09:00
co-authored by Claude Opus 5.5
parent 75b65e03a2
commit 3d4bcaea59
4 changed files with 142 additions and 4 deletions
+3 -3
View File
@@ -51,6 +51,7 @@ import {
type LogicFlow,
} from "./M01_MasterData_UI_LogicLab_Flow";
import { askBasicInfo } from "./M01_MasterData_UI_LogicLab_Basic";
import { saveRevised } from "./M01_MasterData_UI_LogicLab_Revise";
import { buildLogicSide } from "./M01_MasterData_UI_LogicLab_Side";
import { createViewPicker } from "./M01_MasterData_UI_LogicLab_View";
import { buildHeadBar } from "./M01_MasterData_UI_Head";
@@ -501,9 +502,8 @@ export async function mountM01LogicLab(
showLoadingOverlay();
try {
// 키 없는 새 줄은 서버가 새 키를 줌(added) — 저장 뒤 그 로직을 엶
const saved = (await saveFiles([...byFile.values()])) as {
added?: { file: string; key: string }[];
};
const saved = await saveRevised((ok) => saveFiles([...byFile.values()], ok));
if (!saved) return; // 개정 확인 취소 = 캐시 그대로
const was = opened;
drafts = {};
await reload();
@@ -0,0 +1,103 @@
/* =============================================================================
* 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;
}
}
@@ -263,7 +263,8 @@ export const runText = (body: {
export const saveFiles = (
files: SaveFile[],
): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files });
개정확인 = false,
): Promise<{ files: { file: string; version: string }[] }> => request("/save", { files, 개정확인 });
/** 본떠 만들기 — 정본·자체 둘 다 본뜸 · 새 키(`GX…`)는 서버가 줌(계약 5장) */
export const copyLogic = (
+34
View File
@@ -0,0 +1,34 @@
"""713-2 정본 개정 확인 창 지킴 시험 — 글로 잡음(화면 · 서버 없음).
- `/save` 몸에 `개정확인` 이 실림 · 로직 화면 [저장]은 `saveRevised` 한 길(개정 409 → 창 → 다시 보냄)
- 창은 부품 상자(모달 · 목록 줄 · 배지)만 · 옛 `stale` 409 는 이 창 대상 밖
"""
from pathlib import Path
M01 = Path(__file__).resolve().parents[2] / "M01_MasterData"
def _read(name: str) -> str:
return (M01 / f"M01_MasterData_UI_{name}.ts").read_text(encoding="utf-8")
def test_save_body_carries_confirm():
api = _read("Logic_Api")
assert 'request("/save", { files, 개정확인 })' in api
def test_lab_save_goes_through_revise():
lab = _read("LogicLab")
assert "saveRevised((ok) => saveFiles(" in lab
assert "await saveFiles(" not in lab.replace("saveRevised((ok) => saveFiles(", "")
def test_revise_window_parts_only():
rev = _read("LogicLab_Revise")
for part in ("ui_template_modal", "ui_template_list_row", "ui_template_badge"):
assert f"@ui/{part}" in rev
assert "<button" not in rev and "document.createElement" not in rev
# 개정 409 만 창 — 옛 stale 409 는 부르는 쪽 토스트 그대로
assert "개정?: ReviseHit[]" in rev and "if (!hits) throw error" in rev
assert "save(true)" in rev