Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-09-16 11:28:12 +09:00
6 changed files with 109 additions and 21 deletions
+20 -5
View File
@@ -132,8 +132,11 @@ export interface TableMeta {
formula?: Record<string, string>;
}
/** 덮개 판정 낱말 셋 — 서버가 줌 · 이것만 씀(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<string, unknown>, key: string): CellInfo {
const overrides = row["@overrides"] as
Record<string, { original: unknown; current?: unknown; state?: OverrideState }> | undefined;
| Record<
string,
{ original: unknown; current?: unknown; state?: OverrideState; was?: string; now?: string }
>
| undefined;
const filled = row["@formula"] as Record<string, string> | undefined;
const override = overrides?.[key];
// 판정은 서버 state 만 — 화면이 값을 견줘 따로 정하면 판정이 두 벌이 됨(2026-09-15 브레인).
@@ -164,7 +175,11 @@ export function cellInfo(meta: TableMeta, row: Record<string, unknown>, 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,
};
+36 -11
View File
@@ -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<typeof L>[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<void> {
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<void> {
const from = source;
+4 -3
View File
@@ -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<OverrideState, Parameters<typeof L>[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;
};
/**
* 고친 것 목록을 표 한 벌로 — 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)과 쪽 나눔은 서버가 함.
+13 -1
View File
@@ -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,
+20 -1
View File
@@ -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
+16
View File
@@ -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",