"""실무 호표 → 구조물 조합 항목 한 단계(PLAN 704-4 뒷부분 · `_제안_704-4_규격표.md` 6장 끝) — 사전조합 안 거침. [구조물 조합으로] = 확인 창 몸(체크한 줄 · 통째 · `M02_Hopyo_Build.check` 그대로)으로 줄을 만들어(`build(save=False)` · 사전조합 SC 문서 안 씀 · 옛 [사전조합 생성] 길은 그대로) 이 층 구조물집계표에 새 항목(SU · `M02_Item_Personal.create_item` · 경로 「실무자료」) + 그 일위대가 문서에 줄을 담음(수량 = 1 단위 몫 `{손}`). 규격 묶음 = 같은 파일에서 이름 · 단위 같고 줄 구성(줄 이름 · 단위 차례) 같은 · 규격만 다른 호표(5-A 「구성 같은 묶음」) → 한 항목의 규격 열(704-4) — 열 이름 = 규격 글에서 달라지는 칸(「H=1.5」 → 이름 H · 열 1.5) · 아니면 이름 「규격」 · 열마다 같은 몸으로 만든 줄 수량(줄 차례 같음 · 다르면 400) · 기본 열 = 연 호표. """ from __future__ import annotations import re from pathlib import Path from typing import Any from M02_MasterTemplete import M02_Hopyo_Build as hb from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Item_Personal as personal from M02_MasterTemplete import M02_Labor_Spec as spec from M02_MasterTemplete import M02_MasterTemplete_Store as store PATH = ["실무자료"] _TOKEN = re.compile(r"[\s,]+") _PAIR = re.compile(r"^([A-Za-z가-힣Φφ]+)\s*[=:]?\s*([\d.]+)\D{0,3}$") def _flat(text: Any) -> str: return re.sub(r"\s+", "", str(text or "")) def _shape(h: dict[str, Any]) -> tuple: return tuple((_flat(x.get("이름")), _flat(x.get("단위"))) for x in h.get("줄") or []) def columns(specs: list[str]) -> tuple[str, list[str]]: """규격 글들 → (규격 이름, 열 이름) — 처음 달라지는 칸이 모두 「H=1.5」 꼴이면 (H, [1.5 …]) · 아니면 (「규격」, 그 칸 글) · 20 자 자름 · 겹치면 뒤에 차례.""" parts = [[t for t in _TOKEN.split(s.strip()) if t] for s in specs] width = max((len(p) for p in parts), default=0) at = next((i for i in range(width) if len({p[i] if i < len(p) else "" for p in parts}) > 1), 0) cells = [p[at] if at < len(p) else "" for p in parts] hits = [_PAIR.match(c) for c in cells] if all(hits) and len({h.group(1) for h in hits}) == 1: name, cols = hits[0].group(1), [h.group(2) for h in hits] else: name, cols = "규격", [c or s for c, s in zip(cells, specs, strict=True)] out: list[str] = [] for c in cols: c = (c.strip() or "열")[: spec.NAME_MAX] while c in out: c = f"{c[: spec.NAME_MAX - 2]}·{len(out) + 1}" out.append(c) return name[: spec.NAME_MAX], out def siblings(source: Path, key: str, no: int) -> dict[str, Any]: """규격 묶음 제안 — 같은 이름 · 단위 · 줄 구성 · 규격 다른 호표(연 호표 먼저) → `{묶음 [{번, 규격, 열}], 이름}` · 없으면 묶음 빈 것.""" units = hopyo_store.read(source, key).get("호표") or [] me = next((h for h in units if int(h["번"]) == int(no)), None) if me is None: raise store.StoreError(404, f"없는 호표 「제{no}호표」") same = [ h for h in units if _flat(h.get("이름")) == _flat(me.get("이름")) and _flat(h.get("단위")) == _flat(me.get("단위")) and _shape(h) == _shape(me) ] same.sort(key=lambda h: (int(h["번"]) != int(no), int(h["번"]))) specs = list(dict.fromkeys(_flat(h.get("규격")) for h in same)) if len(same) < 2 or len(specs) != len(same): return {"묶음": [], "이름": None} name, cols = columns([str(h.get("규격") or "") for h in same]) rows = [ {"번": int(h["번"]), "규격": h.get("규격") or "", "열": c} for h, c in zip(same, cols, strict=True) ] return {"묶음": rows, "이름": name} def _doc(folder: Path, source: Path, key: str, no: int, lines: list, whole: Any, who: Any) -> dict: maker = hb.Builder(folder, source, key, no, who) checked, pairs = maker.check(list(lines or []), whole) return {**maker.build(checked, pairs, save=False), "호표": maker.hopyo} def make_item( folder: Path, source: Path, key: str, no: int, lines: list[dict[str, Any]] | None = None, whole: dict[str, Any] | None = None, bundle: list[int] | None = None, name: str | None = None, who: Any = None, ) -> dict[str, Any]: """[구조물 조합으로] → `{key, 열, 이름, 줄수, 열수, 빠진줄}` — `bundle` = 규격 열로 묶을 호표 번(연 호표 빼고).""" main = _doc(folder, source, key, no, lines, whole, who) doc, hopyo = main["문서"], main["호표"] title = (name or "").strip() or doc["이름"] cols_doc = None if bundle: found = {r["번"]: r for r in siblings(source, key, no)["묶음"]} want = [int(no), *(int(b) for b in bundle if int(b) != int(no))] if any(b not in found for b in want): raise store.StoreError(400, "규격 묶음은 이름 · 단위 · 줄 구성이 같은 호표만") title = (name or "").strip() or str(hopyo.get("이름") or doc["이름"]) qty: dict[str, dict[str, Any]] = {} for b in want: got = ( main["문서"] if b == int(no) else _doc(folder, source, key, b, lines, whole, who)["문서"] ) col = found[b]["열"] for part in ("담은로직", "자재"): mine, theirs = doc[part], got[part] if len(mine) != len(theirs): raise store.StoreError(400, f"제{b}호표 줄 구성이 다름 — 묶지 않고 따로 만듦") for one, other in zip(mine, theirs, strict=True): hand = (other.get("수량") or {}).get("손") if hand is not None: qty.setdefault(str(spec.line_key(one)), {})[col] = hand cols_doc = { "이름": siblings(source, key, no)["이름"], "열": [found[b]["열"] for b in want], "기본": found[int(no)]["열"], "수량": qty, } with store.at(folder): made = personal.create_item(folder, PATH, title, doc.get("단위")) have = store.read("labor", made["열"]) body = {**have["문서"], "담은로직": doc["담은로직"], "자재": doc["자재"]} body["단위"] = doc.get("단위") or body.get("단위") if cols_doc: body["규격"] = cols_doc store.write("labor", made["열"], have["판"], body) count = { "줄수": len(doc["담은로직"]) + len(doc["자재"]), "열수": len(cols_doc["열"]) if cols_doc else 1, } return {**made, **count, "빠진줄": main["빠진줄"]}