Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 고친 것 목록을 표 한 벌로 — 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)과 쪽 나눔은 서버가 함.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user