feat(z01): 기준일 칸(없으면 「판 모름」) · 단추 글자·칸 설명 값에 쉼표와 단위(「원래 값 17 L 으로 되돌리기」) · 저장이 안 되면 칸 설명 줄에 까닭이 남음(수 못 읽음 · 서버 거절) · 고친 것 목록 값에 쉼표 · 고친 뒤 선택 테두리 유지(브레인 셋)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -33,11 +33,25 @@ export function cellText(value: unknown, unit?: string | null): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/** 줄마다 판의 기준일 — 기초단가 한 표에 시기가 다른 판이 섞여 줄마다 보임(2026-09-15 브레인). */
|
||||
export const EFFECTIVE_DATE_KEY = "effective_date";
|
||||
|
||||
/** 한 줄의 칸들 — columns 차례대로만(columns 에 없는 열은 안 그림) · 줄에 그 열이 없으면 「—」. */
|
||||
export function rowCells(row: Record<string, unknown>, columns: MasterColumn[]): string[] {
|
||||
return columns.map((column) =>
|
||||
Object.hasOwn(row, column.key) ? cellText(row[column.key], column.unit) : "—",
|
||||
);
|
||||
return columns.map((column) => {
|
||||
// 기준일 없는 자료(machine_operating 등)는 빈칸이 아니라 모른다고 말함.
|
||||
if (column.key === EFFECTIVE_DATE_KEY) return cellText(row[column.key]) || "판 모름";
|
||||
return Object.hasOwn(row, column.key) ? cellText(row[column.key], column.unit) : "—";
|
||||
});
|
||||
}
|
||||
|
||||
/** 단추 글자·칸 설명에 넣는 값 — 수는 늘 쉼표 · 단위(이름표)를 뒤에 붙여 무엇인지 보이게. */
|
||||
export function valueWithUnit(value: unknown, unit?: string | null): string {
|
||||
const text =
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 10 })
|
||||
: cellText(value, unit);
|
||||
return text && unit ? `${text} ${unit}` : text;
|
||||
}
|
||||
|
||||
/** 기초단가 표 머리 — 열 하나에 한 문장이라 표에 한 번만 옴(2026-09-15 브레인 ④). */
|
||||
|
||||
@@ -12,7 +12,6 @@ import { t as L } from "@ui/ui_template_locale";
|
||||
import type { MasterRows, RowsQuery } from "./Z01_MasterData_Api_Fetch";
|
||||
import {
|
||||
cellInfo,
|
||||
cellText,
|
||||
clampPage,
|
||||
columnTitle,
|
||||
latestOnly,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
parseCellInput,
|
||||
rowCells,
|
||||
shownColumns,
|
||||
valueWithUnit,
|
||||
type CellInfo,
|
||||
type MasterColumn,
|
||||
} from "./Z01_MasterData_UI_Cells";
|
||||
@@ -51,10 +51,10 @@ function fill(key: Parameters<typeof L>[0], value: string): string {
|
||||
|
||||
/** 덮개 판정 한 줄 — 서버 state 그대로 · state 가 없으면 버그로 드러냄. */
|
||||
function stateText(info: CellInfo, column: MasterColumn): string {
|
||||
const original = cellText(info.original, column.unit);
|
||||
const original = valueWithUnit(info.original, column.unit);
|
||||
switch (info.state) {
|
||||
case "source_changed":
|
||||
return `${fill("Z01_MasterData_State_SourceChanged", cellText(info.current, column.unit))} (${fill("Z01_MasterData_Edit_OriginalWas", original)})`;
|
||||
return `${fill("Z01_MasterData_State_SourceChanged", valueWithUnit(info.current, column.unit))} (${fill("Z01_MasterData_Edit_OriginalWas", original)})`;
|
||||
case "orphan":
|
||||
return L("Z01_MasterData_State_Orphan");
|
||||
case "missing":
|
||||
@@ -201,6 +201,7 @@ export function buildRowsView(): RowsView {
|
||||
td.classList.add("is-selected");
|
||||
td.addEventListener("click", () => {
|
||||
if (td.querySelector("input")) return;
|
||||
grid.querySelector("td.is-selected")?.classList.remove("is-selected");
|
||||
selectCell(index, column);
|
||||
td.classList.add("is-selected");
|
||||
if (canEdit) startEdit(td, index, column);
|
||||
@@ -225,15 +226,22 @@ export function buildRowsView(): RowsView {
|
||||
next.disabled = end.disabled = state.page >= pages;
|
||||
}
|
||||
|
||||
/** 칸 설명 줄 — 고칠 수 있나 · 못 고치는 까닭 · 원래 값(되돌리기) · 식과 값 넣은 식. */
|
||||
function selectCell(index: number, column: MasterColumn): void {
|
||||
/**
|
||||
* 칸 설명 줄 — 고칠 수 있나 · 못 고치는 까닭 · 원래 값(되돌리기) · 식과 값 넣은 식.
|
||||
* `failure` = 방금 저장이 안 된 까닭 — 토스트만 뜨고 사라지면 사람은 저장된 줄 앎(브레인 ③).
|
||||
*/
|
||||
function selectCell(index: number, column: MasterColumn, failure?: string): void {
|
||||
if (!last) return;
|
||||
grid.querySelector("td.is-selected")?.classList.remove("is-selected");
|
||||
selected = { index, key: column.key };
|
||||
const row = last.rows[index];
|
||||
const info = cellInfo(last, row, column.key);
|
||||
const lines: HTMLElement[] = [el("strong", { text: columnTitle(column) })];
|
||||
const line = (text: string) => el("span", { className: "z01-master__detail-line", text });
|
||||
if (failure) {
|
||||
const failed = line(fill("Z01_MasterData_Edit_NotSaved", failure));
|
||||
failed.classList.add("is-warning");
|
||||
lines.push(failed);
|
||||
}
|
||||
if (source?.save) {
|
||||
lines.push(
|
||||
line(
|
||||
@@ -250,10 +258,10 @@ export function buildRowsView(): RowsView {
|
||||
// 단추 글자에 무엇으로 바뀌는지 값을 넣음 — 안 보이고 누르면 금액이 말없이 바뀜(브레인).
|
||||
const target =
|
||||
info.state === "source_changed"
|
||||
? fill("Z01_MasterData_Edit_TakeSource", cellText(info.current, column.unit))
|
||||
? fill("Z01_MasterData_Edit_TakeSource", valueWithUnit(info.current, column.unit))
|
||||
: info.state === "orphan"
|
||||
? null
|
||||
: fill("Z01_MasterData_Edit_RevertTo", cellText(info.original, column.unit));
|
||||
: fill("Z01_MasterData_Edit_RevertTo", valueWithUnit(info.original, column.unit));
|
||||
if (target && source?.save) {
|
||||
lines.push(
|
||||
createButton({
|
||||
@@ -283,8 +291,14 @@ export function buildRowsView(): RowsView {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
const value = save ? parseCellInput(input.value, current) : current;
|
||||
if (value === undefined) showToast(L("Z01_MasterData_Edit_Invalid"), "error");
|
||||
if (value === undefined || value === current) {
|
||||
if (value === undefined) {
|
||||
const why = fill("Z01_MasterData_Edit_Invalid", input.value);
|
||||
showToast(why, "error");
|
||||
draw();
|
||||
selectCell(index, column, why);
|
||||
return;
|
||||
}
|
||||
if (value === current) {
|
||||
draw();
|
||||
return;
|
||||
}
|
||||
@@ -314,8 +328,11 @@ export function buildRowsView(): RowsView {
|
||||
"success",
|
||||
);
|
||||
} catch (error) {
|
||||
showToast(error instanceof Error ? error.message : L("Z01_MasterData_Edit_Failed"), "error");
|
||||
const why = error instanceof Error ? error.message : L("Z01_MasterData_Edit_Failed");
|
||||
showToast(why, "error");
|
||||
if (from !== source || table !== last) return;
|
||||
draw();
|
||||
selectCell(index, column, why);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
saveBasePrice,
|
||||
VALUES_TABLE_ID,
|
||||
} from "./Z01_MasterData_Api_Fetch";
|
||||
import { valueWithUnit } from "./Z01_MasterData_UI_Cells";
|
||||
import type { RowsView } from "./Z01_MasterData_UI_Rows";
|
||||
|
||||
export function buildSidePanel(groups: MasterGroup[], view: RowsView): HTMLElement {
|
||||
@@ -147,6 +148,10 @@ async function loadOverrides(query: RowsQuery): Promise<MasterRows> {
|
||||
],
|
||||
rows: items.slice(start, start + query.size).map((item) => ({
|
||||
...item,
|
||||
// 값은 쉼표를 붙여 — 목록 줄에는 단위가 안 실려 옴.
|
||||
value: valueWithUnit(item.value),
|
||||
original: valueWithUnit(item.original),
|
||||
...(Object.hasOwn(item, "current") ? { current: valueWithUnit(item.current) } : {}),
|
||||
// state 가 안 오면 때우지 않고 드러냄(서버 버그).
|
||||
state: L(STATE_LABEL[item.state] ?? "Z01_MasterData_State_Missing"),
|
||||
kind: L(`Z01_MasterData_Base_${item.kind}` as const),
|
||||
|
||||
@@ -25,9 +25,20 @@ _RUNNER = """
|
||||
import { writeFileSync } from "node:fs";
|
||||
import {
|
||||
cellInfo, cellText, clampPage, columnTitle, latestOnly, pageCount, parseCellInput, rowCells,
|
||||
shownColumns,
|
||||
shownColumns, valueWithUnit,
|
||||
} from "./Z01_MasterData_UI_Cells.js";
|
||||
|
||||
// 기준일 — 한 표에 시기가 다른 판이 섞이니 줄마다 보임 · 없으면 「판 모름」(2026-09-15 브레인 ①)
|
||||
const dated = [
|
||||
{ key: "occupation_name", label: "직종명", unit: null, hidden: false },
|
||||
{ key: "effective_date", label: "기준일", unit: null, hidden: false },
|
||||
];
|
||||
const editions = [
|
||||
rowCells({ occupation_name: "보통인부", effective_date: "2026-01-01" }, dated),
|
||||
rowCells({ occupation_name: "기계운전", effective_date: null }, dated),
|
||||
rowCells({ occupation_name: "굴착기" }, dated),
|
||||
];
|
||||
|
||||
// 기초단가 — 표 수준 editable·locked·formula · 줄 수준 @id·@overrides·@formula (2026-09-15 브레인)
|
||||
const meta = {
|
||||
editable: ["daily_wage_krw"],
|
||||
@@ -102,6 +113,15 @@ writeFileSync(process.argv[2], JSON.stringify({
|
||||
parseCellInput("보통인부2", "보통인부"),
|
||||
parseCellInput("3.5", null),
|
||||
].map((v) => (v === undefined ? "invalid" : v)),
|
||||
editions,
|
||||
withUnit: [
|
||||
valueWithUnit(17, "원"),
|
||||
valueWithUnit(41500000, "원"),
|
||||
valueWithUnit(0.17, ""),
|
||||
valueWithUnit(12345.678, null),
|
||||
valueWithUnit(null, "원"),
|
||||
valueWithUnit("5억 미만", ""),
|
||||
],
|
||||
}));
|
||||
"""
|
||||
|
||||
@@ -173,3 +193,7 @@ def test_칸_글자와_숨김_열(tmp_path: Path) -> None:
|
||||
assert plain["editable"] is False and plain["reason"] is None and plain["rule"] is None
|
||||
# 수 칸은 쉼표를 떼고 수로 · 못 읽으면 저장 안 함 · 글자 칸은 글자 그대로 · 빈 칸에 수를 적으면 수
|
||||
assert got["parsed"] == [12340, "invalid", "invalid", "보통인부2", 3.5]
|
||||
# 기준일이 없으면 빈칸·「—」가 아니라 「판 모름」
|
||||
assert [row[1] for row in got["editions"]] == ["2026-01-01", "판 모름", "판 모름"]
|
||||
# 단추 글자·칸 설명의 값 — 쉼표 + 단위(이름표) · 단위 없으면 수만
|
||||
assert got["withUnit"] == ["17 원", "41,500,000 원", "0.17", "12,345.678", "", "5억 미만"]
|
||||
|
||||
@@ -161,7 +161,11 @@ export const ui_locales_b3 = {
|
||||
Z01_MasterData_Edit_RevertTo: ["원래 값 {value} 으로 되돌리기", "Revert to original {value}"],
|
||||
Z01_MasterData_Edit_TakeSource: ["새 원본 값 {value} 으로 받기", "Take new source value {value}"],
|
||||
Z01_MasterData_Edit_OriginalWas: ["고칠 때 원본 {value}", "source when edited {value}"],
|
||||
Z01_MasterData_Edit_Invalid: ["수로 못 읽음 — 저장 안 함", "Not a number — not saved"],
|
||||
Z01_MasterData_Edit_Invalid: [
|
||||
"「{value}」 를 수로 못 읽음",
|
||||
"Cannot read 「{value}」 as a number",
|
||||
],
|
||||
Z01_MasterData_Edit_NotSaved: ["⚠ 저장 안 됨 — {value}", "⚠ Not saved — {value}"],
|
||||
Z01_MasterData_Edit_Saved: ["저장했음 — 덮개 파일에 쌓임", "Saved to the override file"],
|
||||
Z01_MasterData_Edit_Reverted: ["덮개를 뺐음 — 원본 값을 씀", "Override removed — using source"],
|
||||
Z01_MasterData_Edit_Failed: ["저장 못 함", "Save failed"],
|
||||
|
||||
Reference in New Issue
Block a user