Files
Aislo/M02_MasterTemplete/M02_Hopyo_Sangeun.py
T
eomsangdonandClaude Opus 5.5 a22af84b62 feat(785-5): 만들기 · 길 — 확인한 고른 것 → SC · SK · 장부 · 새 M02_Hopyo_Router · 판정 · 검산 · 옮기기
- 새 M02_Hopyo_Router — 옛 hopyo 길 모두 M02_Labor_Router 에서 옮김(옛 화면 785-7 까지 돎) · 새 길 hopyo-judge 목록 · 호표 · 블록 · hopyo/{키}/check · hopyo/{키}/move · main 등록 · 시동 warm · Fit.ensure 걷음 · 올림 때 엑셀 사전(Row.lexicon) · 블록 풀이 뒤 셈 · 끝내기 때 저장본 지움
- 새 M02_Hopyo_Judge — 1차 줄 판(Row) + 단산 줄 2차 풀이(Clause → Solve) · 엑셀 칸(이름 · 규격 · 단위 · 단가) · 블록 금액 · 절 몫 · 블록 풀이 파생 저장본(HP 폴더 _derived · 입력 지문 · 지문 다를 때만) · judged(folder, key) · 검산(입력값 = 엔진이 쓴 칸 모두 · 지역 · 자재지역 · 조종원직종 비움 · 맡은 절 단위 줄 · 몫과 품 대조 · 금액 = 일위대가 로직 줄과 같은 셈)
- 새 M02_Hopyo_Make — 고른 것 → SC 줄(같은 별칭 · 줄 본뜬 {호표, 차례, 행} · 수량 = 엑셀 × 몫 × 배 · 같은 기초 둘째 = 한 줄 SC 사본 · 별도계상 자재 · 이음 · 율 · 고정 · 참조) · SK 더한줄(바탕 없음 · SkUnit.adds · Sangeun.Maker.write) · 하위 먼저 · 입력 필요 · 단위 환산 못 정함 = 그 줄(블록)만 막힘 · 장부 고른 것 꼴 짝 · 이미 옮긴 것 = 덮음 없으면 409(남의 SC 덮지 않음) · 확인한 줄 몸(Lines) = Build 에서 옮김
- Build 나눔 — 모두 고정 길 · Builder · _order · _origins · build_all 그대로(B09 빌림) · 확인한 줄 몸 → Make.Lines
- SkUnit · Sangeun — SkPatch · Fit · Basis 부름 걷음(기계경비 기초 차례 SkUnit 으로 · 근거 꼬리표 = Clause.basis · 맞춤 셈 409 없앰 · 재료 고름 MO 됨) · Kids 가 _same_mark 가짐
- 시험 새 test_642_5_make(판정 · 검산 · 옮기기 · 예시 1 · 2 · 3 · 덮음 409 · 막는 길) · test_m02_hopyo_excel 마당에 새 Router 더함 · HOPYO_MAX_BYTES 자리(길 옮긴 까닭)
- 낡은 시험 고침: test_718_1 · test_729 「맞춤 셈 전 409」 뺌(맞춤 뒤 셈 안 기다림) · test_628 맞춤셈 캐시 넣기 뺌(근거 = 조항 색인) · test_729 근거 없음 꼴 · 고정 이음 유가 칸(785-3 LINKS 늘어난 까닭)

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

