Files
Aislo/resources/tester/test_m02_hopyo_build.py
T
eomsangdonandClaude Opus 5.5 bf5d130e32 feat(M02): 338-3b 확인한 줄만 사전조합 — Build 새 몸 · 재검사 · 별칭 장부 씀 · 장부 길 · 옛 즉시 확정 길 없앰
- M02_Hopyo_Build — 몸 줄만(없으면 400) · 대상 여럿 같은 별칭 · 배 · 몫 · 통째 · 하위 호표 확인 전 409 · 본뜬 원본 · 확인 · 같은 본뜬 덮음 · confirmed(folder, source)
- M02_Labor_Router — hopyo-build 엄격 몸(extra forbid) · hopyo-ledger GET · PUT · hopyo-memory 없앰 · picks 확인 · 올린 뒤 Fit.ensure · 시동 Ours.warm(있을 때만) · ?to= 만들 층
- M02_Hopyo_Ledger — times_of 나눔
- 시험 test_m02_hopyo_build.py 새로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
2026-10-15 23:21:09 +09:00

238 lines
12 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.
"""일위대가 실무자료 → 사전조합 만들기(PLAN 324-3 · 338-3 · `M02_Hopyo_Build`) — 확인한 줄만 ·
몸 없음 400 · 빠진 줄 까닭 · 로직 여럿 같은 별칭 · 같은 로직 감쌈 · 배 · 몫 · 자재 · 통째 ·
재검사 400(없는 로직 · 모르는 입력 · 배 ≤ 0 · 금액 칸) · 하위 호표 409 → 확인 뒤 참조 · 장부 씀 ·
`본뜬.확인` · 덮음 · 옛 즉시 확정 길 404 · 장부 길(보기 · 고치기 · 권한) · picks 확인 ·
master_data 원본 그대로. 층은 임시 폴더.
"""
import json
from M02_MasterTemplete import M02_Hopyo_Build as hb
from M02_MasterTemplete import M02_Hopyo_Store as hs
from M02_MasterTemplete import M02_Template_Layers as layers
# 올리기 길 시험의 `client` 마당을 같이 씀
# ruff: noqa: F811
from resources.tester.test_m02_hopyo_excel import URL, _post, client, master_hash # noqa: F401
DIG = "구조물터파기(인력100%)|토사|m3"
FILL = "구조물되메우기(인력100%)||m3"
SOIL = "잔토처리(인력100%)||m3"
BUILD = "/api/m02/layers/{}/hopyo-build"
LEDGER = "/api/m02/layers/{}/hopyo-ledger"
def _build(client, no, lines=None, layer="personal", want=200, **extra):
body = {"층": "personal", "키": "HP-000001", "번": no, **extra}
if lines is not None:
body["줄"] = lines
res = client.post(BUILD.format(layer), json=body)
assert res.status_code == want, res.text
return res.json()
def _doc(layer_dir, key):
return json.loads((layer_dir / "precombo" / f"{key}.json").read_bytes())
def _book(layer_dir):
return json.loads((layer_dir / hs.DIRNAME / f"{hs.MEMORY}.json").read_bytes())["줄"]
def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
before = master_hash()
assert _post(client).status_code == 200
lines = [
{"차례": 1, "대상": [{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1}]},
{"차례": 2, "대상": [{"로직": "GF000113", "배": 2}]},
{"차례": 3, "뺌": "우리에 없음"},
]
got = _build(client, 3, lines)
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
assert (doc["이름"], doc["단위"], doc["구분"]) == ("임도표지석 910×600×150", "개소", "실무자료")
assert doc["상세구분"].endswith("(공통단가).xls")
origin = doc["본뜬"]
assert (origin["층"], origin["키"], origin["원본"]) == (
"실무자료",
"HP-000001 제3호표",
["personal", "7/42"],
)
assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"]
assert doc["담은로직"] == [
{
"로직": "GF000101",
"입력값": {"지역": "서울"},
"별칭": "구조물터파기(인력100%)",
"수량": {"손": 0.14},
"메모": "실무 0.14㎥ × 1 · 토사",
},
{
"로직": "GF000113",
"입력값": {},
"별칭": "구조물되메우기(인력100%)",
"수량": {"손": 0.1}, # 엑셀 0.05 × 배 2
"메모": "실무 0.05㎥ × 2",
},
]
assert doc["자재"] == [] # 표지석 = 몸에 없음 → 안 들어감
assert [(x["이름"], x["금액"], x["까닭"]) for x in got["빠진줄"]] == [
("임도표지석(받침,글새김포함)", 1400000, "확인 안 함"),
("잔토처리(인력100%)", 3097.1, "뺌 — 우리에 없음"),
]
assert got["비교"]["엑셀"] == 1415169 and got["비교"]["우리"] > 0
assert got["다시씀"] is False and got["장부"] == {"씀": 3}
book = _book(mine)
assert book[DIG]["갈래"] == "짝" and book[DIG]["짝"] == [
{"로직": "GF000101", "입력값": {"지역": "서울"}, "배": 1, "몫": None}
]
assert book[DIG]["처음파일"].endswith("(공통단가).xls")
assert book[DIG]["원래"] == {"품명": "구조물터파기(인력100%)", "규격": "토사", "단위": "㎥"}
assert book[SOIL]["갈래"] == "우리에 없음" and book[FILL]["짝"][0]["배"] == 2
assert hb.confirmed(mine) == {"HP-000001 제3호표": got["참조"]["키"]}
assert hb.confirmed(mine, ("system", None)) == {} # 다른 파일 층 = 따로
assert master_hash() == before
def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
assert _post(client).status_code == 200
lines = [
{"차례": 0, "대상": [{"자재": "MT004258", "배": 1}]},
{"차례": 1, "대상": [{"로직": "GF000101"}, {"로직": "GF000113", "배": 0.1, "몫": 0.1}]},
{"차례": 3, "대상": [{"로직": "GF000101"}]},
]
got = _build(client, 3, lines)
mine = layers.personal_dir(7, 42)
doc = _doc(mine, got["참조"]["키"])
first, second, wrapped = doc["담은로직"]
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
inner = _doc(mine, wrapped["참조"]["키"])
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101"
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [
("MT004258", {"손": 1}, "직접계산")
]
stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침"))
assert stone["짝"][0]["자재"] == "MT004258"
files = sorted(p.name for p in (mine / "precombo").glob("*.json"))
again = _build(client, 3, lines[:1]) # 다시 확인 = 같은 본뜬 덮음
assert again["다시씀"] is True and again["참조"] == got["참조"]
assert _doc(mine, got["참조"]["키"])["담은로직"] == []
assert sorted(p.name for p in (mine / "precombo").glob("*.json")) == files
def test_몸_없음_재검사_400(client):
assert _post(client).status_code == 200
one = {"로직": "GF000101", "배": 1}
_build(client, 3, want=400) # 옛 몸(줄 없음) = 확인한 줄이 없음
_build(client, 3, [], want=400)
bad = [
[{"차례": 9, "대상": [one]}], # 호표 밖 줄
[{"차례": 1, "대상": [one]}, {"차례": 1, "대상": [one]}], # 겹친 줄
[{"차례": 1, "대상": []}], # 대상 · 뺌 없음
[{"차례": 1, "대상": [one], "뺌": "안 씀"}],
[{"차례": 1, "대상": [{"로직": "XX"}]}],
[{"차례": 1, "대상": [{"로직": "GC000422"}]}], # 돈 아닌 로직
[{"차례": 1, "대상": [{**one, "입력값": {"없는칸": 1}}]}],
[{"차례": 1, "대상": [{**one, "배": 0}]}],
[{"차례": 1, "대상": [{**one, "배": -1}]}],
[{"차례": 1, "대상": [{**one, "몫": 1.5}]}],
[{"차례": 1, "대상": [{**one, "자재": "MT004258"}]}],
[{"차례": 1, "대상": [{"자재": "MT000000"}]}],
[{"차례": 1, "대상": [{"호표": 3}]}], # 자기를 담음
]
for lines in bad:
_build(client, 3, lines, want=400)
for extra in ({"금액": 100}, {"품": {"보통인부": 1}}): # 금액 · 품 칸 = 받지 않음
assert (
client.post(
BUILD.format("personal"),
json={
"층": "personal",
"키": "HP-000001",
"번": 3,
"줄": [{"차례": 1, "대상": [{**one, **extra}]}],
},
).status_code
== 422
)
assert client.post(BUILD.format("personal"), json={
"층": "personal", "키": "HP-000001", "번": 3, "금액": 1,
"줄": [{"차례": 1, "대상": [one]}]}).status_code == 422 # fmt: skip
assert not (layers.personal_dir(7, 42) / "precombo").exists() or not list(
(layers.personal_dir(7, 42) / "precombo").glob("SC-*.json")
) # 틀린 몸은 아무것도 안 씀
assert client.post("/api/m02/layers/personal/hopyo-memory", json={"줄": {}}).status_code in (
404,
405,
) # 옛 즉시 확정 길 없음
def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
assert _post(client).status_code == 200
ref = [{"차례": 0, "대상": [{"호표": 46, "배": 1}]}]
assert "제46호표 먼저 확인" in _build(client, 45, ref, want=409)["detail"]
child = _build(client, 46, [{"차례": 0, "뺌": "안 씀"}])
assert any(x["까닭"].startswith("% 줄") for x in child["빠진줄"])
got = _build(client, 45, ref)
mine = layers.personal_dir(7, 42)
line = _doc(mine, got["참조"]["키"])["담은로직"][0]
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3}
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
doc = _doc(mine, whole["참조"]["키"])
assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"]
assert doc["담은로직"][0]["수량"] == {"손": 1}
assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"}
head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|"))
assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석"
_build(client, 3, [ref[0] | {"차례": 1}], 통째={"대상": [{"로직": "GF000101"}]}, want=400)
def test_길_권한과_위_층_파일_picks_확인(client):
client.who.update(role="SYSTEM_ADMIN")
assert _post(client, "system").status_code == 200
client.who.update(role="USER")
lines = [{"차례": 1, "대상": [{"로직": "GF000101"}]}]
_build(client, 3, lines, layer="system", 층="system", want=403) # 마스터 쓰기 = 시스템 관리자
got = _build(client, 3, lines, 층="system") # 마스터 파일 → 내 층 사전조합
assert got["참조"]["층"] == "personal" and got["참조"]["주인"] == "7/42"
_build(client, 99, lines, 층="system", want=404)
assert client.get(URL.format("personal")).json()["줄"] == [] # 파일은 마스터에만
rows = client.get("/api/m02/layers/personal/hopyo-picks").json()["줄"]
three = next(r for r in rows if r["번"] == 3)
assert three["확인"] == {"층": "personal", "주인": "7/42", "키": got["참조"]["키"]}
assert next(r for r in rows if r["번"] == 4)["확인"] is None
def test_장부_길_보기_고치기_권한(client):
before = master_hash()
assert _post(client).status_code == 200
_build(client, 3, [{"차례": 1, "대상": [{"로직": "GF000101"}]}])
rows = client.get(LEDGER.format("personal")).json()["줄"]
assert [(r["열쇠"], r["층"], r["고침"]) for r in rows] == [(DIG, "personal", True)]
put = {
"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "GF000103", "배": 2}]}, SOIL: {"갈래": "안 씀"}}
}
res = client.put(LEDGER.format("personal"), json=put)
assert res.status_code == 200, res.text
assert res.json()["씀"] == 2
book = _book(layers.personal_dir(7, 42))
assert book[DIG]["짝"][0] == {"로직": "GF000103", "입력값": {}, "배": 2, "몫": None}
assert book[DIG]["처음파일"].endswith("(공통단가).xls") # 처음파일 둠
bad = {"줄": {DIG: {"갈래": "짝", "짝": [{"로직": "XX"}]}}}
assert client.put(LEDGER.format("personal"), json=bad).status_code == 400
assert client.put(LEDGER.format("personal"), json={"줄": {DIG: None}}).status_code == 200
assert DIG not in _book(layers.personal_dir(7, 42))
assert client.put(LEDGER.format("system"), json=put).status_code == 403 # 위 층 = 읽기만
client.who.update(role="SYSTEM_ADMIN")
assert client.put(LEDGER.format("system"), json=put).status_code == 200
client.who.update(role="USER")
rows = client.get(LEDGER.format("personal")).json()["줄"]
assert {(r["층"], r["고침"]) for r in rows} == {("personal", True), ("system", False)}
assert client.get(LEDGER.format("system")).json()["줄"][0]["고침"] is False
assert master_hash() == before