Files
Aislo/resources/tester/test_m02_item_keys.py
T
eomsangdonandClaude Opus 5.5 760e43c8df feat(M02): 항목 키 체계 · 품번 새 모양(PLAN 26-4a)
- 항목 키 = 마스터 SS + 6자리 · 회사 SC{회사}- + 6자리 · 개인 SP{사용자}- + 6자리(장부 key_head · 층 폴더로 가름) · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사 · 개인에 저장 · 새 항목은 그 층 새 키 + 본뜬 곳
- 회사 · 개인 층의 옛 SS 키는 장부를 맞출 때(처음 읽을 때 한 번) 새 모양으로 옮김(숫자 여섯 그대로 · 겹치면 다음 번호) · 옛 키는 줄 옛키 에 남김 · 옛 키로도 찾음(store.find_combo · 나무 고침 · 입력 목록 · 항목 복사 · M02_Item_Source.row_of)
- summary_item 키 검사 — SC · SP 는 그 층 · 머리 번호 = owner 일 때만 · 옛 SS 는 어느 층이든 받음
- 품번(도번 SD · 면적번 SA · 재료번 SM) 새 일련 = 붙임표 + 6자리(SD-000001) — 옛 8자리(제목행 자리) · 12장 옛 6자리(SD010101)와 모양이 달라 안 겹침 · 옛 번호는 그대로 유효 · 새 모양끼리 셈 · 지운 번호 안 씀
- 시험 test_m02_item_keys 7(층마다 키 모양 · 안 바뀜 · 다시 안 씀 · 복사 본뜬 곳 · 옛 SS 옮김 · 옛 키로 찾음 · 겹친 숫자 · 참조 키 모양 · 120 형제 깊이 6 번호 · 층마다 번호 따로 · 키대장 안 겹침) · 번호 · 키 시험 기대값을 새 모양으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
2026-09-30 09:04:39 +09:00

214 lines
11 KiB
Python

"""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"