Files
Aislo/resources/tester/test_804_2_line_stack.py
T
eomsangdonandClaude Opus 5.5 ae8d098a10 fix(815-3): 합친 뒤 시험 셋 가름 · 시간당 경비 식 조종원 차례 · 호표 기계 글 머리 꼬리 · 합친 판 요약 다시 잼
- 809_1 이름 = 진짜 흠 + 낡은 기대 — 읽기기(812-1)가 지은 율 줄 이름 「율」이 엑셀 글 머리로 안 바뀜 → `Import_Shape._MADE` 에 율 더함 · 합계 = 차림 글줄 · 기계 = 한 줄(이름 · 규격 칸) 은 낡은 기대 고침 · 금액 15,935 · 336 · 723 · 16,994 그대로
- 812_9 별칭 = 낡은 기대 — 815-2 가 아스팔트페이버 별칭을 마스터 기계 줄 칸으로 옮겨 「넣기 전 못 찾음」 이 안 맞음 · 시험은 마스터 칸으로 봄 · 부르는 곳 없는 `M02_Machine_Find.learn`(옛 장부 쓰기) · `by_code` · `same_kind`(걷은 연결 찾기 몫) 지움
- 804_2 hourly = 낡은 기대 — 814-1 이 GC000268 목록을 117 → 209 로 늘려 겹친 116 대 고름이 GC000269 로 뒤집힘(116 대 모두 원 단위 같음) · 같은 로직 줄만 대던 시험 → 같은 갈래 식(부착 · 손료만 · 조종원) 줄 모두 댐
- 진짜 흠(브레인 갈래 가) — GC000272 「조종원 없는 기종」 목록에 기계표 조종원 1 인 9 대가 겹쳐 목록 길이 차례로 고름 → `logic_of` = 기계표 조종원 > 0 이면 조종원 호표 든 식 먼저 · 불러오기 `hourly_link` 도 같은 길 · 바뀐 4 대 = 진동롤러(핸드가이드식) 0.7ton 6,557 → 41,969 · 래머 80kg 1,959 → 23,708 · 플레이트 콤팩터 1.5ton 2,855 → 24,604 · 발전기 100kW 45,535 → 67,284(M01 문서 금액 그대로) · 시험에 9 대 굳힘
- 778_5 기계 표 = 진짜 흠 — 812-9 찾기가 호표 기계 글 「운 반 : 덤프트럭(4.5Ton)」 · 「…[hr] : 90%」 · 「(90%)」 머리 꼬리를 안 뗌 → `Hopyo_Fixed._eqs` 쌍점 조각 · 율 꼬리 뗌 · 이어짐 187 · 고를 것 1 · 못 이음 2(조합 한 줄 · 괄호 깨짐) · 모두 이음 블록 161
- 721 단추 자리 = 낡은 기대(808-1 [단가 갱신][재생성] tools 더함)
- 815-1 보고 요약에 「합친 뒤」 열 — 기계 이음 95.7 % · 우리 값 원파일 1,891 · 0 원 줄 블록 147

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