323 lines
17 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 718-1 · 729-1 · 778-5 D-3 · 요구 621 · 623) — 산근(단산) 블록마다
줄 판정 모음(`M02_Hopyo_SkUnit`) → 사용자가 확인한 블록만 단위 SK.
모두 줄로만(바탕 기초 안 씀 · 조항 색인 기초 = 근거 꼬리표만) · 자동 없음 — `확인 [{번, 고름?: {차례: 고름}}]`
(블록마다 [확인] · [옮기기])에 든 블록만 만듦 · 확인 안 한 블록 = 판정만(안 만듦) · 줄 다 이어지지 않은 확인 블록 =
`못만듦` · 하위 단산 줄 = 그 블록 SK 참조(확인 목록에 있으면 먼저 만듦 · 없으면 못만듦).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
바탕 없음 · 본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래 「단위」, 근거 {로직, 이름}, 못옮김 []} ·
같은 본뜬(키 · 원본)이 이 층에 있으면 그 키에 덮음 · 만드는 층 = 부르는 층.
판정 · 확인 = 모든 블록(780-1 · 요구 627) — 일위대가가 부르는 블록 + 일위대가 밖(내역서 등이 부름) · 줄마다
`부름 [호표 번]`([] = 일위대가 밖) · 밖 블록 = 이름 · 단위 블록 것 · 근거 꼬리표 없음(맞춤셈 열쇠 = 부르는 줄).
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음) · 다른 출처가 쓰는 SK 는 내용을
안 바꿈(바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` · 위 층 같은 것 = `위층같음 [{번, 층, 주인, 키}]`(알림만).
일위대가 만들기(`M02_Hopyo_SkLink`)가 부르는 `Maker.plan(번)`(확인 없음) = 고름 대기 「확인 전」 — 못 이음.
"""
from __future__ import annotations
import json
from pathlib import Path
from types import SimpleNamespace
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_Clause as clause
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_SkUnit as skunit
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Hopyo_Units as units
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Same as same
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_Hopyo_Kids import _same_mark
ORIGIN = "실무자료"
KIND = "sangeun"
WAIT_WHY = "확인 전 — [단가산출]에서 블록 [확인] 뒤 [옮기기]"
WAIT = "고름 대기"
skpatch = SimpleNamespace(WAIT=WAIT, SK="SK") # 옛 이름(SkLink 가 읽음 · 785-7 지움)
def mark(key: str, no: int) -> str:
return f"{key} 단산{no}"
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 not isinstance(got, dict) or got.get("층") != ORIGIN:
continue
for one in [got, *(got.get("또") or [])]: # 또 = 같은 내용으로 이 SK 를 쓴 뒤 출처(733-1)
mark_ = str((one or {}).get("키") or "") if isinstance(one, dict) else ""
if not mark_ or (key is not None and not mark_.startswith(f"{key} ")):
continue
if source is None or not one.get("원본") or tuple(one["원본"]) == tuple(source):
out[mark_] = path.stem
return out
def origin_of(doc: dict, mark_: str, source: refs.Here | None = None) -> dict[str, Any]:
"""그 블록 출처의 본뜬(갈래 · 못옮김 · 놓음 …) — 또 가운데 `mark_` · 없으면 제 본뜬."""
own = doc.get("본뜬") or {}
want = {"키": mark_, "원본": list(source) if source else None}
also = [x for x in own.get("또") or [] if isinstance(x, dict)]
return next((x for x in also if _same_mark(x, want)), own)
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 callers(hp: dict[str, Any]) -> dict[int, list]:
"""{단산 번: 부르는 호표 번 [차례대로 · 겹침 뺌]} — 없는 번 = 일위대가 밖."""
out: dict[int, list] = {}
for h in hp.get("호표") or []:
for line in h.get("줄") or []:
ref = line.get("참조") or {}
if ref.get("종류") == "단산" and ref.get("번"):
got = out.setdefault(int(ref["번"]), [])
if h.get("번") not in got:
got.append(h.get("번"))
return out
def _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
def _sure(raw: Any) -> dict[int, dict[int, Any]]:
"""확인 목록 `[{번, 고름?: {차례: 고름}}]` → `{번: {차례: 고름}}`(고름 꼴은 `M02_Hopyo_SkUnit` 이 봄)."""
if not raw:
return {}
if not isinstance(raw, list) or len(raw) > 500:
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]")
out: dict[int, dict[int, Any]] = {}
for one in raw:
try:
picks = one.get("고름") or {}
out[int(one["번"])] = {int(k): v for k, v in picks.items()}
except (TypeError, ValueError, KeyError, AttributeError) as e:
raise _bad("확인은 [{번, 고름?: {차례: 고름}}]") from e
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 "식"
keep = {k: plan[k] for k in ("갈래", "엑셀품", "못옮김", "근거") if k in plan}
keep |= {"놓음": plan["놓음"]} if plan.get("놓음") else {}
return {
"종류": "단가산출",
"이름": name,
"단위": unit,
"구분": ORIGIN,
"상세구분": str(hp.get("파일") or ""),
"본뜬": {"층": ORIGIN, "키": mark(hp["키"], no), "파일": hp.get("파일"), **sure, **keep},
"바탕": plan["바탕"],
"입력값": plan["입력값"],
**({"환산": plan["환산"]} if plan.get("환산") else {}),
"열린입력": [],
"고름": {},
**{k: plan[k] for k in ("자재표시", "덮개", "더한줄", "뺀줄")},
}
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)
self.done = True # 옛 이름(SkLink) — 맞춤셈 안 기다림(785-5)
self.here = refs.where(source)
self.whole = m01.loaded()[1]
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
self.same, self.reused, self.above = same.Same(), [], []
self.keys = made(source, key, self.here) # {본뜬 키: SK} — 만든 것 더해 감
self.judged: dict[int, dict] = {}
self._units: units.Units | None = None
def units(self) -> units.Units:
if self._units is None:
books = ledger.books(self.source)
self._units = units.Units(books, ledger.doc_id(self.source, self.key))
return self._units
def basis(self, no: int) -> dict[str, Any] | None:
"""근거 꼬리표(참고) — 조항 색인(`M02_Hopyo_Clause.basis`) · 표지(기초 없음 · 옛 번호 · 준용 ·
번호 없음) 있으면 표지 · 후보 하나면 그 기초 · 여럿이면 블록 풀이(`M02_Hopyo_Judge` 저장본 ›
그 블록만 셈) 첫 고름 · 일위대가 밖 블록 = 없음."""
if no not in self.first:
return None
got = clause.basis(self.blocks[no])
if got["표지"]:
return {"로직": None, "이름": got["글"], "판정": got["표지"]}
cands = got["후보"]
top = cands[0]["키"] if len(cands) == 1 else self._solved(no)
if not top:
return None
return {"로직": top, "이름": (self.whole.logic(top) or {}).get("이름"), "판정": None}
def _solved(self, no: int) -> str | None:
from M02_MasterTemplete import M02_Hopyo_Judge as judge # Judge → 여기 — 늦게 읽음
cache = judge.solved(self.source, self.key) if getattr(self, "key", None) else None
line = {k: v for k, v in self.first[no].items() if k != "_호표"}
got = (cache or {}).get(str(no)) or judge._solve(judge.master(), self.blocks[no], line, [])
return next((s["고름"] for s in got["절"] if s["고름"]), None)
def ref_of(self, sub: dict[str, Any]) -> dict[str, Any] | None:
"""하위 줄 → 참조 칸 — 단산 = 이 HP 그 블록 SK · 일위 = 확인된 그 호표 SC · 없으면 None."""
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build # Build → SkLink → 여기
if sub.get("종류") == "단산":
got = self.keys.get(mark(self.key, int(sub["번"])))
else:
done = hopyo_build.confirmed(store.root(), self.here)
got = done.get(hopyo_build.mark(self.key, int(sub["번"])))
layer, owner = refs.here()
return {"층": layer, "주인": owner, "키": got} if got else None
def judge(self, no: int, picks: dict | None = None) -> dict[str, Any]:
line, block = self.first.get(no) or {}, self.blocks[no]
unit = str(line.get("단위") or block.get("단위") or "").strip()
try:
got = skunit.judge(block, self.units(), self.ref_of, picks, unit)
except skunit.PickError as e:
raise _bad(f"단산{no} · {e}") from e
self.judged[no] = got
return got
def plan(self, no: int, picks: dict | None = None) -> dict[str, Any]:
"""확인한 블록 → 단위 SK 꼴 · 확인 없음(`picks` None) · 고를 줄 남음 = 고름 대기."""
if picks is None:
return {"갈래": WAIT, "까닭": WAIT_WHY, "후보": []}
got = self.judge(no, picks)
if got["고를줄"]:
return {"갈래": WAIT, "까닭": got["판정"], "후보": []}
return skunit.plan(got, self.basis(no))
def make(self, no: int, sure: dict[int, dict], out: dict, path: tuple = ()) -> None:
"""확인한 블록 하나 — 확인 목록의 하위 단산 먼저 · 못 만들면 `못만듦`."""
mark_ = mark(self.key, no)
if no in path or any(x["번"] == no for x in out["만듦"] + out["못만듦"]):
return
for one in self.blocks[no].get("항목") or []:
sub = one.get("하위") or {}
m = int(sub.get("번") or 0)
if sub.get("종류") == "단산" and m in sure and mark(self.key, m) not in self.keys:
self.make(m, sure, out, (*path, no))
plan = self.plan(no, sure[no])
got = self.judged[no]
lost = [r for r in got["줄"] if r["상태"] == units.LINKED and r["차례"] not in got["담음"]]
name = self.blocks[no].get("이름") or (self.first.get(no) or {}).get("이름")
if plan["갈래"] == WAIT or lost:
why = plan.get("까닭") or f"하위 「{lost[0]['이름']}」 — 그 블록 먼저 확인"
out["못만듦"].append({"번": no, "이름": name, "까닭": why})
return
key = self.write(no, plan, self.keys.get(mark_))
self.keys[mark_] = key
out["만듦"].append({"번": no, "이름": name, "키": key, "갈래": skunit.UNIT, "못옮김": []})
def write(self, no: int, plan: dict, have: str | None) -> str:
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
doc = _doc(self.first.get(no) or {}, self.blocks[no], no, self.hp, plan, self.sure)
root, mine = store.root(), doc["본뜬"]
self.above += [{"번": no, **r} for r in self.same.find_above(root, KIND, doc)]
if have:
raw, old, own, rest = self._read(have, mine)
owner = _same_mark(own, mine)
if self.same.finger(KIND, old) == self.same.finger(KIND, doc): # 내용 같음 = 출처만
self._put(
have, raw, old, *((mine, rest) if owner else (own, [*rest, self._entry(doc)]))
)
return have
if owner and not rest: # 혼자 쓰던 SK — 제자리 덮음
store.write(KIND, have, store.version_of(raw), {**doc, "키": have})
self.same.add(root, KIND, have, doc)
return have
head = {k: v for k, v in rest[0].items() if k != "이름"} if owner else own
self._put(have, raw, old, head, rest[1:] if owner else rest) # 떼어 냄
found = self.same.find_same(root, KIND, doc)
if found:
raw, old, own, rest = self._read(found[0], mine)
self._put(found[0], raw, old, own, [*rest, self._entry(doc)])
self.reused.append({"번": no, "키": found[0]})
return found[0]
key = refs.create(KIND, doc)["이름"]
self.same.add(root, KIND, key, doc)
return key
@staticmethod
def _entry(doc: dict) -> dict[str, Any]:
return {**doc["본뜬"], "이름": doc["이름"]}
@staticmethod
def _read(key: str, mine: dict) -> tuple[bytes, dict, dict, list]:
"""(바이트, 문서, 제 본뜬(또 뺌), 또 가운데 `mine` 뺀 것)."""
raw = (store.root() / KIND / f"{key}.json").read_bytes()
doc = json.loads(raw)
got = doc.get("본뜬") or {}
own = {k: v for k, v in got.items() if k != "또"}
rest = [x for x in got.get("또") or [] if isinstance(x, dict) and not _same_mark(x, mine)]
return raw, doc, own, rest
@staticmethod
def _put(key: str, raw: bytes, doc: dict, own: dict, also: list) -> None:
body = {**doc, "본뜬": {**own, "또": also} if also else own}
if body != doc:
store.write(KIND, key, store.version_of(raw), body)
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출] — 블록마다 줄 판정 · 확인한 블록만 단위 SK → `{블록, 판정 [{번, 이름, 단위, 판정, 고를줄,
우리금액, 엑셀금액, 근거, 만든, 줄, 부름}], 만듦 [{번, 이름, 키, 갈래, 못옮김}], 못만듦 [{번, 이름, 까닭}], 그대로,
같은것씀, 위층같음}` · `picks` = 확인 목록."""
sure = _sure(picks)
maker = Maker(folder, key, who)
bad = sorted(set(sure) - set(maker.blocks))
if bad:
raise _bad(f"없는 블록 「단산{bad[0]}」", 404)
out: dict[str, Any] = {"블록": len(maker.blocks), "만듦": [], "못만듦": []}
for no in sorted(sure):
maker.make(no, sure, out)
rows = []
call = callers(maker.hp)
for no in sorted(maker.blocks):
line, block = maker.first.get(no), maker.blocks[no]
got = maker.judged.get(no) or maker.judge(no)
name = block.get("이름") or (line or {}).get("이름")
head = {
"번": no,
"이름": name,
"단위": (line or block).get("단위"),
"부름": call.get(no, []),
}
keep = ("판정", "고를줄", "우리금액", "엑셀금액", "줄")
head |= {k: got[k] for k in keep} | {"근거": maker.basis(no)}
rows.append({**head, "만든": maker.keys.get(mark(key, no))})
out["판정"] = rows
out["그대로"] = sum(1 for r in rows if r["만든"] and r["번"] not in sure)
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
return out