# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀 """810-3 지킴 — 원파일 가져온 식 화면(줄 쌓기 · 옵션 칸 · 추정 칸 · 카드). (a) 화면 셈 거울 — EST 여섯 · STC 일곱 산근 블록 모두: 화면 길(TS importView → calcDoc) = 서버 길 (`M02_Doc_Import.run` 계산기) 답 전부 같음(줄 · 비목 · 합 · 표지) (b) 옵션 칸 — 원파일 수 하나 변수(출처 「원파일」) = 옵션 가로 칸(`sourceOf`) · 줄 쌓기 공식 묶음에도 보임(812-3) · 금액 줄 단가로 쓰는 수 · 엑셀 단가 기억 연결 = 옵션 칸 아님 (c) 추정 칸 — 문서 머리 `가져온.추정` 줄 수 = 원본 확정 추정 수(EST) · 원코드 · 규격 머리 (d) 소계 줄 — 합 밖 · 비목 없음(화면 비목 칸 빈칸 · 종류 「소계」 — `line_stack_edit` · `line_stack_out` 글 대조) 원파일 없으면 그 시험만 skip(다른 PC). 추정 표지 수 표 = tmp/s8103/guess.json(일회 기록). """ from __future__ import annotations import json import subprocess import sys from collections import Counter from functools import lru_cache from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] HERE = Path(__file__).resolve().parent for p in (ROOT, HERE): if str(p) not in sys.path: sys.path.insert(0, str(p)) import test_802_1_formula_doc as t1 # noqa: E402 import test_810_2_native_doc as t2 # noqa: E402 from test_810_1_estx import FILES as EST # noqa: E402 from test_810_1_stc import FILES as STC # noqa: E402 from M02_MasterTemplete import M02_Doc_Import as dim # noqa: E402 HELPER = HERE / "helper_810_3_screen.cjs" class Keep(dim.Links): """서버 연결 + 기록(요소 · 표 · 문서) — 같은 값을 화면 셈에 넘김.""" def __init__(self, master, basis): super().__init__(master, basis) self.log, self.docs = {}, {} self.rec = t1.Rec(self) def element(self, p): return self.rec._keep("요소", p, super().element) def table(self, p): return self.rec._keep("표", p, super().table) def doc(self, key): got = super().doc(key) if got is not None: self.docs[key] = got return got def _plain(v): return json.loads(json.dumps(t1.ml._norm(v), ensure_ascii=False, default=str)) @lru_cache(maxsize=16) def _file(kind: str, tag: str) -> list[tuple[int, dict, dict, dict]]: """블록마다 (번, 원본, 문서, 서버 답 · 기록) — 서버 길 그대로.""" saved = t2.native(kind, tag) out = [] for no in t2.fn.numbers(saved): emitted = t2.fn.emit(saved, no) sk = t2.sk_of(emitted) doc = dim.derive(sk, None, t2.whole()) links = Keep(t2.whole(), dim.basis_of(sk)) mine = dim.view(doc, dim.basis_of(sk)) links.learn(mine) want = t1.mc.calc(mine, links) out.append( (no, emitted["원본"], doc, {"답": want, "기록": links.rec.log, "문서": links.docs}) ) return out def _screen(rows) -> dict: docs, log, keys = {}, {}, [] for no, _, doc, srv in rows: key = f"블록{no}" docs[key] = _plain(doc) keys.append(key) log |= srv["기록"] docs |= {k: _plain(v) for k, v in srv["문서"].items()} pay = {"문서들": docs, "대상": keys, "기록": _plain(log), "기준": "엑셀"} proc = subprocess.run( ["node", str(HELPER)], input=json.dumps(pay, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(proc.stdout) def _cases(): return [("EST", k) for k in EST] + [("STC", k) for k in STC] @pytest.mark.parametrize(("kind", "tag"), _cases()) def test_screen_calc_mirror_and_option_cells(kind: str, tag: str) -> None: rows = _file(kind, tag) got = _screen(rows) for no, src, doc, srv in rows: key = f"블록{no}" t1._same(t1.ml._norm(srv["답"]), got["답"][key], f"{tag} {key}") shape = got["꼴"][key] nums = [ v["열쇠"] for v in doc["변수"] if v.get("출처") == "원파일" and not any(str(m.get("단가")) == v["열쇠"] for m in doc.get("금액") or []) ] assert set(nums) <= set(shape["옵션"]), (tag, key, nums, shape["옵션"]) # 옵션 칸 = 공식 묶음에도 보임(812-3 · 같은 값) assert set(shape["옵션"]) <= set(shape["공식"]), (tag, key) for v in doc["변수"]: if v["열쇠"] in shape["옵션"]: assert v["연결"]["갈래"] == "숫자" and "엑셀" not in v["연결"] def test_guess_head_counts_and_table() -> None: table, total = {}, Counter() for kind, tag in _cases(): try: rows = _file(kind, tag) except pytest.skip.Exception: continue n = Counter() for _, src, doc, _ in rows: head = doc["가져온"] assert head["원파일"] is True guess = head.get("추정") or [] assert len(guess) == (src.get("확정") or {}).get("추정", 0), (tag, doc["이름"]) assert all(g["글"] and isinstance(g["행"], int) for g in guess) if kind == "EST": assert head.get("원코드") # EST 블록 코드(D… · AAA…) n["블록"] += 1 n["추정 칸"] += len(guess) n["추정 블록"] += bool(guess) n["값 옵션"] += sum(1 for v in doc["변수"] if v.get("출처") == "원파일") n["소계 줄"] += sum(1 for m in doc["금액"] if m.get("합밖") and not m.get("비목")) for k in ("확정", "표지"): n[k] += (src.get("확정") or {}).get(k, 0) table[f"{kind} {tag}"] = dict(n) total.update(n) if not table: pytest.skip("원파일 없음") table["합"] = dict(total) (ROOT / "tmp" / "s8103").mkdir(parents=True, exist_ok=True) (ROOT / "tmp" / "s8103" / "guess.json").write_text( json.dumps(table, ensure_ascii=False, indent=1), encoding="utf-8" ) assert total["블록"] and total["추정 칸"] and total["값 옵션"] and total["소계 줄"] def test_subtotal_shape_and_screen_text() -> None: out, doc, got = t2._block(7) # 지장목 제거 — 인부소계 sub = next(x for x in doc["금액"] if x["이름"] == "인부소계") assert sub["합밖"] and "비목" not in sub edit = (ROOT / "A00_Common" / "line_stack_edit.ts").read_text(encoding="utf-8") assert "const bare = !!line.합밖 && !line.비목" in edit and '"K_소계"' in edit outx = (ROOT / "A00_Common" / "line_stack_out.ts").read_text(encoding="utf-8") assert "cols: bare ? {} :" in outx side = (ROOT / "M02_MasterTemplete" / "M02_MasterTemplete_Hopyo_Side.ts").read_text( encoding="utf-8" ) assert 'accept: ".xls,.xlsx,.estx,.stc"' in side def test_origin_card_native_line() -> None: js = r""" const fs = require("fs"), path = require("path"); const ts = require(path.join(process.cwd(), "config", "node_modules", "typescript")); const at = path.join(process.cwd(), "M02_MasterTemplete", "logic", "M02_Sangeun_Origin.ts"); const js = ts.transpileModule(fs.readFileSync(at, "utf8"), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, }).outputText; const box = { exports: {} }; new Function("exports", "module", "require", js)(box.exports, box, () => ({})); const got = JSON.parse(fs.readFileSync(0, "utf8")); process.stdout.write(JSON.stringify(got.map((m) => box.exports.originOf(m)))); """ est = {"로직": {}, "원본": {"갈래": "EST", "시트": "산근", "코드": 7, "원파일": True, "원코드": "D0000000118", "행": [2, 49], "확정": {"확정": 13, "표지": 0, "추정": 2}}} # fmt: skip xl = {"로직": {}, "원본": {"갈래": "EST", "시트": "산근", "코드": 7, "행": [2, 49]}} stc = {"로직": {}, "원본": {"갈래": "STmate", "시트": "단가산출근거", "코드": "D00014", "원파일": True, "행": [2, 25], "확정": {"확정": 199, "표지": 1}}} # fmt: skip proc = subprocess.run( ["node", "-e", js], input=json.dumps([est, xl, stc], ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", cwd=ROOT, check=True, ) a, b, c = json.loads(proc.stdout) assert a["요약"] == "원파일 · EST 산근 D0000000118 · 행 2~49" assert a["확정"] == "확정 13 · 표지 0 · 추정 2 · 확정률 100.0 %" assert "요약" not in b and "확정" not in b # 엑셀 길 = 그대로 assert c["요약"] == "원파일 · STmate 단가산출근거 D00014 · 행 2~25" assert c["확정"].endswith("확정률 99.5 %")