"""M02 단가산출(SK) · 일위대가(SC) 펼침 · 초기화 · 옮기기(PLAN 740-1 · 요구 581 · 분석 579-2). 서로 독립 — 연결 고리 = M01 키만. M02 문서를 넣으면(찾아 넣기 `-flat` · 저장 · 읽기 · 층 사이 복사) 그 문서 몸을 M01 키 줄 + 그 문서 덮개 사본으로 펼쳐 담음 → 원본이 나중에 바뀌어도 넣은 쪽 금액 그대로. 줄 `본뜬 {층, 주인, 키, 이름, 때}` = 출처 정보(셈에 안 씀). SK 에 SK 더한줄 로직 한 줄 = 원본 몸 통째(바탕 기초 · 입력값 · 덮개 · 뺀줄 · 고름 · 자재표시 · 켜쌓기율 · 더한줄 — `M02_Sangeun_Doc.BODY` · % 덧줄까지 같은 금액) · 수량 = 넣은 수 ÷ 배(728 환산) · 바탕 없는 SK = 그 더한줄 × 넣은 수 SK 에 SC SC 줄마다 더한줄(로직 줄 = 로직 한 줄 + 패치 몸 · 고정 · 자재 = 재료) · 수량 = 넣은 수 × 줄 수량(저장 `수량.손` › 실무수량 › 1 · 742-1) ÷ 배(물량 알약 배) SC 에 SK 로직 줄 하나 + 패치(= SK 몸) · 별칭 = SK 이름 · 같은 자리(안 줄 알약 키 그대로) · 바탕 없는 SK · 단위 환산 SK = 못 넣음(값 단위가 달라짐) SC 에 SC 사본 블록 — 안쪽 SC 몸 통째(M01 키 줄 + 덮개 · 실무수량)를 그 줄 참조 꼴 `{층: 사본, 주인: 몸 지문, 키: 본 SC, 몸}` 에 담음(`M02_Labor_Ref.copy_ref` · 원본 연결 없음 · 하위 호표 끝수 단계 · B09 하위 대가 블록 그대로 · 같은 기초 겹침도 블록 안) 바탕 SK 참조 · 패치 있는 SK 참조 줄 = 위 덮개를 아래 덮개에 겹쳐 한 벌(`compose`) 못 펼친 줄 = 저장 · 읽기 · 옮기기에선 옛 참조 그대로(값 그대로) · 넣기 길(`strict`)에선 400 까닭. 초기화(`reset`) = 덮개 · 뺀줄 · 더한줄 단가 걷음(안쪽 넣은 몸 · 사본 블록까지) · 이음 기계 고정 줄 단가 = M01 기계 한 시간 값 다시 받음(736-3) · 항목 · 수량 · 입력값 · 고름 · 자재표시 · 켜쌓기율 · 기본정보 남김 → 값 = 연결된 M01 키 값. 옮기기(`migrate`) = 층 문서 펼쳐 다시 씀 — 전후 1 단위 금액 같을 때만(안 줄 수량 모두 1 로 대조) · 공유 storage 는 브레인이 돌림. """ from __future__ import annotations import json import re import time from decimal import Decimal from typing import Any from M02_MasterTemplete import M02_Labor_Fixed as fixed from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Sangeun_Doc as sd ADD = re.compile(r"^add:#(\d+)$") FAIL = (ArithmeticError, KeyError, TypeError, ValueError) # 못 펼침(DocError · FormulaError 포함) EMPTY = (None, {}, [], "") Read = Any def _whole() -> Any: from M01_MasterData import M01_MasterData_Store as m01 return m01.loaded()[1] def _d(value: Any) -> Decimal: return Decimal(str(value)) def mark(ref: refs.Ref, doc: dict) -> dict[str, Any]: """줄 출처 — 펼친 원본 {층, 주인, 키, 이름, 때(UTC)}.""" when = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) return { **{k: ref.get(k) for k in ("층", "주인", "키")}, "이름": doc.get("이름") or "", "때": when, } def _read(ref: refs.Ref, read: Read, trail: tuple) -> tuple[dict, tuple]: rid = refs.ref_id(ref) if rid in trail: raise sd.DocError("순환 — " + " → ".join(t.rsplit("|", 1)[-1] for t in (*trail, rid)), 422) if len(trail) >= refs.DEPTH: raise sd.DocError(f"참조가 {refs.DEPTH}단을 넘음", 422) got = read(ref) if not isinstance(got, dict): raise sd.DocError(f"없는 참조 「{ref.get('키')}」") return got, (*trail, rid) # ── SK ──────────────────────────────────────────────────────────────── def flat_sk(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool = True) -> dict: """SK 문서 → M01 키만(더한줄 참조 펼침 · 바탕 참조 겹침) · `strict` False = 못 펼친 줄 그대로.""" adds = [] for one in doc.get("더한줄") or []: ref = one.get("참조") if isinstance(one, dict) else None if not isinstance(ref, dict): adds.append(one) continue try: adds += into_sk(ref, one.get("수량", 1), read, whole, trail) except FAIL as e: if strict: raise sd.DocError(f"「{ref.get('키')}」 · {e}") from e adds.append(one) out = {**doc, "더한줄": adds} base = doc.get("바탕") or {} if isinstance(base.get("참조"), dict): try: under, path = _read(base["참조"], read, trail) under = flat_sk(sd.clean(under, None, whole), read, whole, path) out = compose(under, out, whole) except FAIL: if strict: raise return out def into_sk(ref: refs.Ref, qty: Any, read: Read, whole: Any, trail: tuple = ()) -> list[dict]: """SK 에 넣을 더한줄 — 넣는 문서(SK · SC)를 M01 키 줄로(`qty` = 그 문서 단위 수).""" src, path = _read(ref, read, trail) kind, q, tag = refs.kind_of(ref.get("키")), _d(qty), mark(ref, src) src = sd.clean(src, None, whole) if kind == "sangeun" else src if kind == "sangeun": return sk_adds(flat_sk(src, read, whole, path), q, whole, tag) if kind == "precombo": return sc_adds(flat_sc(src, read, whole, path), q, whole, tag) raise sd.DocError("옛 단가승격은 펼치지 않음") def filled_adds(doc: dict, whole: Any) -> list[dict]: """더한줄 로직 빈 입력 = 그 SK 입력값(셈 `_fill` 의 공통 옵션 몫) — 옮겨도 같은 옵션.""" common, out = doc.get("입력값") or {}, [] for one in doc.get("더한줄") or []: if one.get("종류") == "로직": try: names = [str(s.get("이름")) for s in whole.logic(one["로직"]).get("입력") or []] except Exception: # noqa: BLE001 — 없는 로직 = 검사 · 셈이 까닭 names = [] mine = dict(one.get("입력값") or {}) for n in names: if mine.get(n) in EMPTY and common.get(n) not in EMPTY: mine[n] = common[n] one = {**one, "입력값": mine} out.append(one) return out def _scaled(one: dict, q: Decimal, tag: dict) -> dict: return {**one, "수량": sd.plain(_d(one.get("수량") or 0) * q), "본뜬": tag} def sk_adds(y: dict, q: Decimal, whole: Any, tag: dict) -> list[dict]: """펼친 SK → 더한줄 — 바탕 기초 있으면 한 줄(몸 통째) · 없으면 그 더한줄 × q.""" from M02_MasterTemplete import M02_Sangeun_Calc as calc adds = filled_adds(y, whole) code = (y.get("바탕") or {}).get("로직") if not code: return [_scaled(one, q, tag) for one in adds] row = whole.logic(code) names = {str(s.get("이름")) for s in row.get("입력") or []} unit = calc.base_unit(row) entry: dict[str, Any] = { "종류": "로직", "로직": code, "이름": y.get("이름") or code, "단위": unit[0] if unit else "", "수량": sd.plain(q / calc.ratio_of(y, row, whole)), "입력값": {k: v for k, v in (y.get("입력값") or {}).items() if k in names}, } entry |= { k: y[k] for k in ("덮개", "뺀줄", "고름", "자재표시", "켜쌓기율") if y.get(k) not in EMPTY } return [{**entry, **({"더한줄": adds} if adds else {}), "본뜬": tag}] def _hand(one: dict, kept: Any) -> Decimal: """SC 안 줄 수량 — 저장 `수량.손`(742-1 사용자 입력) › 실무수량 › 1.""" raw = one.get("수량") got = raw.get("손") if isinstance(raw, dict) else None return _d(got if got not in EMPTY else kept if kept not in EMPTY else 1) def sc_adds(z: dict, q: Decimal, whole: Any, tag: dict) -> list[dict]: """펼친 SC → SK 더한줄 — 줄 수량 = q × `_hand` · 로직 줄 ÷ 물량 알약 배.""" from M02_MasterTemplete import M02_Labor_Patch as patch from M02_MasterTemplete import M02_Sangeun_Calc as calc hands = (z.get("본뜬") or {}).get("실무수량") or {} lines, mats = hands.get("줄") or {}, hands.get("자재") or {} out = [] for at, one in enumerate(z.get("담은로직") or []): n = q * _hand(one, lines.get(str(at))) if one.get("로직"): row = whole.logic(one["로직"]) body = patch.as_doc(one, whole) entry = { "종류": "로직", "로직": one["로직"], "이름": one.get("별칭") or row.get("이름") or "", } entry |= {"수량": sd.plain(n / calc.times_of(row)), "입력값": body["입력값"]} keep = ("덮개", "뺀줄", "고름", "자재표시", "켜쌓기율", "더한줄") entry |= {k: body[k] for k in keep if body.get(k) not in EMPTY} elif fixed.is_fixed(one) and not refs.is_rate(one): entry = { "고정": one["고정"], "이름": one["고정"].get("이름") or "", "수량": sd.plain(n), } elif refs.is_copy(one.get("참조")): # 하위 호표 사본 블록 = 그 줄들도 더한줄로 out += sc_adds(one["참조"]["몸"], n, whole, tag) continue else: raise sd.DocError("비율 고정 줄 · 참조 줄은 단가산출에 못 담음") out.append({**entry, "본뜬": tag}) for mat in z.get("자재") or []: code = str(mat.get("코드") or "") if not code.startswith("MT"): raise sd.DocError(f"개인 자재 「{code}」 는 단가산출에 못 담음") n = q * _hand(mat, mats.get(code)) one = { "종류": "재료", "요소": code, "이름": mat.get("이름") or "", "단위": mat.get("단위") or "", } apart = {"별도계상": True} if mat.get("별도계상") == "별도계상" else {} out.append({**one, "수량": sd.plain(n), **apart, "본뜬": tag}) return out # ── 겹침(바탕 SK · 패치 SK 참조) ───────────────────────────────────────── def _merge(a: dict | None, b: dict) -> dict: """아래 덮개 a 위에 위 덮개 b — 품 qty 바꿈 · 배율 곱 · 단가 위 것.""" if not a: return dict(b) out = {k: v for x in (a, b) for k, v in x.items() if k in sd.PARTS} if "qty" in b: out["qty"] = b["qty"] elif "배율" in b and "qty" in a: out["qty"] = sd.plain(_d(a["qty"]) * _d(b["배율"])) elif "배율" in b: out["배율"] = sd.plain(_d(a.get("배율", 1)) * _d(b["배율"])) else: out |= {k: a[k] for k in ("qty", "배율") if k in a} return out | ({"원값": a["원값"]} if "원값" in a else {}) def _cover_add(add: dict, cover: dict) -> dict: """위 덮개를 아래 더한줄 항목에 — 수량 · 그 비목 단가만(그 밖 = 못 겹침).""" out = dict(add) if "qty" in cover: out["수량"] = cover["qty"] elif "배율" in cover: out["수량"] = sd.plain(_d(add.get("수량") or 0) * _d(cover["배율"])) prices = {sd.PARTS[k]: cover[k] for k in sd.PARTS if k in cover} if prices and (add.get("종류") not in sd.ADD_PART or set(prices) != {add.get("비목")}): raise sd.DocError("더한줄 단가 덮개를 못 겹침") if prices: out["단가"] = prices[add["비목"]] return out def compose(under: dict, over: dict, whole: Any) -> dict: """펼친 바탕 SK `under` 위에 `over`(덮개 · 뺀줄 · 고름 · 자재표시 · 더한줄 · 입력값) → 한 벌 — 위 줄 정체를 아래 바탕 정체로 옮김(`patched` 의 `정체` 짝) · 짝 잃은 위 고침은 버림(값 없음).""" base = under.get("바탕") row = whole.logic(base["로직"]) if (base or {}).get("로직") else sd.empty_row(under) got = sd.patched(row, under) where = dict(zip(sd.line_ids(got["줄"]), got["정체"])) covers = {c: dict(v) for c, v in (under.get("덮개") or {}).items()} drop, picks = list(under.get("뺀줄") or []), dict(under.get("고름") or {}) marks, adds, gone = ( dict(under.get("자재표시") or {}), [dict(x) for x in under.get("더한줄") or []], set(), ) def to(cid: str) -> tuple[str, Any] | None: o = where.get(cid) hit = ADD.match(o) if o else None return None if o is None else ("add", int(hit[1])) if hit else ("base", o) for cid, cover in (over.get("덮개") or {}).items(): if t := to(cid): if t[0] == "base": covers[t[1]] = _merge(covers.get(t[1]), cover) else: adds[t[1]] = _cover_add(adds[t[1]], cover) for cid in over.get("뺀줄") or []: if (t := to(cid)) and t[0] == "add": gone.add(t[1]) elif t and t[1] not in drop: drop.append(t[1]) for name, mine, into in ( ("고름", over.get("고름"), picks), ("자재표시", over.get("자재표시"), marks), ): for cid, value in (mine or {}).items(): if (t := to(cid)) and t[0] == "add": raise sd.DocError(f"더한줄 {name}를 못 겹침") if t: into[t[1]] = value rate = over.get("켜쌓기율", under.get("켜쌓기율")) out = {**over, "바탕": base, "덮개": covers, "뺀줄": drop, "고름": picks, "자재표시": marks} out["더한줄"] = [a for k, a in enumerate(adds) if k not in gone] + list( over.get("더한줄") or [] ) out["입력값"] = {**(under.get("입력값") or {}), **(over.get("입력값") or {})} return out | ({"켜쌓기율": rate} if rate is not None else {}) # ── SC ──────────────────────────────────────────────────────────────── def sk_line(y: dict, one: dict, whole: Any, tag: dict) -> dict: """펼친 SK → 일위대가 로직 줄 하나(+ 패치) — 옛 참조 줄 `one` 의 별칭 · 메모 · 수량 · 패치 이어받음.""" from M02_MasterTemplete import M02_Labor_Patch as patch from M02_MasterTemplete import M02_Sangeun_Calc as calc if one.get(patch.FIELD): y = compose(y, {"입력값": {}, **one[patch.FIELD]}, whole) code = (y.get("바탕") or {}).get("로직") if not code: raise sd.DocError("바탕 기초 없는 단가산출은 일위대가 줄로 못 담음 — 그 안 줄을 따로 넣음") row = whole.logic(code) if calc.ratio_of(y, row, whole) != calc.times_of(row): raise sd.DocError("단위 환산 단가산출은 일위대가 줄로 못 담음(값 단위가 바뀜)") ids = sd.line_ids(row) names = {str(s.get("이름")) for s in row.get("입력") or []} at = {c: str(n) for n, c in enumerate(ids)} body = {k: y[k] for k in ("덮개", "뺀줄") if y.get(k) not in EMPTY} body |= {"더한줄": adds} if (adds := filled_adds(y, whole)) else {} line: dict[str, Any] = { "로직": code, "입력값": {k: v for k, v in (y.get("입력값") or {}).items() if k in names}, "별칭": one.get("별칭") or y.get("이름"), "메모": one.get("메모"), "수량": one.get("수량"), } for name in ("고름", "자재표시"): if got := {at[c]: v for c, v in (y.get(name) or {}).items() if c in at}: line[name] = got if y.get("켜쌓기율") is not None: line["켜쌓기율"] = y["켜쌓기율"] return {**line, **({patch.FIELD: body} if body else {}), "본뜬": tag} def _ids(doc: dict) -> list[Any]: from M02_MasterTemplete import M02_Labor_Doc as labor lines = [labor.line_id(x) for x in doc.get("담은로직") or [] if isinstance(x, dict)] return lines + [f"자재|{m.get('코드')}" for m in doc.get("자재") or [] if isinstance(m, dict)] def _twice(doc: dict) -> str | None: seen: set[str] = set() for one in _ids(doc): key = json.dumps(one, ensure_ascii=False, sort_keys=True, default=str) if key in seen: return str(one) seen.add(key) return None def flat_sc(doc: dict, read: Read, whole: Any, trail: tuple = (), strict: bool = True) -> dict: """SC 문서 → M01 키만 — SK 참조 = 로직 줄 + 패치(같은 자리) · SC 참조 = 사본 블록(같은 자리) · `strict` False = 못 펼친 줄 그대로(겹침 · 바탕 없음 · 환산 · 없는 참조).""" lines = [] for at, one in enumerate(doc.get("담은로직") or []): ref = one.get("참조") if isinstance(one, dict) else None kind = refs.kind_of(ref.get("키")) if isinstance(ref, dict) else None if kind not in ("sangeun", "precombo") or refs.is_copy(ref): lines.append(one) continue try: src, path = _read(ref, read, trail) tag = mark(ref, src) if kind == "sangeun": y = flat_sk(sd.clean(src, None, whole), read, whole, path) put = sk_line(y, one, whole, tag) else: body = flat_sc(src, read, whole, path) put = {**one, "참조": refs.copy_ref(body, ref["키"]), "본뜬": tag} rest = (doc.get("담은로직") or [])[at + 1 :] trial = {"담은로직": [*lines, put, *rest], "자재": doc.get("자재") or []} if twice := _twice(trial): raise sd.DocError(f"같은 줄이 겹침 「{twice}」 — 펼쳐 넣지 못함") except FAIL as e: if strict: raise sd.DocError(f"「{ref.get('키')}」 · {e}") from e put = one lines.append(put) return {**doc, "담은로직": lines} # ── 문 ──────────────────────────────────────────────────────────────── def flat_doc( kind: str, doc: Any, read: Read = refs.read_ref, strict: bool = False, me: Any = None ) -> Any: """층 문서 하나 펼침(저장 · 읽기 · 복사 · 옮기기) — 참조 없으면 받은 그대로 · `me` = 이 문서 참조(자기 자신을 담은 줄 = 안 펼침 → 저장 검사가 422).""" if not isinstance(doc, dict) or kind not in refs.PREFIX or not refs.refs_in(doc): return doc whole, trail = _whole(), (refs.ref_id(me),) if isinstance(me, dict) else () if kind == "sangeun": return flat_sk(doc, read, whole, trail, strict) return flat_sc(doc, read, whole, trail, strict) def flat_lines(kind: str, ref: Any, qty: Any = 1, doc: Any = None) -> dict[str, Any]: """찾아 넣기(`-flat` 길) — `kind` 문서에 `ref` 를 넣을 줄 · 못 펼침 = StoreError 400 · SC = 로직 줄(SK) · 사본 블록 줄(SC) 하나 · 지금 화면 문서 `doc` 과 겹치면 400.""" from M02_MasterTemplete import M02_MasterTemplete_Store as store at = refs.here() try: ref = refs.check_ref(ref, "넣을 문서", at) whole = _whole() if kind == "sangeun": return {"더한줄": into_sk(ref, qty if qty not in EMPTY else 1, refs.read_ref, whole)} got = flat_sc({"담은로직": [{"참조": ref, "수량": None}]}, refs.read_ref, whole) host = doc if isinstance(doc, dict) else {} trial = {"담은로직": [*(host.get("담은로직") or []), *got["담은로직"]]} if twice := _twice({**trial, "자재": host.get("자재") or []}): raise sd.DocError(f"이미 담은 줄과 겹침 「{twice}」") return {"담은로직": got["담은로직"], "자재": []} except FAIL as e: raise store.StoreError(getattr(e, "status", 400), str(e)) from e # ── 초기화 ──────────────────────────────────────────────────────────── def _reprice(fx: dict) -> tuple[dict, int]: """이음 기계 고정 줄(736-3) 단가 = M01 기계 한 시간 값 다시 받음 — (줄, 바뀐 수).""" from M02_MasterTemplete import M02_Machine_Hourly as machine key = (fx.get("이음") or {}).get("기계") got = machine.hourly(str(key)) if key else {} if not got.get("ok"): return fx, 0 price = {k: got[k] for k in sd.PARTS.values()} return {**fx, "단가": price}, int(price != fx.get("단가")) def _reset_adds(adds: list) -> tuple[list, int]: out, n = [], 0 for one in adds or []: one = dict(one) if isinstance(one, dict) else one if not isinstance(one, dict): out.append(one) continue if "단가" in one: one.pop("단가") n += 1 n += len(one.pop("덮개", None) or {}) + len(one.pop("뺀줄", None) or []) one.pop("바탕판", None) if one.get("더한줄"): one["더한줄"], k = _reset_adds(one["더한줄"]) n += k if isinstance(one.get("고정"), dict): one["고정"], k = _reprice(one["고정"]) n += k out.append(one) return out, n def _reset_sc(doc: dict) -> tuple[dict, int]: """일위대가 초기화 — 줄 패치 · 사본 블록 몸 · 이음 기계 고정 줄.""" from M02_MasterTemplete import M02_Labor_Patch as patch lines, n = [], 0 for one in doc.get("담은로직") or []: if not isinstance(one, dict): lines.append(one) continue ref, body = one.get("참조"), one.get(patch.FIELD) if refs.is_copy(ref): inner, k = _reset_sc(ref["몸"]) n += k one = {**one, "참조": refs.copy_ref(inner, ref["키"])} elif isinstance(one.get("고정"), dict): fx, k = _reprice(one["고정"]) n += k one = {**one, "고정": fx} elif body: adds, k = _reset_adds(body.get("더한줄") or []) n += k + len(body.get("덮개") or {}) + len(body.get("뺀줄") or []) keep = {x: v for x, v in body.items() if x not in ("덮개", "뺀줄", "더한줄")} rest = {**keep, **({"더한줄": adds} if adds else {})} one = {x: v for x, v in one.items() if x != patch.FIELD} one = {**one, **({patch.FIELD: rest} if rest else {})} lines.append(one) return {**doc, "담은로직": lines}, n def reset(kind: str, doc: dict) -> tuple[dict, int]: """[초기화] — (걷은 문서, 걷힌 고침 수) · 먼저 펼침(옛 참조 줄도 M01 키로).""" doc = flat_doc(kind, doc) if kind == "sangeun": adds, n = _reset_adds(doc.get("더한줄") or []) n += len(doc.get("덮개") or {}) + len(doc.get("뺀줄") or []) return {**doc, "덮개": {}, "뺀줄": [], "더한줄": adds}, n return _reset_sc(doc) def reset_doc(kind: str, key: str | None, doc: Any, look: bool, version: str | None) -> dict: """`-reset` 길 — 미리 보기 = 걷힐 수 · 아니면 지금 층에 씀(판이 오면 그 판일 때만).""" from M02_MasterTemplete import M02_MasterTemplete_Store as store if doc is None: if not key: raise store.StoreError(400, "문서 또는 키") found = store.read(kind, key) doc, version = found["문서"], version or found["판"] if not isinstance(doc, dict): raise store.StoreError(400, "문서가 묶음 아님") got, n = reset(kind, doc) if look: return {"걷힘": n, "문서": got} name = str(got.get("키") or key or "") if not version: version = store.read(kind, name)["판"] store.write(kind, name, version, got) return {"걷힘": n, "문서": store.read(kind, name)["문서"]} # ── 옮기기 ──────────────────────────────────────────────────────────── def _value(ref: refs.Ref, read: Read, whole: Any) -> Any: from M02_MasterTemplete.M02_Labor_Expand import Expand, inner_pills given = {p["키"]: 1 for p in inner_pills(ref, read)} got = Expand(whole, read, given)(ref).get("기준") return None if got is None else {k: round(float(v), 6) for k, v in got.items()} def migrate(apply: bool = False) -> list[dict[str, Any]]: """저장소 층(마스터 · 회사 · 개인) SK · SC 의 M02 참조 줄을 펼쳐 다시 씀 — 줄마다 결과 {어디, 키, 펼침, 같음, 씀, 까닭} · `apply` False = 보기만.""" from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Template_Layers as layers whole, out = _whole(), [] for kind in ("sangeun", "precombo"): for where_, path in layers.layer_files(kind): try: doc = json.loads(path.read_bytes()) except (OSError, ValueError): continue if not refs.refs_in(doc): continue layer, owner = refs.where(path.parent.parent) ref = {"층": layer, "주인": owner, "키": path.stem} got = flat_doc(kind, doc) left = [ r.get("키") for r in refs.refs_in(got) if refs.kind_of(r.get("키")) != "promote" and not refs.is_copy(r) ] row = { "어디": where_, "키": path.stem, "펼침": left == [], "남은참조": left, "씀": False, } def mine(r: refs.Ref, _got: dict = got, _rid: str = refs.ref_id(ref)) -> dict | None: return _got if refs.ref_id(r) == _rid else refs.read_ref(r) row["전"], row["후"] = _value(ref, refs.read_ref, whole), _value(ref, mine, whole) row["같음"] = row["전"] == row["후"] if not row["같음"]: row["까닭"] = "전후 금액 다름 — 안 옮김" elif apply and got != doc: with store.at(path.parent.parent): store.write(kind, path.stem, store.read(kind, path.stem)["판"], got) row["씀"] = True out.append(row) return out if __name__ == "__main__": # 브레인 — `python -m M02_MasterTemplete.M02_Library_Flat [--apply]` import sys for one in migrate("--apply" in sys.argv): print(json.dumps(one, ensure_ascii=False, default=str))