Files
eomsangdonandClaude Opus 5.5 48848bb287 refactor(785-7): 옛 맞춤 층 Python 지움 · 서버 새 길로 정리 · 낡은 시험 정리
- 지움(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>
2026-11-05 01:15:20 +09:00

294 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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