Files
Aislo/resources/tester/test_810_3_native_screen.py
T
eomsangdonandClaude Opus 5.5 624fd3a555 feat(812-3): 가져온 단가산출 좌측 차림 — 경쟁사 머리 줄 = 항목 · 변수 대입 = 공식 묶음 · 근거 글 = 글 줄
- 새 M02_Doc_NativeLayout — 원본 글줄 차림 표지로 금액 줄 묶음(= 항목 제목) + 문서 쌓기(기본과 다를 때만) · EST 금액 차례 = 원본 차례(셈 같을 때만) · 합계 절 뺌 · 줄 쌓기 모델 stackOf · saveOf · hiddenOf 파이썬 거울
- M02_Doc_NativeEst.tags — 글줄 차림 머리(단 > 0 · 소계 구간 첫 글 · 번호 글) · 합계 · 글(분수 그림 위아래 줄 뺌)
- M02_San_Emit.stmate — 글 줄 표지 · 원본 차림 표지(계 줄마다 항목)
- M02_Doc_Import_Shape.shape — 차림 tag · apply 부름
- 줄 쌓기 hiddenOf — 가져온 값 옵션 칸도 공식 묶음에 보임(원파일 꼬리표 · 같은 값) · 옵션 가로 칸 고침 = 수 그대로(글이면 셈 「—」 흠 고침)
- 시험 test_812_3 — 견본 다섯(거창 1 · 2 · 41 · 15 · 봉화 14) · 전수 2,220 블록 금액 같음 · TS 왕복 · 거울 100 % · M01 1,518 차림 그대로
- 낡은 시험 고침 test_810_3 — 옵션 칸 ∩ 공식 묶음 = 빈 → 옵션 칸 ⊆ 공식 묶음(812-3 으로 가져온 값이 공식 묶음에도 보임)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 07:21:35 +09:00

215 lines
8.8 KiB
Python

# 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 %")