Files
Aislo/resources/tester/test_z01_master_draft.py
T
eomsangdonandClaude Opus 5 feab848e69 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
2026-09-18 23:55:45 +09:00

185 lines
7.5 KiB
Python

"""Z01 고친 것 초안 — [저장] 전까지 sessionStorage 에만 · 판이 다르면 버림 (PLAN 1-0 · 1-4 · 2026-09-18).
① 기초단가 — 서버 값과 같아지면 고침에서 빠짐 · 지울 줄의 칸 고침은 안 보냄 · 새 줄은 data 만 보냄
② 같은 판이면 탭을 다시 열어도 초안이 살아남 · 판이 다르면 비고 저장소에서도 지움
③ 공종 — 보낼 몸은 지울 줄을 빼고 화면 표시 칸(tmp·deleted)을 뗌(빠지면 삭제 = 전체 줄 보내기)
④ 고친 것이 없으면 dirty 아님 · 고치면 dirty · 새 줄은 「added」 · 고친 칸만 짚음
⑤ 갈래는 처음 나온 차례로 묶음 · 새 줄은 제 갈래 끝 바로 뒤(보낸 차례 = sort_order)
⑥ 보낼 줄은 id + 계약 칸 열하나만 — GET 전용 칸(sort_order·is_added·changed_columns)이 실리면 422
TS 를 실제로 돌린다(`test_z01_master_cells` 와 같은 방식).
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
SOURCE = PROJECT_ROOT / "Z01_MasterData" / "Z01_MasterData_UI_Draft.ts"
_RUNNER = """
import { writeFileSync } from "node:fs";
// 브라우저 sessionStorage 대역
const store = new Map();
globalThis.sessionStorage = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
};
const {
BaseDraft, addRow, changedFields, freshBody, groupByVariant, isDirty, workItemPayload,
} = await import("./Z01_MasterData_UI_Draft.js");
// ① 기초단가
const base = new BaseDraft("base:labor", 7);
base.setEdit("labor_const/1001", "daily_wage_krw", 250000, 215907);
base.setEdit("labor_const/1002", "daily_wage_krw", 100, 100); // 서버 값과 같음 — 안 쌓임
base.setEdit("labor_const/1003", "daily_wage_krw", 1, 2);
base.setEdit("labor_const/1003", "daily_wage_krw", 2, 2); // 되돌림 — 빠짐
base.setEdit("labor_const/9999", "daily_wage_krw", 5, 4);
base.toggleDelete("labor_const/9999");
const added = base.addRow();
base.setAdded(added.tmp, "occupation_name", "새 직종");
base.setAdded(added.tmp, "daily_wage_krw", 300000);
base.setAdded(added.tmp, "daily_wage_krw", null); // 비우면 칸을 뺌
const payload = base.payload();
const count = base.count;
const stored = JSON.parse(store.get("z01-draft:base:labor"));
// ② 같은 판이면 살아남 · 다른 판이면 비고 저장소에서 지움
const again = new BaseDraft("base:labor", 7);
const restored = again.payload();
const newer = new BaseDraft("base:labor", 8);
const dropped = { count: newer.count, left: store.has("z01-draft:base:labor") };
const toggled = new BaseDraft("base:oil", 1);
toggled.toggleDelete("x");
toggled.toggleDelete("x");
const emptied = { count: toggled.count, left: store.has("z01-draft:base:oil") };
// ③④⑤ 공종
const row = (id, variant, extra = {}) => ({
id, variant, resource_kind: "labor", resource_key: "labor_const/1001", resource_name: "보통인부",
resource_spec: "", amount: 0.5, amount_unit: "인", alternative_amount: null,
group_ratio_pct: null, raw_cell: null, pum_table_id: "T-1",
// GET 전용 칸 — PUT 에 실리면 422(extra=forbid)
sort_order: id, is_added: false, changed_columns: [], ...extra,
});
const detail = {
item: { work_item_key: "FW-00001", axis: "forest", code: null, number: "1-1", name: "벌목",
path_name: "벌목", parent_key: null, parent_mode: null, basis_quantity: 1,
basis_unit: "ha", status: "needs_input", edition: "2026", revision: 3 },
rows: [row(1, "갑"), row(2, "을"), row(3, "갑")],
source: [], notes: [],
};
const body = freshBody(detail);
const clean = isDirty(detail, body);
body.rows[0].amount = 0.7;
body.rows[1].deleted = true;
addRow(body, "을", "new-1");
body.item.status = "reviewed";
const sent = workItemPayload(detail.item.revision, body);
writeFileSync(process.argv[2], JSON.stringify({
payload, count, stored,
restored, dropped, emptied,
clean,
dirty: isDirty(detail, body),
sent,
changed: [...changedFields(body.rows[0], detail.rows)],
addedRow: changedFields(body.rows[2], detail.rows),
untouched: [...changedFields(body.rows[3], detail.rows)],
groups: groupByVariant(body.rows),
freshUntouched: freshBody(detail).rows[0].amount,
}));
"""
def test_초안_쌓기와_보낼_몸(tmp_path: Path) -> None:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(SOURCE),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--ignoreConfig",
"--noCheck",
"--noResolve",
],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "output.json"
subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(result)],
cwd=str(PROJECT_ROOT),
check=True,
capture_output=True,
)
got = json.loads(result.read_text(encoding="utf-8"))
# ① 서버 값과 같은 고침은 안 쌓임 · 지울 줄(9999)의 칸 고침은 안 보냄 · 새 줄은 data 만
assert got["payload"] == {
"base_revision": 7,
"edits": [
{"row_key": "labor_const/1001", "column": "daily_wage_krw", "value": 250000},
],
"added": [{"data": {"occupation_name": "새 직종"}}],
"deleted": ["labor_const/9999"],
}
# 고친 칸 2(1001·9999) + 새 줄 1 + 지울 줄 1
assert got["count"] == 4
assert got["stored"]["revision"] == 7
# ② 같은 판이면 그대로 살아남 · 판이 바뀌면 비고 저장소에서도 지움 · 다 되돌리면 저장소에서 빠짐
assert got["restored"] == got["payload"]
assert got["dropped"] == {"count": 0, "left": False}
assert got["emptied"] == {"count": 0, "left": False}
# ③ 지울 줄(id 2) 빠짐 · 새 줄은 id 없이 · 화면 표시 칸 없음 · 머리 상태 실림
assert got["clean"] is False
assert got["dirty"] is True
sent = got["sent"]
assert sent["base_revision"] == 3
assert sent["item"] == {
"name": "벌목",
"basis_quantity": 1,
"basis_unit": "ha",
"status": "reviewed",
}
assert [r.get("id") for r in sent["rows"]] == [1, None, 3]
# ⑥ id + 계약 칸만(새 줄은 id 없음) · 글자 칸은 null 아닌 빈 글자
fields = {
"variant",
"resource_kind",
"resource_key",
"resource_name",
"resource_spec",
"amount",
"amount_unit",
"alternative_amount",
"group_ratio_pct",
"raw_cell",
"pum_table_id",
}
assert [set(r) for r in sent["rows"]] == [fields | {"id"}, fields, fields | {"id"}]
assert sent["rows"][1]["variant"] == "을" and sent["rows"][1]["pum_table_id"] == ""
# ④ 고친 칸만 · 새 줄은 줄째 · 안 고친 줄은 빈 목록 · 서버 값(detail)은 안 건드림
assert got["changed"] == ["amount"]
assert got["addedRow"] == "added"
assert got["untouched"] == []
assert got["freshUntouched"] == 0.5
# ⑤ 갈래는 처음 나온 차례(갑 → 을) · 새 줄은 제 갈래로
assert got["groups"] == [
{"variant": "갑", "indexes": [0, 3]},
{"variant": "을", "indexes": [1, 2]},
]