Files
Aislo/resources/tester/test_802_3_doc_screen.py
eomsangdonandClaude Opus 5.5 a003587aca fix(802-3): 선택으로 꺼진 금액 줄 숨김 · 값 식 한 번 · 옮김 군더더기 걷음(브레인 사진 지적)
- 663-1: 수량 · 단가가 만약으로 0 을 탄 금액 줄 = 안 보임(GC000211 폼타이 9.5 · 7.9 ㎜) · 규격 바꾸면 그 줄만 · [선택으로 꺼진 줄 보기] = 고침 모드에서만 · 정의 영역도 꺼진 줄 기호 뺌
  1,518 문서 금액 줄 5,912 중 기본 옵션에서 꺼짐 145 줄(107 문서) · 옵션에 따라 꺼질 수 있는 줄 305 줄(141 문서)
- 식 안 만약이 0 · 1 을 타면 그 항(+ 0 · × 1)도 안 보임 — 고침 글은 그대로
- 수 하나 · 기호 하나 줄 값 식 = 한 번만(「= 1 = 1」 없음) · 수량 없음 「× 1」 자리는 줄에 마우스 올릴 때만
- 옮김 slim(master_doc_convert) — + 0 · × 1 · ÷ 1 지움 · A × k ÷ 100 → A × k% · 빌더 다시(목록 xlsx 는 안 씀) · 대조 그대로(기본 1,519 같음 · 옵션 다름 1 · 저장 1,518) · 31 파일 글만
- 시험 test_802_3 + 군더더기 없음 1(12) · 802_1 · 802_2 통과

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

