From 8a0fd4d03db34242c818d9a0ec1fe82bc1ed5383 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 10:51:04 +0900 Subject: [PATCH] =?UTF-8?q?feat(729-1):=20=EC=8B=A4=EB=AC=B4=20=EB=8B=A8?= =?UTF-8?q?=EA=B0=80=EC=82=B0=EC=B6=9C=20=E2=86=92=20=EA=B8=B0=EC=B4=88=20?= =?UTF-8?q?=C2=B7=20SK=20=C2=B7=20=EA=B3=A0=EB=A6=84=20=EB=8C=80=EA=B8=B0?= =?UTF-8?q?=20=E2=80=94=20=EB=B8=94=EB=A1=9D=20=EB=A7=9E=EC=B6=A4=20?= =?UTF-8?q?=ED=8C=90=EC=A0=95=20=E2=86=92=20=EC=97=91=EC=85=80=20=ED=92=88?= =?UTF-8?q?=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20=EB=8D=AE=EA=B0=9C=20=C2=B7=20?= =?UTF-8?q?=EB=8D=94=ED=95=9C=EC=A4=84=20=C2=B7=20=EB=BA=80=EC=A4=84=20?= =?UTF-8?q?=C2=B7=20=EB=B3=84=EC=B9=AD=20=EC=A0=9C=EC=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M02_Hopyo_SkPatch: 판정 맞음 = 기초(바탕 = 1위 기초 · 덮개 없음) · 다른 점 = 바탕 기초 + 인력 덮개(qty · 배율) · 엑셀만 직종 · 장비(GC000267 · GC000268 기계 옵션 찾기) 더한줄 · 우리만 줄 뺀줄 · 블록 안 자재 덮개 · 옮기지 못한 차이 = 본뜬.못옮김 · 후보 = 고름 대기 - M02_Hopyo_Sangeun: 바탕 없는 SK + 고정 한 줄 걷음 · 블록마다 기초 · SK · 고름 대기 · 고른 기초(바탕 {번: 로직}) · 별칭 장부 짝 = 기초까지만 · 맞춤 셈 전 409 - 여덟 파일 206 블록 = 기초 82 · SK 40 · 고름 대기 84 · SK 품 어긋남 0 Co-Authored-By: Claude Opus 5.5 --- M02_MasterTemplete/M02_Hopyo_Sangeun.py | 200 +++++++++++----- M02_MasterTemplete/M02_Hopyo_SkPatch.py | 294 ++++++++++++++++++++++++ 2 files changed, 437 insertions(+), 57 deletions(-) create mode 100644 M02_MasterTemplete/M02_Hopyo_SkPatch.py diff --git a/M02_MasterTemplete/M02_Hopyo_Sangeun.py b/M02_MasterTemplete/M02_Hopyo_Sangeun.py index af0c86cda..9d071aecf 100644 --- a/M02_MasterTemplete/M02_Hopyo_Sangeun.py +++ b/M02_MasterTemplete/M02_Hopyo_Sangeun.py @@ -1,11 +1,16 @@ -"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 요구서 내역12 4장 22) — -산근(단산) 블록 = 바탕 없는 단가산출. +"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) — +산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기. -블록마다 SK 하나 — 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 1 단위 단가 -(노무비 · 재료비 · 경비)를 고정 더한줄 하나로(`M02_Hopyo_Fixed` 꼴 · 엑셀 숫자 그대로 · -서버가 파일에서 읽음) · 이름 = 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · -구분 「실무자료」 · 상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, -확인} · 같은 본뜬이 이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층(마법사에서 고른 층). +블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만 +셈) → `M02_Hopyo_SkPatch.plan`: + 기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) · + 고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯 → 사용자가 고른 기초 `바탕 {번: {로직, + 입력값?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면 그 키에 덮음). +SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 · +본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이 +이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층. +별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 · +요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716). """ from __future__ import annotations @@ -15,10 +20,13 @@ from pathlib import Path from typing import Any from common_util.common_util_time import utc_now_text -from M02_MasterTemplete import M02_Hopyo_Fixed as hfix +from M01_MasterData import M01_MasterData_Store as m01 +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Ledger as ledger +from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store -from M02_MasterTemplete import M02_Labor_Fixed as fixed from M02_MasterTemplete import M02_Labor_Ref as refs +from M02_MasterTemplete import M02_MasterTemplete_Store as store ORIGIN = "실무자료" KIND = "sangeun" @@ -28,76 +36,154 @@ def mark(key: str, no: int) -> str: return f"{key} 단산{no}" -def made(folder: str | Path, key: str | None = None) -> dict[str, str]: - """이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만).""" +def made( + folder: str | Path, key: str | None = None, source: refs.Here | None = None +) -> dict[str, str]: + """이 층 실무자료 단가산출 `{본뜬 키: SK 키}`(`key` = 그 HP 것만 · `source` = 파일 층으로 거름).""" out = {} for path in sorted((Path(folder) / KIND).glob("SK-*.json")): try: got = json.loads(path.read_bytes()).get("본뜬") except (OSError, ValueError, AttributeError): continue - if isinstance(got, dict) and got.get("층") == ORIGIN: - if key is None or str(got.get("키") or "").startswith(f"{key} "): - out[str(got.get("키"))] = path.stem + if not isinstance(got, dict) or got.get("층") != ORIGIN: + continue + if key is not None and not str(got.get("키") or "").startswith(f"{key} "): + continue + if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source): + out[str(got.get("키"))] = path.stem return out -def _doc(line: dict, block: dict, no: int, hp: dict, stamp: str, who: Any) -> dict[str, Any]: +def calls(hp: dict[str, Any]) -> tuple[dict[int, dict], dict[int, dict]]: + """(블록 {번: 블록}, 부르는 첫 줄 {번: 줄 + `_호표`}).""" + blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None} + first: dict[int, dict] = {} + for h in hp.get("호표") or []: + for line in h.get("줄") or []: + ref = line.get("참조") or {} + no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0 + if no in blocks and no not in first: + first[no] = {**line, "_호표": h.get("번")} + return blocks, first + + +def _bad(text: str, status: int = 400) -> store.StoreError: + return store.StoreError(status, text) + + +def _picks(raw: Any) -> dict[int, dict[str, Any]]: + """사용자가 고른 기초 `{번: {로직, 입력값?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력.""" + if not raw: + return {} + if not isinstance(raw, dict) or len(raw) > 500: + raise _bad("바탕은 {블록 번: {로직, 입력값}}") + logics = ledger._known()[0] + out = {} + for no, one in raw.items(): + if not isinstance(one, dict) or str(one.get("로직") or "") not in logics: + raise _bad(f"단산{no} · 돈 로직이 아님 「{(one or {}).get('로직')}」") + given = one.get("입력값") + if given is not None: + bad = set(given) - ledger.input_names(one["로직"]) if isinstance(given, dict) else {1} + if bad: + raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}") + out[int(no)] = {"로직": one["로직"], **({"입력값": given} if given is not None else {})} + return out + + +def _doc(line: dict, block: dict, no: int, hp: dict, plan: dict, sure: dict) -> dict[str, Any]: name = str(block.get("이름") or line.get("이름") or "").strip() or f"단산{no}" unit = str(line.get("단위") or block.get("단위") or "").strip() or "식" - spec = str(line.get("규격") or "").strip() - price = {k: hfix._plain(v) for k, v in hfix._parts(line.get("단가")).items()} - fx = { - "이름": name, - "규격": spec, - "단위": unit, - "단가": price, - "이음": dict.fromkeys(fixed.LINKS), - "출처": {"파일": hp.get("파일"), "호표": line.get("_호표"), "줄": int(line.get("차례", 0))}, - } + keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김")} return { "종류": "단가산출", "이름": name, "단위": unit, "구분": ORIGIN, "상세구분": str(hp.get("파일") or ""), - "본뜬": { - "층": ORIGIN, - "키": mark(hp["키"], no), - "파일": hp.get("파일"), - "원본": list(refs.here()), - "확인": {"누가": who, "때": stamp}, - }, - "바탕": None, - "입력값": {}, + "본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep}, + "바탕": plan["바탕"], + "입력값": plan["입력값"], "열린입력": [], "고름": {}, - "자재표시": {}, - "덮개": {}, - "더한줄": [{"고정": fx, "이름": name, "단위": unit, "수량": 1}], - "뺀줄": [], + **{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")}, } -def build(folder: Path, key: str, who: Any = None) -> dict[str, Any]: - """[단가산출 만들기] — 산근 블록마다 바탕 없는 단가산출 → - `{블록, 만듦 [{번, 이름, 키}], 그대로}`.""" - hp = hopyo_store.read(folder, key) - blocks = {int(b["번"]): b for b in hp.get("단산") or [] if b.get("번") is not None} - calls: dict[int, dict] = {} - for h in hp.get("호표") or []: - for line in h.get("줄") or []: - ref = line.get("참조") or {} - no = int(ref.get("번") or 0) if ref.get("종류") == "단산" else 0 - if no in blocks and no not in calls: - calls[no] = {**line, "_호표": h.get("번")} - have, stamp = made(folder, key), utc_now_text() - out: dict[str, Any] = {"블록": len(calls), "만듦": [], "그대로": 0} - for no in sorted(calls): - if mark(key, no) in have: +def _alias(line: dict, plan: dict) -> dict[str, Any]: + origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")} + clauses = (line.get("근거") or {}).get("출처") or [] + pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None} + return { + "원래": {k: str(v or "") for k, v in origin.items()}, + "갈래": "짝", + "짝": [pair], + "엑셀조항": clauses if isinstance(clauses, list) else [], + } + + +class Maker: + """한 번 만들기 — 파일 · 맞춤셈 · 이 층 SK 를 한 번 읽음(`source` = 파일 층 · 쓰기 = `store.root()`).""" + + def __init__(self, source: Path, key: str, who: Any = None) -> None: + self.source, self.key, self.who = Path(source), key, who + self.hp = hopyo_store.read(source, key) + self.blocks, self.first = calls(self.hp) + got = fit.result(source, key) or {} + self.cache, self.done = got.get("블록") or {}, bool(got.get("끝")) + self.here = refs.where(source) + self.whole = m01.loaded()[1] + self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}} + + def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]: + line, block = self.first[no], self.blocks[no] + entry = self.cache.get(fit.unit_key(line)) + if entry is None and not pick: + entry = fit.compute(block, line) + return skpatch.plan(entry, block, line, self.whole, pick) + + def write(self, no: int, plan: dict, have: str | None) -> str: + doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure) + if have: + raw = (store.root() / KIND / f"{have}.json").read_bytes() + store.write(KIND, have, store.version_of(raw), {**doc, "키": have}) + return have + return refs.create(KIND, doc)["이름"] + + +def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]: + """[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래}], + 그대로, 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}], 별칭}` · `picks` = 고른 기초.""" + chosen = _picks(picks) + maker = Maker(folder, key, who) + bad = sorted(set(chosen) - set(maker.first)) + if bad: + raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404) + if not maker.done and set(maker.first) - set(chosen): + fit.ensure(folder, key) + raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409) + have = made(folder, key, maker.here) + out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []} + out.update({skpatch.BASE: 0, skpatch.SK: 0}) + books, alias = ledger.books(folder), {} + for no in sorted(maker.first): + old = have.get(mark(key, no)) + if old and no not in chosen: out["그대로"] += 1 continue - doc = _doc(calls[no], blocks[no], no, hp, stamp, who) - got = refs.create(KIND, doc) - out["만듦"].append({"번": no, "이름": doc["이름"], "키": got["이름"]}) + plan = maker.plan(no, chosen.get(no)) + line = maker.first[no] + if plan["갈래"] == skpatch.WAIT: + head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")} + out["고름대기"].append({**head, "단위": line.get("단위"), **plan}) + continue + got = maker.write(no, plan, old) + out[plan["갈래"]] += 1 + name = maker.blocks[no].get("이름") or line.get("이름") + out["만듦"].append({"번": no, "이름": name, "키": got, "갈래": plan["갈래"]}) + found = ledger.recall(line, folder, books) + if no in chosen or found is None or found.get("규격다름"): + alias[ledger.key(line)] = _alias(line, plan) + out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None) return out diff --git a/M02_MasterTemplete/M02_Hopyo_SkPatch.py b/M02_MasterTemplete/M02_Hopyo_SkPatch.py new file mode 100644 index 000000000..04be3e43f --- /dev/null +++ b/M02_MasterTemplete/M02_Hopyo_SkPatch.py @@ -0,0 +1,294 @@ +"""M02 실무 단가산출(산근 블록) → 단가산출(SK) 꼴(PLAN 729-1 · 요구서 내역19 1장 7 · 3장 6 · +분석 19-B D-1). + +블록 맞춤셈 한 칸(`M02_Hopyo_Verdict.block` 꼴 · 캐시 `M02_Hopyo_Fit.result`) → 갈래 셋: + 기초 = 판정 맞음(또는 견줘 차이 없음) — 바탕 = 1위 기초 · 입력값 = 옵션 거꾸로 찾은 값 · 덮개 없음 + SK = 다른 점 — 바탕 = 1위 기초(조합 둘째부터 = 더한줄 로직 · 수량 = 몫) + 차이 = 엑셀 품 그대로 + 인력 = 직종마다 덮개 qty(줄 하나) · 배율(여럿) · 엑셀만 직종 = 더한줄 인력 · 우리만 = 뺀줄 + 장비 = 시간 같은 같은 갈래 줄은 그대로 · 같은 갈래 남은 줄 = 덮개 qty · 엑셀만 = 더한줄 + 기계경비 로직(굴착기 GC000267 · 그 밖 GC000268 · 기계 = 옵션 거꾸로 찾기) · 남은 우리 = 뺀줄 + 자재 = 같은 이름 우리 줄 수량 덮개 · 옮기지 못한 차이 = `못옮김`(버리지 않고 본뜬에 적음) + 고름 대기 = 후보 · 숫자 없음 — SK 안 만듦 · 후보 다섯(사용자가 고른 기초 `pick` = 견줘 SK · 기초). +cid = `kind:키#n`(첫 기초 바탕 줄만 · `M02_Sangeun_Doc.line_ids`) · 더한줄 · 덮개 qty = 바탕 행 몫 +(1 단위 품 × 바탕 결과단위 앞 수 · 셈이 ÷ 배) · 더한줄 로직 수량 = 몫 × 바탕 배 ÷ 그 기초 배. +품 견줌 = 우리 셈(`M02_Sangeun_Calc.run` 줄 수량 ÷ 배) · 같음 = `Option.near`(0.5 % + 엑셀 자릿수 반올림). +우리만 자재 줄은 그대로(판정 견줌도 안 봄 · 엑셀은 자재를 일위대가 줄로 따로 적는 일이 많음). +서버 단독 · 읽기만(쓰기 = `M02_Hopyo_Sangeun`). +""" + +from __future__ import annotations + +import re +from decimal import Decimal +from typing import Any + +from M02_MasterTemplete import M02_Hopyo_Combine as combine +from M02_MasterTemplete import M02_Hopyo_Finger as finger +from M02_MasterTemplete import M02_Hopyo_Fit as fit +from M02_MasterTemplete import M02_Hopyo_Option as option +from M02_MasterTemplete import M02_Hopyo_Ours as ours +from M02_MasterTemplete import M02_Sangeun_Calc as calc +from M02_MasterTemplete.M02_Hopyo_Words import mtypes, specnums + +BASE, SK, WAIT = "기초", "SK", "고름 대기" +KEYS = ("덮개", "자재표시", "더한줄", "뺀줄", "못옮김") +TINY = 1e-9 +_ADD = re.compile(r"^add:#(\d+)$") + + +def _near(a: float, b: float) -> bool: + return abs(a - b) <= TINY or option.near(a, b, fit.TOL[0], option.ru(a)) + + +def _num(x: float) -> int | float: + got = Decimal(repr(float(x))).normalize() + return int(got) if got == got.to_integral_value() else float(got) + + +# ── 우리 셈 → 1 단위 품 ────────────────────────────────────────────── +def _types(sub: str, item: dict, args: str | None, given: dict, auto: dict) -> tuple[set, set]: + """기계경비 줄 갈래 · 규격 — `M02_Hopyo_Ours._engine` 과 같은 읽기(중간값 없이).""" + ts = set(ours.FIXED_MACH.get(sub, set())) | mtypes(item.get("이름")) + sp = set(specnums(item.get("이름"))) + for v in ours._args(args).values(): + lit = v.strip("'\"") + if not ours.EQ_CODE.match(lit): + lit = str(given.get(lit) or auto.get(lit) or "") + if ours.EQ_CODE.match(lit): + a, b = ours.eq_of(lit) + ts, sp = ts | a, sp | b + return ts, sp + + +def parts(doc: dict, whole: Any) -> list[dict[str, Any]]: + """SK 문서 → 줄마다 `{cid, 갈래 인력|장비|자재|기초|그밖, 이름 · 종류 · 규격, 품(1 단위)}` · + 더한줄 기초 = 그 기초 1 단위 품 × 줄 몫(`L` · `X`).""" + got = calc.resolve(doc, whole) + items = got["줄"]["호표"] + ran = calc.run(doc, whole) + times = Decimal(str(ran["배"])) + labor = ours._labor_names() + auto = {} + if (doc.get("바탕") or {}).get("로직"): + auto = option.m01.mau.values(whole, whole.logic(doc["바탕"]["로직"])) + qty = {x["cid"]: x.get("수량") for x in ran.get("줄") or [] if x.get("cid")} + out = [] + for item, cid in zip(items, got["정체"]): # 별도계상 재료 = 셈 줄에서 빠짐 → 품 None + raw = qty.get(cid) + one: dict[str, Any] = {"cid": cid, "갈래": "그밖"} + one["품"] = None if raw is None else float(Decimal(str(raw)) / times) + kind, found = item.get("종류"), ours._SUB.match(str(item.get("요소") or "")) + if kind == "인력": + one.update(갈래="인력", 이름=labor.get(str(item.get("요소")), str(item.get("이름")))) + elif kind == "재료": + one.update(갈래="자재", 이름=str(item.get("이름") or "")) + elif kind == "로직" and found: + sub = found.group(1) + if ours.is_mach_logic(whole.logic(sub)): + ts, sp = _types(sub, item, found.group(2), got["입력값"], auto) + one.update(갈래="장비", 종류=ts, 규격=sp) + elif hit := _ADD.match(cid): + given = (doc.get("더한줄") or [])[int(hit[1])].get("입력값") or {} + v = ours.run(sub, given) or {"L": {}, "X": []} + k = (one["품"] or 0.0) * ours.times_of(whole.logic(sub)) + one.update(갈래="기초", 로직=sub, L={t: q * k for t, q in v["L"].items()}) + one["X"] = [(set(a), set(b), q * k) for a, b, q in v["X"]] + out.append(one) + return out + + +def measure(doc: dict, whole: Any) -> dict[str, Any]: + """SK 1 단위 품 — `{인력 {직종: 품}, 장비 [{갈래, 시간}], 자재 {이름: 수량}}`(시험 · 견줌).""" + labor: dict[str, float] = {} + machines, mats = [], {} + for one in parts(doc, whole): + if one["갈래"] == "인력" and one["품"]: + labor[one["이름"]] = labor.get(one["이름"], 0.0) + one["품"] + elif one["갈래"] == "장비" and one["품"]: + machines.append({"갈래": sorted(one["종류"]), "시간": one["품"]}) + elif one["갈래"] == "자재" and one["품"]: + mats[one["이름"]] = mats.get(one["이름"], 0.0) + one["품"] + elif one["갈래"] == "기초": + for t, q in one["L"].items(): + labor[t] = labor.get(t, 0.0) + q + machines += [{"갈래": sorted(a), "시간": q} for a, _, q in one["X"] if q] + return {"인력": labor, "장비": machines, "자재": mats} + + +# ── 차이 → 덮개 · 더한줄 · 뺀줄 ────────────────────────────────────── +class _Plan: + def __init__(self, doc: dict, whole: Any) -> None: + self.doc, self.whole = doc, whole + self.tb = float(calc.times_of(whole.logic(doc["바탕"]["로직"]))) + self.lines = parts(doc, whole) + self.lost: list[str] = [] + self.lb = {name: key for key, name in ours._labor_names().items()} + + def cover(self, cid: str, **one: float) -> None: + self.doc["덮개"][cid] = {k: _num(v) for k, v in one.items()} + + def drop(self, cid: str) -> None: + if cid not in self.doc["뺀줄"]: + self.doc["뺀줄"].append(cid) + + def add(self, one: dict[str, Any]) -> None: + self.doc["더한줄"].append(one) + + def labor(self, excel: dict[str, float]) -> None: + mine = [x for x in self.lines if x["갈래"] == "인력" and not x["cid"].startswith("add:")] + mine = [{**x, "품": x["품"] or 0.0} for x in mine] + extra: dict[str, float] = {} + for x in self.lines: + for t, q in (x.get("L") or {}).items(): + extra[t] = extra.get(t, 0.0) + q + for t in sorted(set(excel) | {x["이름"] for x in mine if x["품"]} | set(extra)): + a, rows = excel.get(t, 0.0), [x for x in mine if x["이름"] == t] + b, f = sum(x["품"] for x in rows), extra.get(t, 0.0) + if a and _near(a, b + f): + continue + want = a - f + if want <= TINY: + for x in rows: + self.drop(x["cid"]) + if want < -TINY: + self.lost.append(f"{t} 엑셀 {a:.6g} · 더한 기초만 {f:.6g}") + elif not rows: + if t in self.lb: + unit = "인" + self.add({"종류": "인력", "요소": self.lb[t], "이름": t, "단위": unit, + "수량": _num(want * self.tb)}) # fmt: skip + else: + self.lost.append(f"{t} 엑셀 {a:.6g} — 우리 인력에 없음") + elif len(rows) == 1 or not b: + self.cover(rows[0]["cid"], qty=want * self.tb) + for x in rows[1:]: + self.drop(x["cid"]) + else: + for x in rows: + self.cover(x["cid"], 배율=want / b) + + def machines(self, excel: list[dict[str, Any]], line: dict[str, Any]) -> None: + mine = [x for x in self.lines if x["갈래"] == "장비" and x["품"]] + fixed = [(a, q) for x in self.lines for a, _, q in x.get("X") or [] if q] + used, rest = set(), [] + for m in excel: + kinds, h = set(m["갈래"]), m["시간"] + hit = next((x for x in mine if x["cid"] not in used and x["종류"] & kinds + and _near(h, x["품"])), None) # fmt: skip + if hit is not None: + used.add(hit["cid"]) + continue + at = next((i for i, (a, q) in enumerate(fixed) if a & kinds and _near(h, q)), None) + if at is not None: + fixed.pop(at) + continue + rest.append(m) + for m in rest: + kinds, sp = set(m["갈래"]), set(m.get("규격") or []) + same = [x for x in mine if x["cid"] not in used and x["종류"] & kinds] + same.sort(key=lambda x: not (sp and sp & x["규격"])) + if same: + used.add(same[0]["cid"]) + self.cover(same[0]["cid"], qty=m["시간"] * self.tb) + else: + self._add_machine(m, line) + for x in mine: + if x["cid"] not in used: + self.drop(x["cid"]) + self.lost += [f"우리만 장비 {'·'.join(sorted(a))} — 더한 기초 안" for a, _ in fixed] + + def _add_machine(self, m: dict[str, Any], line: dict[str, Any]) -> None: + """엑셀만 장비 → 기계경비 로직 줄(기계 = 그 갈래 · 규격 옵션 거꾸로 찾기).""" + fp = finger.empty(line) + fp["장비"] = [{**m, "시간": 1.0, "밑시간": 1.0, "율": []}] + kinds = set(m["갈래"]) + first = next((c for k, c in combine.MACHINE.items() if k in kinds), combine.MACHINE_ANY) + for code in [first, *sorted(combine.MACHINE_SET - {first})]: + got = option.find(code, fp, []) + if got["수준"] <= 1: + name = " ".join([*m["갈래"], *(f"{x:g}" for x in m.get("규격") or [])]) + self.add({"종류": "로직", "로직": code, "입력값": got["입력"], "이름": name, + "단위": "시간", "수량": _num(m["시간"] * self.tb)}) # fmt: skip + return + self.lost.append(f"엑셀만 장비 {'·'.join(m['갈래'])} {m['시간']:.6g} 시간 — 기계 못 찾음") + + def materials(self, excel: list[dict[str, Any]]) -> None: + mine = [x for x in self.lines if x["갈래"] == "자재" and not x["cid"].startswith("add:")] + for m in excel: + words = option.mat_words(m["이름"]) + rows = [x for x in mine if option._named(words, x["이름"])] + if rows and all(x["품"] is None for x in rows): # 우리 = 별도계상 · 엑셀 = 블록 안 + self.doc["자재표시"][rows[0]["cid"]] = {"별도계상": "직접계산"} + self.cover(rows[0]["cid"], qty=m["품"] * self.tb) + continue + rows = [x for x in rows if x["품"] is not None] + if rows and _near(m["품"], sum(x["품"] for x in rows)): + continue + if rows: + self.cover(rows[0]["cid"], qty=m["품"] * self.tb) + for x in rows[1:]: + self.drop(x["cid"]) + else: + self.lost.append(f"엑셀만 자재 {m['이름']} {m['품']:.6g}") + + +def _extras(entry: dict, code: str, whole: Any, tb: float) -> list[dict[str, Any]]: + """조합 둘째부터(체크된 로직) = 더한줄 로직 — 수량 = 몫 × 바탕 배 ÷ 그 기초 배.""" + out = [] + for c in entry.get("조합") or []: + if not c.get("체크") or c["로직"] == code: + continue + given = ((entry.get("옵션") or {}).get(c["로직"]) or {}).get("입력") or {} + times = ours.times_of(whole.logic(c["로직"])) + qty = float(c.get("몫") or 1) * tb / times + out.append({"종류": "로직", "로직": c["로직"], "입력값": given, "수량": _num(qty)}) + return out + + +def _wait(entry: dict) -> dict[str, Any]: + keep = ("로직", "이름", "단위", "판정", "글") + cands = [{k: c.get(k) for k in keep} for c in (entry.get("후보") or [])[: fit.TOP_OPT]] + why = (entry.get("판정") or {}).get("글") or "후보 없음" + return {"갈래": WAIT, "까닭": f"맞는 기초 없음 — {why}", "후보": cands} + + +def plan( + entry: dict | None, + block: dict | None, + line: dict[str, Any], + whole: Any, + pick: dict | None = None, +) -> dict[str, Any]: + """블록 한 칸 → `{갈래 기초|SK|고름 대기, 바탕, 입력값, 덮개, 자재표시, 더한줄, 뺀줄, 못옮김, 엑셀품, + 후보?}` · + `pick` = 사용자가 고른 기초 `{로직, 입력값?}`(옵션 없으면 거꾸로 찾음 · 늘 견줌).""" + entry = entry or {} + state = (entry.get("판정") or {}).get("상태") + efp = finger.excel(block, line) + if pick: + code = str(pick["로직"]) + given = pick.get("입력값") + if given is None: + given = option.find(code, efp, (block or {}).get("글") or [])["입력"] + elif state in ("맞음", "다른 점") and entry.get("차례"): + code = entry["차례"][0] + given = ((entry.get("옵션") or {}).get(code) or {}).get("입력") or {} + else: + return _wait(entry) + doc = {"키": "SK-000000", "이름": "견줌", "단위": line.get("단위") or "식", + "바탕": {"로직": code}, "입력값": dict(given), "덮개": {}, "뺀줄": [], "자재표시": {}} # fmt: skip + tb = float(calc.times_of(whole.logic(code))) + doc["더한줄"] = [] if pick else _extras(entry, code, whole, tb) + from M02_MasterTemplete import M02_Hopyo_Verdict as verdict # 판정이 Fit 을 부름 — 늦게 읽음 + + excel = verdict.excel_qty(efp) + head = {"바탕": doc["바탕"], "입력값": doc["입력값"], "엑셀품": excel} + if state == "맞음" and not pick and not doc["더한줄"]: + return {**head, "갈래": BASE, **dict.fromkeys(KEYS[:2], {}), **dict.fromkeys(KEYS[2:], [])} + work = _Plan(doc, whole) + work.labor({t: x["품"] for t, x in efp["인력"].items() if x.get("품")}) + work.machines([m for m in efp["장비"] if m.get("시간")], line) + work.materials([m for m in efp["자재"] if m.get("품")]) + out = {k: doc[k] for k in KEYS[:4]} + changed = any(out.values()) + return {**head, **out, "갈래": SK if changed else BASE, "못옮김": work.lost}