From fe10ef86abd07f33724282716d3569f5317a096b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 16 Sep 2026 11:19:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(z01):=20=EB=8D=AE=EA=B0=9C=20=ED=8C=90?= =?UTF-8?q?=EC=A0=95=20=EB=84=B7=EC=A7=B8=20meaning=5Fchanged=20=E2=80=94?= =?UTF-8?q?=20=EA=B0=99=EC=9D=80=20=EB=B2=88=ED=98=B8=EC=97=90=20=EB=94=B4?= =?UTF-8?q?=20=EB=AC=BC=EA=B1=B4(=EB=AC=B8=ED=99=94=EC=9E=AC=EB=AA=A9?= =?UTF-8?q?=EA=B3=B5=20=E2=86=92=20=ED=95=9C=EC=8B=9D=EB=AA=A9=EA=B3=B5)?= =?UTF-8?q?=20=C2=B7=20=EA=B0=92=20=EC=96=B4=EA=B8=8B=EB=82=A8=EB=B3=B4?= =?UTF-8?q?=EB=8B=A4=20=EC=84=B8=EA=B2=8C(=EC=A4=84=20=EC=A7=84=ED=95=9C?= =?UTF-8?q?=20=EB=B6=89=EC=9D=8C=20=C2=B7=20=EC=B9=B8=20=EA=BD=89=20?= =?UTF-8?q?=EC=B0=AC=20=EB=B6=89=EC=9D=8C=20=C2=B7=20=E2=9B=94)=20=C2=B7?= =?UTF-8?q?=20=EC=98=9B=20=EC=9D=B4=EB=A6=84/=EC=A7=80=EA=B8=88=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=82=98=EB=9E=80=ED=9E=88=20=C2=B7=20?= =?UTF-8?q?=E3=80=8C=EC=83=88=20=EC=9B=90=EB=B3=B8=20=EA=B0=92=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9B=EA=B8=B0=E3=80=8D=20=EB=8C=80=EC=8B=A0=20?= =?UTF-8?q?=E3=80=8C=EA=B3=A0=EC=B9=9C=20=EA=B0=92=20=EB=B2=84=EB=A6=AC?= =?UTF-8?q?=EA=B3=A0=20=EC=A7=80=EA=B8=88=20=EC=A4=84=20=EA=B0=92=20?= =?UTF-8?q?=E2=97=8B=20=EC=93=B0=EA=B8=B0=E3=80=8D=20+=20=ED=95=9C=20?= =?UTF-8?q?=EB=B2=88=20=EB=8D=94=20=EB=AC=BC=EC=9D=8C=20=C2=B7=20=EB=AA=A8?= =?UTF-8?q?=EC=95=84=20=EB=B3=B4=EA=B8=B0=20=EC=9D=B4=EB=A6=84=20=E3=80=8C?= =?UTF-8?q?=E2=9B=94=20=EB=9C=BB=20=EB=B0=94=EB=80=9C=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq --- Z01_MasterData/Z01_MasterData_UI_Cells.ts | 25 +++++++++--- Z01_MasterData/Z01_MasterData_UI_Rows.ts | 47 +++++++++++++++++----- Z01_MasterData/Z01_MasterData_UI_Side.ts | 7 ++-- Z01_MasterData/Z01_MasterData_UI_Style.css | 14 ++++++- resources/tester/test_z01_master_cells.py | 21 +++++++++- ui_template/ui_template_locale_b3.ts | 16 ++++++++ 6 files changed, 109 insertions(+), 21 deletions(-) diff --git a/Z01_MasterData/Z01_MasterData_UI_Cells.ts b/Z01_MasterData/Z01_MasterData_UI_Cells.ts index 9ca85955..b4e9b850 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Cells.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Cells.ts @@ -132,8 +132,11 @@ export interface TableMeta { formula?: Record; } -/** 덮개 판정 낱말 셋 — 서버가 줌 · 이것만 씀(2026-09-15 브레인). */ -export type OverrideState = "edited" | "source_changed" | "orphan"; +/** + * 덮개 판정 낱말 넷 — 서버가 줌 · 이것만 씀(2026-09-15~16 브레인). + * meaning_changed = 번호는 그대로인데 줄의 뜻이 바뀜(문화재목공 → 한식목공) — 같은 코드에 딴 물건. + */ +export type OverrideState = "edited" | "source_changed" | "meaning_changed" | "orphan"; export interface CellInfo { editable: boolean; @@ -143,8 +146,12 @@ export interface CellInfo { original?: unknown; /** 고친 칸의 서버 판정 — 안 고친 칸 null · 서버가 state 를 안 주면 「missing」(때우지 않음) */ state: OverrideState | "missing" | null; - /** source_changed 칸만 — 지금 원본 값 */ + /** source_changed·meaning_changed 칸만 — 지금 원본 값 */ current?: unknown; + /** meaning_changed 칸만 — 고칠 때 줄 이름 */ + was?: string; + /** meaning_changed 칸만 — 지금 줄 이름 */ + now?: string; rule: string | null; /** 이 줄 값을 넣은 식 — 줄마다 다를 때만 옴 */ filled: string | null; @@ -153,7 +160,11 @@ export interface CellInfo { /** 칸 하나의 고침 상태 — 줄의 `@overrides`·`@formula` 는 해당할 때만 실려 옴. */ export function cellInfo(meta: TableMeta, row: Record, key: string): CellInfo { const overrides = row["@overrides"] as - Record | undefined; + | Record< + string, + { original: unknown; current?: unknown; state?: OverrideState; was?: string; now?: string } + > + | undefined; const filled = row["@formula"] as Record | undefined; const override = overrides?.[key]; // 판정은 서버 state 만 — 화면이 값을 견줘 따로 정하면 판정이 두 벌이 됨(2026-09-15 브레인). @@ -164,7 +175,11 @@ export function cellInfo(meta: TableMeta, row: Record, key: str overridden: override !== undefined, ...(override ? { original: override.original } : {}), state, - ...(state === "source_changed" ? { current: override?.current } : {}), + ...(state === "source_changed" || state === "meaning_changed" + ? { current: override?.current } + : {}), + ...(override?.was !== undefined ? { was: override.was } : {}), + ...(override?.now !== undefined ? { now: override.now } : {}), rule: meta.formula?.[key] ?? null, filled: filled?.[key] ?? null, }; diff --git a/Z01_MasterData/Z01_MasterData_UI_Rows.ts b/Z01_MasterData/Z01_MasterData_UI_Rows.ts index 2e8fae42..8cc9b8c9 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Rows.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Rows.ts @@ -7,7 +7,13 @@ * (2026-09-15 브레인 — 계산으로 나오는 칸은 못 고치고 왜 못 고치는지 옆에 보일 것). * ========================================================================== */ -import { createButton, createInputField, el, showToast } from "@ui/ui_template_elements"; +import { + createButton, + createInputField, + el, + showConfirmDialog, + showToast, +} from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import type { MasterRows, RowsQuery } from "./Z01_MasterData_Api_Fetch"; import { @@ -58,6 +64,9 @@ function fill(key: Parameters[0], value: string): string { function stateText(info: CellInfo, column: MasterColumn): string { const original = valueWithUnit(info.original, column.unit); switch (info.state) { + case "meaning_changed": + // 값이 틀린 것보다 나쁨 — 같은 번호에 딴 물건이 앉음. 옛 이름·지금 이름을 나란히. + return `${L("Z01_MasterData_State_MeaningChanged")} — ${fill("Z01_MasterData_State_Was", info.was ?? "?")} / ${fill("Z01_MasterData_State_Now", info.now ?? "?")} · ${fill("Z01_MasterData_Edit_OriginalWas", original)}`; case "source_changed": return `${fill("Z01_MasterData_State_SourceChanged", valueWithUnit(info.current, column.unit))} (${fill("Z01_MasterData_Edit_OriginalWas", original)})`; case "orphan": @@ -242,10 +251,11 @@ export function buildRowsView(): RowsView { const td = el("td", { text: texts[i], attrs: { title: texts[i] } }); if (info.state) { // 원본이 바뀐 칸은 줄째 또렷이 — 흐리면 옛 값이 조용히 계속 쓰임(2026-09-15 브레인). - td.classList.add("is-overridden", `is-${info.state.replace("_", "-")}`); + td.classList.add("is-overridden", `is-${info.state.replaceAll("_", "-")}`); if (info.state !== "edited") { - td.textContent = `⚠ ${texts[i]}`; - tr.classList.add("has-warning"); + // 뜻 바뀜은 값 어긋남보다 나쁨 — 표시부터 더 세게(2026-09-16 브레인). + td.textContent = `${info.state === "meaning_changed" ? "⛔" : "⚠"} ${texts[i]}`; + tr.classList.add(info.state === "meaning_changed" ? "has-danger" : "has-warning"); } td.title = stateText(info, column); } @@ -310,18 +320,21 @@ export function buildRowsView(): RowsView { if (info.state !== "edited") stateLine.classList.add("is-warning"); lines.push(stateLine); // 단추 글자에 무엇으로 바뀌는지 값을 넣음 — 안 보이고 누르면 금액이 말없이 바뀜(브레인). + // 뜻이 바뀐 줄에서 「새 원본 값으로 받기」는 위험 — 글자를 가르고 한 번 더 물음(브레인 ③). const target = - info.state === "source_changed" - ? fill("Z01_MasterData_Edit_TakeSource", valueWithUnit(info.current, column.unit)) - : info.state === "orphan" - ? null - : fill("Z01_MasterData_Edit_RevertTo", valueWithUnit(info.original, column.unit)); + info.state === "meaning_changed" + ? fill("Z01_MasterData_Edit_DropOnMeaning", valueWithUnit(info.current, column.unit)) + : info.state === "source_changed" + ? fill("Z01_MasterData_Edit_TakeSource", valueWithUnit(info.current, column.unit)) + : info.state === "orphan" + ? null + : fill("Z01_MasterData_Edit_RevertTo", valueWithUnit(info.original, column.unit)); if (target && source?.save) { lines.push( createButton({ label: target, - variant: "ghost", - onClick: () => void commit(index, column, null), + variant: info.state === "meaning_changed" ? "danger" : "ghost", + onClick: () => void drop(index, column, info), }), ); } @@ -365,6 +378,18 @@ export function buildRowsView(): RowsView { input.addEventListener("blur", () => finish(true)); } + /** 고침 버리기 — 뜻이 바뀐 줄은 한 번 더 묻고(딴 물건에 값이 붙은 자리라 되돌리기와 뜻이 다름). */ + async function drop(index: number, column: MasterColumn, info: CellInfo): Promise { + if (info.state === "meaning_changed") { + const ok = await showConfirmDialog( + `${fill("Z01_MasterData_State_Was", info.was ?? "?")} / ${fill("Z01_MasterData_State_Now", info.now ?? "?")}\n${L("Z01_MasterData_Edit_MeaningConfirm")}`, + L("Z01_MasterData_Edit_DropConfirm"), + ); + if (!ok) return; + } + await commit(index, column, null); + } + /** 저장 — null 은 덮개에서 그 칸을 빼 원래 값으로 되돌림. */ async function commit(index: number, column: MasterColumn, value: unknown): Promise { const from = source; diff --git a/Z01_MasterData/Z01_MasterData_UI_Side.ts b/Z01_MasterData/Z01_MasterData_UI_Side.ts index 56111cb7..c50d6fed 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Side.ts +++ b/Z01_MasterData/Z01_MasterData_UI_Side.ts @@ -20,7 +20,7 @@ import { saveBasePrice, VALUES_TABLE_ID, } from "./Z01_MasterData_Api_Fetch"; -import { valueWithUnit } from "./Z01_MasterData_UI_Cells"; +import { valueWithUnit, type OverrideState } from "./Z01_MasterData_UI_Cells"; import type { RowsView } from "./Z01_MasterData_UI_Rows"; export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLElement { @@ -118,11 +118,12 @@ export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLEleme return panel; } -const STATE_LABEL = { +const STATE_LABEL: Record[0]> = { + meaning_changed: "Z01_MasterData_List_MeaningChanged", source_changed: "Z01_MasterData_List_SourceChanged", orphan: "Z01_MasterData_List_Orphan", edited: "Z01_MasterData_List_Edited", -} as const; +}; /** * 고친 것 목록을 표 한 벌로 — 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)과 쪽 나눔은 서버가 함. diff --git a/Z01_MasterData/Z01_MasterData_UI_Style.css b/Z01_MasterData/Z01_MasterData_UI_Style.css index a8959587..b62321c4 100644 --- a/Z01_MasterData/Z01_MasterData_UI_Style.css +++ b/Z01_MasterData/Z01_MasterData_UI_Style.css @@ -271,10 +271,22 @@ background: color-mix(in srgb, var(--color-danger) 8%, var(--color-canvas)); } -.z01-master__grid tr.has-warning td:first-child { +.z01-master__grid tr.has-warning td:first-child, +.z01-master__grid tr.has-danger td:first-child { box-shadow: inset 4px 0 0 var(--color-danger); } +/* 뜻 바뀜 — 값 어긋남보다 나쁨(같은 번호에 딴 물건). 줄째 더 진하게. */ +.z01-master__grid tr.has-danger td { + background: color-mix(in srgb, var(--color-danger) 18%, var(--color-canvas)); +} + +.z01-master__grid tr td.is-meaning-changed { + background: var(--color-danger); + color: var(--color-primary-text); + font-weight: var(--font-weight-bold); +} + /* tr 을 넣어 줄 배경(tr.has-warning td)보다 무겁게 — 칸이 줄보다 더 진해야 함 */ .z01-master__grid tr td.is-source-changed, .z01-master__grid tr td.is-orphan, diff --git a/resources/tester/test_z01_master_cells.py b/resources/tester/test_z01_master_cells.py index 26cdc294..a88ad823 100644 --- a/resources/tester/test_z01_master_cells.py +++ b/resources/tester/test_z01_master_cells.py @@ -111,6 +111,21 @@ const withState = (state, current) => ({ const changedRow = withState("source_changed", 250000); const sameRow = withState("edited", 270000); const orphanRow = withState("orphan", undefined); +// 뜻 바뀜 — 번호는 그대로인데 줄이 딴 물건이 됨(노임 3001 문화재목공 → 한식목공) +const meaningRow = { + "@id": "labor_const/3001", + daily_wage_krw: 300000, + "@overrides": { + daily_wage_krw: { + original: 250000, + value: 300000, + current: 260000, + state: "meaning_changed", + was: "문화재목공", + now: "한식목공", + }, + }, +}; const wait = (ms, value, fail) => new Promise((ok, no) => setTimeout(() => (fail ? no(new Error(value)) : ok(value)), ms)); @@ -156,6 +171,7 @@ writeFileSync(process.argv[2], JSON.stringify({ cellInfo(meta, changedRow, "daily_wage_krw"), cellInfo(meta, sameRow, "daily_wage_krw"), cellInfo(meta, orphanRow, "daily_wage_krw"), + cellInfo(meta, meaningRow, "daily_wage_krw"), ], parsed: [ parseCellInput("12,340", 100), @@ -234,7 +250,7 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None: assert got["raceError"] == [None, "new"] assert got["latestError"] == "thrown boom" # 고칠 수 있는 칸 · 고쳐짐이면 원래 값 · 못 고치는 칸은 까닭과 식 · 줄마다 값 넣은 식 - wage, hourly, plain, changed, same, orphan = got["info"] + wage, hourly, plain, changed, same, orphan, meaning = got["info"] # state 가 안 온 고친 칸은 화면이 때우지 않고 「missing」으로 드러냄(서버 버그) assert wage == { "editable": True, @@ -249,6 +265,9 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None: assert changed["state"] == "source_changed" and changed["current"] == 250000 assert same["state"] == "edited" and "current" not in same assert orphan["state"] == "orphan" + # 뜻 바뀜 — 옛 이름·지금 이름을 나란히 보여야 사람이 딴 물건인지 가림 + assert meaning["state"] == "meaning_changed" and meaning["current"] == 260000 + assert meaning["was"] == "문화재목공" and meaning["now"] == "한식목공" assert hourly["state"] is None and plain["state"] is None assert hourly["editable"] is False and hourly["reason"] == "계산으로 나옴" assert hourly["overridden"] is False and "original" not in hourly diff --git a/ui_template/ui_template_locale_b3.ts b/ui_template/ui_template_locale_b3.ts index 5b7bfdc9..4adc3605 100644 --- a/ui_template/ui_template_locale_b3.ts +++ b/ui_template/ui_template_locale_b3.ts @@ -171,6 +171,22 @@ export const ui_locales_b3 = { "⚠ 고친 뒤 원본이 바뀜 — 고친 값을 그대로 씀 · 새 원본 값 {value}", "⚠ Source changed after edit — edited value still used · new source {value}", ], + Z01_MasterData_State_MeaningChanged: [ + "⛔ 고쳐짐 — 줄의 뜻이 바뀜(같은 번호에 다른 물건)", + "⛔ Edited — the row now means something else (same code, different item)", + ], + Z01_MasterData_State_Was: ["고칠 때: {value}", "when edited: {value}"], + Z01_MasterData_State_Now: ["지금: {value}", "now: {value}"], + Z01_MasterData_Edit_DropOnMeaning: [ + "고친 값 버리고 지금 줄 값 {value} 쓰기", + "Drop the edit and use the current row value {value}", + ], + Z01_MasterData_Edit_MeaningConfirm: [ + "줄의 뜻이 바뀌었음 — 고친 값을 버리면 지금 줄(다른 물건일 수 있음) 값을 씀. 버릴까?", + "This row now means something else. Dropping the edit uses the current row's value. Continue?", + ], + Z01_MasterData_Edit_DropConfirm: ["고친 값 버리기", "Drop the edit"], + Z01_MasterData_List_MeaningChanged: ["⛔ 뜻 바뀜", "⛔ Meaning changed"], Z01_MasterData_State_Orphan: [ "⚠ 주인 없음 — 갱신으로 그 줄이 사라짐", "⚠ Orphan — the row disappeared on update",