367 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""PLAN 804-2 — 단가산출 「줄 쌓기」 탭(요구 670~677) · 수식 문서 ↔ 줄 쌓기 한 벌.
① 왕복 1,518 전수 — 문서 → 줄 → 문서 같음 · 금액 줄 · 기호 모두 한 번 ·
차림 고침(항목 · 글 줄 · 가나다) 다시 읽어 같음 · 차림 칸 `쌓기` 든 문서도 서버 셈 같음
② 금액 — 화면 길(연결 기록 + TS 계산기) = 서버 정본 셈(1,518 전수)
③ 기계 펼침 — 시간당 경비 문서를 넘긴 입력으로 셈 = 부른 쪽 단가 묶음(원 단위) ·
`M02_Machine_Hourly.hourly`(같은 로직 · 같은 입력) 와 같음
④ 틀 풀기 — 단가산출 결과 연결을 이 문서 식으로 옮김 = 금액 같음(전수) ·
Cm 고침 → 값 · 지움 = 표지(오류 없음)
⑤ 저장 — `쌓기` 든 문서 저장(임시 폴더) · 다시 읽어 같음 · 정본 폴더 안 바뀜
⑥ 화면 이음 — M01 · M02 보기 셋 · 같은 길 · 같은 캐시 열쇠 · KaTeX 미리 묶기 뺌
"""
from __future__ import annotations
import copy
import importlib
import json
import os
import shutil
import subprocess
from decimal import Decimal
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M01_MasterData import M01_MasterData_Doc as doc_screen
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Machine_Hourly as mh
mr = importlib.import_module("master_doc_record") # M01 store 가 scripts 길을 둔 뒤
ROOT = Path(__file__).resolve().parents[2]
HERE = os.path.dirname(os.path.abspath(__file__))
COSTS = ("노무비", "재료비", "경비", "계")
def _ts(job: str, cases: list[dict]) -> list:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_804_2_line_stack.cjs")],
input=json.dumps(dc.ml._norm({"할일": job, "문서들": cases}), ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
)
assert proc.returncode == 0, proc.stderr[-3000:]
return json.loads(proc.stdout)
@pytest.fixture(scope="module")
def world():
docs = dc.folder()
links = dc.ml.Links(dc.skc.whole_(), docs)
cases, py = [], {}
for key, doc in docs.items():
rec = mr.Rec(links)
rec.prefetch(doc)
py[key] = dc.mc.calc(doc, rec)
cases.append({"문서": doc, "연결": rec.out()})
return {"docs": docs, "links": links, "py": py, "cases": dict(zip(docs, cases))}
def _num(v):
return None if v is None else float(v)
def test_왕복_전수_1513(world):
docs = world["docs"]
assert len(docs) >= 1513 # 813-2 지움 6
got = _ts("왕복", [{"문서": d} for d in docs.values()])
bad = {f: [k for k, g in zip(docs, got) if not g[f]] for f in ("같음", "금액", "기호", "차림")}
assert bad == {"같음": [], "금액": [], "기호": [], "차림": []}
assert not any(g["쌓기"] for g in got) # 기본 차림 = 문서에 `쌓기` 안 씀
# 차림 칸이 든 문서 — 서버 셈이 같음(계산기는 차림을 안 봄)
for key, g in list(zip(docs, got))[::7]:
laid = json.loads(json.dumps(g["차림문서"]), parse_float=Decimal, parse_int=Decimal)
assert "쌓기" in laid
a, b = dc.mc.calc(docs[key], world["links"]), dc.mc.calc(laid, world["links"])
assert [_num(a.get(f)) for f in (*COSTS, "결과")] == [
_num(b.get(f)) for f in (*COSTS, "결과")
], key
# 못 고치는 칸 목록(보존됨) — 지금 문서엔 금액 줄 기호만
notes = {n for g in got for n in g["못"]}
assert notes <= {"단계", "조정", "번지", "합밖", "줄 자리", "금액 줄 기호"}
def test_금액_화면길_서버정본_전수(world):
keys = list(world["docs"])
got = _ts("셈", [world["cases"][k] for k in keys])
diff = [
(k, f)
for k, g in zip(keys, got)
for f in (*COSTS, "결과")
if _num(world["py"][k].get(f)) != (None if g.get(f) is None else pytest.approx(g[f]))
]
assert diff == [] and not any(g["누락"] for g in got)
world["셈"] = dict(zip(keys, got))
def _hourly_inputs(logic_key: str) -> dict:
"""그 로직의 기본 입력(기계 밖)."""
rows = {r["키"]: r for r in mh._family()}
row = rows.get(logic_key)
if not row:
return {}
return {
i["이름"]: str((i.get("고르기") or [""])[0])
for i in row.get("입력") or []
if i.get("이름") != "기계"
}
def _kind(row: dict) -> tuple:
return (*mh._rank(row)[:3], any(x.get("이름") == "조종원" for x in row.get("호표") or []))
# 기계표 조종원 1 인 · 「조종원 없는 기종」 GC000272 목록에도 겹친 기계(815-3) — 조종원 든 식
OPERATED = (
"EQ000094",
"EQ000111",
"EQ000112",
"EQ000454",
"EQ000153",
"EQ000154",
"EQ000155",
"EQ000195",
"EQ000234",
)
def test_조종원_있는_기계_조종원_식():
rows = {r["키"]: r for r in mh._family()}
for key in OPERATED:
h = mh.hourly(key)
assert h["ok"] and h["노무비"] > 0 and _kind(rows[h["로직"]])[3], (key, h)
def test_기계펼침_원단위_hourly(world):
got = world.get("셈") or dict(zip(world["docs"], _ts("셈", list(world["cases"].values()))))
eq = m01.loaded()[1].index["EQ"]
by_code: dict[str, list[str]] = {}
for key, row in eq.items():
by_code.setdefault(str(row.get("원문번호")), []).append(key)
lines = same_parent = same_hourly = 0
memo: dict[str, dict] = {}
rows = {r["키"]: r for r in mh._family()}
for k, g in got.items():
for s in g["펼침"]:
if s["갈래"] != "기계":
continue
lines += 1
# 펼친 셈 = 부른 쪽 단가 묶음(화면 「노무비 : 59,020」 의 그 수)
assert all(float(s["값"][f]) == float(s[f]) for f in COSTS), (k, s["키"])
same_parent += 1
code = str(s["입력"].get("기계"))
for key in by_code.get(code, []):
if key not in memo: # 같은 기계는 한 번만 셈
memo[key] = mh.hourly(key)
h = memo[key]
if not h.get("ok"):
continue
pick = mh.logic_of(code)
own = rows.get(s["키"])
want = _hourly_inputs(s["키"])
given = {n: str(v) for n, v in s["입력"].items() if n != "기계"}
# 같은 갈래 로직(부착 · 손료만 · 조종원 같음 — 268 · 269 겹친 기계 = 값 같음)
if own is None or _kind(own) != _kind(pick):
break # 조합(부착) · 손료만 · 조종원 없는 식을 고른 줄
if any(want.get(n) != v for n, v in given.items()):
break # 기본 밖 입력(조종원 직종 · 보정작업 등)을 넘긴 줄
assert all(float(h[f]) == float(s[f]) for f in COSTS), (k, key, h, s)
same_hourly += 1
break
assert lines >= 1000 and same_parent == lines and same_hourly >= 800
def test_틀풀기_전수_금액같음_Cm고침(world):
cases = list(world["cases"].values())
got = [x for g in _ts("펼침", cases) for x in g]
assert len(got) >= 20 # 단가산출 결과 연결(굴착기 Q 등) 전부
bad = [g["기호"] for g in got if g["전"] != g["후"] or not g["왕복"]]
assert bad == []
one = _ts(
"펼침",
[{**world["cases"]["GF000099"], "기호": "Q_작업", "고침": [{"기호": "Cm", "글": "20"}]}],
)[0][0]
assert one["전"]["계"] == 5286 and one["후"]["계"] == 5286
assert one["옮김"][-1] == "Q_작업" and {"q", "K", "f", "E", "Cm"} <= set(one["옮김"])
assert one["고친"]["계"] == 5434 # Cm 18 → 20 = Q 58.212 → 52.391
gone = _ts("펼침", [{**world["cases"]["GF000099"], "기호": "Q_작업", "지움": ["Cm"]}])[0][0]
assert gone["고친"]["계"] is None and "정의 필요" in gone["표지"] # 지워도 오류 없음
@pytest.fixture
def client(tmp_path: Path, monkeypatch):
for path in doc_screen.folder().glob("수식_*.json"):
shutil.copy(path, tmp_path / path.name)
monkeypatch.setattr(doc_screen, "folder", lambda: tmp_path)
doc_screen._memo.clear()
app = FastAPI()
app.include_router(router_module.router)
yield TestClient(app)
doc_screen._memo.clear()
def test_저장_쌓기_다시읽어같음(client, tmp_path):
read = client.get("/api/m01/logic/doc", params={"key": "GF000099"}).json()
doc = copy.deepcopy(read["문서"])
got = _ts("왕복", [{"문서": doc}])[0]
laid = got["차림문서"]
res = client.post("/api/m01/logic/doc/save", json={"문서": laid})
assert res.status_code == 200, res.text
assert res.json()["셈"]["계"] == read["셈"]["계"] == 5286
again = client.get("/api/m01/logic/doc", params={"key": "GF000099"}).json()
assert (
again["문서"]["쌓기"] == laid["쌓기"] and _ts("왕복", [{"문서": again["문서"]}])[0]["같음"]
)
text = (tmp_path / res.json()["파일"]).read_text(encoding="utf-8")
assert '"쌓기": [' in text and "< 근거 0 >" in text
real = (ROOT / "resources/master_data/수식문서" / res.json()["파일"]).read_text(
encoding="utf-8"
)
assert "< 근거 0 >" not in real
def _src(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def test_화면_이음():
lab = _src("M01_MasterData/M01_MasterData_UI_LogicLab.ts")
assert (
'base: "/m01/logic/doc",' in lab and 'cache: "m01_doc_drafts",' in lab
) # 804-7 page = 페이지 [저장]
assert "stack: stack.root" in lab and "=> stack.canLeave();" in lab # 804-6 수식 탭 = 줄 쌓기
assert '{ value: "stack", label: tl("View_Equation") }' in _src(
"M01_MasterData/M01_MasterData_UI_LogicLab_View.ts"
)
assert '[data-view="stack"]' in _src("M01_MasterData/M01_MasterData_UI_LogicLab_Style.css")
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
assert 'cache: "m02_sk_doc_drafts"' in tabs and tabs.count("skDocPath(opt.scope, k, p)") == 1
vite = _src("vite.config.ts")
assert 'exclude: ["0_old", "katex"]' in vite and "katex/dist/katex.mjs" in vite
pkg = json.loads(_src("config/package.json"))
assert "katex" in pkg["dependencies"]
lic = json.loads(_src("config/node_modules/katex/package.json"))["license"]
assert lic == "MIT"
for f in (ROOT / "A00_Common").glob("line_stack*.ts"):
assert len(f.read_text(encoding="utf-8").splitlines()) <= 700, f.name
# ── 804-3 줄 쌓기 수정 1차(요구 678) ──
def _tbl(world, key, **kw):
return _ts("표고름", [{**world["cases"][key], **kw}])[0]
def _calc(world, doc):
return dc.mc.calc(
json.loads(json.dumps(doc), parse_float=Decimal, parse_int=Decimal), world["links"]
)
def test_표고름_값_출처(world):
# 옵션에 매인 조건(규격 = S_폼타이규격) — 줄은 옵션 그대로 · 칸만 바뀜
a = _tbl(world, "GC000211", 기호="Q_3t^표", 줄=1, 칸="4t/㎡")
assert a["묶임"] == {"규격": ["폼타이규격"]} and a["전줄"] == 2
assert a["연결"] == {
"갈래": "표",
"표": "QC000429",
"조건": {"규격": "S_폼타이규격"},
"칸": "4t/㎡",
}
assert a["값"] == 0.72 and a["후줄"] == 2 # 12.7㎜ 줄 · 4t/㎡
# 옵션에 안 매인 조건 — 고른 줄 값이 조건으로 · 값 = 표 칸 · 서버 셈 같음
b = _tbl(
world,
"GC000211",
기호="Q_시공^표",
줄=4,
칸="시공량",
앞=[{"기호": "Q_시공^표", "조건": {"유형": "'보통'"}}],
)
assert b["묶임"] == {} and b["전줄"] == 3
assert b["연결"]["조건"] == {"유형": "'간단'"} and b["연결"]["표"] == "QC000431"
assert b["값"] == 50 and b["후줄"] == 4
py = _calc(world, b["문서"])
assert [_num(py.get(f)) for f in COSTS] == [b[f] for f in COSTS]
assert b["계"] != world["py"]["GC000211"]["계"] # 시공량 20 → 50 = 금액 바뀜
# 조건 없는 표(한 줄) — 칸만
c = _tbl(world, "GC000211", 기호="Q_합판2^표", 줄=0, 칸="각재")
assert (
c["연결"] == {"갈래": "표", "표": "QC000427", "조건": {}, "칸": "각재"} and c["값"] == 0.038
)
def test_찾아넣기_요소키_금액(world):
h = mh.hourly("EQ000016")
assert h["ok"] and h["로직"] and h["원문번호"] == "0201-0012"
picks = [
{"갈래": "인력", "고른": {"키": "LB000002", "이름": "보통인부", "단위": "인"}},
{"갈래": "재료", "고른": {"키": "MT000018", "이름": "합판(내수), 서울", "규격": "12t×1220×2440mm", "단위": "㎡"}},
{"갈래": "기계", "고른": {"키": h["로직"], "이름": "굴착기(무한궤도)", "규격": "0.12㎥", "입력": {"기계": "'0201-0012'"}}},
{"갈래": "단가산출", "고른": {"키": "GF000099", "이름": "측구터파기 토사", "단위": "㎥"}},
] # fmt: skip
got = _ts("넣기", [{"문서": world["docs"]["GC000211"], "넣을": picks}])[0]
assert got["갈래"] == ["인력", "재료", "기계", "단가산출"]
doc = got["문서"]
links = {v["기호"] + ("_" + v["아래"] if v.get("아래") else ""): v["연결"] for v in doc["변수"]}
assert [links[i] for i in got["기호"]] == [
{"갈래": "요소", "키": "LB000002"},
{"갈래": "요소", "키": "MT000018"},
{"갈래": "단가산출", "키": h["로직"], "입력": {"기계": "'0201-0012'"}},
{"갈래": "단가산출", "키": "GF000099", "입력": {}},
]
new = doc["금액"][got["처음"] :]
assert [(m["단위"], m["비목"], m["수량"]) for m in new] == [
("인", "노무비", "1"),
("㎡", "재료비", "1"),
("hr", "묶음", "1"),
("㎥", "묶음", "1"),
]
# 금액 = 서버 정본 셈 — 넣은 넷 = 노임 + 자재 + 기계 한 시간(hourly) + 단가산출 계
before, after = world["py"]["GC000211"], _calc(world, doc)
want = 172068 + 11354 + float(h["계"]) + float(world["py"]["GF000099"]["계"])
assert float(after["계"]) - float(before["계"]) == pytest.approx(want, abs=2)
ts = _ts("셈", [{"문서": doc, "연결": _record(world, doc)}])[0]
assert not ts["누락"], ts
assert [ts[f] for f in COSTS] == [_num(after.get(f)) for f in COSTS]
# 다시 고름 — 키 · 이름만 바뀜
again = _ts(
"넣기",
[{"문서": world["docs"]["GC000211"], "넣을": picks[:1], "바꿈": [{"차례": 0, "고른": {"키": "LB000007", "이름": "형틀목공"}}]}],
)[0] # fmt: skip
m = again["문서"]["금액"][again["처음"]]
assert m["이름"] == "형틀목공" and m["수량"] == "1"
assert any(v["연결"] == {"갈래": "요소", "키": "LB000007"} for v in again["문서"]["변수"])
# 기계 다시 고름(같은 로직) — 기종만 바뀌고 지역 · 보정작업 넘김 그대로
gf = world["docs"]["GF000099"]
i = next(k for k, m in enumerate(gf["금액"]) if m["단가"] == "P_굴착기무한궤")
one = _ts("바꿈", [{"문서": gf, "줄": i, "기호": "P_굴착기무한궤", "갈래": "기계", "고른": picks[2]["고른"]}])[0] # fmt: skip
link = next(v["연결"] for v in one["변수"] if v.get("아래") == "굴착기무한궤")
assert link["입력"] == {"기계": "'0201-0012'", "지역": "S_지역", "보정작업": "S_보정작업"}
assert one["금액"][i]["이름"] == "굴착기(무한궤도)" and one["금액"][i]["규격"] == "0.12㎥"
def _record(world, doc):
rec = mr.Rec(world["links"])
d = json.loads(json.dumps(doc), parse_float=Decimal, parse_int=Decimal)
rec.prefetch(d)
dc.mc.calc(d, rec) # 요소 값은 셈하며 기록됨(화면 미리 셈과 같은 길)
return rec.out()
def test_들여쓰기_수준():
got = _ts("넣기", [{"문서": {"금액": [], "변수": []}, "넣을": []}])[0]
assert got["수준"] == [{"item": 2, "line": 3}] # 가나다 항목 = 2 · 줄 3
out = _src("A00_Common/line_stack_out.ts")
assert "stackLevels(items)" in out and '"--lstk-depth", String(r.lv)' in out
assert "rows(sub, got, h, depth + 1, path, lv + 1)" in out # 펼친 블록 = 그 줄 + 1
edit = _src("A00_Common/line_stack_edit.ts")
assert "openStackPick" in edit and "/links" not in edit and "Machine" not in edit