"""M02 항목 키 체계 · 품번 겹침 점검(PLAN 26-4a). 키 — 마스터 `SS` + 6자리 · 회사 `SC{회사}-` + 6자리 · 개인 `SP{사용자}-` + 6자리 · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사는 새 키 + 본뜬 곳 · 회사 · 개인 층의 옛 SS 키는 새 모양으로 옮기고 옛 키로도 찾음. 번호 — 새 일련 `SD-000001`(붙임표 + 6자리)이 옛 8자리(`SD01020300`) · 12장 6자리(`SD010101`)와 모양이 달라 겹치지 않음 · 99 넘는 형제 · 깊이 6 에서도 넘침 없음 · 층마다 따로 셈. """ from __future__ import annotations import json import re from pathlib import Path from typing import Any import pytest from common_util.common_util_summary_item import parse_summary_item from config import config_system from M02_MasterTemplete import M02_Item_Source as source from M02_MasterTemplete import M02_MasterTemplete_Book as book from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Template_Layers as layers from resources.tester.test_m02_structure import write_sample_table from resources.tester.test_m02_tree import Tree, _app ROOT = Path(__file__).resolve().parents[2] NEW = re.compile(r"^S[DAM]-\d{6}$") @pytest.fixture() def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: system = tmp_path / "system" system.mkdir() write_sample_table(system) monkeypatch.setattr(store, "FOLDER", system) monkeypatch.setattr(layers, "SYSTEM_ROOT", system) store.renumber() storage = tmp_path / "storage" storage.mkdir() monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "ADMIN", "is_master": False} return {"client": _app(session), "session": session, "storage": storage, "system": system} def _numbers(root: Path, cid: str) -> set[str]: return { json.loads((root / kind / f"{cid}.json").read_bytes())[key] for kind, key, _, _ in store.PARTS } # ── 키 ───────────────────────────────────────────── def test_층마다_키_모양_안_바뀜_다시_안_씀(world): client = world["client"] company = Tree(client, "company") company.do(op="add-item", 경로=["돌쌓기"], 이름="찰쌓기") company.do(op="add-item", 경로=["돌쌓기"], 이름="메쌓기") keys = [r["key"] for r in company.now["items"]] assert keys == ["SC7-000001", "SC7-000002"] # 이름 바꾸기 · 옮기기 · 표 저장에도 키 그대로(폴더 뜻 없음) company.do(op="rename", 키="SC7-000001", 이름="찰쌓기2") company.do(op="move", 키="SC7-000002", 새경로=["다른폴더"]) assert {r["key"]: r["경로"] for r in company.now["items"]} == { "SC7-000001": ["돌쌓기", "찰쌓기2"], "SC7-000002": ["다른폴더", "메쌓기"], } # 지운 키는 다시 안 씀 company.do(op="delete", 키="SC7-000002") company.do(op="add-item", 경로=[], 이름="새것") assert [r["key"] for r in company.now["items"]] == ["SC7-000001", "SC7-000003"] # 개인 층 = SP{사용자} · 층 사이 복사 = 새 키 + 본뜬 곳 personal = Tree(client, "personal") personal.do(op="add-item", 경로=[], 이름="내것") assert [r["key"] for r in personal.now["items"]] == ["SP42-000001"] body = {"from_layer": "company", "from_owner": "7", "key": "SC7-000001"} made = client.post("/api/m02/layers/personal/items/copy", json=body).json() assert made["key"] == "SP42-000002" table = client.get("/api/m02/layers/personal/templates/table/구조물집계표").json()["문서"] col = next(c for c in table["열"] if c["id"] == made["열"]) assert col["본뜬"] == {"layer": "company", "owner": "7", "key": "SC7-000001"} # 마스터는 SS 그대로 assert Tree(client, "system").now["items"][0]["key"].startswith("SS") def _legacy_personal(storage: Path, rows: list[tuple[str, str]], after: int) -> Path: """옛 개인 층 — 집계표 열 + SS 키 장부(26-4a 앞 모양).""" folder = storage / "7" / "42" / "templates" for kind in store.KINDS: (folder / kind).mkdir(parents=True, exist_ok=True) cols = [{"id": cid, "머리": [cid, None, None, None], "꼴": "수"} for _, cid in rows] table = {"층": ["종류", "분류1", "분류2", "분류3"], "열": cols, "줄": [], "합계줄": []} (folder / "table" / "구조물집계표.json").write_text(json.dumps(table), encoding="utf-8") book_rows = [{"키": key, "열": cid, "종류": cid} for key, cid in rows] doc = {"그룹": "구조물집계표조합", "다음": after, "줄": book_rows} (folder / store.COMBO_FILE).write_text(json.dumps(doc), encoding="utf-8") return folder def test_옛_SS_키는_새_모양으로_옮기고_옛_키로도_찾음(world): folder = _legacy_personal(world["storage"], [("SS000001", "a"), ("SS000003", "b")], 5) raw = json.loads((folder / store.COMBO_FILE).read_bytes()) # 복사본 manifest 가 든 옛 줄 client = world["client"] rows = client.get("/api/m02/items?layer=personal").json() assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000003"] # 숫자 여섯 그대로 migrated = json.loads((folder / store.COMBO_FILE).read_bytes()) assert [r["옛키"] for r in migrated["줄"]] == [["SS000001"], ["SS000003"]] assert migrated["다음"] == 5 # 옛 키로도 찾음 — 입력 목록 · 나무 고침 · 복사본 원본 찾기(M02_Item_Source) assert client.get("/api/m02/layers/personal/items/SS000003/inputs").status_code == 200 tree_ = Tree(client, "personal") tree_.do(op="rename", 키="SS000001", 이름="a2") assert tree_.item(["a2"])["key"] == "SP42-000001" assert source.row_of(migrated, "SS000003")["키"] == "SP42-000003" assert source.row_of(raw, "SS000003")["키"] == "SS000003" # 옮기기 전 파일도 그대로 읽힘 # 새 항목 = 다음 번호(옛 번호 · 지운 번호 안 씀) tree_.do(op="add-item", 경로=[], 이름="c") assert tree_.item(["c"])["key"] == "SP42-000005" def test_옛_숫자가_이미_있으면_다음_번호로(world): folder = _legacy_personal(world["storage"], [("SP42-000001", "a"), ("SS000001", "b")], 2) rows = world["client"].get("/api/m02/items?layer=personal").json() assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000002"] assert json.loads((folder / store.COMBO_FILE).read_bytes())["줄"][1]["옛키"] == ["SS000001"] def test_집계_항목_참조_키_모양(): ok = [ {"layer": "system", "owner": None, "key": "SS000004"}, {"layer": "company", "owner": "7", "key": "SC7-000001"}, {"layer": "personal", "owner": "7/42", "key": "SP42-000001"}, {"layer": "company", "owner": "7", "key": "SS000001"}, # 옛 키 {"layer": "personal", "owner": "7/42", "key": "SS000002"}, # 옛 키 ] for item in ok: assert parse_summary_item({**item, "name": "x"})["key"] == item["key"] bad = [ {"layer": "company", "owner": "8", "key": "SC7-000001"}, # 다른 회사 {"layer": "personal", "owner": "7/43", "key": "SP42-000001"}, # 다른 사람 {"layer": "personal", "owner": "7/42", "key": "SC7-000001"}, # 층 머리 틀림 {"layer": "system", "owner": None, "key": "SC7-000001"}, {"layer": "company", "owner": "7", "key": "SC7-00001"}, # 자리수 {"layer": "company", "owner": "7", "key": "SD-000001"}, # 번호는 키가 아님 ] for item in bad: with pytest.raises(ValueError): parse_summary_item({**item, "name": "x"}) # ── 번호 ──────────────────────────────────────────── def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world): """마스터 옛 8자리는 그대로 · 새 열은 `-` + 6자리 · 99 넘는 형제 · 깊이 6 에서도 한 모양.""" system = world["system"] # 견본 열 둘을 옛 8자리(제목행 자리)로 — 마스터 51 항목이 지금 이 모양 for cid, code in (("rv", "01010100"), ("c1", "01010200")): for kind, key, head, _ in store.PARTS: path = system / kind / f"{cid}.json" doc = json.loads(path.read_bytes()) path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8") old = {cid: _numbers(system, cid) for cid in store._column_ids()} assert old["rv"] == {"SD01010100", "SA01010100", "SM01010100"} first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1 with store.at(system): got = store.read("table", store.STRUCTURE_TABLE) doc = got["문서"] deep = ["a", "b", "c", "d", "e", "f"] doc["열"] += [ {"id": f"x{i}", "머리": [*deep[:5], f"항목{i}"], "꼴": "수"} for i in range(120) ] store.write("table", store.STRUCTURE_TABLE, got["판"], doc) new = [_numbers(system, f"x{i}") for i in range(120)] tails = [{n[2:] for n in nums} for nums in new] assert all(len(t) == 1 for t in tails) # 세 파일 같은 뒷자리 assert [t.pop() for t in tails] == [f"-{i:06d}" for i in range(first, first + 120)] assert all(NEW.match(n) for nums in new for n in nums) assert not any(store._OLD_CODE.match(n) for nums in new for n in nums) # 12장 6자리로 안 읽힘 assert {cid: _numbers(system, cid) for cid in old} == old # 옛 번호 그대로 # 지운 번호 다시 안 줌 · 셈은 새 모양끼리 with store.at(system): got = store.read("table", store.STRUCTURE_TABLE) doc = got["문서"] doc["열"] = [c for c in doc["열"] if c["id"] != "x119"] store.write("table", store.STRUCTURE_TABLE, got["판"], doc) got = store.read("table", store.STRUCTURE_TABLE) got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"}) store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"]) last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음 assert _numbers(system, "y") == {f"SD{last}", f"SA{last}", f"SM{last}"} def test_층마다_번호는_따로_파일은_열_id_로(world): client = world["client"] Tree(client, "company").do(op="add-item", 경로=[], 이름="회사것") Tree(client, "personal").do(op="add-item", 경로=[], 이름="내것") storage = world["storage"] company = next((storage / "7" / "templates" / "structure").glob("*.json")) mine = next((storage / "7" / "42" / "templates" / "structure").glob("*.json")) assert json.loads(company.read_bytes())["도번"] == json.loads(mine.read_bytes())["도번"] assert company.parent != mine.parent # 같은 번호여도 층 폴더가 달라 안 겹침 def test_키_머리는_M01_키대장과_안_겹침(): ledger = json.loads((ROOT / "resources/master_data/_키대장.json").read_text("utf-8"))["다음"] for head in ("SS", "SC", "SP", "SD", "SA", "SM", "UM"): assert head not in ledger, head assert "UA" in ledger # 일위대가 조합은 M01 키대장 몫(M02 는 읽기만) assert book.HEAD == "SS"