Files
Aislo/resources/tester/test_802_4b2_m02_screen.py
T
eomsangdonandClaude Opus 5.5 7480aebc96 feat(804-6): 옛 수식 탭 걷음 · 줄 쌓기 = 첫 「수식」 탭(요구 686-1)
- M01 단가산출 기초 · M02 단가산출 상세 보기 = 수식(줄 쌓기 · 기본) · 단가산출 상세 둘
- 줄 쌓기에 M02 사본 [단가 갱신] · [재생성](미리 셈 → 알림 → 저장 · 고침 있으면 막음) · 판 바뀜(409) 문서 그대로면 새 판으로 다시 저장 옮김
- 옛 수식 문서 화면 걷음(formula_doc_screen · defs · edit · lines · math · math_model · rows · sub · formula_doc.css) · 이미 안 부르던 796-2 수식 나무 화면(LogicLab_Equation · Defs · Var · css · M01_Logic_Equation_Model · sheet_formula_cell · ui_template_equation_defs · edit) 걷음 — 18 파일 4,860 줄
- formula_doc_render = 줄 쌓기가 쓰는 reach 만 · 옵션 가로 CSS 는 line_stack.css 로
- 남김 = 계산기 common_util_formula_doc* · 서버 길 · 옵션 · 사본 표지 · 합계 상자 · KaTeX · 시험 페이지 tools/line-stack-test
- 시험: test_796_2 지움(걷은 화면 원본 검사뿐) · 799_1 · 800_2 걷은 화면 줄만 뺌 · 799_2 · 802_4b2 · 804_2 보기 · 화면 파일 대상 바꿈 · test_804_6 새로(걷은 파일 · 부르는 곳 없음 · 첫 탭 · 사본 할일)

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

