"""M02 일위대가 실무자료 엑셀 지문(PLAN 338-1 · 설계 2-2) — 호표 줄 + 이어진 단가산출 블록 → 직종 품 · 장비 시간 · 자재 수량 · 율 · 계수 · 조건 · 조항 · 몫. 금액 안 씀(식 맨 앞 단가는 뗌). 엑셀꼴 = 블록 `글`(머리 뺀 몸 줄)을 줄마다 풂 — 절 `^\\d+\\.` · 작은 절 `^[가-하]\\.` · 직종 줄 「직종 : 단가 × 식 = 금액」(우리 인력 이름 · 옛이름) · 장비 = 「노무비 · 경비 · 재료비 : 단가 ÷ Q」 + 지금 장비(노무비 > 경비 > 재료비) · 자재 = 「이름 단가 × 수량 = 금액」 · 「: 별도계상」 · 못 풀면 금액 ÷ 단가(「역산」 · 품 되찾기만). STmate = 블록 `자원`(L · X · M · S) 품이 곧 품(노무비 · 재료비 · 경비 세 줄 같은 품은 한 번) · 섞음 = 이름 · 규격 몫 둘(작은 쪽 인력 · 큰 쪽 장비). 몫(조합용 · 설계 2-7) = 맨 윗 절마다 노임 · 장비 벡터(`M02_Hopyo_Shares`). 블록 없는 기계(hr) 줄 = `machine` · 노임 · 장비를 바로 적은 호표 = `direct`. 값 · 규칙은 336 실험(C4 `s1_excel_fp` · K5 `stmate`) 그대로. """ from __future__ import annotations import re from collections import Counter from typing import Any from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis from M02_MasterTemplete import M02_Hopyo_Shares as shares from M02_MasterTemplete.M02_Hopyo_Shares import trade from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, parse_rhs, specnums COST = {"노무비", "재료비", "경비"} SKIPW = { "소계", "계", "합계", "인부소계", "자재소계", "주자재소계", "운반", "상차", "기계", "다짐", "적사", "살수", "집토", "암절취", "채집", "인력", "장비", "작업조건", "1일시공량", "작업면적", "1시간작업량", "기계채집", "구조물터파기", } _COLON = re.compile( r"^\s*(?:\d+\)|[0-9가-힣]\.)?\s*(?P[^::=]{1,40}?)\s*[::]\s*(?P.*)$" ) _MAT = re.compile( r"^\s*(?P[^\d\s::=][^::=]*?[A-Za-z가-힣)\]])\s+(?P[\d,]+(?:\.\d+)?\s*[×xX*÷/].*=.*)$" ) _PCT = re.compile(r"([가-힣]+)의\s*([\d.]+)\s*%") _COEF = re.compile( r"(? str: return re.sub(r"\s+", "", str(s or "")) def _ratio(rhs: str) -> float | None: """식을 못 풀 때만 — 금액 ÷ 단가(품 되찾기용).""" m = re.match(r"^\s*([\d,]+(?:\.\d+)?)", rhs) n = _LASTNUM.search(rhs) if not m or not n: return None p, a = float(m.group(1).replace(",", "")), float(n.group(1).replace(",", "")) return a / p if p else None def rates_in(text: Any) -> set[float]: """글 속 섞음 비율 후보 — (N%) · 8:2 · 9:1.""" t = str(text or "") out = {round(float(n) / 100, 4) for n in re.findall(r"(\d+(?:\.\d+)?)\s*%", t)} out = {v for v in out if 0 < v < 1} for a, b in re.findall(r"(\d+)\s*:\s*(\d+)", t): a, b = float(a), float(b) if a and b and a + b <= 20: out |= {round(a / (a + b), 4), round(b / (a + b), 4)} return out def empty(line: dict[str, Any], block: dict[str, Any] | None = None) -> dict[str, Any]: block = block or {} basis = line.get("근거") or {} name, spec = str(line.get("이름") or ""), str(line.get("규격") or "") return { "이름": name, "규격": spec, "단위": str(line.get("단위") or ""), "출처": [list(x) for x in block.get("출처") or basis.get("출처") or []], "인력": {}, # 직종: {품, 밑품, 율, 어떻게} "장비": [], # {갈래, 규격, 시간, 밑시간, 율, 절, 글} "자재": [], # {이름, 품, 밑품} "율": [], # [바탕, %] "계수": {}, "조건": {}, "이름조건": {k: sorted(v) for k, v in conds(f"{name} {spec}").items()}, "옛판": False, "몫": [], } def _labor(fp: dict, name: str, q: float, base: float, rates: list, how: str) -> None: one = fp["인력"].setdefault(name, {"품": 0.0, "밑품": 0.0, "율": [], "어떻게": []}) one["품"] += q one["밑품"] += base one["율"] += rates one["어떻게"].append(how) def _round(fp: dict[str, Any]) -> dict[str, Any]: for one in fp["인력"].values(): one["품"], one["밑품"] = round(one["품"], 6), round(one["밑품"], 6) return fp def excel(block: dict[str, Any] | None, line: dict[str, Any]) -> dict[str, Any]: """HP 블록(설계 3-1) + 그 줄(이름 · 규격 · 단위) → 지문 · 블록이 없거나 글 · 자원이 없으면 기계(hr) 줄 지문(장비 1 시간) 또는 빈 지문.""" if block and block.get("자원"): return _round(_stmate(block, line)) if block and block.get("글"): return _round(_text(block, line)) return machine(line) def machine(line: dict[str, Any]) -> dict[str, Any]: """블록 없는 기계(hr) 줄 → 장비 [{갈래, 규격, 시간 1}].""" fp = empty(line) text = f"{fp['이름']} {fp['규격']}" types = mtypes(text) if types: hours = 1.0 if _HOURS.search(fp["단위"]) else None one = {"갈래": sorted(types), "규격": sorted(specnums(text)), "시간": hours} fp["장비"] = [{**one, "밑시간": None, "율": [], "절": "0", "글": fp["이름"]}] fp["조건"] = fp["이름조건"] return fp def _text(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]: """엑셀꼴 — 블록 글 줄마다 풂(C4 `north`).""" fp = empty(line, block) head = f"{block.get('번', '')}.{block.get('이름', '')} [{block.get('단위', '')}]" text = [head, *[str(t) for t in block.get("글") or []]] front = [t for t in text[:5]] + [t for t in text if "※" in t or "작업조건" in t] head_text = " ".join([fp["이름"], fp["규격"], *front]) fp["조건"] = {k: sorted(v) for k, v in conds(head_text).items()} fp["옛판"] = bool(OLD.search(" ".join(text[:6]))) sec, sub, mach, label = 0, "", None, "" got_cost: dict[tuple, tuple] = {} item_rates = rates_in(f"{fp['이름']} {fp['규격']}") sec_rates: set[float] = set() def unmix(got: dict | None) -> dict | None: """율 메모가 없는데 맨 인자가 섞음 비율 후보와 같으면 율로 가름.""" if got and not got["율"]: for v in got.get("인자") or []: if round(v, 4) in item_rates | sec_rates and 0 < v < 1: return {**got, "밑품": got["밑품"] / v, "율": [[round(v, 4), "섞음"]]} return got for raw in text[1:]: row = raw.rstrip() h, s = _HEAD.match(row), _SUB.match(row) if h or s: if h: sec, sub = int(h.group(1)), "" sec_rates = rates_in(h.group(2)) else: sub = s.group(1) sec_rates = sec_rates | rates_in(s.group(2)) title = (h or s).group(2) ts = mtypes(title) mach = (ts, specnums(title)) if ts else None label = title.strip() if ts else "" for k, v in _PCT.findall(row): fp["율"].append([k, float(v)]) cm = _COLON.match(row) name = re.sub(r"^[0-9가-힣]\.", "", _flat(cm.group("name"))) if cm else "" who = hopyo_basis.trade_names().get(name) if cm else None if who: rhs = cm.group("rhs") got = unmix(parse_rhs(rhs)) if got: _labor(fp, who, got["품"], got["밑품"], got["율"], "식") elif r := _ratio(rhs): _labor(fp, who, r, r, [], "역산") continue if cm and name in COST: got = unmix(parse_rhs(cm.group("rhs"))) if got and mach: key = (sec, sub, label) prio = 0 if (name == "노무비" and got["단가"] > 0) else 1 if name == "경비" else 2 if key not in got_cost or prio < got_cost[key][0]: got_cost[key] = (prio, mach, got) continue if cm and name not in SKIPW and re.search(r"(별도|별산)", cm.group("rhs")): fp["자재"].append({"이름": cm.group("name").strip(), "품": None, "밑품": None}) continue mm = _MAT.match(row) if mm and not cm and (got := parse_rhs(mm.group("rhs"))): fp["자재"].append( {"이름": mm.group("name").strip(), "품": got["품"], "밑품": got["밑품"]} ) continue if not (h or s): ts = mtypes(row) if ts and "%" not in row and not cm: mach, label = (ts, specnums(row)), row.strip() for k, v in _COEF.findall(row): fp["계수"].setdefault(k, float(v)) for (sec_, sub_, lab), (_, (ts, sp), got) in got_cost.items(): where = f"{sec_}.{sub_}" if sub_ else str(sec_) one = {"갈래": sorted(ts), "규격": sorted(sp), "시간": got["품"], "밑시간": got["밑품"]} fp["장비"].append({**one, "율": got["율"], "절": where, "글": lab}) fp["몫"] = shares.sections(text[1:]) return fp def _stmate(block: dict[str, Any], line: dict[str, Any]) -> dict[str, Any]: """STmate — 자원 품이 곧 품(K5 `stmate`).""" fp = empty(line, block) text = [str(t) for t in block.get("글") or []][:60] fp["조건"] = { k: sorted(v) for k, v in conds(" ".join([fp["이름"], fp["규격"], *text[:4]])).items() } fp["옛판"] = bool(OLD.search(" ".join(text[:4]))) for t in text: for k, v in _COEF.findall(t): fp["계수"].setdefault(k, float(v)) for k, v in _PCT.findall(t): fp["율"].append([k, float(v)]) rates = rates_in(f"{fp['이름']} {fp['규격']}") lab_rate = min(rates) if len(rates) >= 2 else None mach_rate = max(rates) if len(rates) >= 2 else None seen: Counter = Counter() for x in block.get("자원") or []: kind, q = x.get("갈래"), x.get("품") nm, sp = str(x.get("이름") or ""), str(x.get("규격") or "") if not q: continue seen[(x.get("코드"), round(float(q), 9))] += 1 if seen[(x.get("코드"), round(float(q), 9))] > 1: # 노무비 · 재료비 · 경비 세 줄 같은 품 continue q = float(q) ts = mtypes(f"{nm} {sp}") if kind == "L": if who := trade(nm): base = q / lab_rate if lab_rate else q _labor(fp, who, q, base, [[lab_rate, "섞음"]] if lab_rate else [], "QTY") elif kind == "X" or (kind == "S" and ts): one = {"갈래": sorted(ts), "규격": sorted(specnums(sp) | specnums(nm)), "시간": q} base = q / mach_rate if mach_rate else q fp["장비"].append({**one, "밑시간": base, "율": [], "절": "0", "글": f"{nm} {sp}"}) elif kind in ("M", "S"): fp["자재"].append({"이름": f"{nm} {sp}".strip(), "품": q, "밑품": q}) fp["몫"] = shares.sections_stm(block.get("자원") or [], text) return fp def direct(hopyo: dict[str, Any]) -> dict[str, Any] | None: """노임 · 장비를 바로 적은 호표 → 줄 수량 합 지문(호표 1 단위) · 근거글 출처 — 없으면 None.""" lines = hopyo.get("줄") or [] fp = empty({k: hopyo.get(k) for k in ("이름", "규격", "단위")}) texts = [str(t) for t in hopyo.get("근거글") or []] texts += [f"{x.get('이름')} {x.get('규격')}" for x in lines if x.get("갈래") == "근거글"] fp["출처"] = hopyo_basis.sources(" ".join(texts)) hit = False for x in lines: q = x.get("수량") if not isinstance(q, (int, float)) or q <= 0: continue if kind_of(x) == "노임" and (who := trade(x.get("이름"))): _labor(fp, who, float(q), float(q), [], "QTY") hit = True elif kind_of(x) == "기계": text = f"{x.get('이름')} {x.get('규격')}" if ts := mtypes(text): one = {"갈래": sorted(ts), "규격": sorted(specnums(text)), "시간": float(q)} fp["장비"].append({**one, "밑시간": float(q), "율": [], "절": "0", "글": text}) hit = True return _round(fp) if hit and fp["인력"] else None def kind_of(line: dict[str, Any]) -> str: """줄 갈래 + 「노임」(단위 인 + 우리 직종 · 나 창 읽기 전에도).""" kind = str(line.get("갈래") or "") unit = _flat(line.get("단위")) if kind in ("기타", "자재", "노임") and unit in TRADE_UNITS and trade(line.get("이름")): return "노임" return kind