diff --git a/M01_MasterData/M01_MasterData_Store_Cache.py b/M01_MasterData/M01_MasterData_Store_Cache.py index c704de769..7de46444a 100644 --- a/M01_MasterData/M01_MasterData_Store_Cache.py +++ b/M01_MasterData/M01_MasterData_Store_Cache.py @@ -16,12 +16,17 @@ import threading from decimal import Decimal from pathlib import Path +from config import config_system + SCRIPTS = Path(__file__).resolve().parents[1] / "resources/master_data/scripts" sys.path.insert(0, str(SCRIPTS)) import check_master as cm # noqa: E402 import master_material as mm # noqa: E402 (자재품목 테이블ID — 재료 고르기는 그 묶음을 훑음) +# 구조물 일위대가 문서(M02 `labor/<열>.json` · 39-1b) 시스템 층 — storage 층은 따로 · 시험이 바꿈 +LABOR_ROOTS: tuple[Path, ...] = (config_system.PROJECT_ROOT / "resources" / "master_template",) + _FRESH = threading.RLock() _READ: dict[str, tuple[tuple, dict]] = {} # 파일 이름 → (자국, 읽은 내용) _INDEX: dict = {} # 폴더 · 자국 · files · whole @@ -217,7 +222,36 @@ def check_saved(files: dict, changed: list[str], book: dict, narrowed=None) -> l out += cm.check_logics(files, whole, book, narrowed and narrowed[:2]) if narrowed and narrowed[2]: # 요소·표를 고쳤으면 연결 · 준용도 봄(로직 좁힘과 따로) out += cm.check_links(whole) - return out + cm.mcb.check_all(files, whole) + return out + cm.mcb.check_all(files, whole) + labor_errors(whole) + + +def _labor_files() -> list[Path]: + base = Path(config_system.STORAGE_BASE_DIR) + roots = [*LABOR_ROOTS] + if base.is_dir(): + roots += [*base.glob("*/templates"), *base.glob("*/*/templates")] # 프로젝트 빼고 + return [p for root in roots for p in sorted((root / "labor").glob("[!._]*.json"))] + + +def labor_errors(whole) -> list[str]: + """모든 층 구조물 일위대가가 담은 로직이 마스터에 없으면 한 줄씩 — 쓰는 로직 지우기를 막음 + (실무문서 = 마스터 조합은 `check_all` 몫).""" + out = [] + for path in _labor_files(): + try: + doc = json.loads(path.read_bytes()) + except (OSError, ValueError): + continue + for one in doc.get("담은로직") or []: + code = str(one.get("로직") or "") if isinstance(one, dict) else "" + try: + whole.logic(code) + except cm.mf.FormulaError: + where = f"{path.parent.parent.name}/{path.stem}" + out.append( + f"구조물 일위대가 {where} 이 담은 로직 「{code}」 이 없음 — 쓰는 로직은 못 지움" + ) + return out def warm(folder: Path) -> threading.Thread: diff --git a/resources/tester/test_m02_labor_doc.py b/resources/tester/test_m02_labor_doc.py index d4ae1e3f8..9290a00b8 100644 --- a/resources/tester/test_m02_labor_doc.py +++ b/resources/tester/test_m02_labor_doc.py @@ -211,3 +211,30 @@ def test_길_틀린_문서_400_없는_열_404_남의_개인_층_403(client): assert client.post(url.format("system", "없는열"), json={}).status_code == 404 other = client.post(url.format("personal", "rv") + "?owner=7/99", json={}) assert other.status_code == 403 + + +def test_로직_지우기_검사는_모든_층_일위대가를_봄(tmp_path, monkeypatch): + from config import config_system + from M01_MasterData import M01_MasterData_Store_Cache as cache + + system, storage = tmp_path / "system", tmp_path / "storage" + for folder, code in ( + (system / "labor", STONE), + (storage / "7" / "templates" / "labor", TEN), # 회사 층 + (storage / "7" / "42" / "templates" / "labor", STONE), # 개인 층 + (storage / "7" / "42" / "p1" / "templates" / "labor", STONE), # 프로젝트 — 안 봄 + ): + folder.mkdir(parents=True) + body = {"담은로직": [{"로직": code}]} + (folder / "a.json").write_text(json.dumps(body), encoding="utf-8") + monkeypatch.setattr(cache, "LABOR_ROOTS", (system,)) + monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) + + class Gone: # STONE 을 지운 마스터 + def logic(self, code): + if code == STONE: + raise cache.cm.mf.FormulaError("없음") + + got = cache.labor_errors(Gone()) + assert len(got) == 2 and all(STONE in x for x in got) + assert any("system/a" in x for x in got) and any("templates/a" in x for x in got)