Files
Aislo/M02_MasterTemplete/M02_Hopyo_Line.py
T
eomsangdonandClaude Opus 5.5 57af9be22f fix(784-1): 「우리 품」 잘못 보임 뿌리 고침 — 못 맞춤 빈칸 · 가드 칸 자동값 0 · 수 칸 짝 · 자재 단위 배 1 · 절 하나 덧 없음 · 엑셀 값 표지
- A 옵션 찾기 못 맞춤(수준 9 · 거리 ≥ 1) = 우리 품 · 입력 안 냄 · 「못맞춤」 표지 · 꼬리표 「→ –(우리 셈 조건 못 찾음)」
- B 자동값 「만약(X > 0, X, 기본)」 칸 = 나눗수 퍼뜨림에서 뺌(0 = 원문 기본) · 옵션 후보엔 옛 1 도 둠
- C 하나씩 조합 — 후보 ≤ 4 수 칸도 둘씩 짝
- D 엑셀이 값으로 넣은 「0 = 식」 칸 출처 「엑셀 값 · 식 아님」 · 꼬리표 「원문 식이면 품 · 금액(조건)」
- E 결과단위 「자재」 = 배 1 · 공/㎥ ≡ 공㎥
- F 품 비교 우리 품 × 대상 몫
- G 절 하나 블록 = 덧 로직은 후보만(이중 계상 막음)
- H 1위 옵션 수준 ≤ 2 면 그 입력을 대상 입력값으로
- I 규격 「L=…cm」 를 거리 칸에 안 넣음 · 수 고르기는 제 단위 붙은 수만 · 엑셀 수 「1 ㎡」 를 m 로 안 읽음
- 우리지문 · 맞춤 캐시 판 올림(FP 2 · CALC 3)
- 새 지킴 시험 test_635_hopyo_ours_qty
- 낡은 시험 둘(test_628 표지 수 18 · test_621 S1 울진소광 해시)은 HEAD 에서도 실패 — 782-3 마스터 합침 뒤 값 · 손대지 않음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:41:09 +09:00

