- 지움(git rm) 10 파일 3,374 줄: M02_Hopyo_Finger · Ours · Option · Fit · Combine · Verdict · Line · SkLink · SkPatch · Basis
- Match: 흐름 몫(candidates · name_cands · fix_typo · clause · b_order · _logic_specs · _line_mod · units_of · _state · view · guess · picks · chain) 지움 · 이름 도구 몫 · line_key 남김
- Words: conds · cond_score · close · cosine · jacc 지움 / Shares: sections 지움(trade · eq_key 남김)
- 옮김: Finger.kind_of → Fixed.kind_of · SkLink(sk_line · kept · left · LEFT · WHY) → Sangeun · SkPatch 기계 몫(MACH_IN · MACHINE · SOLO · _mach_logics) → Units.mach_logics
- Sangeun: 옛 /sangeun 몸(build · _sure · Maker.basis · judge · plan · make) 지움 · Kids: first · proposal · order 지움 · Build: build() · unlinked() 지움(모두 고정 Builder · build_all · _order · _origins 남김)
- Router: 옛 길 지움(GET hopyo/{키}/{번} · guess · hopyo-picks · hopyo-build · hopyo-unlinked · hopyo/{키}/sangeun) · GET hopyo/{키} = Judge.view(1차 줄 판정 줄상태 · 만든 · 블록 풀이 셈)
- Marks: 칠 = Judge.judged 만 · Forget: 맞춤 캐시 지움 줄 걷음 · Ledger: Ours.version 판 칸 걷음 · Item: Kids.first 걷음
- Row(덧): 단산 줄 = 장부 짝이 있어도 늘 「2차 필요」(장부 = 1차 후보 힌트만)
시험 정리(까닭 = 지운 길 · 모듈만 시험함):
- 지움 11 파일: test_m02_hopyo_finger · fit · golden · option · ours · basis · seam · flow · draft_calc · test_635 · test_628(지문 · 순위 · 옵션 거꾸로 · 맡은 절 · 옛 확인 흐름)
- 함수 지움: /sangeun · /hopyo-build · /hopyo-unlinked · hopyo-picks · 옛 줄 맞춤 보기를 부르던 것(572 · 718_1 · 729 · 739_1 · 777_1 · 778_5 · 780_1 · labor_national · 705_3 · m02_hopyo_match · 746_1)
- test_621 S2(지문 해시) 지움 · S1 남김 · test_m02_hopyo_build = 장부 길 시험만(PUT 으로 심음)
- 새 길로: 779_2(줄상태 = 1차 판정 · 칠 셈 전 None) · grid · stmate(hopyo-judge 줄 판) · 778_1 · 778_2(Units.mach_logics) · 642_6(Marks 원문) · 642_3 장부 짝 단산 줄 2차 지킴
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
294 lines
16 KiB
Python
294 lines
16 KiB
Python
"""M02 일위대가 실무자료 줄 잇기 한 벌(PLAN 778-5 · D-2 · 요구 622 · 623 · 624) — 단위 줄
|
||
(`M02_Hopyo_UnitLine`) → 우리 계산 단위 + 상태. 형식(EST · STmate) · 대상(단가산출 블록 · 일위대가 줄)을
|
||
모름 — 일위대가 줄 판(`M02_Hopyo_Row`)도 씀.
|
||
|
||
잇기 = `{상태 이어짐|고를 것|못 이음, 방법, 단위, 후보, 꼬리표, 글}` · 자동 없음(제안만 · 사용자가 확인).
|
||
L 노무 = 직종 LB(이름 · 옛이름) · 같은 이름 여럿 = 건설업 먼저(작업반장 · 요구 624-1) · 단위 `{노임}`
|
||
X 기계 = EQ(`Fixed.Linker._machine` · 778-1 괄호 낱말) → 기계경비 기초 로직(`mach_logics` ·
|
||
778-2) · 단위 `{기계, 로직, 할증?}` · 없는 규격 = 가까운 규격 후보만(`Elem.machines` · 자동 고름 없음)
|
||
M 자재 = MT(`material` — 별칭 장부 › 이름 · 규격 같음 › 바탕 이름 · 숫자 규격 같음(치수 차례 · 구분자
|
||
무관) · 글자 든 규격 = 후보로만) · 손료율 = 방법 「수량 × 손료율 × 단가」 · 별도 = 단위 `{자재?,
|
||
별도계상}` · 단위 `{자재}`
|
||
R 율 = 778-4 꼴 `{기준, 비목}` — 기준 = `기준꼴`(인력 · 재료 묶음 · 노무비 · 줄 목록 `{행: [...]}`) ·
|
||
우리에 없는 율 항목(설치비 · 제경비)도 율 줄 · 기준 못 찾음 = 고를 것(글 기준 제안)
|
||
S 하위 = `{하위 {종류 단산|일위, 번}}`(SK · SC 참조) · E 값 = `{값: 금액}`(고정 금액 줄)
|
||
별칭 장부 짝(자재) = 숫자보다 먼저. 서버 단독 · 읽기만.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from typing import Any
|
||
|
||
from M02_MasterTemplete import M02_Hopyo_Elem as elem
|
||
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
|
||
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
|
||
from M02_MasterTemplete import M02_Hopyo_Match as hm
|
||
from M02_MasterTemplete import M02_Hopyo_Material as hmat
|
||
from M02_MasterTemplete import M02_Hopyo_Shares as shares
|
||
from M02_MasterTemplete import M02_Hopyo_UnitLine as unit_line
|
||
from M02_MasterTemplete.M02_Hopyo_Words import mtypes
|
||
|
||
LINKED, PICK, NONE = "이어짐", "고를 것", "못 이음"
|
||
HOW = {
|
||
"L": "노무 = 직종 × 품 × 노임",
|
||
"X": "기계 = 시간 × 시간당 기계경비",
|
||
"M": "자재 = 수량 × 단가",
|
||
"R": "율 = 기준 × 율",
|
||
"S": "하위 = 하위 값 × 수량",
|
||
"E": "값 = 고정 금액",
|
||
}
|
||
WEAR = "자재 = 수량 × 손료율 × 단가"
|
||
APART = "자재 = 별도계상(0 원)"
|
||
TRADE_FIRST = "건설업" # 같은 이름 노임 여럿 = 건설업 먼저(임도 = 토목 · 요구 624-1)
|
||
LETTERS = "글자 규격 — 확인"
|
||
# 율 기준꼴(`UnitLine.base_rows`) → 778-4 기준(묶음 · 비목) · 그 밖 = 줄 목록
|
||
BASES = {"인력": "인력", "인력(블록)": "인력", "자재": "재료", "자재(블록)": "재료"}
|
||
BASES |= {k: k for k in ("노무비", "재료비", "경비")} # 일위대가 % 줄 기준(규격 글)
|
||
_PART_WORDS = (("공구손료", "경비"), ("재료", "재료비"), ("자재", "재료비"), ("소모", "재료비"))
|
||
MACH_IN = "기계" # 기계경비 기초 기계 입력 칸
|
||
MACHINE = {"굴착기": "GC000267"} # 굴착기(무한궤도) 시간당 기계경비 · 그 밖 장비 = GC000268
|
||
MACHINE_ANY = "GC000268"
|
||
MACHINE_SET = {"GC000267", "GC000268", "GC000269", "GC000275"}
|
||
# (주연료, 조종원) 없는 기종 → 그 기계경비 기초(M01 로직이 부르는 것과 같음 · 778-2)
|
||
SOLO = {(False, False): "GC000273", (True, False): "GC000272", (False, True): "GC000276"}
|
||
|
||
|
||
def mach_logics(whole: Any, kinds: set, eq: dict | None = None) -> list[str]:
|
||
"""기계경비 기초 차례 — 갈래 첫 기초(굴착기 = 무한궤도) › 그 밖 · `eq` 가 있으면
|
||
연료 · 조종원 없는 기종 = 그 기초 하나 · 아니면 그 기계(또는 같은 이름 기계)를 고르기에 둔 기초가 앞."""
|
||
first = next((c for k, c in MACHINE.items() if k in kinds), MACHINE_ANY)
|
||
order = [first, *sorted(MACHINE_SET - {first})]
|
||
if not eq:
|
||
return order
|
||
has = eq.get("값") if isinstance(eq.get("값"), dict) else {}
|
||
if solo := SOLO.get((bool(has.get("주연료")), bool(has.get("조종원")))):
|
||
return [solo]
|
||
eqs = whole.numbers.get("EQ", {})
|
||
|
||
def takes(code: str) -> bool:
|
||
ins = whole.logic(code).get("입력") or []
|
||
opts = next((s.get("고르기") or [] for s in ins if s.get("이름") == MACH_IN), [])
|
||
same = any((eqs.get(o) or {}).get("이름") == eq.get("이름") for o in opts)
|
||
return eq.get("원문번호") in opts or same
|
||
|
||
hit = [c for c in order if takes(c)]
|
||
return hit + [c for c in order if c not in hit]
|
||
|
||
|
||
def _cands(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||
return [{**hm._public(r), "점수": r.get("점수"), "판정": "후보"} for r in rows]
|
||
|
||
|
||
def _split(me: dict[str, Any]) -> list[dict[str, Any]]:
|
||
"""이름에 붙은 규격 · 괄호 이름 — 「이름 규격」 · 「이름(규격)」(끝 괄호에 수) · 「유로폼(패널)」 =
|
||
유로폼패널 · 「패널(유로폼)」 = 유로폼패널 · 끝 「[㎥]」 뗌."""
|
||
name = re.sub(r"\[[^\]]*\]\s*$", "", str(me.get("이름") or "")).strip()
|
||
spec = str(me.get("규격") or "")
|
||
pairs = []
|
||
if not spec:
|
||
head, _, rest = name.partition(" ")
|
||
pairs += [(head, rest)] if rest else []
|
||
if hit := re.match(r"^(.+?)\s*[((]([^()()]*\d[^()()]*)[))]\s*$", name):
|
||
pairs.append((hit.group(1), hit.group(2)))
|
||
out = []
|
||
for n, sp in [*pairs, (name, spec)]:
|
||
if hit := re.fullmatch(r"([^()]+)\(([^()\d]+)\)", n.strip()):
|
||
a, b = hit.group(1).strip(), hit.group(2).strip()
|
||
out += [{**me, "이름": a + b, "규격": sp}, {**me, "이름": b + a, "규격": sp}]
|
||
elif (n, sp) != (me.get("이름"), spec):
|
||
out.append({**me, "이름": n, "규격": sp})
|
||
return out
|
||
|
||
|
||
def material(line: dict[str, Any], got: dict[str, Any], full: bool = True) -> dict[str, Any]:
|
||
"""자재 줄(별도계상 글 뗌) → `{상태, 찾음 [M01 자재], 글, 꼬리표, 후보}` — 이름 · 규격 같음 하나 ›
|
||
바탕 이름 · 숫자 규격 같음(이름에 붙은 규격 「이름 규격」 · 「이름(규격)」도) · 글자 규격 = 고를 것."""
|
||
me, apart = hmat.clean(line)
|
||
tags = [hmat.APART] if apart else []
|
||
found = hm._exact(me, "자재", got)
|
||
if len(found) == 1:
|
||
return {"상태": LINKED, "찾음": found, "글": "이름 · 규격 같음", "꼬리표": tags, "후보": []}
|
||
tries = [me, *_split(me)]
|
||
for one in tries:
|
||
sure, more = hmat.sure(one)
|
||
if not sure:
|
||
continue
|
||
rows = [r["one"] for r in sure]
|
||
if hmat.lettered(one.get("규격")):
|
||
cands = [{**hm._public(r), "점수": None, "판정": "후보", "글": LETTERS} for r in rows]
|
||
return {"상태": PICK, "찾음": [], "글": LETTERS, "꼬리표": tags + more, "후보": cands}
|
||
return {"상태": LINKED, "찾음": rows, "글": "바탕 이름 · 규격 같음", "꼬리표": tags + more,
|
||
"후보": []} # fmt: skip
|
||
cands = hmat.candidates(me) if full else []
|
||
state = PICK if cands or not full else NONE
|
||
return {
|
||
"상태": state,
|
||
"찾음": [],
|
||
"글": "후보만" if cands else "없음",
|
||
"꼬리표": tags,
|
||
"후보": cands,
|
||
}
|
||
|
||
|
||
def rate_part(name: Any) -> str:
|
||
"""율 금액이 드는 비목 — 공구손료 = 경비 · 재료 · 자재 · 소모 = 재료비 · 그 밖 경비."""
|
||
flat = re.sub(r"\s", "", str(name or ""))
|
||
return next((part for word, part in _PART_WORDS if word in flat), "경비")
|
||
|
||
|
||
def _text_base(one: dict[str, Any]) -> str:
|
||
"""율 줄 기준 글(「노무비의」 · 「주재료의」 · STmate 표지) → 778-4 기준 제안."""
|
||
text = re.sub(r"\s", "", f"{one.get('기준') or ''} {one.get('기준글') or ''} {one.get('이름')}")
|
||
if "주재료" in text or "주자재" in text or "M:" in text:
|
||
return "재료"
|
||
if "재료비" in text:
|
||
return "재료비"
|
||
if "경비" in text and "노무" not in text:
|
||
return "경비"
|
||
return "노무비"
|
||
|
||
|
||
class Units:
|
||
"""한 번 잇기 — M01 · 목록 · 장부를 한 번 읽음(`books` = `Ledger.books` · 없으면 장부 안 봄)."""
|
||
|
||
def __init__(self, books: list | None = None, doc: str | None = None) -> None:
|
||
self.linker = hfix.Linker(by_name=True)
|
||
self.whole, self.got = self.linker.whole, self.linker.got
|
||
self.books, self.doc = books, doc
|
||
self.eqs = {str(r.get("키")): r for r in self.whole.numbers.get("EQ", {}).values()}
|
||
self.lb: dict[str, list[dict]] = {}
|
||
for row in self.whole.numbers.get("LB", {}).values():
|
||
self.lb.setdefault(str(row.get("이름") or ""), []).append(row)
|
||
|
||
def line(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
kind = one.get("묶음")
|
||
how = HOW.get(str(kind), HOW["E"])
|
||
got = getattr(self, f"_{kind}", self._E)(one)
|
||
return {"방법": how, "후보": [], "꼬리표": [], "글": "", **got}
|
||
|
||
def _L(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
name = shares.trade(one.get("직종") or one.get("이름")) or ""
|
||
rows = self.lb.get(name, [])
|
||
cands = [{"키": str(r.get("키")), "갈래": "노임", "이름": r.get("이름"),
|
||
"구분": r.get("구분"), "단가": elem._num(r.get("값"))} for r in rows] # fmt: skip
|
||
if len(rows) == 1:
|
||
return {"상태": LINKED, "단위": {"노임": cands[0]["키"]}}
|
||
first = [c for c in cands if c["구분"] == TRADE_FIRST]
|
||
if len(first) == 1:
|
||
rest = [c for c in cands if c is not first[0]]
|
||
tag = f"기본 — {TRADE_FIRST} {name}"
|
||
return {"상태": LINKED, "단위": {"노임": first[0]["키"]}, "후보": rest, "꼬리표": [tag]}
|
||
named = elem.labor(str(one.get("이름") or ""))
|
||
return {"상태": PICK if named or cands else NONE, "단위": None, "후보": cands or named}
|
||
|
||
def _X(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
line = {"이름": one.get("이름"), "규격": one.get("규격") or ""}
|
||
code = self.linker._machine(line)
|
||
if code is None:
|
||
cands = elem.machines(f"{line['이름']} {line['규격']}")
|
||
return {"상태": PICK if cands else NONE, "단위": None, "후보": cands,
|
||
"글": "가까운 규격 — 고름" if cands else "같은 기계 없음"} # fmt: skip
|
||
eq = self.eqs.get(code) or {}
|
||
kinds = mtypes(f"{eq.get('상세구분')} {eq.get('이름')}")
|
||
unit = {"기계": code, "로직": mach_logics(self.whole, kinds, eq)[0]}
|
||
unit |= {"할증": one["할증"]} if one.get("할증") else {}
|
||
return {
|
||
"상태": LINKED,
|
||
"단위": unit,
|
||
"글": f"{eq.get('이름')} {eq.get('규격') or ''}".strip(),
|
||
}
|
||
|
||
def _M(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
how = WEAR if one.get("손료율") else HOW["M"]
|
||
apart = one.get("어떻게") == "별도" or one.get("별도")
|
||
line = {
|
||
"이름": one.get("이름"),
|
||
"규격": one.get("규격") or "",
|
||
"단위": one.get("단위") or "",
|
||
}
|
||
line |= {"갈래": hmat.APART} if apart else {}
|
||
pair = self._alias(line)
|
||
if pair:
|
||
unit = {"자재": pair} | ({"별도계상": True} if apart else {})
|
||
return {"상태": LINKED, "방법": how, "단위": unit, "꼬리표": ["별칭 장부 짝"]}
|
||
got = material(line, self.got)
|
||
apart = apart or hmat.APART in got["꼬리표"]
|
||
found = [r for r in got["찾음"] if r.get("자재")]
|
||
if got["상태"] == LINKED and found:
|
||
unit = {"자재": found[0]["자재"]} | ({"별도계상": True} if apart else {})
|
||
return {
|
||
"상태": LINKED,
|
||
"방법": how,
|
||
"단위": unit,
|
||
"글": got["글"],
|
||
"꼬리표": got["꼬리표"],
|
||
}
|
||
if apart: # 별도계상 = 0 원 줄(우리 자재 못 찾아도 셈 같음)
|
||
return {"상태": LINKED, "방법": APART, "단위": {"별도계상": True}, "후보": got["후보"]}
|
||
state = got["상태"] if got["상태"] != LINKED else PICK # 「전국」 자리표 = 지역 고름
|
||
return {"상태": state, "방법": how, "단위": None, "후보": got["후보"] or _cands(got["찾음"]),
|
||
"글": got["글"], "꼬리표": got["꼬리표"]} # fmt: skip
|
||
|
||
def _alias(self, line: dict[str, Any]) -> str | None:
|
||
"""별칭 장부 자재 짝(자동 정확 열쇠 · 하나) — 숫자보다 먼저."""
|
||
if self.books is None:
|
||
return None
|
||
got = ledger.recall(line, "", self.books, doc=self.doc)
|
||
if not got or got.get("제안") or got.get("규격다름") or got.get("갈래") != "짝":
|
||
return None
|
||
pairs = [p.get("자재") for p in got.get("짝") or []]
|
||
return pairs[0] if len(pairs) == 1 and pairs[0] else None
|
||
|
||
def _R(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
part = rate_part(one.get("이름"))
|
||
shape = one.get("기준꼴")
|
||
tags = ["이름 없음"] if one.get("이름없음") else []
|
||
if shape in BASES:
|
||
return {"상태": LINKED, "단위": {"기준": BASES[shape], "비목": part}, "꼬리표": tags}
|
||
if shape and one.get("기준줄"):
|
||
unit = {"기준": {"행": list(one["기준줄"])}, "비목": part}
|
||
return {"상태": LINKED, "단위": unit, "글": shape, "꼬리표": tags}
|
||
guess = {"기준": _text_base(one), "비목": part}
|
||
return {"상태": PICK, "단위": None, "후보": [guess], "글": "기준 못 찾음 — 기준 고름",
|
||
"꼬리표": tags} # fmt: skip
|
||
|
||
def _S(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
sub = one.get("하위") or {}
|
||
if not sub.get("번"):
|
||
return {"상태": NONE, "단위": None, "글": "하위 번 없음"}
|
||
return {"상태": LINKED, "단위": {"하위": {"종류": sub.get("종류"), "번": int(sub["번"])}}}
|
||
|
||
def _E(self, one: dict[str, Any]) -> dict[str, Any]:
|
||
got = one.get("금액")
|
||
if got is None:
|
||
got = unit_line.money(one.get("단가"))
|
||
if got is None:
|
||
return {"상태": NONE, "단위": None, "글": "금액 없음"}
|
||
return {"상태": LINKED, "방법": HOW["E"], "단위": {"값": float(got)}}
|
||
|
||
|
||
def hopyo_unit(line: dict[str, Any]) -> dict[str, Any]:
|
||
"""일위대가 줄(`UnitLine.check_hopyo` 투영 뒤) → 단위 줄 꼴 — 이름 · 규격 · 단위 · 품 = 수량 · 별도."""
|
||
one = {k: line.get(k) for k in ("묶음", "이름", "규격", "단위", "할증")}
|
||
one.update(품=line.get("수량"), 금액=unit_line.money(line.get("금액")), 직종=None)
|
||
if line.get("갈래") == "별도계상":
|
||
one["어떻게"] = "별도"
|
||
if one["묶음"] == "R":
|
||
base = str(line.get("기준") or "노무비")
|
||
one.update(기준=base, 기준꼴={"주재료비": "자재"}.get(base, base))
|
||
one["품"] = float(line["수량"]) / 100 if line.get("수량") is not None else None
|
||
ref = line.get("참조") or {}
|
||
if one["묶음"] == "S" and ref.get("번"):
|
||
one["하위"] = {"종류": ref.get("종류"), "번": ref.get("번")}
|
||
return one
|
||
|
||
|
||
def summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, int]]:
|
||
"""묶음별 `{묶음: {이어짐, 고를 것, 못 이음}}` — 줄 연결 표."""
|
||
out: dict[str, dict[str, int]] = {}
|
||
for kind, state in rows:
|
||
one = out.setdefault(kind, dict.fromkeys((LINKED, PICK, NONE), 0))
|
||
one[state] += 1
|
||
return out
|