feat(Z01): 기초단가 표 [저장] 방식 교체 · 공종 편집 화면 신설 — PLAN 1-4

- 기초단가 표: 칸 나갈 때 즉시 저장 없앰 · 고친 칸·새 줄·지울 줄은 초안(sessionStorage z01-draft:base:{kind})에 쌓고 [저장] 한 번에 보냄 · [고친 것 버리기]·[줄 추가]·[줄 삭제]·[초기값으로] · 409 면 초안 지키고 알림 · 서버가 revision 을 줄 때만 고치기 단추 섬
- 옛 덮개 길(칸 단위 PUT · 고친 것 모아 보기 · 덮개 판정 표시)과 그 사전 글자 뺌 — 계약 1-0 에서 서버 쪽 삭제
- 공종 편집(Z01_MasterData_UI_WorkItem.ts · _WorkItem_Rows.ts): 왼쪽 목록(상태별 개수·검색) / 머리 / 구성 줄 표(갈래별 · 자원 찾기 = 기초단가 검색) / 품셈 원문 표+[주] 읽기 전용 / [저장]·[초기값으로]·[검수 완료] · 초안 열쇠 z01-draft:work_item:{key}
- 공종 API(sub_laptop_3 e1c71c3e) 모양에 맞춤 — 줄은 id + 계약 칸 열하나만 보냄(extra=forbid) · 저장·초기값 응답(새 GET 모양)으로 바로 다시 그림 · 저장된 고침(changed_columns·is_added) 표시
- 초안 순수 함수 Z01_MasterData_UI_Draft.ts + 시험 test_z01_master_draft.py

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145cr1DMC5Ft8fouQSXtp69
This commit is contained in:
2026-09-18 23:55:45 +09:00
co-authored by Claude Opus 5
parent 9629430aff
commit feab848e69
10 changed files with 2012 additions and 309 deletions
+27 -68
View File
@@ -12,25 +12,22 @@ import { t as L } from "@ui/ui_template_locale";
import {
fetchBasePrices,
fetchMasterRows,
fetchOverrides,
resetBasePrices,
saveBasePrices,
type MasterGroup,
type MasterRows,
type RowsQuery,
saveBasePrice,
VALUES_TABLE_ID,
type WorkItemAxis,
} from "./Z01_MasterData_Api_Fetch";
import {
groupKinds,
valueWithUnit,
type OverrideState,
type ServedKind,
} from "./Z01_MasterData_UI_Cells";
import { groupKinds, type ServedKind } from "./Z01_MasterData_UI_Cells";
import type { RowsView } from "./Z01_MasterData_UI_Rows";
const AXES: WorkItemAxis[] = ["forest", "const"];
export function buildSidePanel(
groups: MasterGroup[],
kinds: ServedKind[],
view: RowsView,
openWorkItems: (axis: WorkItemAxis) => void,
): HTMLElement {
const panel = el("div", { className: "z01-master__side" });
const activate = (button: HTMLElement): void => {
@@ -59,29 +56,32 @@ export function buildSidePanel(
title: `${box.label} › ${served.label}`,
key: `base-prices/${served.kind}`,
load: (query) => fetchBasePrices(served.kind, query),
save: (rowId, values) => saveBasePrice(served.kind, rowId, values),
edit: {
scope: `base:${served.kind}`,
save: (body) => saveBasePrices(served.kind, body),
reset: (rowKeys) => resetBasePrices(served.kind, rowKeys),
},
});
});
grid.append(button);
}
return { label: box.label, grid };
});
const overridesButton = el("button", {
className: "z01-master__base z01-master__base--wide",
attrs: { type: "button" },
text: L("Z01_MasterData_Overrides"),
});
overridesButton.addEventListener("click", () => {
activate(overridesButton);
view.show({
title: L("Z01_MasterData_Overrides"),
key: "overrides",
searchable: false,
// 갈래 이름도 서버 것 — 목록 줄의 kind 를 표 이름으로 바꿔 보임.
load: (query) => loadOverrides(query, new Map(kinds.map((k) => [k.kind, k.label]))),
// 공종 편집 — 구성 줄을 고치고 「입력 필요」 공종을 메움(PLAN 1-4).
const workGrid = el("div", { className: "z01-master__base-grid" });
for (const axis of AXES) {
const button = el("button", {
className: "z01-master__base",
attrs: { type: "button" },
text: L(axis === "forest" ? "Z01_WorkItem_Axis_Forest" : "Z01_WorkItem_Axis_Const"),
});
});
boxes[0]?.grid.append(overridesButton);
button.addEventListener("click", () => {
activate(button);
openWorkItems(axis);
});
workGrid.append(button);
}
const tree = el("nav", { className: "z01-master__tree" });
for (const group of groups) {
@@ -132,54 +132,13 @@ export function buildSidePanel(
panel.append(
...boxes.map((box) => section(box.label, box.grid)),
section(L("Z01_WorkItem_Title"), workGrid),
section(L("Z01_MasterData_Tree"), tree),
);
attachCollapsible(panel);
return panel;
}
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",
};
/**
* 고친 것 목록을 표 한 벌로 — 정렬(원본 바뀜 → 주인 없음 → 고쳐짐)과 쪽 나눔은 서버가 함.
* 화면이 다시 줄 세우면 맨 위가 그 쪽 안에서만 맨 위가 됨(2026-09-15 브레인 ②). 서버에 검색이 없어 검색 칸은 숨김.
*/
async function loadOverrides(
query: RowsQuery,
kindLabels: Map<string, string>,
): Promise<MasterRows> {
const { items, total } = await fetchOverrides(query);
const column = (key: string, label: Parameters<typeof L>[0]) => ({ key, label: L(label) });
return {
columns: [
column("state", "Z01_MasterData_List_State"),
column("kind", "Z01_MasterData_List_Kind"),
column("row_label", "Z01_MasterData_List_Row"),
column("column_label", "Z01_MasterData_List_Column"),
column("value", "Z01_MasterData_List_Value"),
column("original", "Z01_MasterData_List_Original"),
column("current", "Z01_MasterData_List_Current"),
],
rows: items.map((item) => ({
...item,
value: valueWithUnit(item.value, item.unit),
original: valueWithUnit(item.original, item.unit),
...(Object.hasOwn(item, "current")
? { current: valueWithUnit(item.current, item.unit) }
: {}),
// state 가 안 오면 때우지 않고 드러냄(서버 버그).
state: L(STATE_LABEL[item.state] ?? "Z01_MasterData_State_Missing"),
kind: kindLabels.get(item.kind) ?? item.kind,
})),
total,
};
}
function section(title: string, body: HTMLElement): HTMLElement {
return el("section", {
className: "z01-master__section ui-collapsible ui-sidebar-section",