Files
Aislo/resources/tester/test_m02_combo_branch.py
T
eomsangdonandClaude Opus 5.5 e76e638013 feat(M02): 조합 갈라 내기 길 · 자재 조합(UM) 표 · 본뜬키 칸 · 분류 더하기 열 복사
- POST /api/m02/structures/{열}/combo — 고친 담은 로직으로 새 UA(같은 로직 집합이면 있는 키) · 집계표 그 열만 새 키 · 판 어긋나면 조합 만들기 전 409 · SS 장부 맞춤
- 일위대가 조합 줄에 본뜬키 칸(원래 키) · 기존 37 줄 null · M01 고치기에서 안 보내도 유지
- 자재 조합 표 resources/master_data/자재조합.json(키 UM · 대장 등록 · 틀 검사) · POST /structures/{열}/material-combo · GET /material-combos/{키}
- 구조물집계표조합 칸 자재조합 · 분류 더하기로 복사한 열은 저장 때 원본 열의 일위대가 · 자재조합 키와 구조물 도면 · 면적산출 · 재료산출 내용을 새 번호로 받음
- Api_Fetch branchCombo · branchMaterialCombo · fetchMaterialCombo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vX53nJGC8ucBRFJns2n7B
2026-09-28 18:56:51 +09:00

212 lines
10 KiB
Python

