From 3d4bcaea5997d9d197f8a5f66ceb06b2686f7369 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 15:42:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(713-2):=20M01=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B8=20=EA=B0=9C=EC=A0=95=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=B0=BD=20=E2=80=94=20/save=20=EA=B0=9C=EC=A0=95=20409=20?= =?UTF-8?q?=EB=B0=9B=EC=9C=BC=EB=A9=B4=20=EB=B0=94=EB=80=8C=EB=8A=94=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B8=20=EC=A4=84(=ED=8C=8C=EC=9D=BC=20=C2=B7=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=C2=B7=20=EB=B0=94=EB=80=9C)=20=EC=B0=BD?= =?UTF-8?q?=20=C2=B7=20=E3=80=8C=EC=9B=90=EB=AC=B8=20=EA=B0=9C=EC=A0=95=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=E3=80=8D=20=ED=99=95=EC=9D=B8=20=EB=92=A4=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=EB=AA=B8=EC=97=90=20=EA=B0=9C=EC=A0=95?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20true=20=EB=8B=A4=EC=8B=9C=20=EB=B3=B4?= =?UTF-8?q?=EB=83=84=20=C2=B7=20=EC=B7=A8=EC=86=8C=20=3D=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 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 --- M01_MasterData/M01_MasterData_UI_LogicLab.ts | 6 +- .../M01_MasterData_UI_LogicLab_Revise.ts | 103 ++++++++++++++++++ M01_MasterData/M01_MasterData_UI_Logic_Api.ts | 3 +- resources/tester/test_713_2_revise_ui.py | 34 ++++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_LogicLab_Revise.ts create mode 100644 resources/tester/test_713_2_revise_ui.py diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 877fb2cf8..7af76d2a3 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -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(); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Revise.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Revise.ts new file mode 100644 index 000000000..624452773 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Revise.ts @@ -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; + +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; + } +} diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index 947a8a044..1f4264f10 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -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 = ( diff --git a/resources/tester/test_713_2_revise_ui.py b/resources/tester/test_713_2_revise_ui.py new file mode 100644 index 000000000..274ff2bfd --- /dev/null +++ b/resources/tester/test_713_2_revise_ui.py @@ -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 "