"""조합 갈라 내기 · 분류 더하기 복사 (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_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 = ( {"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"}, {"코드": "MT000009", "이름": "모래", "규격": 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) own = master / combo_store.FILE 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) # 자재 조합 저장 검사 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"}]