Files
Aislo/resources/tester/test_717_3_sangeun_modal.py
T
eomsangdonandClaude Opus 5.5 9d2b8e4021 feat(750-1): SK 셈 답 하나에 보기 넷 — 텍스트 수식 글 · 시험 계산 중간 · 일위대가 줄 모달 줄모드
- 새 `M02_Sangeun_Views` — SK 고친 줄(`_Over`) · 같은 입력 · 같은 고름 위에서 `master_text.lines` + `annotate` = `글` · 줄마다 cid · 금액 · 소계 · 계 = SK 줄 · 기준(별도계상 뺌 · 단위 환산 ÷배 · 못 셈 까닭) · `중간` = 시험 계산 중간값
- `calc_doc` — 문서 `줄모드` true = 일위대가 그 줄 셈(`line` · 별도계상 기본) · 몸 `보기` false = 덧칸 안 지음(바탕 원값 셈)
- `master_text.lines` · `master_sheet_line.annotate` — `files` 자리에 Master 도 받음(파일 묶음 길 그대로 · M01 `/text` 답 같음)
- `lineDoc` — `줄모드: true`(일위대가 줄 모달 89,299.60 = 그 줄 금액)
- 시험 `test_717_3` — lineDoc = as_doc 대조에서 `줄모드` 칸 따로 확인(서버 as_doc 은 `line` 인자로 줄 셈 · 문서 칸 없음)
- 새 지킴 `test_750_1_sk_views` — 변형 일곱 · 참조 바탕 · 줄 모달 · 보기 false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:32:23 +09:00

179 lines
7.8 KiB
Python

# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""일위대가 줄 [단가산출 보기] 모달(PLAN 717-3 · 724-1) — 임시 storage · 순수 함수는 Node 로.
화면 문서(`logic/M02_Sangeun_Cover_Model.lineDoc`) = 서버 `M02_Labor_Patch.as_doc` 꼴
(같은 고름 · 자재표시 cid) ·
모달 셈 길(`/sangeun/calc` = `calc_doc`) 값 = 일위대가 셈의 그 줄 값 · 고친 칸 = 덮개 칸 이름 ·
패치 정리 순수 함수 · `normalizeLabor` 가 줄 `패치` 를 지킴 ·
화면 이음(모달 = 줄 모드 SK 상세 · 공용 상세).
"""
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", "logic", "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"].pop("줄모드") is True and got["ref"].pop("줄모드") is True # 750-1
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_패치_정리_순수_함수():
"""원값 뺌 · 빈 칸 뺌 · 다 비면 null(줄 칸 안 둠) — 덮개 고침 자체는 717-2b `setLineCover`."""
one = {"덮개": {"labor:LB1#0": {"qty": 0.5, "원값": {"qty": 0.2}}, "x:a#0": {"원값": {}}}}
got = _node(
{
"clean": ["cleanPatch", one],
"empty": ["cleanPatch", {"덮개": {}, "뺀줄": [], "더한줄": []}],
"none": ["cleanPatch", None],
"keep": ["cleanPatch", {"뺀줄": ["a:b#0"]}],
}
)
assert got["clean"] == {"덮개": {"labor:LB1#0": {"qty": 0.5}}} # 원값 = 서버 몫
assert got["empty"] is None and got["none"] is None
assert got["keep"] == {"뺀줄": ["a:b#0"]}
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 "mountRowSk(host," in modal # 724-1 — 줄 모드 SK 상세(공용 상세)
assert "!done && sk.take() && !(await askConfirm(" in modal # 적용 안 한 고침 물음
assert "saveAsSk(o.row, now(), name, o.unit)" in modal # 새 SK = 내 개인 층(saveAsSk)
sk = (M02 / "M02_MasterTemplete_Labor_RowSk.ts").read_text("utf-8")
assert '{ layer: "personal", owner: null }' in sk
cover = (M02 / "M02_MasterTemplete_Sangeun_Cover.ts").read_text("utf-8")
assert "/sangeun/calc" in cover and "/sangeun/from-line" in cover
assert "coverTable" not in cover and not (M02 / "M02_Sangeun_Cover_Model.ts").exists()
for name in (
"M02_MasterTemplete_Labor_Rows.ts",
"M02_MasterTemplete_Labor_Detail.ts",
"M02_MasterTemplete_Labor_SangeunModal.ts",
"M02_MasterTemplete_Sangeun_Cover.ts",
"logic/M02_Sangeun_Cover_Model.ts",
):
assert len((M02 / name).read_text("utf-8").splitlines()) <= 700, name