Files
Aislo/resources/tester/test_717_3_sangeun_modal.py
T
eomsangdonandClaude Opus 5.5 c818dcae5e feat(717-3): 일위대가 줄 [단가산출 보기] 모달 — 인력·재료·경비 · 단가산출 상세 고침 = 그 일위대가 줄 덮개 · [단가산출로 저장]
- 줄 단추 = 로직 줄 · SK 참조 줄(Labor_Rows `sangeun`) · 프로젝트 복사본(셈 주입)엔 없음
- 모달(Labor_SangeunModal) = 일위대가 머리 「1 ㎡ 당」 · 이 줄 수량 · 1 단위 금액 · 보기 고르기(기본 인력·재료·경비 = 공용 계산서 calc_sheet · 상세 = 품 · 단가 칸 고침)
- 고친 줄 = 표 부품 「바뀜」 + 원래 값 곁 + 「이 일위대가에서 고침」 꼬리표 · [적용] = 줄 칸 패치(화면 캐시 · 저장은 [저장]) · 적용 안 하고 닫으면 물음
- [단가산출로 저장] = 내 개인 층 새 SK(from-line) · 자기 개인 층 일위대가면 그 줄을 새 SK 참조 줄로
- 셈 = 서버 /sangeun/calc(저장 안 함 · 최신 응답만) · 화면 문서 = 서버 as_doc 꼴(Sangeun_Cover_Model · 고름 · 자재표시 cid)
- 고침 표 · 셈 길 부름 = Sangeun_Cover(717-2b 도 씀) · normalizeLabor 가 줄 패치를 지킴(떨구면 [저장]에서 사라짐)
- 시험 test_717_3 5 통과 · 고친 대상 166 통과(1 실패 = test_175 B09 Store_Combo._PER · 전부터 · 719-2 몫) · typecheck 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:52:56 +09:00

199 lines
9.1 KiB
Python

# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""일위대가 줄 [단가산출 보기] 모달(PLAN 717-3) — 임시 storage · 순수 함수는 Node 로.
화면 문서(`M02_Sangeun_Cover_Model.lineDoc`) = 서버 `M02_Labor_Patch.as_doc` 꼴(같은 고름 · 자재표시 cid) ·
모달 셈 길(`/sangeun/calc` = `calc_doc`) 값 = 일위대가 셈의 그 줄 값 · 고친 칸 = 덮개 칸 이름 ·
덮개 고침 순수 함수 · 계산서 묶음 · `normalizeLabor` 가 줄 `패치` 를 지킴 · 화면 이음.
"""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Patch as patch
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
PCT = "GC001109" # 원/㎡ · 인력 둘 + 「노무비 * 3 / 100」
TWO_MATS = "GC000565" # 원/m · 재료 줄 둘
COVER = {"덮개": {"labor:LB000036#0": {"qty": 0.5}}}
MODEL = r"""
const fs = require("fs");
const path = require("path");
const root = process.argv[1];
const ts = require(path.join(root, "config", "node_modules", "typescript"));
const f = path.join(root, "M02_MasterTemplete", "M02_Sangeun_Cover_Model.ts");
const js = ts.transpileModule(fs.readFileSync(f, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, () => ({}));
const m = box.exports;
const job = JSON.parse(fs.readFileSync(0, "utf8"));
const out = {};
for (const [k, [fn, ...args]] of Object.entries(job)) out[k] = m[fn](...args);
process.stdout.write(JSON.stringify(out));
"""
def _node(job: dict) -> dict:
node = shutil.which("node")
if not node:
pytest.skip("node 없음")
got = subprocess.run(
[node, "-e", MODEL, str(ROOT)],
input=json.dumps(job, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
)
assert got.returncode == 0, got.stderr
return json.loads(got.stdout)
def _whole():
return m01.loaded()[1]
def test_화면_문서_서버_as_doc_꼴():
"""고름 · 자재표시(호표 차례) → cid · 패치 · 참조 줄 = 서버 `as_doc` 와 같은 몸."""
import master_pick as mp
whole = _whole()
row = whole.logic(TWO_MATS)
item = row["호표"][2]
want = mp.search(whole, item["요소"], {}, item.get("단위"))[1]
drop = {"뺀줄": ["material:" + row["호표"][1]["이름"] + "#0"]}
one = {
"로직": TWO_MATS,
"입력값": {},
"고름": {"2": want},
"자재표시": {"2": {"별도계상": "직접계산"}},
}
ref = {"참조": {"층": "personal", "주인": None, "키": "SK-000001"}, "입력값": {}}
ids = sd.line_ids(row)
got = _node(
{
"logic": ["lineDoc", one, ids, drop],
"ref": ["lineDoc", ref, None, COVER],
"bare": ["lineDoc", one, None, None],
"needs": ["needsIds", one],
"needsRef": ["needsIds", ref],
}
)
assert got["logic"] == patch.as_doc({**one, "패치": drop}, whole)
assert got["ref"] == patch.as_doc({**ref, "패치": COVER}, whole)
assert got["bare"]["고름"] == {} and got["bare"]["덮개"] == {}
assert got["needs"] is True and got["needsRef"] is False
def test_모달_셈_길_값_일위대가_줄_값(world):
"""모달 셈(`calc_doc` · 저장 안 함) 기준 = 일위대가 셈 그 줄 기준 · 고친 줄 덮개 칸 = qty · 정체 = 바탕 줄."""
whole = _whole()
line = {"로직": PCT, "입력값": {}, "수량": {"손": 1}}
doc = _node({"d": ["lineDoc", line, None, COVER]})["d"]
bare = _node({"d": ["lineDoc", line, None, None]})["d"]
with store.at(world["personal"]):
got = calc.calc_doc(doc)
was = calc.calc_doc(bare)
mine = labor.calc(labor.clean({"담은로직": [{**line, "패치": COVER}]}), {})["줄"][0]
plain = labor.calc(labor.clean({"담은로직": [line]}), {})["줄"][0]
assert got["기준"] == pytest.approx(mine["기준"])
assert was["기준"] == pytest.approx(plain["기준"])
assert got["기준"]["계"] != was["기준"]["계"] # 품 0.5 로 바뀜
assert was["정체"] == sd.line_ids(whole.logic(PCT))
cover = [x for x in got["줄"] if x["cid"] == "labor:LB000036#0"][0]
assert cover["덮개"] == ["qty"] and cover["수량"] == pytest.approx(0.5)
def test_덮개_순수_함수_계산서_묶음():
"""칸 고침 · 풂 · 원값 뺌 · 단가 칸 비목 · 묶음 = 비목마다 서버 값 그대로."""
lines = [
{"cid": "labor:LB1#0", "이름": "보통인부", "수량": 0.2, "단가": 100, "금액": 20,
"비목": {"노무비": 20, "재료비": 0, "경비": 0}},
{"cid": "logic:GC1#0", "이름": "하위", "수량": 1, "금액": 30,
"비목": {"노무비": 10, "재료비": 20, "경비": 0}},
{"cid": "pct:공구#0", "이름": "공구손료", "수량": 0.6, "율": 3, "금액": 0.6,
"비목": {"노무비": 0, "재료비": 0, "경비": 0.6}},
] # fmt: skip
calc_ = {"기준": {"노무비": 30, "재료비": 20, "경비": 0.6, "계": 50.6}, "줄": lines}
one = {"덮개": {"labor:LB1#0": {"qty": 0.5, "원값": {"qty": 0.2}}}}
got = _node(
{
"set": ["setCover", None, "labor:LB1#0", "labor", 120],
"qty": ["setCover", {"덮개": {"x:a#0": {"배율": 2}}}, "x:a#0", "qty", 1],
"off": ["setCover", {"덮개": {"labor:LB1#0": {"qty": 1}}}, "labor:LB1#0", "qty", None],
"clean": ["cleanPatch", one],
"empty": ["cleanPatch", {"덮개": {}, "뺀줄": [], "더한줄": []}],
"keep": ["cleanPatch", {"뺀줄": ["a:b#0"]}],
"p0": ["priceKey", lines[0]],
"p1": ["priceKey", lines[1]],
"p2": ["priceKey", lines[2]],
"q2": ["qtyOf", lines[2]],
"groups": ["sheetGroups", calc_],
}
)
assert got["set"] == {"덮개": {"labor:LB1#0": {"labor": 120}}}
assert got["qty"] == {"덮개": {"x:a#0": {"qty": 1}}} # 품은 qty · 배율 중 하나
assert got["off"] is None and got["empty"] is None
assert got["clean"] == {"덮개": {"labor:LB1#0": {"qty": 0.5}}} # 원값 = 서버 몫
assert got["keep"] == {"뺀줄": ["a:b#0"]}
assert (got["p0"], got["p1"], got["p2"], got["q2"]) == ("labor", None, None, 3)
groups = got["groups"]
assert [g["비목"] for g in groups] == ["노무비", "재료비", "경비"]
assert [g["소계"] for g in groups] == [30, 20, 0.6]
assert groups[0]["줄"][0] == {"이름": "보통인부", "글": "100 * 0.2 = 20", "금액": 20}
assert [x["이름"] for x in groups[0]["줄"]] == ["보통인부", "하위"]
assert groups[2]["줄"][0]["글"] == "3 % = 0.6"
def test_normalizeLabor_줄_패치_지킴():
node = shutil.which("node")
if not node:
pytest.skip("node 없음")
doc = {"담은로직": [{"로직": PCT, "패치": COVER}, {"로직": TWO_MATS}]}
got = subprocess.run(
[node, str(ROOT / "resources/tester/helper_m02_labor_normalize.cjs")],
input=json.dumps({"doc": doc, "column": "c"}, ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
)
assert got.returncode == 0, got.stderr
rows = json.loads(got.stdout)["담은로직"]
assert rows[0]["패치"] == COVER and "패치" not in rows[1]
def test_화면_이음():
"""줄 단추 = 로직 줄 · SK 참조 줄만 · 셈 주입(프로젝트 복사본)엔 없음 · 적용 안 한 고침 물음 · 700 줄."""
rows = (M02 / "M02_MasterTemplete_Labor_Rows.ts").read_text("utf-8")
assert 'sangeun && !fx && (!row.참조 || row.참조.키.startsWith("SK-"))' in rows
detail = (M02 / "M02_MasterTemplete_Labor_Detail.ts").read_text("utf-8")
assert "const sk = opt.calc ? {} : { sangeun: openSk };" in detail
assert 'edit && opt.layer === "personal" && !library.owner' in detail
modal = (M02 / "M02_MasterTemplete_Labor_SangeunModal.ts").read_text("utf-8")
assert "same(patch, applied) || askConfirm(" in modal
assert '{ layer: "personal", owner: null }' in modal # 새 SK = 내 개인 층
assert "if (mine !== seq) return;" in modal # 최신 응답만
cover = (M02 / "M02_MasterTemplete_Sangeun_Cover.ts").read_text("utf-8")
assert "/sangeun/calc" in cover and "/sangeun/from-line" in cover
assert 'tr.classList.add("is-changed")' in cover # 표 부품 「바뀜」 상태
for name in (
"M02_MasterTemplete_Labor_Rows.ts",
"M02_MasterTemplete_Labor_Detail.ts",
"M02_MasterTemplete_Labor_SangeunModal.ts",
"M02_MasterTemplete_Sangeun_Cover.ts",
"M02_Sangeun_Cover_Model.ts",
):
assert len((M02 / name).read_text("utf-8").splitlines()) <= 700, name