488 lines
26 KiB
Python
Raw 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 338-1 · 설계 3-3 · 3-4) — 줄 상태 · 체크 제안 · 대상 목록
·
후보 · 품 비교 · 호표 통째 · 셈 · 만든 · 미리 셈(`guess`).
상태 차례 = ① % ② 호표 참조(만들 층에 확인된 사전조합이면 체크) ③ 노임(그릇 없음) ④ 장부 「우리에
없음 ·
안 씀」 → 뺌 ⑤ 장부 짝 → 별칭(근거 다름이면 체크 안 함 · 숫자 판정 맞음이 다른 로직이면
⑥ 판정 먼저 + 꼬리표 「장부와 다름」 · 짝은 후보 끝 덤 · 358-3) ⑥
맞춤셈
판정(맞음 = 체크 · 다른 점 · 후보) ⑦ 셈 전 · 숫자 없음 → 이름 · 근거 후보(「셈 중」 · 「숫자 판정
없음」).
장부 짝이 규격 다름(품명|단위 느슨 열쇠)이면 ⑤ 아님 — 꼬리표 「별칭 · 규격 다름」 · 짝은 후보
끝 덤만.
자재 · 별도계상 = 이름 · 규격 같음 하나 · 바탕 이름 · 규격 수 같은 한 품목이면 맞음(358-2).
로직 줄 후보 묶음 표시(358-1) = 이름후보(옛 324 다섯 `Match.name_cands`) · 후보마다
뜻겹침 · 출처같음 · 책 — 차례 · 무게 · 상태는 그대로.
입력값 출처(칸마다) = 별칭 > 블록(옵션 찾기 값이 규격 읽기와 다름) > 규격(`spec_inputs`) > 자동값.
체크 = 처음 제안일 뿐 — 들어감은 [확인하고 사전조합 만들기](다 창)만. 저장 없음 · 셈은 서버 단독.
장부 = `M02_Hopyo_Ledger`(다 창) · 확인된 사전조합 = `M02_Hopyo_Build.confirmed`(다 창).
④ ⑤ 장부 짝 = 자동 짝만(마스터 · 이 불러오기에서 이은 짝 · 739-1) · 회사 · 개인 층 짝 =
「내 제안」 — 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안 {갈래, 짝, 대상, 층, 때}`
(고르기 창 맨 위 · 한 번 눌러 고름).
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Hopyo_Basis as hopyo_basis
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
from M02_MasterTemplete import M02_Hopyo_Finger as finger
from M02_MasterTemplete import M02_Hopyo_Fit as fit
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_Option as option
from M02_MasterTemplete import M02_Hopyo_Ours as ours
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Hopyo_Verdict as verdict
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
SKIP = ("우리에 없음", "안 씀")
MINE = "내 제안"
LOOSE = "별칭 · 규격 다름"
LEDGER_OFF = "장부와 다름"
OUTSIDE = ("%", "노임", "뺌") # 연결 셈 밖(358-3 ②)
class Ctx:
"""보기 한 번 — 파일 · 장부 · 맞춤셈 · 확인된 사전조합을 한 번 읽어 줄마다 넘김."""
def __init__(self, folder: Path, key: str, to: Path | None = None) -> None:
self.folder, self.key = Path(folder), key
self.to = Path(to or folder)
self.doc = hopyo_store.read(folder, key)
self.got = hm.targets()
self.units = {int(h["번"]): h for h in self.doc.get("호표") or []}
self.books = ledger.books(self.to)
self.doc_id = ledger.doc_id(self.folder, key)
self.calc = fit.ensure(folder, key)
self.cache = (fit.result(folder, key) or {}).get("블록") or {}
self.done = hopyo_build.confirmed(self.to, refs.where(self.folder))
self.names = {x["로직"]: x for x in self.got["로직"]}
self.mats = {x["자재"]: x for x in self.got["자재"]}
def _target(code: str, line: dict, ctx: Ctx, found: dict | None, use: bool, **extra) -> dict:
"""대상 하나(로직) — 입력값 · 못읽음 · 출처 · 환산 · 몫 · 배."""
one = ctx.names.get(code) or {"이름": "", "단위": ""}
vals, unread = hm.spec_inputs(line, hm._logic_specs(code))
src = {k: "규격" for k in vals}
use = use or (found or {}).get("수준", 9) <= 2 # 맞춘 옵션 입력 = 품 비교와 같은 입력
typed = ((found or {}).get("식아님") or {}).get("칸") or {} # 엑셀이 값으로 넣은 「0 = 식」 칸
for k, v in ((found or {}).get("입력") or {}).items() if use else ():
if str(vals.get(k)) != str(v):
vals[k], src[k] = v, "엑셀 값 · 식 아님" if k in typed else "블록"
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
share = extra.pop("몫", None)
times = conv["배"] * share if conv["배"] is not None and share else conv["배"]
head = {"로직": code, "이름": one["이름"], "단위": one["단위"], "입력값": vals}
head.update(못읽음=[u for u in unread if u not in vals], 출처=src, 환산=conv)
return {**head, "몫": share, "배": times, **extra}
def _pair(pair: dict, line: dict, ctx: Ctx) -> dict:
"""장부 짝 → 대상(입력값 출처 = 별칭)."""
vals = dict(pair.get("입력값") or {})
if pair.get("로직"):
one = ctx.names.get(pair["로직"]) or {"이름": "", "단위": ""}
head = {"로직": pair["로직"]}
elif pair.get("자재"):
one, head = ctx.mats.get(pair["자재"]) or {"이름": "", "단위": ""}, {"자재": pair["자재"]}
else:
h = ctx.units.get(int(pair.get("호표") or 0)) or {}
one, head = (
{"이름": h.get("이름", ""), "단위": h.get("단위", "")},
{"호표": pair.get("호표")},
)
conv = {"배": pair.get("배"), "갈래": "별칭"}
if "자재" in head: # 세 칸(361-2) = 만들기에 적힐 값(`Material.marks`)
head["세칸"] = hmat.marks(line, one["이름"])
return {**head, "이름": one["이름"], "단위": one["단위"], "입력값": vals, "못읽음": []} | {
"출처": {k: "별칭" for k in vals},
"환산": conv,
"몫": pair.get("몫"),
"배": pair.get("배"),
}
def _qty(entry: dict, found: dict | None, share: float | None = None) -> dict:
"""품 비교 — 엑셀 · 우리 · 같음(직종 · 장비 갈래마다 R 허용) · 같은 갈래 여러 줄은 합쳐 견줌
(화면 품 비교 열쇠 = 갈래 가나다 차례 + 로 묶음) · 몫 = 대상 몫(섞인 줄 10% = 우리 × 0.1)."""
ex = entry.get("품") or {"인력": {}, "장비": [], "자재": []}
us = (found or {}).get("우리품") or option.ours_qty(None)
if share:
k = lambda q: round(q * share, 6) # noqa: E731
us = {"인력": {t: k(q) for t, q in us["인력"].items()}} | {
"장비": [{**x, "시간": k(x["시간"])} for x in us["장비"]],
"자재": [{**x, "수량": k(x["수량"])} for x in us["자재"]],
}
same = {}
for t, a in ex["인력"].items():
b = us["인력"].get(t)
same[t] = bool(b) and option.near(a, b, fit.TOL[0], option.ru(a))
sums: dict[str, list[float]] = {} # 열쇠 → [엑셀 합, 반올림 폭 합, 우리 합]
for m in ex["장비"]:
one = sums.setdefault("+".join(sorted(m["갈래"])), [0.0, 0.0, 0.0])
one[0] += m["시간"]
one[1] += option.ru(m["시간"])
for x in us["장비"]:
if (one := sums.get("+".join(sorted(x["갈래"])))) is not None:
one[2] += x["시간"]
for name, (a, r, b) in sums.items():
same[name] = option.near(a, b, fit.TOL[0], r)
return {"엑셀": ex, "우리": us, "같음": same}
def _picked(entry: dict) -> list[dict]:
"""체크할 조합 — 로직이 하나뿐이면 조합 아님(기계 몫은 꼬리표)."""
combo = [c for c in entry.get("조합") or [] if c["체크"]]
return combo if len(combo) > 1 else []
def _check(entry: dict, line: dict, ctx: Ctx) -> bool:
"""체크 제안 한 규칙(358-3 · 목록 수 = 확인 창) — 판정 맞음 · 대상(조합 또는 1위) 있음 ·
대상마다 환산 배 있음(추정 아님)."""
codes = [c["로직"] for c in _picked(entry)] or (entry.get("차례") or [])[:1]
if entry["판정"]["상태"] != "맞음" or not codes:
return False
for code in codes:
unit = (ctx.names.get(code) or {}).get("단위", "")
conv = hm.factor(line.get("단위"), unit, line.get("규격"))
if conv["배"] is None or conv["갈래"] == "추정":
return False
return True
def _numeric(line: dict, ctx: Ctx, entry: dict) -> dict:
"""⑥ 맞춤셈 판정 → 상태 · 대상 · 후보 · 품 · 글 · 꼬리표."""
judged = entry["판정"]
state = judged["상태"]
top = (entry.get("차례") or [None])[0]
found = (entry.get("옵션") or {}).get(top)
use = state in ("맞음", "다른 점")
combo = _picked(entry)
share = None
if combo:
targets = [
_target(
c["로직"], line, ctx, entry["옵션"].get(c["로직"]), use, 몫=c["몫"], 어디=c["어디"]
)
for c in combo
]
else:
part = next((c for c in entry.get("조합") or [] if c["로직"] == top), {})
share = part.get("몫") if part.get("몫") not in (None, 1, 1.0) else None # 섞인 줄 인력 몫
extra = {"몫": share, "어디": part["어디"]} if share else {}
targets = [_target(top, line, ctx, found, use, **extra)] if top else []
tags = list(judged.get("꼬리표") or [])
tags += [f"조합 — {' + '.join(c['로직'] for c in combo)}"] if combo else []
for c in entry.get("조합") or []:
if not c["체크"]:
tags.append(f"기계 몫 {c['로직']} — 시간 = 엑셀 값(우리 셈 아님)")
if any({"블록", "엑셀 값 · 식 아님"} & set(t["출처"].values()) for t in targets):
tags.append("옵션 — 블록에서")
tags += [f"근거 {b} {n}" for b, n in (entry.get("지문") or {}).get("출처") or []]
out = {"상태": state, "체크": _check(entry, line, ctx), "대상": targets}
out.update(
후보=entry.get("후보") or [], 글=judged["글"], 꼬리표=tags, 품=_qty(entry, found, share)
)
if found and found.get("갈리는칸"):
out["갈리는칸"] = found["갈리는칸"]
return out
def _names_only(line: dict, ctx: Ctx, why: str, full: bool) -> dict:
"""⑦ 숫자 판정 없음 · 셈 전 — 이름 · 근거 후보(차례만 · 대상 = 1위)."""
if not full:
return {"상태": "후보", "체크": False, "대상": [], "후보": [], "글": why, "꼬리표": [why]}
order = hm.b_order(line, ctx.got)
top = order["top"]
cands = [{**c, "판정": "후보", "글": why} for c in order["후보"]]
targets = [_target(top[0], line, ctx, None, False)] if top else []
state = "후보" if top else "없음"
return {
"상태": state,
"체크": False,
"대상": targets,
"후보": cands,
"글": why,
"꼬리표": [why],
}
def _material(line: dict, ctx: Ctx, full: bool) -> dict:
"""자재 · 별도계상 줄(358-2 · `M02_Hopyo_Material`) — 「별도계상」 글 = 꼬리표 · 맞음 =
이름 · 규격 같음 하나 · 또는 바탕 이름 · 규격 수 같은 한 품목(줄 글 지역 · 지역 없는 것 ·
「전국」 자리표 · 361-1) · 후보 = 그 품목 변형 모두(`변형` · 고르기 창이 접음)."""
me, _ = hmat.clean(line)
hit = units.material(line, ctx.got, full) # 줄 잇기 한 벌(778-5 · 단가산출 블록 자재와 같음)
found, text, tags = hit["찾음"], hit["글"], hit["꼬리표"]
if hit["상태"] == units.PICK and hit["후보"] and not found: # 글자 규격 = 후보로만(요구 624-2)
cands = hit["후보"] if hit["글"] == units.LETTERS else []
cands = cands or (hmat.candidates(me) if full else [])
return {"상태": "후보", "체크": False, "대상": [], "후보": cands if full else [], "글": text,
"꼬리표": tags} # fmt: skip
if found:
target = _mat_target(found[0], line)
conv = target["환산"]
ok = conv["배"] is not None and conv["갈래"] != "추정"
many = len(found) > 1
cands = [
{**hm._public(x), "점수": None, "판정": "맞음" if not at else "후보", "글": text}
| ({"변형": True} if many and "자재" in x else {})
for at, x in enumerate(found)
]
out = {"상태": "맞음", "체크": ok, "대상": [target], "후보": cands if full else []}
return {**out, "글": text, "꼬리표": tags}
cands = hmat.candidates(me) if full else []
state = "후보" if cands or not full else "없음"
return {"상태": state, "체크": False, "대상": [], "후보": cands, "꼬리표": tags}
def _mat_target(one: dict, line: dict, times: Any = None) -> dict:
"""자재 · 「전국」 자리표 → 대상(배 = 환산 · `times` 주면 그것 · 세칸 = 만들기에 적힐 세 칸)."""
conv = hm.factor(line.get("단위"), one["단위"], line.get("규격"))
target = {**hm._public(one), "입력값": {}, "못읽음": [], "출처": {}, "환산": conv}
target["세칸"] = hmat.marks(line, one["이름"])
ok = isinstance(times, (int, float)) and not isinstance(times, bool) and times > 0
return {**target, "몫": None, "배": times if ok else conv["배"]}
def match_line(line: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
"""줄 → 맞춤(설계 3-3) — `full` 아니면 상태 · 체크만(호표 목록 수)."""
out: dict[str, Any] = {"열쇠": hm.line_key(line), "체크": False, "대상": [], "후보": []}
kind = finger.kind_of(line)
ref = line.get("참조") or {}
if kind == "%":
return {**out, "상태": "%", "글": "% 줄 — 우리 로직 덧줄이 셈"}
if kind == "호표" or ref.get("종류") == "일위":
no = int(ref.get("번") or 0)
h = ctx.units.get(no) or {}
conv = hm.factor(line.get("단위"), h.get("단위"), line.get("규격"))
made = ctx.done.get(hopyo_build.mark(ctx.key, no))
target = {"호표": no, "이름": h.get("이름", ""), "단위": h.get("단위"), "입력값": {}}
target.update(못읽음=[], 출처={}, 환산=conv, 몫=None, 배=conv["배"])
ok = bool(made) and conv["배"] is not None and conv["갈래"] != "추정"
text = f"하위 일위대가 {made} 사본" if made else f"제{no}호표 — 저장 때 하위로 함께 만듦"
return {**out, "상태": "호표", "체크": ok, "대상": [target], "글": text}
if kind == "노임":
return {**out, "상태": "노임", "글": "노임 · 그릇 없음"}
alias = ledger.recall(line, ctx.to, ctx.books, doc=ctx.doc_id)
mine, alias = (alias, None) if alias and alias.get("제안") else (None, alias)
loose, alias = (alias, None) if alias and alias.get("규격다름") else (None, alias)
entry = ctx.cache.get(fit.unit_key(line)) if kind not in hm.MATERIAL_KINDS else None
if alias and alias.get("갈래") in SKIP:
return {**out, "상태": "뺌", "뺌": alias["갈래"], "별칭": _alias_info(alias, False)}
if alias and alias.get("갈래") == "짝" and alias.get("짝") and _ledger_off(entry, alias):
loose, alias = alias, None # 숫자 판정 맞음 = 장부 짝 아닌 로직 → 판정 먼저(358-3 ①)
loose = {**loose, "_까닭": LEDGER_OFF}
if alias and alias.get("갈래") == "짝" and alias.get("짝"):
got = _aliased(line, ctx, alias, entry)
elif kind in hm.MATERIAL_KINDS:
got = _material(line, ctx, full)
elif entry and entry["판정"]["글"] not in (fit.NO_NUMBER, "우리 로직 준비 중"):
got = _numeric(line, ctx, entry) if full else _brief(entry, line, ctx)
else:
why = "셈 중" if not ctx.calc["끝"] else fit.NO_NUMBER
got = _names_only(line, ctx, why, full)
got = _loose(got, loose, line, ctx) if loose and full else got
got = _mine(got, mine, line, ctx) if mine else got
return {**out, **(_grouped(got, line, ctx) if full and kind not in hm.MATERIAL_KINDS else got)}
def _mine(got: dict, mine: dict, line: dict, ctx: Ctx) -> dict:
"""내 제안(739-1 · 승인 전 회사 · 개인 층 짝) — 자동 아님 · 꼬리표 + `제안`(고르기 맨 위)."""
pairs = [p for p in mine.get("짝") or [] if p.get("로직") or p.get("자재")]
kind = mine.get("갈래")
word = MINE if kind == "짝" else f"{MINE} — {kind}"
head = {"갈래": kind, "짝": pairs, "층": mine.get("층"), "때": mine.get("때")}
targets = [_pair(p, line, ctx) for p in pairs]
return {**got, "꼬리표": [*(got.get("꼬리표") or []), word], "제안": {**head, "대상": targets}}
def _grouped(got: dict, line: dict, ctx: Ctx) -> dict:
"""묶음 표시(358-1) — 후보 · 이름후보(옛 324 다섯)마다 뜻겹침 · 출처같음(조항 ≥ 0.9) · 책 ·
차례 · 무게 · 상태 그대로(아무것도 숨기지 않음 · 묶음은 화면 몫)."""
mine = hm.words(line.get("이름"))
theirs = hm.fix_typo(((line.get("근거") or {}).get("출처")) or [])
table = hopyo_basis.ours()
def mark(c: dict) -> dict:
one, our = ctx.names.get(c.get("로직")), table.get(c.get("로직"))
if one is None:
return c
same = bool(theirs and our) and hm.clause(theirs, our)[0] >= 0.9
book = (our or {}).get("책", "")
return {**c, "뜻겹침": hm.near(mine, one["words"]), "출처같음": same, "책": book}
named = [mark(c) for c in hm.name_cands(line, ctx.got)]
cands = [mark(c) for c in got.get("후보") or []]
# 745-1 미리 고름 점 — 「다른 점」 1위 + 출처 같음 · 누르기는 사용자
top = cands[0] if cands else {}
if top.get("판정") == "다른 점" and top.get("출처같음"):
top["미리"] = True
return {**got, "후보": cands, "이름후보": named}
def _ledger_off(entry: dict | None, alias: dict) -> bool:
"""장부 짝(정확 열쇠)이 숫자 판정 맞음 1위를 안 담음."""
codes = {p.get("로직") for p in alias.get("짝") or []}
return bool(entry and entry["판정"]["상태"] == "맞음" and entry["차례"][0] not in codes)
def _loose(got: dict, alias: dict, line: dict, ctx: Ctx) -> dict:
"""규격 다른 별칭 · 숫자 판정과 다른 장부 짝 — 꼬리표(「별칭 · 규격 다름」 · 「장부와 다름」) +
짝 로직 · 자재가 후보에 없으면 끝에 덤(상태 · 대상 · 옵션은 이 줄 숫자 그대로)."""
word = alias.get("_까닭") or LOOSE
tags = [*(got.get("꼬리표") or []), word]
have = {c.get("로직") or c.get("자재") for c in got.get("후보") or []}
have |= {t.get("로직") or t.get("자재") for t in got.get("대상") or []}
extra = []
for p in alias.get("짝") or []:
code = p.get("로직") or p.get("자재")
if code and code not in have:
one = {
k: v
for k, v in _pair(p, line, ctx).items()
if k in ("로직", "자재", "이름", "단위")
}
extra.append({**one, "점수": None, "판정": "후보", "글": word})
return {**got, "꼬리표": tags, "후보": [*(got.get("후보") or []), *extra]}
def _brief(entry: dict, line: dict, ctx: Ctx) -> dict:
"""목록 수(`full` 아님) — 체크는 확인 창과 같은 `_check`."""
return {
"상태": entry["판정"]["상태"],
"체크": _check(entry, line, ctx),
"글": entry["판정"]["글"],
}
def _alias_info(alias: dict, basis_off: bool) -> dict:
keep = ("층", "주인", "때", "누가", "처음파일", "규격다름")
return {**{k: alias.get(k) for k in keep}, "근거다름": basis_off}
def _aliased(line: dict, ctx: Ctx, alias: dict, entry: dict | None) -> dict:
"""⑤ 장부 짝(정확 열쇠 · 숫자 판정 맞음 1위를 담음 또는 판정 맞음 아님) — 체크 = 근거 같음."""
targets = [_pair(p, line, ctx) for p in alias["짝"]]
codes = {t["로직"] for t in targets if t.get("로직")}
theirs = ((line.get("근거") or {}).get("출처")) or []
table = hopyo_basis.ours()
off = bool(theirs and codes) and all(
hm.clause(theirs, table[c])[0] < 0.9 for c in codes if c in table
)
tags = ["별칭 — " + " · ".join(str(x) for x in (alias.get("층"), alias.get("때")) if x)]
tags += ["근거 다름"] * off
out = {"상태": "별칭", "체크": not off, "대상": targets, "글": "별칭 장부 짝", "꼬리표": tags}
out["별칭"] = _alias_info(alias, off)
if entry and entry.get("품"):
out["품"] = _qty(entry, (entry.get("옵션") or {}).get(next(iter(codes), None)))
return out
def whole(hopyo: dict, ctx: Ctx) -> dict | None:
"""직접 줄 호표 통째 제안 — 장부 짝(호표 열쇠) 먼저 · 맞춤셈 「호표:번」."""
head = {k: hopyo.get(k) for k in ("이름", "규격", "단위")}
alias = ledger.recall(head, ctx.to, ctx.books, whole=True, doc=ctx.doc_id)
if alias and alias.get("갈래") == "짝" and alias.get("짝") and not alias.get("규격다름"):
return { # 내 제안(739-1) = 체크 안 함 · 한 번 눌러 고름
"대상": [_pair(p, head, ctx) for p in alias["짝"]],
"글": MINE if alias.get("제안") else "별칭 장부 짝",
"체크": not alias.get("제안"),
}
entry = ctx.cache.get(f"호표:{hopyo['번']}")
if not entry or not entry.get("차례"):
return None
top = entry["차례"][0]
state = entry["판정"]["상태"]
target = _target(top, head, ctx, entry["옵션"].get(top), state in ("맞음", "다른 점"))
return {"대상": [target], "글": f"통째 = {entry['판정']['글']}", "체크": state == "맞음"}
def hopyo_view(hopyo: dict[str, Any], ctx: Ctx, full: bool = True) -> dict[str, Any]:
"""호표 하나 — 줄마다 맞춤 · `맞춤` = [체크 제안 수, 줄 수] · 연결할(% · 노임 · 뺌 밖 줄 수) ·
연결된(그중 체크 제안 수 · 358-3) · 통째 · 셈 · 만든."""
lines = [{**one, "맞춤": match_line(one, ctx, full)} for one in hopyo.get("줄") or []]
check = sum(1 for one in lines if one["맞춤"]["체크"])
need = [one for one in lines if one["맞춤"]["상태"] not in OUTSIDE]
out = {**hopyo, "줄": lines, "맞춤": [check, len(lines)], "연결할": len(need)}
out["연결된"] = sum(1 for one in need if one["맞춤"]["체크"])
if full:
made = ctx.done.get(hopyo_build.mark(ctx.key, int(hopyo["번"])))
out.update(통째=whole(hopyo, ctx), 셈=ctx.calc)
out["만든"] = {"키": made, "확인": True, **_made(ctx.to, made)} if made else None
return out
def _made(folder: Path, key: str) -> dict[str, Any]:
"""만든 사전조합의 줄 갈래(705-3) — `품셈` = 품셈으로 넣은 엑셀 줄 차례(옛 것 = None) · `고정` 줄 수."""
try:
doc = json.loads((Path(folder) / "precombo" / f"{key}.json").read_bytes())
except (OSError, ValueError):
return {"품셈": None, "고정": 0}
fixed = sum(1 for x in doc.get("담은로직") or [] if isinstance(x, dict) and "고정" in x)
return {"품셈": (doc.get("본뜬") or {}).get("품셈"), "고정": fixed}
def guess(
folder: Path, key: str, no: int, at: int, target: Any, to: Path | None = None
) -> dict[str, Any]:
"""미리 셈(저장 안 함) — 고른 대상 · 옵션으로 엔진 한 번 → {대상, 품: {엑셀, 우리, 같음},
글}."""
ctx = Ctx(folder, key, to)
hopyo = ctx.units.get(int(no))
lines = (hopyo or {}).get("줄") or []
if not 0 <= int(at) < len(lines):
raise store.StoreError(404, f"없는 줄 「제{no}호표 {at}」")
line = lines[int(at)]
picked = target if isinstance(target, list) else [target]
if not picked or not all(isinstance(t, dict) for t in picked):
raise store.StoreError(400, "대상 꼴이 틀림")
entry = ctx.cache.get(fit.unit_key(line))
if entry is None:
block = next(
(
b
for b in ctx.doc.get("단산") or []
if b.get("번") == (line.get("참조") or {}).get("번")
),
None,
)
entry = {"품": verdict.excel_qty(finger.excel(block, line))}
targets, total = [], {"L": {}, "X": [], "M": {}, "S": {}}
for t in picked:
if t.get("로직"):
if t["로직"] not in ctx.names:
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
given = t.get("입력값")
one = _target(t["로직"], line, ctx, None, False, 몫=t.get("몫"))
if isinstance(t.get("배"), (int, float)) and t["배"] > 0:
one["배"] = t["배"] # 사용자가 정한 배는 그대로
if isinstance(given, dict):
one["입력값"] = {**one["입력값"], **given}
one["출처"].update({k: "고름" for k in given})
v = ours.run(t["로직"], one["입력값"]) or {"L": {}, "X": [], "M": {}, "S": {}}
k = float(t.get("몫") or 1)
for name, q in v["L"].items():
total["L"][name] = total["L"].get(name, 0.0) + q * k
total["X"] += [[a, b, q * k] for a, b, q in v["X"]]
for name, q in v["M"].items():
total["M"][name] = total["M"].get(name, 0.0) + q * k
targets.append(one)
elif t.get("자재") in ctx.mats:
targets.append(_pair({"자재": t["자재"], "배": t.get("배")}, line, ctx))
elif t.get("전국") and (one := hmat.national(line)):
targets.append(_mat_target(one, line, t.get("배")))
else:
raise store.StoreError(400, "돈 로직 · 우리 자재가 아님")
qty = _qty(entry, {"우리품": option.ours_qty(total)})
return {"대상": targets, "품": qty, "글": "우리 품 — 엔진 한 번(저장 안 함)"}