Files
Aislo/M02_MasterTemplete/M02_Hopyo_Build.py
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

437 lines
24 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 324-3 · 338-3 · 설계 3-6 · 3-7).
[확인하고 사전조합 만들기] 몸 = 사용자가 체크한 줄만 —
`줄 [{차례, 대상 [{로직|자재|호표, 입력값, 배, 몫}] | 뺌}]` · 직접 줄 호표 = `통째 {대상}`.
몸이 비면 = 모두 고정(705-3 · [모두 고정으로 들여오기]).
서버 재검사(브라우저 값을 받아 적지 않음 · 금액 · 품 칸 없음) — 차례 = 그 호표 줄 · 대상 하나 =
돈 로직 · M01 자재품목 · 확인된 호표 중 하나 · 입력 이름 = 그 로직 입력 · 배 0 넘음 · 몫 (0, 1] ·
호표 = 만들 층에 확인된 사전조합(확인 안 된 하위 = 409 · 하위 먼저 = 옮기기 `M02_Hopyo_Make` · 777-1).
호표 하나 = 사전조합 하나 — 이름 = 품명 + 규격 · 단위 = 엑셀 단위 · 구분 「실무자료」 ·
상세구분 = 파일 · 본뜬 = {층 「실무자료」, 키 「HP-###### 제N호표」, 파일, 원본 [층, 주인],
확인 {누가, 때}} · 만드는 층 = 부르는 층(일위대가 문서 층).
로직 = {로직, 별칭 = 엑셀 품명 40 자, 메모 「실무 0.14㎥ × 1 · 규격」, 입력값,
수량 {손 = 엑셀 수량 × 배}} · 한 줄 로직 여럿 = 같은 별칭 · 같은 로직 둘째부터 = 한 줄
사전조합으로 감싸 참조(같은 로직 두 번 금지) ·
자재 = {코드, 수량 {손}, 세 칸}(같은 자재 = 수량 더함 · 세 칸 = `Material.marks`
별도계상 · 자재구분 · 운반비 · 358-2) · 로직 자재와 같은 품목 = 사용자 자재에서 빼고
세 칸만 그 로직 `자재표시` 로(빠진 줄에 까닭) · 호표 = 확인된 사전조합 참조 ·
「전국」 자재(361-1 · 키 · 단가 없음 · 서버가 줄에서 다시 셈) = 일위대가 자재 칸이 M01 키만
받아 `본뜬.전국자재 [{이름, 규격, 단위, 수량, 세 칸}]` 에 적고 빠진 줄에 까닭(금액 없음 ·
지역은 프로젝트에서) · 장부엔 안 남김.
같은 본뜬 사전조합이 있으면 확인한 내용으로 덮음(`다시씀`) · 없으면 같은 내용 SC 재사용(`재사용` · `M02_Hopyo_Kids.save`) ·
`하위` = 부르는 하위 호표 `[{번, 키, 새로}]`(부모 줄 = 하위 사본) · 확인한 줄은 별칭 장부에 남음
(`M02_Hopyo_Ledger` · 호표 참조 줄 뺌).
빠짐 0(705-3) — 확인 안 한 줄 · 노임 · % · 「우리에 없음」 줄 = 고정 줄(`M02_Hopyo_Fixed` · 엑셀 숫자
그대로) · 확인 안 한 호표 줄 = 확인된 하위 사전조합 참조(단위 같음 · 아니면 고정) · 빠진 줄 = 「안 씀」 ·
통째 대신 · 로직 자재 겹침 · 전국만. `본뜬.품셈` = 품셈으로 넣은 엑셀 줄 차례([품셈으로 바꾸기] 첫 체크) ·
`줄수 {품셈, 고정, 호표, 빠짐, 이음, 이음후보}` · 만든 값 ↔ 엑셀 호표 단가(`비교` · 같음 = 비목마다 1 원 안).
엔진 · 일위대가 계산은 그대로(`M02_Labor_Ref.Expand`).
이음(729-2 · 요구서 내역19 1장 8) = 확인 안 한 단가산출 줄 = 그 블록 SK 참조(이 층 · 같은 원본 · 없으면
`M02_Hopyo_Sangeun` 으로 그 블록만 만듦 · 고름 대기면 못 이음) · 확인한 로직 줄이 차이 SK 의 바탕이면 그 SK
참조(차이 보존) · 노임 = 이름 · 기계 = EQ(`M02_Hopyo_Fixed.Linker`) · 사용자 이음 대상 `{노임|기계: 키}` ·
`{비율: 기준}` = 고정 줄 + 그 이음 · 남은 줄(`Fixed.linked` 아님 · 「우리에 없음」 고른 줄 뺌) 있으면 409
`{까닭, 남은줄}`(저장 안 함 · `save` 거짓 = 막지 않고 `남은줄` 만).
수량(742-1 · 요구 584-1) = 엑셀 수량을 줄 · 자재 저장 수량(`{손}`)으로 · 같은 값 `본뜬.실무수량 {줄: {차례: 수},
자재: {코드: 수}}`(출처 기록 · 엑셀 대조 `M02_Hopyo_Compare` — 하위 호표 것까지 `excel_given`).
785-5 — 확인한 줄 몸 = `M02_Hopyo_Make.Lines`(옛 대상 꼴 `put` · 새 고른 것 꼴 `picks` = 몸 `{차례: {고른}}`) ·
여기는 모두 고정 길 · 줄 차례 · 쓰기(`Builder` · `_order` · `_origins` · `build_all` = B09 빌림).
"""
from __future__ import annotations
from decimal import Decimal
from pathlib import Path
from typing import Any
from common_util.common_util_time import utc_now_text
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Hopyo_Fixed as hfix
from M02_MasterTemplete import M02_Hopyo_Kids as hkids
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Material as hmat
from M02_MasterTemplete import M02_Hopyo_Sangeun as sangeun
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as lsame
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_Hopyo_Compare import ( # noqa: F401 — 부르는 쪽 이름 그대로
compare,
excel_given,
)
ORIGIN = "실무자료"
LINES_MAX = 500
UNCHECKED = "확인 안 함"
PERCENT = "% 줄 — 우리 로직 덧줄이 셈"
WHOLE = "호표 통째로 대신"
TWICE = "로직 자재와 같은 품목 — 세 칸만 로직 자재표시로(수량 = 로직 셈)"
NATIONAL = "전국 — 지역은 프로젝트에서(자재 키 · 단가 없음 · 본뜬.전국자재)"
def mark(key: str, no: int, at: int | None = None) -> str:
"""본뜬 키 — 호표 「HP-000001 제3호표」 · 감싼 줄 「… 2째 줄」."""
head = f"{key} 제{no}호표"
return head if at is None else f"{head} {at + 1}째 줄"
_origins = hkids.entries # B09 가 빌려 씀(또 칸까지)
_same_source = hkids.same_source
def confirmed(folder: str | Path, source: refs.Here | None = None) -> dict[str, str]:
"""층에 확인된 실무 사전조합 `{본뜬 키: SC 키}`(`source` = 파일 층 (층, 주인)으로 거름)."""
return {
got["키"]: key
for key, got, _ in _origins(Path(folder))
if got.get("확인") and _same_source(got, source)
}
def _num(value: Decimal) -> int | float:
value = value.normalize()
return int(value) if value == value.to_integral_value() else float(value)
def _memo(line: dict[str, Any], one: dict[str, Any]) -> str:
qty = line.get("수량")
spec = str(line.get("규격") or "").strip()
head = f"실무 {qty if qty is not None else ''}{line.get('단위') or ''} × {one['배']}"
head += f"(몫 {one['몫']})" if one.get("몫") is not None else ""
return f"{head} · {spec}" if spec else head
def _missing(line: dict[str, Any], why: str) -> dict[str, Any]:
keep = ("차례", "이름", "규격", "수량", "단위")
return {
**{k: line.get(k) for k in keep},
"금액": (line.get("금액") or {}).get("계"),
"까닭": why,
}
def _is_sk(one: dict[str, Any]) -> bool:
return str((one.get("참조") or {}).get("키") or "").startswith("SK-")
def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
class Builder:
"""한 번 만들기 — 파일 · 우리 쪽 목록 · 확인된 호표를 한 번 읽고 몸을 검사한 뒤 씀."""
def __init__(self, folder: Path, source: Path, key: str, no: int, who: Any) -> None:
self.folder = Path(folder)
self.doc = hopyo_store.read(source, key)
self.key, self.no, self.who = key, int(no), who
self.source = refs.where(source)
self.done = confirmed(folder, self.source)
self.known = ledger._known()
self.mat_names = {x["자재"]: x["이름"] for x in hopyo_match.targets()["자재"]}
self.units = {int(h["번"]) for h in self.doc.get("호표") or []}
self.hopyo = next((h for h in self.doc.get("호표") or [] if int(h["번"]) == self.no), None)
if self.hopyo is None:
raise _bad(f"없는 호표 「제{no}호표」", 404)
self.src, self.dry, self._sk = Path(source), False, None
self.same, self.reused = lsame.Same(), False # 같은 내용 SC 찾기(한 요청 메모)
self.strict = True # 남은 줄 있으면 409(붙여넣기 `build_all` = 거짓)
self.doc_id = ledger.doc_id(source, key) # 739-1 이 불러오기에서 이은 짝
# ── 재검사 ───────────────────────────────────────────────────────
def _target(self, raw: dict[str, Any], where: str, line: dict | None = None) -> dict[str, Any]:
if raw.get("노임") or raw.get("기계") or raw.get("비율"):
return self._link(raw, where, line)
if raw.get("전국"):
return self._national(raw, where, line)
if raw.get("호표") is None:
return ledger.check_pair(
{k: v for k, v in raw.items() if k != "호표"}, where, self.known
)
if raw.get("로직") or raw.get("자재") or raw.get("입력값"):
raise _bad(f"호표 대상은 호표 · 배만 「{where}」")
child = int(raw["호표"])
if child == self.no:
raise _bad(f"호표가 자기를 담음 「{where}」")
if mark(self.key, child) not in self.done and not self.dry:
raise _bad(f"제{child}호표 먼저 확인", 409)
times, part = ledger.times_of(raw, where)
return {"호표": child, "배": times, "몫": part}
def _link(self, raw: dict[str, Any], where: str, line: dict | None) -> dict[str, Any]:
"""사용자 이음 대상 — `{노임: LB}` · `{기계: EQ}` · `{비율: 기준 비목}`(% 줄 · 수량 = 율)."""
keys = [k for k in ("노임", "기계", "비율") if raw.get(k)]
extra = set(raw) - {"노임", "기계", "비율", "배", "몫", "입력값"}
if len(keys) != 1 or extra or raw.get("입력값"):
raise _bad(f"이음 대상은 노임 · 기계 · 비율 중 하나만 「{where}」")
kind, code = keys[0], str(raw[keys[0]])
if kind == "비율":
if code not in labor.COSTS or line is None or hfix.kind_of(line) != "%":
raise _bad(f"비율은 % 줄 · 노무비 · 재료비 · 경비 「{where}」")
if line.get("수량") is None:
raise _bad(f"% 줄 수량(율) 없음 「{where}」")
return {"이음": {"비율": code}}
if code[:2] != hfix.fixed.LINKS[kind] or not hfix.fixed._has(m01.loaded()[1], code):
raise _bad(f"{kind} 이음 「{code}」 M01 에 없음 「{where}」")
return {"이음": {kind: code}}
def _national(self, raw: dict[str, Any], where: str, line: dict | None) -> dict[str, Any]:
"""「전국」 대상 — 줄에서 서버가 다시 셈(이름 · 규격 · 단위 = 서버 것)."""
if set(raw) - {"전국", "배", "몫", "입력값"} or raw.get("입력값"):
raise _bad(f"전국 대상은 전국 · 배 · 몫만 「{where}」")
one = hmat.national(line) if line is not None else None
if one is None:
raise _bad(f"전국 자재 없음 — 지역 변형만 있는 자재 줄만 「{where}」")
times, part = ledger.times_of(raw, where)
return {**{k: one[k] for k in ("전국", "이름", "규격", "단위")}, "배": times, "몫": part}
def check(self, lines: list[dict[str, Any]], whole: dict[str, Any] | None) -> tuple[dict, list]:
"""몸 재검사 → ({차례: 확인 줄}, 통째 대상) — 틀리면 400 · 하위 호표 409."""
if whole is not None and lines:
raise _bad("통째 · 줄 중 하나만")
if whole is not None and not whole.get("대상"):
raise _bad("통째 대상이 없음")
if len(lines) > LINES_MAX:
raise _bad(f"줄은 {LINES_MAX} 개 안")
have = self.hopyo.get("줄") or []
out: dict[int, dict[str, Any]] = {}
for raw in lines:
at = int(raw.get("차례", -1))
if not 0 <= at < len(have) or at in out:
raise _bad(f"없는 줄 · 겹친 줄 「제{self.no}호표 {at}」")
where = f"제{self.no}호표 {at + 1}째 줄"
targets = raw.get("대상") or []
if bool(raw.get("뺌")) == bool(targets):
raise _bad(f"대상 · 뺌 중 하나만 「{where}」")
if raw.get("뺌") and raw["뺌"] not in ledger.KINDS[1:]:
raise _bad(f"뺌 까닭이 틀림 「{where}」")
if len(targets) > 20:
raise _bad(f"대상은 스물 이하 「{where}」")
out[at] = {"뺌": raw.get("뺌")} if raw.get("뺌") else {
"대상": [self._target(t, where, have[at]) for t in targets]
} # fmt: skip
pairs = [self._target(t, f"제{self.no}호표 통째") for t in (whole or {}).get("대상") or []]
if any("호표" in p for p in pairs) or len(pairs) > 20:
raise _bad("통째 대상은 로직 · 자재만")
return out, pairs
# ── 쓰기 ─────────────────────────────────────────────────────────
def _origin(self, origin: str, stamp: str) -> dict[str, Any]:
sure = {"누가": self.who, "때": stamp}
file = self.doc.get("파일")
return {
"층": ORIGIN,
"주인": None,
"키": origin,
"파일": file,
"원본": list(self.source),
"확인": sure,
}
def build(
self, checked: dict[int, dict[str, Any]], whole: list[dict[str, Any]], save: bool = True
) -> dict:
"""`save` 거짓 = 사전조합 안 씀 · `{문서, 빠진줄, 줄수}`(704-4 [구조물 조합으로])."""
from M02_MasterTemplete import M02_Hopyo_Make as hmake # 확인한 줄 몸(785-5)
stamp = utc_now_text()
hopyo, file = self.hopyo, self.doc.get("파일") or ""
rows = hopyo.get("줄") or []
where = {"파일": file, "호표": self.no}
body = hmake.Lines(self, rows, where, stamp, hfix.Linker(self.strict and save))
lines, mats, missing, left = body.lines, body.mats, body.missing, body.left
taken, linker, kids, put = body.taken, body.linker, body.kids, body.put
nation, mat_lines = body.nation, body.mat_lines
poomsem, kid_n = [], 0
book: dict[str, dict[str, Any]] = {}
if whole:
for one in whole:
put({"수량": 1, "단위": hopyo.get("단위"), "이름": hopyo.get("이름")}, one, None)
book[ledger.hopyo_key(hopyo)] = self._row(hopyo, "짝", whole)
for at, line in enumerate(rows):
got = checked.get(at)
if got and "고른" in got: # 새 길 고른 것(785-5 · Make 가 검사 · 다시 셈)
poomsem.append(at)
body.picks(at, line, got["고른"])
pairs = [hmake.ledger_pick(p) for p in got["고른"]]
book[ledger.key(line)] = self._row(line, "짝", pairs)
continue
if whole:
missing.append(_missing(line, WHOLE))
continue
if got and got.get("뺌"):
book[ledger.key(line)] = self._row(line, got["뺌"], [])
if got and got.get("뺌") == "안 씀":
missing.append(_missing(line, f"뺌 — {got['뺌']}"))
continue
if not got or got.get("뺌"): # 빠짐 0(705-3) — 하위 호표 · SK 참조 · 아니면 고정 줄
kid = self._kid(line, kids)
kid_n += bool(kid)
sk, why = (None, None) if kid or got else sangeun.sk_line(self, line, save)
others = [x for x in rows if x is not line]
one = kid or sk or hfix.line_of(line, others, where, taken, linker)
lines.append(one)
if not hfix.linked(one, bool(got)):
left.append(sangeun.left(line, why))
continue
poomsem.append(at)
alias = str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None
for one in got["대상"]:
put(line, one, alias)
pairs = [p for p in got["대상"] if "호표" not in p and "전국" not in p]
if pairs:
book[ledger.key(line)] = self._row(line, "짝", pairs)
for code in self._inside(lines, mats): # 로직 자재와 같은 품목 = 세 칸만 로직 자재표시로
missing.append(_missing(mat_lines[code], TWICE))
name = " ".join(x for x in (hopyo["이름"], hopyo.get("규격")) if x)
doc = {
"이름": name,
"단위": hopyo.get("단위") or None,
"구분": ORIGIN,
"상세구분": file,
"담은로직": lines,
"자재": list(mats.values()),
"본뜬": self._origin(mark(self.key, self.no), stamp),
}
if nation:
doc["본뜬"]["전국자재"] = list(nation.values())
doc["본뜬"]["품셈"] = poomsem
if self.dry:
return {"남은줄": left}
if left and save and self.strict:
raise store.StoreError(409, {"까닭": sangeun.LEFT, "남은줄": left})
fixed_n = sum(1 for x in lines if "고정" in x)
links = sum(1 for x in lines if any(((x.get("고정") or {}).get("이음") or {}).values()))
count = {"품셈": len(poomsem), "고정": fixed_n, "호표": kid_n}
count |= {"빠짐": sum(1 for x in missing if x["까닭"] != TWICE), "이음": links}
count["단가산출"] = sum(1 for x in lines if _is_sk(x))
sub = [{"번": n, "키": self.done.get(mark(self.key, n))} for n in sorted(kids)]
if not save:
ledger.write(self.folder, book, self.who, file or None, self.doc_id)
head = {"문서": doc, "빠진줄": missing, "남은줄": left, "하위": sub}
return {**head, "줄수": {**count, "이음후보": linker.near}}
ref, again, self.reused = hkids.save(doc, self.same)
wrote = ledger.write(self.folder, book, self.who, file or None, self.doc_id)
out = {"참조": ref, "다시씀": again, "재사용": self.reused, "빠진줄": missing, "하위": sub}
out["장부"] = {"씀": wrote}
return {**out, "줄수": {**count, "이음후보": linker.near}}
def _kid(self, line: dict[str, Any], kids: set[int]) -> dict[str, Any] | None:
"""확인 안 한 호표 줄 → 확인된 하위 사전조합 참조(단위 같음 · 같은 하위 한 번) · 아니면 None."""
ref = line.get("참조") or {}
if hfix.kind_of(line) != "호표" and ref.get("종류") != "일위":
return None
no = int(ref.get("번") or 0)
made = self.done.get(mark(self.key, no)) or (
"SC-미리" if self.dry and no in self.units else None
)
unit = next((h.get("단위") for h in self.doc["호표"] if int(h["번"]) == no), None)
times = hopyo_match.factor(line.get("단위"), unit, line.get("규격"))["배"]
if not made or no in kids or no == self.no or times != 1:
return None
kids.add(no)
qty = Decimal(str(line.get("수량") or 0))
head = {"층": refs.here()[0], "주인": refs.here()[1], "키": made}
return {"참조": head, "별칭": str(line.get("이름") or "")[: labor.ALIAS_MAX].strip() or None,
"메모": f"실무 {qty}{line.get('단위') or ''} · 제{no}호표", "수량": {"손": _num(qty)}} # fmt: skip
def _inside(self, lines: list[dict[str, Any]], mats: dict[str, dict[str, Any]]) -> list[str]:
"""로직 자재와 같은 품목(Labor_Doc `no_twice` 400 막음) — 사용자 자재에서 빼고
세 칸을 그 로직 줄 `자재표시 {재료 줄 차례: 세 칸}` 으로(수량 = 로직 셈) → 옮긴 코드."""
whole, moved = m01.loaded()[1], []
for row in (r for r in lines if r.get("로직")):
got = labor.run_line(whole, row["로직"], row.get("입력값"), None, None)
for m in got.get("재료") or []:
code = (m.get("품목") or {}).get("키")
if code in mats and code not in moved:
marks = {k: mats[code][k] for k in labor.MARKS if k in mats[code]}
row.setdefault("자재표시", {})[str(m["차례"])] = marks
moved.append(code)
for code in moved:
mats.pop(code)
return moved
def _row(self, line: dict[str, Any], kind: str, pairs: list[dict[str, Any]]) -> dict[str, Any]:
"""장부 한 줄 — 엑셀조항 = 줄 근거 출처(있으면)."""
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
clauses = (line.get("근거") or {}).get("출처") or []
return {
"원래": {k: str(v or "") for k, v in origin.items()},
"갈래": kind,
"짝": pairs,
"엑셀조항": clauses if isinstance(clauses, list) else [],
}
def _order(units: dict[int, dict[str, Any]]) -> list[int]:
"""하위 호표 먼저(호표 줄 참조) · 고리는 끊음."""
out: list[int] = []
seen: set[int] = set()
def visit(no: int) -> None:
if no in seen or no not in units:
return
seen.add(no)
for line in units[no].get("줄") or []:
ref = line.get("참조") or {}
if hfix.kind_of(line) == "호표" or ref.get("종류") == "일위":
visit(int(ref.get("번") or 0))
out.append(no)
for no in sorted(units):
visit(no)
return out
def build_all(
folder: Path, source: Path, key: str, who: Any = None, strict: bool = False
) -> dict[str, Any]:
"""[모두 고정으로 들여오기](705-3) — 시트 호표 모두 · 하위 먼저 · 줄 = 고정(하위 = 참조) ·
`strict`(불러오기 길 · 729-2) = 남은 줄 있는 호표는 안 만듦(`실패 {번, 까닭, 남은줄}`) ·
붙여넣기(705-2)는 막지 않음 ·
이미 확인된 호표는 덮지 않음(`그대로`) → `{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보,
같음, 그대로, 다름 [{번, 이름, 엑셀, 우리, 차이}], 실패 [{번, 까닭}], 만듦 [{번, 이름, 키}]}`."""
units = {int(h["번"]): h for h in hopyo_store.read(source, key).get("호표") or []}
sums = dict.fromkeys(("품셈", "고정", "호표", "빠짐", "이음", "이음후보"), 0)
out: dict[str, Any] = {
"호표": len(units),
"줄": 0,
"같음": 0,
"그대로": 0,
"다름": [],
"실패": [],
"만듦": [],
}
shared, failed = lsame.Same(), set()
for no in _order(units):
out["줄"] += len(units[no].get("줄") or [])
bad = next((k for k in hkids.kids_of(units[no]) if k in failed), None)
try:
maker = Builder(folder, source, key, no, who)
maker.strict, maker.same = strict, shared
have = maker.done.get(mark(key, no))
if bad is not None and not have: # 777-1 — 하위 실패 연쇄 까닭 따로
raise store.StoreError(409, {"까닭": f"하위 제{bad}호표 실패", "하위": bad})
if have:
out["그대로"] += 1
ref = {"층": refs.here()[0], "주인": refs.here()[1], "키": have}
else:
made = maker.build({}, [])
ref = made["참조"]
for k in sums:
sums[k] += made["줄수"][k]
got = compare(ref, maker.hopyo)
out["만듦"].append({"번": no, "이름": units[no].get("이름"), "키": ref["키"]})
except store.StoreError as e:
why = e.detail if isinstance(e.detail, dict) else {"까닭": str(e.detail)}
out["실패"].append({"번": no, **why})
failed.add(no)
continue
if got["같음"]:
out["같음"] += 1
else:
out["다름"].append({"번": no, "이름": units[no].get("이름"), **got})
return {**out, **{("호표참조" if k == "호표" else k): v for k, v in sums.items()}}