Files
Aislo/resources/tester/test_802_3_doc_screen.py
T
eomsangdonandClaude Opus 5.5 54fb55487c feat(802-3): M01 단가산출 기초 수식 문서 화면 — 왼쪽 기호 식 · 오른쪽 값 식 · 빈 문서 시작 · 연결 고르기
- 공용 화면 A00_Common/formula_doc_*(M01 · 뒤에 M02 = 길 앞머리만 바꿈) — 옛 수식 나무 화면은 M01 에서 걷음(M02 는 그대로)
- 보이는 수식 그 자리 고침(공용 editEq · 전체 고르기 = 한 줄 글) · 분수 · 첨자 · 만약 = 탄 갈래만 · 반올림 자리 숨김 · 값 식 단위
- 값 식 숫자 누름 = 그 자리 값 · 기호 값 누름 = 정의 줄 · 정의 영역 = 기호 : 이름 — 연결(키 · 값 · 단위 없음 · 탄 갈래로 닿는 기호만)
- 새 기호 = 정의 필요 → 숫자 · 선택 · 입력 · 요소 · 표 · 단가산출 · 식 연결 · 기호 · 첨자 · 이름 고침 · 정의 지움
- 금액 = 경쟁사 꼴 이름 : 단가 × 수량 = 금액 · 묶음 하나 = 계 · 둘 이상 = 가. 이름 소계 + 합계 · 단가산출 연결 펼침 = 그 문서 식 · 값 식
- 셈 = 브라우저 계산기 즉시 · 연결 값 = 서버 기록 캐시(common_util_formula_doc_cache.ts ↔ master_doc_record.py) · 없는 연결만 뒤에서 미리 셈
- 서버 길 GET /api/m01/logic/doc · POST preview · save(서버가 정본으로 다시 셈 · 수식 문서 파일에 씀) · links · GET table (M01_MasterData_Doc.py)
- 연결 대상 안 고른 연결(표 · 요소 · 단가산출) = 연결 끊김 표지(파이썬 · TS 짝 가드 — 서버 500 걷음)
- 시험 test_802_3_doc_screen 11(읽기 · 빈 문서 · 저장 = 서버 다시 셈 · 관리자 아니면 막힘 · 지움 견딤 · 표지 갈래 · 후보)

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

207 lines
8.7 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 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()