"""조합 갈라 내기 · 분류 더하기 복사 (PLAN 15장).
M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄.
· 일위대가(UA) — 새로 만듦(본뜬키 = 원래 키) · 이 열만 새 키 · 다른 열은 옛 키 · 원래 조합 그대로 ·
SS 장부 맞춤 · 담은 로직 집합(차례 · 메모 무시)이 같으면 있는 키 · 또 고치면 또 새로 · 같은 이름 (2)
· 자재 조합(UM) — 같은 규칙 · 처음 만드는 열(본뜬키 없음) · 읽기
· 집계표 판 어긋남 = 409 · 조합 안 만듦
· 분류 더하기로 복사한 열(`본뜬열`) — [저장] 때 원본 키 · 세 파일 내용을 새 번호로 받음
"""
import json
import shutil
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M01_MasterData import M01_MasterData_Store as m01
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M02_MasterTemplete import M02_MasterTemplete_Branch as branch
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_MasterTemplete_Router import router
from resources.tester.test_m02_structure import write_sample_table
REAL = m01.FOLDER
A, B, C = "GF000219", "GF000220", "GF000211" # 돌쌓기 메쌓기(인력 · 장비) · 채집(인력)
M1, M2 = (
{"코드": "MT000001", "이름": "시멘트", "규격": "40kg", "단위": "포"},
{"코드": "MT000002", "이름": "모래", "규격": None, "단위": "㎥"},
)
TABLE = "/api/m02/templates/table/구조물집계표"
@pytest.fixture()
def client(tmp_path, monkeypatch):
master = tmp_path / "master"
master.mkdir()
for path in REAL.glob("*.json"):
if not path.name.startswith("_") or path.name == m01.mk.BOOK.name:
shutil.copy(path, master / path.name)
for name in (combo_store.FILE, branch.MATERIAL_FILE):
own = master / name
data = json.loads(own.read_text(encoding="utf-8"))
data["줄"] = []
own.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
monkeypatch.setattr(m01, "FOLDER", master)
template = tmp_path / "template"
template.mkdir()
write_sample_table(template)
monkeypatch.setattr(store, "FOLDER", template)
app = FastAPI()
app.include_router(router)
return TestClient(app)
def _source(client) -> str:
"""원래 조합(A · B) — rv · c1 두 열이 같이 가리킴."""
got = combo_store.combo_new(
{
"이름": "기슭막이",
"구분": "실무 일위대가",
"상세구분": "13장 구조물",
"단위": "m",
"담은로직": [{"로직": A, "메모": None}, {"로직": B, "메모": None}],
"출처": "자체",
},
"현장",
)
table = client.get(TABLE).json()
for col in table["문서"]["열"]:
if col["id"] in ("rv", "c1"):
col["일위대가"] = got["key"]
assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200
return got["key"]
def _branch(client, column, source, keys, name="", 판="", want=200):
body = {"본뜬키": source, "담은로직": [{"로직": k} for k in keys], "이름": name, "판": 판}
res = client.post(f"/api/m02/structures/{column}/combo", json=body)
assert res.status_code == want, res.text
return res.json()
def _cols(client, field="일위대가") -> dict[str, str | None]:
return {c["id"]: c.get(field) for c in client.get(TABLE).json()["문서"]["열"]}
def _ss(client, field="일위대가") -> dict[str, str | None]:
rows = client.get("/api/m02/structure-combos").json()["줄"]
return {r["열"]: r[field] for r in rows}
def test_새로_만듦_이_열만_새_키_원래_조합_그대로(client):
ua = _source(client)
판 = client.get(TABLE).json()["판"]
got = _branch(client, "rv", ua, [A, B, C], "기슭막이 · 돌기슭막이", 판)
assert got["새로만듦"] is True and got["키"] != ua
assert got["이름"] == "기슭막이 · 돌기슭막이"
assert got["집계표판"] == client.get(TABLE).json()["판"] != 판
cols = _cols(client)
assert (cols["rv"], cols["c1"]) == (got["키"], ua) # 다른 열은 옛 키
assert _ss(client)["rv"] == got["키"] and _ss(client)["c1"] == ua # SS 장부 맞춤
new = combo_store.combo(got["키"])["combo"]
assert tuple(new) == combo_store.mcb.COLS # 열 한 벌 · 본뜬키 칸 포함
assert [x["로직"] for x in new["담은로직"]] == [A, B, C]
assert (new["본뜬키"], new["구분"], new["상세구분"], new["단위"], new["출처"], new["소유"]) == (
ua,
"실무 일위대가",
"13장 구조물",
"m",
"자체",
"현장",
)
assert [x["로직"] for x in combo_store.combo(ua)["combo"]["담은로직"]] == [A, B]
# M01 에서 고쳐도(화면이 본뜬키를 안 보내도) 계보는 남음
one = combo_store.combo(got["키"])
edited = {k: v for k, v in one["combo"].items() if k != "본뜬키"} | {"이름": "고친 이름"}
combo_store.combo_edit(got["키"], one["version"], edited)
assert combo_store.combo(got["키"])["combo"]["본뜬키"] == ua
def test_같은_로직_집합이면_있는_키_또_고치면_또_새로_같은_이름은_2(client):
ua = _source(client)
same = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 원래 조합
assert (same["키"], same["새로만듦"]) == (ua, False)
first = _branch(client, "rv", ua, [A, B, C], "갈래")
again = _branch(client, "rv", first["키"], [A, C], "갈래") # 갈라 낸 것을 또 고침 = 또 새로
assert again["새로만듦"] is True and again["키"] not in (ua, first["키"])
assert again["이름"] == "갈래 (2)"
assert combo_store.combo(again["키"])["combo"]["본뜬키"] == first["키"]
back = _branch(client, "c1", ua, [C, A, B]) # 다른 열이 같게 고침 = 있는 키
assert (back["키"], back["새로만듦"]) == (first["키"], False)
assert _cols(client)["c1"] == first["키"] and _ss(client)["c1"] == first["키"]
def test_집계표_판_어긋나면_409_조합_안_만듦(client):
ua = _source(client)
before = len(m01.read(combo_store.FILE)[0]["줄"])
_branch(client, "rv", ua, [A, B, C], "x", "0000000000000000", want=409)
assert len(m01.read(combo_store.FILE)[0]["줄"]) == before
assert _cols(client)["rv"] == ua
_branch(client, "no", ua, [A, C], want=404) # 글 열(구조물 아님)
_branch(client, "rv", "UA999999", [A, C], want=404) # 없는 원래 조합
def _material(client, column, source, items, name="", want=200):
body = {"본뜬키": source, "담은자재": items, "이름": name}
res = client.post(f"/api/m02/structures/{column}/material-combo", json=body)
assert res.status_code == want, res.text
return res.json()
def test_자재_조합_처음_만듦_같은_내용이면_있는_키_고치면_새로(client):
first = _material(client, "rv", "", [M1, M2])
assert first["새로만듦"] is True and first["키"].startswith("UM")
assert first["이름"] == "돌기슭막이 (형태마다) H=(높이마다)" # 이름 없으면 열 머리
row = client.get(f"/api/m02/material-combos/{first['키']}").json()
assert tuple(row) == ("키", "이름", "담은자재", "본뜬키", "소유", "비고")
assert (row["본뜬키"], row["소유"], row["담은자재"]) == (None, "현장", [M1, M2])
same = _material(client, "c1", "", [M2, M1]) # 차례만 다름 = 있는 키
assert (same["키"], same["새로만듦"]) == (first["키"], False)
fork = _material(client, "c1", first["키"], [M1], "시멘트만")
assert fork["새로만듦"] is True and fork["키"] != first["키"]
assert client.get(f"/api/m02/material-combos/{fork['키']}").json()["본뜬키"] == first["키"]
assert (_cols(client, "자재조합")["rv"], _cols(client, "자재조합")["c1"]) == (
first["키"],
fork["키"],
)
assert _ss(client, "자재조합")["c1"] == fork["키"]
assert client.get("/api/m02/material-combos/UM999999").status_code == 404
_material(client, "rv", "", [{"코드": "없는모양"}], want=422) # M01 저장 검사
def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_받음(client):
ua = _source(client)
table = client.get(TABLE).json()
doc = table["문서"]
rv = next(c for c in doc["열"] if c["id"] == "rv")
rv["자재조합"] = "UM000001"
assert client.put(TABLE, json={"판": table["판"], "문서": doc}).status_code == 200
drawing = client.get("/api/m02/templates/structure/rv").json()
marked = {**drawing["문서"], "도면": {"format": 6, "entities": [{"type": "line"}]}}
put = client.put("/api/m02/templates/structure/rv", json={"판": drawing["판"], "문서": marked})
assert put.status_code == 200
table = client.get(TABLE).json()
doc = table["문서"]
at = next(i for i, c in enumerate(doc["열"]) if c["id"] == "rv")
head = ["돌기슭막이", "(형태마다)", "H=(높이마다) 2", None]
doc["열"].insert(at + 1, {"id": "c9", "머리": head, "단위": None, "꼴": "수", "본뜬열": "rv"})
assert client.put(TABLE, json={"판": table["판"], "문서": doc}).status_code == 200
saved = next(c for c in client.get(TABLE).json()["문서"]["열"] if c["id"] == "c9")
assert "본뜬열" not in saved # 뗌
assert (saved["일위대가"], saved["자재조합"]) == (ua, "UM000001")
copy = client.get("/api/m02/templates/structure/c9").json()["문서"]
assert copy["도면"]["entities"] == [{"type": "line"}] and copy["열"] == "c9"
numbers = client.get("/api/m02/structures/numbers").json()
assert numbers["c9"]["도번"] != numbers["rv"]["도번"] # 새 번호
assert numbers["c9"]["면적번"][2:] == numbers["c9"]["도번"][2:]
for kind in ("area", "material"):
assert client.get(f"/api/m02/templates/{kind}/c9").json()["문서"]["열"] == "c9"
# 그 뒤로는 열마다 따로 — 원본을 고쳐도 복사본은 그대로
drawing = client.get("/api/m02/templates/structure/rv").json()
client.put(
"/api/m02/templates/structure/rv",
json={
"판": drawing["판"],
"문서": {**drawing["문서"], "도면": marked["도면"] | {"entities": []}},
},
)
again = client.get("/api/m02/templates/structure/c9").json()["문서"]
assert again["도면"]["entities"] == [{"type": "line"}]