234 lines
9.7 KiB
Python
Raw Permalink 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 802-3 — M01 단가산출 기초 수식 문서 화면 서버 길(요구 660~665 · 요구서_664).
① 읽기 {문서, 셈, 연결} · 없는 문서 = 빈 문서 · 없는 키 404
② 미리 셈 = 쓰기 없음 · 연결 기록(표 통째 · 단가산출 문서 · 요소 값)
③ 저장 = 서버가 정본으로 다시 셈(보낸 셈 값 무시) · 임시 폴더 파일에만 씀 · 다시 읽어 같음
④ 시스템 관리자 아니면 막힘(main.py 와 같은 의존성)
⑤ 지움 견딤 — 기호 · 줄 · 금액 줄 · 연결 대상 지움 · 순환 · 0 나눔 · 목록 밖 · 전부 지움 = 표지 · 금액 없음(500 없음)
"""
from __future__ import annotations
import copy
import shutil
from pathlib import Path
import pytest
from fastapi import Depends, FastAPI
from fastapi.testclient import TestClient
from common_util import common_util_auth as auth
from M01_MasterData import M01_MasterData_Doc as doc_screen
from M01_MasterData import M01_MasterData_Router as router_module
ROOT = Path(__file__).resolve().parents[2]
KEY = "GC000211" # 합판거푸집 설치 및 해체 — 계 124,419
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
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 _read(c: TestClient, key: str = KEY) -> dict:
res = c.get("/api/m01/logic/doc", params={"key": key})
assert res.status_code == 200, res.text
return res.json()
def _prev(c: TestClient, doc: dict, given: dict | None = None) -> dict:
res = c.post("/api/m01/logic/doc/preview", json={"문서": doc, "입력": given or {}})
assert res.status_code == 200, res.text
return res.json()
def test_읽기_미리셈_연결기록(client):
got = _read(client)
assert got["셈"]["계"] == 124419 and got["셈"]["머리"] == "계" and got["새"] is False
rec = got["연결"]
assert {"QC000428", "QC000429", "QC000431"} <= set(
rec["표"]
) # 표 통째(옵션 바뀌어도 화면이 찾음)
assert all("줄" in t for t in rec["표"].values() if t)
assert any(e["p"].get("키") == "LB000007" and "값" in e for e in rec["요소"])
assert rec["이름"]["QC000431"].startswith("합판거푸집")
# 옵션(입력) 바꿈 = 문서 안 바뀜 · 값만
other = _prev(client, got["문서"], {"폼타이": "사용", "측압": "4t/㎡"})
assert got["셈"]["값"]["Q_조"] == 0
assert other["셈"]["값"]["Q_조"] == other["셈"]["값"]["Q_4t^표"] > 0
def test_없는문서_빈문서_없는키_404(client):
fresh = _read(client, "GC000100") # 802-2a 로 문서 안 넣음
assert fresh["새"] is True and fresh["문서"]["식"] == [] and fresh["문서"]["금액"] == []
assert fresh["셈"]["셈됨"] is False and fresh["셈"]["계"] is None
assert client.get("/api/m01/logic/doc", params={"key": "GX999999"}).status_code == 404
def test_저장_서버가_다시셈_파일에씀(client, tmp_path):
doc = _read(client)["문서"]
doc["식"].append({"기호": "Z_시험", "식": "3600 × 2 ÷ 4"})
doc["셈"] = {"계": 1} # 브라우저 값 — 서버는 안 받음
res = client.post("/api/m01/logic/doc/save", json={"문서": doc})
assert res.status_code == 200, res.text
out = res.json()
assert out["셈"]["값"]["Z_시험"] == 1800 and out["셈"]["계"] == 124419
assert out["파일"] == "수식_건설품셈_06장_철근콘크리트공사.json"
assert "Z_시험" in (tmp_path / out["파일"]).read_text(encoding="utf-8")
again = _read(client)
assert again["문서"]["식"][-1] == {"기호": "Z_시험", "식": "3600 × 2 ÷ 4"}
assert again["셈"]["계"] == 124419
# 정본 폴더는 안 바뀜
real = (ROOT / "resources/master_data/수식문서" / out["파일"]).read_text(encoding="utf-8")
assert "Z_시험" not in real
# 꼴 아님 · 키 없음 = 400
assert client.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "x"}}).status_code == 400
assert (
client.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "수식문서"}}).status_code == 400
)
def test_새문서_저장_빈데서_시작(client, tmp_path):
blank = _read(client, "GC000100")["문서"]
blank["식"] = [{"기호": "Q", "식": "3600 × q ÷ C_m"}]
blank["변수"] = [
{"열쇠": "q", "기호": "q", "연결": {"갈래": "숫자", "값": 0.7}},
{"열쇠": "C_m", "기호": "C", "아래": "m", "연결": {"갈래": "숫자", "값": 18}},
{"열쇠": "w", "기호": "w", "연결": {"갈래": "요소", "키": "LB000002"}},
]
blank["금액"] = [{"이름": "보통인부", "비목": "노무비", "단가": "w ÷ Q"}]
out = client.post("/api/m01/logic/doc/save", json={"문서": blank}).json()
assert out["셈"]["셈됨"] and out["셈"]["값"]["Q"] == 140
assert _read(client, "GC000100")["새"] is False
def test_관리자_아니면_막힘(monkeypatch):
for role, code in (("USER", 403), ("COMPANY_ADMIN", 403), ("SYSTEM_ADMIN", 200)):
app = FastAPI()
app.include_router(
router_module.router,
dependencies=[Depends(auth.verify_session), Depends(auth.require_system_admin)],
)
app.dependency_overrides[auth.verify_session] = lambda role=role: {
"role": role,
"is_master": False,
"company_id": 1,
}
c = TestClient(app)
assert c.get("/api/m01/logic/doc", params={"key": KEY}).status_code == code
if code == 403:
res = c.post("/api/m01/logic/doc/save", json={"문서": {"꼴": "수식문서", "키": KEY}})
assert res.status_code == 403
main = (ROOT / "main.py").read_text(encoding="utf-8")
assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main
def _cuts(doc: dict):
"""지운 · 망가뜨린 문서들."""
for i in range(len(doc["식"])):
one = copy.deepcopy(doc)
del one["식"][i] # 줄 하나 지움
yield one
one = copy.deepcopy(doc)
one["식"][i]["식"] = "없는기호 × 2" # 없는 기호
yield one
for i in range(len(doc["금액"])):
one = copy.deepcopy(doc)
del one["금액"][i]
yield one
for i in range(len(doc["변수"])):
one = copy.deepcopy(doc)
del one["변수"][i] # 기호 하나 지움 → 정의 필요
yield one
one = copy.deepcopy(doc)
kind = one["변수"][i]["연결"]["갈래"]
one["변수"][i]["연결"] = {"갈래": kind} # 연결 대상 아직 안 고름
yield one
one = copy.deepcopy(doc)
one["식"].append({"기호": "Q_시공^표", "식": "Q_시공 × 2"}) # 겹침
yield one
one = copy.deepcopy(doc)
for v in one["변수"]:
if v["열쇠"] == "유형":
v["연결"]["목록"] = ["보통"] # 목록 밖
yield one
one = copy.deepcopy(doc)
one["변수"], one["식"], one["금액"] = [], [], [] # 전부 지움
yield one
def test_지움_견딤_표지_금액없음(client):
doc = _read(client)["문서"]
n = 0
for one in _cuts(doc):
got = _prev(client, one)["셈"]
n += 1
if not got["셈됨"]:
assert got["계"] is None and (got["표지"] or got.get("까닭"))
assert n > 80
@pytest.mark.parametrize(
"line, kind",
[
({"기호": "Q_시공", "식": "Q_형틀목공 × 2"}, "순환"),
({"기호": "Q_시공", "식": "Q_시공^표 ÷ 0"}, "0 나눔"),
({"기호": "Q_시공", "식": "Q_시공^표 × ("}, "식 틀림"),
({"기호": "Q_시공", "식": "Z_없음 × 2"}, "정의 필요"),
],
)
def test_표지_갈래(client, line, kind):
doc = _read(client)["문서"]
doc["식"] = [line if x["기호"] == line["기호"] else x for x in doc["식"]]
got = _prev(client, doc)["셈"]
assert not got["셈됨"] and got["계"] is None
assert kind in {m["갈래"] for m in got["표지"]}
def test_연결_후보_표(client):
res = client.post(
"/api/m01/logic/doc/links", json={"갈래": "단가산출", "검색": "굴착기 시간당"}
)
hits = res.json()["후보"]
assert hits and all("입력" in h for h in hits)
res = client.post("/api/m01/logic/doc/links", json={"갈래": "표", "검색": "토사깍기 기계"})
assert any(h["키"] == "QF000249" for h in res.json()["후보"])
table = client.get("/api/m01/logic/doc/table", params={"key": "QF000249"}).json()
assert "조건" in table and table["줄"]
assert "끊김" in client.get("/api/m01/logic/doc/table", params={"key": "LB000002"}).json()
def test_옮김_군더더기_없음():
"""저장 문서 식 · 금액 글에 `+ 0` · `× 1` · `÷ 1` · `× k ÷ 100` 없음(옮김 `slim` · 값 같음)."""
import master_doc as md
import master_doc_convert as cv
import master_doc_link as ml
def junk(n) -> bool:
if n[0] == "bin":
a, b = n[2], n[3]
if n[1] in ("+", "-", "*", "/") and cv.slim(n) != n:
return True
return junk(a) or junk(b)
if n[0] in ("neg", "pct", "field"):
return junk(n[1])
return n[0] == "call" and any(junk(x) for x in n[2])
bad = []
for key, doc in ml.load_all().items():
texts = [x.get("식") for x in doc.get("식") or []]
texts += [x.get(k) for x in doc.get("금액") or [] for k in ("수량", "단가")]
for t in texts:
if t and junk(md.parse(t)):
bad.append((key, t))
assert not bad, bad[:5]
assert md.text_of(cv.slim(md.parse("(r + 0) × 1 × 2 ÷ 100 ÷ 1"))) == "r × 2%"