"""M01 「만들기」 저장소 — 원문 절 목록 · 표 조건 값 · 자체 로직 만들기·본뜨기·고치기·지우기 · 복사해서 만들기(정본 로직을 별칭 꼴 식으로 펴 보이고 되돌려 저장). 계약: `resources/master_data/_화면_계약.md` 2장·5장·7장 · 파일 모양: `_틀.md` 7장. 자체 로직은 `로직_자체.json` 한 파일 · 키는 `_키대장.json` 의 `GX` 다음 번호(서버가 줌). """ from __future__ import annotations import re from datetime import date from pathlib import Path from M01_MasterData import M01_MasterData_Store as store cm, mk, mcp = store.cm, store.mk, store.mcp OWN = "로직_자체.json" # 자체 로직 한 자리 — 이 파일 줄만 고치고 지울 수 있음 OWNERS = ("현장", "공용") BODY = mk._BODY # resources/knowledge/original/원가계산 BOOKS = {"산림품셈": "산림_표준품셈", "건설품셈": "건설공사_표준품셈"} _HEAD = re.compile(r"^#{2,6}\s*(\d+(?:-\d+)*)\.?\s+(\S.*?)\s*$") _CHAPTER_DIR = re.compile(r"^제(\d\d)장_(.+)$") # ── 원문 절 목록 ─────────────────────────────────────────────────────── def _chapters(book: str): """(부문, 장 번호, 장 제목, 폴더) — 건설품셈은 부문 폴더 아래.""" root = BODY / BOOKS[book] / "본문" tops = sorted(root.glob("*부문")) if book == "건설품셈" else [root] for top in tops: division = top.name.split("_", 1)[1].removesuffix("부문") if book == "건설품셈" else "" for folder in sorted(top.glob("제*장_*")): m = _CHAPTER_DIR.match(folder.name) if m: yield division, m.group(1), m.group(2).replace("_", " "), folder _CACHE: dict[str, list[dict]] = {} def _all_sections(book: str) -> list[dict]: """원문 본문 제목 줄에서 모은 절 — 한 번 세워 두고 다시 씀(원문은 안 바뀜).""" if book in _CACHE: return _CACHE[book] out, seen = [], set() for division, number, title, folder in _chapters(book): for md in sorted(folder.glob("*.md")): for line in md.read_text(encoding="utf-8").split("\n"): m = _HEAD.match(line) if not m or m.group(1).split("-")[0] != str(int(number)): continue if (division, m.group(1)) in seen: continue seen.add((division, m.group(1))) out.append( { "book": book, "division": division, "chapter": f"{division} {number}장 {title}".strip(), "chapterNo": number, "section": m.group(1), "title": m.group(2), } ) _CACHE[book] = out return out def _counts() -> dict[tuple[str, str], list[int]]: """(구분, 절 번호) → [표 수, 로직 수] — 아래 절까지 세도록 절 번호의 모든 앞마디에 얹음.""" out: dict[tuple[str, str], list[int]] = {} for data in store.loaded()[0].values(): group = data.get("그룹") at = 0 if group in cm.mf.TABLE_GROUPS else 1 if group == "로직" else None if at is None: continue for row in data.get(store.items_key(data)) or []: number, sub = str(row.get("원문번호") or ""), str(row.get("구분") or "") parts = number.split("-") for upto in range(1, len(parts) + 1): out.setdefault((sub, "-".join(parts[:upto])), [0, 0])[at] += 1 return out def sections(book: str, division: str = "", chapter: str = "", q: str = "", limit: int = 200): """원문 절 목록 — 만들기 첫 걸음(절을 고르면 그 절의 표만 보임).""" if book not in BOOKS: raise store.StoreError(404, f"없는 원문 「{book}」") if division and division not in cm.DIVISIONS: raise store.StoreError(404, f"없는 부문 「{division}」") counts, hits = _counts(), [] for one in _all_sections(book): if division and one["division"] != division: continue if chapter and one["chapterNo"] != str(chapter).zfill(2): continue if q and q.lower() not in f"{one['section']} {one['title']}".lower(): continue sub = " ".join(x for x in (book, one["division"]) if x) tables, logics = counts.get((sub, one["section"]), [0, 0]) hits.append({**one, "tables": tables, "logics": logics}) return {"total": len(hits), "sections": hits[: min(max(limit, 1), 1000)]} # ── 로직 원문(출처 글 → 품셈 본문 절) ────────────────────────────────── _NUMBER = re.compile(r"(? list[dict]: """md 줄 → 화면 조각 — 제목 {h} · 표 {table} · 글 {p} · 주석 줄은 뺌.""" out: list[dict] = [] for line in lines: text = line.strip() if not text or text.startswith("