228 lines
10 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.
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""802-4b-2 M02 단가산출(SK) 수식 탭 = 공용 수식 문서 화면 — 임시 storage · M01 은 메모리 덮개.
① 길 앞머리 — M02 수식 탭 = 공용 화면(`createDocScreen` · `/m02/layers/{층}/sangeun/{키}/doc`) ·
옛 M02 수식 나무 화면 걷음 · 연결 후보 · 표 길 등록
② 읽기 연결 기록만으로 같은 셈(화면이 네트워크 없이 다시 셈) — 끊은 단가산출 = 끊음 문서 먼저
③ 권한 — 회사 층 일반 계정 = 읽기 · 후보 · 표만(미리 셈 · 저장 403) · 화면 = 읽기만(옵션 잠금)
④ 꺼진 줄 — 만약 0 갈래가 계산 변수를 거쳐 와도 꺼짐(폼타이 「아님」 = 세 줄) · 입력 0 안 숨김 ·
1,518 문서 기본 옵션 — 옛 판정 ⊆ 새 판정 · 꺼진 줄 금액 모두 0
"""
from __future__ import annotations
import copy
import json
import os
import subprocess
from decimal import Decimal
from pathlib import Path
import pytest
from fastapi import HTTPException
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Router as router
from M02_MasterTemplete import M02_Doc_Screen as screen
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_795_1_sk_copy import _sk
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
from resources.tester.test_m02_structure import write_sample_table
ROOT = Path(__file__).resolve().parents[2]
HERE = os.path.dirname(os.path.abspath(__file__))
FORM = "GC000211" # 합판거푸집 — 폼타이 · 측압 · 규격 옵션
BASIC = "GC001148" # 굴착기 시간당
ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub3임시 굴착기"}
def _at(folder, fn, *args):
with store.at(folder):
return fn(*args)
def _src(rel: str) -> str:
return (ROOT / rel).read_text(encoding="utf-8")
def test_길_앞머리_M02_공용화면():
tabs = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Tabs.ts")
assert "createStackScreen({" in tabs and "copy: { route: (k, p) => skDocPath(" in tabs
assert "/m02/layers/${scope.layer}/sangeun/${encodeURIComponent(key)}/doc${tail}" in tabs
assert "admin: opt.edit" in tabs # 층 쓰기 권한 = 고침
scr = _src("A00_Common/line_stack_screen.ts") # 804-6 수식 탭 = 줄 쌓기
assert "cp ? cp.route(key, path) : opt.base + path" in scr
assert "{ 문서: doc, 끊음: cut, 판: version }" in scr # 저장 몸
detail = _src("M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts")
assert 'tabs.draw(/^SK-\\d{6}$/.test(doc.키 ?? "")' in detail # 저장한 SK 만 · 줄 모달 = 글
assert not (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Sangeun_Equation.ts").exists()
main = _src("main.py")
assert '__import__("M02_MasterTemplete.M02_Doc_Screen", fromlist=["router"]).router' in main
for name in ("line_stack_screen", "formula_doc_copy"):
assert len(_src(f"A00_Common/{name}.ts").splitlines()) <= 700, name
class _Replay:
"""화면 캐시 흉내 — 서버 연결 기록만으로 셈(`common_util_formula_doc_cache.ts` 짝)."""
def __init__(self, rec: dict):
self.rec = rec
self.els = {self._k(e["p"]): e for e in rec.get("요소") or []}
@staticmethod
def _k(p) -> str:
return json.dumps(p, ensure_ascii=False, sort_keys=True, default=str)
def element(self, p):
got = self.els[self._k(dc.ml._norm(p))]
if "빈" in got:
raise dc.mc.Gap(got["빈"])
if "끊김" in got:
raise dc.mc.Broken(got["끊김"])
return Decimal(str(got["값"])) if isinstance(got["값"], (int, float)) else got["값"]
def table(self, p):
return dc.ml.Links(_Tables(self.rec["표"]), {}).table(p)
def doc(self, key):
d = (self.rec.get("문서") or {}).get(key)
return json.loads(json.dumps(d), parse_float=Decimal, parse_int=Decimal) if d else d
class _Tables:
"""기록 표만 든 색인(`Links.table` 이 `master.get` 으로 찾음)."""
def __init__(self, tables: dict):
self.tables = tables
def get(self, ref):
return json.loads(json.dumps(self.tables[ref]), parse_float=Decimal, parse_int=Decimal)
def _replay(read: dict) -> dict:
doc = json.loads(json.dumps(read["문서"]), parse_float=Decimal, parse_int=Decimal)
return dc.mc.calc(doc, _Replay(read["연결"]))
def test_연결기록만으로_같은셈_끊음먼저(world):
me = world["personal"]
key = _sk(
me,
바탕={"로직": FORM},
입력값={"폼타이": "사용", "측압": "4t/㎡"},
더한줄=[copy.deepcopy(ADD)],
)
read = _at(me, path.read, key)
rec = read["연결"]
assert {"요소", "표", "문서", "이름"} <= set(rec) and BASIC in rec["문서"]
assert _replay(read)["계"] == read["셈"]["계"]
cut = copy.deepcopy(read["부름"][BASIC])
line = cut["금액"][0]
line["수량"] = f"({line.get('수량') or 1}) × 2" # 끊은 문서 안 줄 고침
saved = _at(me, path.save, key, {"끊음": {BASIC: cut}, "판": read["판"]})
assert saved["연결"]["문서"][BASIC]["금액"][0]["수량"] == line["수량"] # 끊음 먼저
assert saved["셈"]["계"] > read["셈"]["계"]
assert _replay(saved)["계"] == saved["셈"]["계"]
again = _at(me, path.read, key)
assert again["끊음"][BASIC]["금액"][0]["수량"] == line["수량"] and again["판"] == saved["판"]
got = _at(me, path.save, key, {"끊음": {}, "판": again["판"]}) # 다시 잇기
assert got["끊음"] == {} and got["셈"]["계"] == read["셈"]["계"]
def _session(role="USER"):
return {"role": role, "company_id": 7, "user_id": 42}
def test_권한_회사층_일반계정_읽기만(world):
company = world["company"]
company.mkdir(parents=True, exist_ok=True)
write_sample_table(company)
with store.at(company):
store.create_structure("rv")
key = _sk(company, 바탕={"로직": FORM})
read = router.read_doc("company", key, None, _session())
assert read["셈"]["셈됨"] and read["연결"]["표"]
body = router.DocBody(문서=read["문서"])
for fn in (router.preview_doc, router.save_doc):
with pytest.raises(HTTPException) as e:
fn("company", key, body, None, _session())
assert e.value.status_code == 403
found = screen.doc_links(
"company", key, screen.LinksBody(갈래="단가산출", 검색="굴착기 시간당"), None, _session()
)
assert any(c["키"] == "GC000267" for c in found["후보"])
assert "줄" in screen.doc_table("company", key, "QC000431", None, _session())
scr = _src("A00_Common/line_stack_screen.ts")
assert "buildOptions(doc, given, !!cp && !opt.admin," in scr # 사본 읽기만 = 옵션 잠금
copy_src = _src("A00_Common/formula_doc_copy.ts")
assert "if (!edit) return [];" in copy_src and 'createBadge(ft("Read_Only")' in copy_src
# ── ④ 꺼진 줄(TS 판정 그대로 · Node) ─────────────────────────────────
def _off(cases: list[dict]) -> list[dict]:
proc = subprocess.run(
["node", os.path.join(HERE, "helper_802_4b2_off.cjs")],
input=json.dumps(dc.ml._norm(cases), ensure_ascii=False),
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return json.loads(proc.stdout)
def _vals(doc: dict, given: dict | None = None) -> dict:
links = dc.ml.Links(dc.skc.whole_(), dc.folder())
return dc.mc.calc(doc, links, given).get("값") or {}
def test_꺼진줄_계산변수_거침():
doc = dc.folder()[FORM]
names = [x["이름"] for x in doc["금액"]]
tie = [i for i, n in enumerate(names) if n.startswith("폼타이")]
hole = [i for i, n in enumerate(names) if "구멍땜" in n]
assert len(tie) == 3 and hole
off, on, on2 = _off(
[
{"문서": doc, "값": _vals(doc)}, # 폼타이 아님 · 규격 12.7
{"문서": doc, "값": _vals(doc, {"폼타이": "사용"})},
{"문서": doc, "값": _vals(doc, {"폼타이": "사용", "폼타이규격": "9.5㎜"})},
]
)
assert set(tie) <= set(off["새"]) and tie[0] not in off["옛"] # Q_조 = 만약 0 을 거침
assert set(on["새"]) == set(tie) - {tie[0]} and set(on2["새"]) == set(tie) - {tie[1]}
assert not set(hole) & set(off["새"]) # 구멍땜 0 개소 = 입력 0 · 안 숨김
plain = {
"식": [{"기호": "Z", "식": "0"}, {"기호": "Y", "식": "만약(S == 'a', 0, 2)"}],
"금액": [
{"이름": "글 0", "비목": "노무비", "수량": "Z × 3", "단가": "w"},
{"이름": "입력 0", "비목": "노무비", "수량": "X × 3", "단가": "w"},
{"이름": "거침", "비목": "노무비", "수량": "Y × 3 ÷ 10", "단가": "w"},
{"이름": "합", "비목": "노무비", "수량": "Y + 1", "단가": "w"},
{"이름": "순환", "비목": "노무비", "수량": "V", "단가": "w"},
],
}
plain["식"].append({"기호": "V", "식": "V × 2"})
got = _off([{"문서": plain, "값": {"S": "a", "Z": 0, "X": 0, "Y": 0, "w": 5}}])[0]
assert got["새"] == [2]
def test_꺼진줄_전수_1518():
docs = dc.folder()
links = dc.ml.Links(dc.skc.whole_(), docs)
keys, cases, rows = [], [], []
for key, doc in docs.items():
ans = dc.mc.calc(doc, links)
keys.append(key)
cases.append({"문서": doc, "값": ans.get("값") or {}})
rows.append(ans.get("줄") or [])
got = _off(cases)
assert len(keys) >= 1518
new = sum(len(g["새"]) for g in got)
assert all(set(g["옛"]) <= set(g["새"]) for g in got) and new > sum(len(g["옛"]) for g in got)
for key, g, r in zip(keys, got, rows):
for i in g["새"]:
amt = r[i].get("금액") if i < len(r) else None
assert amt is None or float(amt) == 0, (key, i, amt)