Files
Aislo/M02_MasterTemplete/M02_Hopyo_Sangeun.py
T
eomsangdonandClaude Opus 5.5 b1b9794946 feat(739-1): 별칭 장부 중앙 관리 — 자동 짝 = 마스터(승인) · 이 불러오기에서 이은 짝만 · 회사 · 개인 층 짝 = 내 제안
- 장부 읽는 차례: 자동 정확 열쇠(마스터 · 같은 불러오기 `불러오기` 표시) → 제안 정확 열쇠(`제안` True · 자동 아님) → 품명|단위 느슨
- 만들기 · 단가산출 만들기가 이은 짝에 불러오기 열쇠(`doc_id` 층/주인/HP 키) 남김 · 장부 화면 고침은 그 표시 그대로 둠
- 줄 맞춤: 내 제안 = 숫자 판정 그대로 + 꼬리표 「내 제안」 + `제안`(고르기 창 맨 위 · 한 번 눌러 짝 통째) · 호표 통째 제안 = 체크 안 함
- 장부 화면(회사 · 개인 층) = 「내 제안 목록」 · 승인 칸(대기 · 승인 · 반려 = M01 서명 · 반려 기억) · [삭제] → [거두기] · 마스터 층(M01) 그대로
- M01 별칭 장부 모으기 · 승인 · 반려 길 그대로 · 기존 층 장부 파일 안 지움(제안으로 읽힘)
- 낡은 시험 고침: hopyo ledger(아래 층이 이김 → 마스터만 자동 · 제안 · 승인 상태) · hopyo match(별칭 자동 규칙 = 마스터 층 장부로) · hopyo screen(머리 글 · 거두기 단추)
- 새 시험 test_739_1_alias_central(층 짝 자동 0 · 내 제안 · 이은 짝 → 제안 · M01 후보 · 승인 뒤 자동 · 반려 상태)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:40:45 +09:00

314 lines
16 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」, 파일, 원본, 확인, 갈래, 엑셀품, 못옮김} · 같은 본뜬(키 · 원본)이
이 층에 있으면 그대로(겹침 없음) · 만드는 층 = 부르는 층.
같은 내용(733-1 · `M02_Library_Same` 지문) SK 가 이 층에 있으면 새로 안 만들고 그것을 씀 — 이름 = 먼저 만든 것 ·
뒤 출처 = 그 SK `본뜬.또 [그 블록 본뜬 + 이름]`(`made` · `origin_of` 가 읽음 · 갈래 · 못옮김은 출처마다) ·
다른 출처가 쓰는 SK 는 내용을 안 바꿈(고른 기초 · 잇기로 바뀌면 떼고 다시 찾음) · 응답 `같은것씀 [{번, 키}]` ·
위 층(회사 · 마스터) 같은 것 = 응답 `위층같음 [{번, 층, 주인, 키}]`(알림만 · 연결 안 함).
별칭 제안 = 기초 · 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_Library_Same as same
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
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 _same_mark(a: dict, b: dict) -> bool:
"""같은 출처 — 본뜬 키 같음 · 원본(파일 층)이 둘 다 있으면 같음."""
if a.get("키") != b.get("키"):
return False
return not a.get("원본") or not b.get("원본") or tuple(a["원본"]) == tuple(b["원본"])
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 _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, mark_: str, source: refs.Here) -> 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 {}),
"본뜬": origin_of(doc, mark_, source),
}
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()}}
self.same, self.reused, self.above = same.Same(), [], []
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:
"""그 블록 SK 를 씀 → 키 — 다른 출처(또)가 쓰는 SK 의 내용은 안 바꿈(바뀌면 떼고 다시 찾음)."""
doc = _doc(self.first[no], 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 · 고름 대기 → `{블록, 만듦 [{번, 이름, 키, 갈래, 못옮김,
환산?}], 그대로, 남은못옮김 [{번, 이름, 키, 못옮김}], 기초, 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, mark(key, no), maker.here)["본뜬"].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, mark(key, no), maker.here
) # 고른 기초로 만든 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, ledger.doc_id(folder, key)
)
out["같은것씀"], out["위층같음"] = maker.reused, maker.above
return out