Files
Aislo/M02_MasterTemplete/M02_Hopyo_Sangeun.py
T
eomsangdonandClaude Opus 5.5 0c8c31cadd fix(729-5): 별칭 체크 줄이 못옮김 남은 차이 SK 를 가리키면 남은 줄 · 만들기 응답 남은못옮김
- M02_Hopyo_SkLink.kept: 확인한 로직 줄이 차이 SK 바탕일 때 그 SK 에 못옮김이 남으면 남은 줄 {까닭, 단산, SK, 못옮김}(sk_line 과 같은 규칙) — hopyo-unlinked 에 나오고 hopyo-build 409
- M02_Hopyo_Sangeun.build: 응답에 남은못옮김 [{번, 이름, 키, 못옮김}](그대로 SK 중 못옮김 남은 것) · 잇기만 보냈는데 1위 기초가 고름 대기면 그 SK 바탕으로 다시 셈(고른 기초 SK 잇기)
- 시험: test_729 에 별칭 체크 줄 · 그대로 SK 남은못옮김 · 잇기 뒤 사라짐 · 차이 SK 참조

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:13:54 +09:00

245 lines
12 KiB
Python

"""M02 일위대가 불러오기 → 단가산출(PLAN 718-1 · 729-1 · 요구서 내역19 1장 7 · 3장 6) —
산근(단산) 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기.
블록마다 그 블록을 부르는 첫 엑셀 줄(「단산N참조」)의 맞춤셈(`M02_Hopyo_Fit` 캐시 · 없으면 그 블록만
셈) → `M02_Hopyo_SkPatch.plan`:
기초 = SK 바탕 = 1위 기초 · 입력값 · 덮개 없음 · SK = 바탕 기초 + 덮개 · 더한줄 · 뺀줄(엑셀 품 그대로) ·
고름 대기 = SK 안 만듦(바탕 없는 SK 로 몰래 채우지 않음) · 후보 다섯(성격 다른 단위 = `계수` 칸 뜻) →
사용자가 고른 기초 `바탕 {번: {로직?, 입력값?, 환산?, 잇기?}}` 로 다시 부르면 그 기초에 견줘 SK(이미 있으면
그 키에 덮음) · 잇기 `[{id, 노임|기계|자재: 키} | {id, 뺌}]` = 못옮김 → SK 더한줄 · 남은 못옮김 = 응답
`만듦[].못옮김` · 본뜬 그대로(그 SK 를 쓰는 일위대가 만들기 = 남은 줄 409 · `M02_Hopyo_SkLink`) ·
그대로 SK 의 못옮김 = 응답 `남은못옮김 [{번, 이름, 키, 못옮김}]` · 잇기만 보냈는데 1위 기초가 고름 대기면
그 SK 바탕(로직 · 입력값 · 환산)으로 다시 셈(729-5).
SK = 이름 블록 이름(없으면 줄 품명) · 단위 = 줄 단위(없으면 블록 · 「식」) · 구분 「실무자료」 · 상세구분 = 파일 ·
본뜬 = {층 「실무자료」, 키 「HP-###### 단산N」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
별칭 제안 = 기초 · SK 만든 블록의 엑셀 줄 → 이 층 별칭 장부 짝 = 바탕 기초까지만(차이 SK 는 이 층 문서 ·
요구서 3장 6) · 장부에 이미 짝 · 갈래가 있는 줄은 그대로 · 승인 길 = M01 별칭 장부(716).
"""
from __future__ import annotations
import json
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_Fit as fit
from M02_MasterTemplete import M02_Hopyo_Ledger as ledger
from M02_MasterTemplete import M02_Hopyo_SkPatch as skpatch
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Doc as sd
ORIGIN = "실무자료"
KIND = "sangeun"
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
if key is not None and not str(got.get("키") or "").startswith(f"{key} "):
continue
if source is None or not got.get("원본") or tuple(got["원본"]) == tuple(source):
out[str(got.get("키"))] = path.stem
return out
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 _bad(text: str, status: int = 400) -> store.StoreError:
return store.StoreError(status, text)
def _picks(raw: Any) -> dict[int, dict[str, Any]]:
"""사용자 고름 `{번: {로직?, 입력값?, 환산?, 잇기?}}` 재검사 — 돈 로직만 · 입력 이름 = 그 로직 입력 ·
환산 = SK 꼴(`M02_Sangeun_Doc`) · 잇기 = `[{id, …}]`(id 짝 · 키는 `M02_Hopyo_SkPatch` 가 봄)."""
if not raw:
return {}
if not isinstance(raw, dict) or len(raw) > 500:
raise _bad("바탕은 {블록 번: {로직, 입력값}}")
logics = ledger._known()[0]
out = {}
for no, one in raw.items():
code = str((one or {}).get("로직") or "") if isinstance(one, dict) else "?"
if code and code not in logics:
raise _bad(f"단산{no} · 돈 로직이 아님 「{code}」")
if not code and not (one.get("환산") or one.get("잇기")):
raise _bad(f"단산{no} · 로직 · 환산 · 잇기 중 하나")
pick: dict[str, Any] = {}
given = one.get("입력값") if code else None
if code:
pick["로직"] = code
if given is not None:
bad = set(given) - ledger.input_names(code) if isinstance(given, dict) else {1}
if bad:
raise _bad(f"단산{no} · 모르는 입력 {sorted(map(str, bad))}")
pick["입력값"] = given
if one.get("환산"):
try:
pick["환산"] = sd._swap_how(one["환산"])
except sd.DocError as e:
raise _bad(f"단산{no} · {e}") from e
links = one.get("잇기")
if links:
if not isinstance(links, list) or not all(
isinstance(x, dict) and x.get("id") for x in links
):
raise _bad(f"단산{no} · 잇기는 [{{id, 노임|기계|자재: 키}}]")
pick["잇기"] = links
out[int(no)] = pick
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 ("갈래", "엑셀품", "못옮김")}
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 ("자재표시", "덮개", "더한줄", "뺀줄")},
}
def _old_base(folder: Path, key: str) -> dict[str, Any]:
"""이미 있는 SK 의 바탕 고름 `{로직, 입력값, 환산?}` · `본뜬` — 읽기 실패 = 빈 것."""
try:
doc = json.loads((Path(folder) / KIND / f"{key}.json").read_bytes())
except (OSError, ValueError):
return {}
base = {"로직": (doc.get("바탕") or {}).get("로직"), "입력값": doc.get("입력값") or {}}
return {
**base,
**({"환산": doc["환산"]} if doc.get("환산") else {}),
"본뜬": doc.get("본뜬") or {},
}
def _alias(line: dict, plan: dict) -> dict[str, Any]:
origin = {"품명": line.get("이름"), "규격": line.get("규격"), "단위": line.get("단위")}
clauses = (line.get("근거") or {}).get("출처") or []
pair = {"로직": plan["바탕"]["로직"], "입력값": plan["입력값"], "배": 1, "몫": None}
return {
"원래": {k: str(v or "") for k, v in origin.items()},
"갈래": "짝",
"짝": [pair],
"엑셀조항": clauses if isinstance(clauses, list) else [],
}
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)
got = fit.result(source, key) or {}
self.cache, self.done = got.get("블록") or {}, bool(got.get("끝"))
self.here = refs.where(source)
self.whole = m01.loaded()[1]
self.sure = {"원본": list(self.here), "확인": {"누가": who, "때": utc_now_text()}}
def plan(self, no: int, pick: dict | None = None) -> dict[str, Any]:
line, block = self.first[no], self.blocks[no]
entry = self.cache.get(fit.unit_key(line))
if entry is None and not (pick or {}).get("로직"):
entry = fit.compute(block, line)
try:
return skpatch.plan(entry, block, line, self.whole, pick)
except skpatch.LinkError as e:
raise _bad(f"단산{no} · {e}") from e
def write(self, no: int, plan: dict, have: str | None) -> str:
doc = _doc(self.first[no], self.blocks[no], no, self.hp, plan, self.sure)
if have:
raw = (store.root() / KIND / f"{have}.json").read_bytes()
store.write(KIND, have, store.version_of(raw), {**doc, "키": have})
return have
return refs.create(KIND, doc)["이름"]
def build(folder: Path, key: str, who: Any = None, picks: Any = None) -> dict[str, Any]:
"""[단가산출 만들기] — 산근 블록마다 기초 · SK · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, SK, 고름대기 [{번, 이름, 단위, 까닭, 후보}],
별칭}` · `picks` = 고름."""
chosen = _picks(picks)
maker = Maker(folder, key, who)
bad = sorted(set(chosen) - set(maker.first))
if bad:
raise _bad(f"없는 블록 · 부르는 줄 없음 「단산{bad[0]}」", 404)
if not maker.done and set(maker.first) - set(chosen):
fit.ensure(folder, key)
raise _bad("맞춤 셈 중 — 끝나면 다시 누름", 409)
have = made(folder, key, maker.here)
out: dict[str, Any] = {"블록": len(maker.first), "만듦": [], "그대로": 0, "고름대기": []}
out["남은못옮김"] = []
out.update({skpatch.BASE: 0, skpatch.SK: 0})
books, alias = ledger.books(folder), {}
for no in sorted(maker.first):
old = have.get(mark(key, no))
line = maker.first[no]
name = maker.blocks[no].get("이름") or line.get("이름")
if old and no not in chosen:
out["그대로"] += 1
lost = _old_base(folder, old).get("본뜬", {}).get("못옮김")
if lost:
out["남은못옮김"].append({"번": no, "이름": name, "키": old, "못옮김": lost})
continue
plan = maker.plan(no, chosen.get(no))
if plan["갈래"] == skpatch.WAIT and old and not chosen[no].get("로직"):
base = _old_base(folder, old) # 고른 기초로 만든 SK 의 잇기 = 그 바탕 그대로
if base.get("로직"):
pick = {k: v for k, v in base.items() if k != "본뜬"}
plan = maker.plan(no, {**pick, **chosen[no]})
if plan["갈래"] == skpatch.WAIT:
head = {"번": no, "이름": maker.blocks[no].get("이름") or line.get("이름")}
out["고름대기"].append({**head, "단위": line.get("단위"), **plan})
continue
got = maker.write(no, plan, old)
out[plan["갈래"]] += 1
made_ = {"번": no, "이름": name, "키": got, "갈래": plan["갈래"], "못옮김": plan["못옮김"]}
out["만듦"].append({**made_, **({"환산": plan["환산"]} if plan.get("환산") else {})})
found = ledger.recall(line, folder, books)
if no in chosen or found is None or found.get("규격다름"):
alias[ledger.key(line)] = _alias(line, plan)
out["별칭"] = ledger.write(folder, alias, who, maker.hp.get("파일") or None)
return out