Files
Aislo/M02_MasterTemplete/M02_Doc_Import.py
T
eomsangdonandClaude Opus 5.5 ae8d098a10 fix(815-3): 합친 뒤 시험 셋 가름 · 시간당 경비 식 조종원 차례 · 호표 기계 글 머리 꼬리 · 합친 판 요약 다시 잼
- 809_1 이름 = 진짜 흠 + 낡은 기대 — 읽기기(812-1)가 지은 율 줄 이름 「율」이 엑셀 글 머리로 안 바뀜 → `Import_Shape._MADE` 에 율 더함 · 합계 = 차림 글줄 · 기계 = 한 줄(이름 · 규격 칸) 은 낡은 기대 고침 · 금액 15,935 · 336 · 723 · 16,994 그대로
- 812_9 별칭 = 낡은 기대 — 815-2 가 아스팔트페이버 별칭을 마스터 기계 줄 칸으로 옮겨 「넣기 전 못 찾음」 이 안 맞음 · 시험은 마스터 칸으로 봄 · 부르는 곳 없는 `M02_Machine_Find.learn`(옛 장부 쓰기) · `by_code` · `same_kind`(걷은 연결 찾기 몫) 지움
- 804_2 hourly = 낡은 기대 — 814-1 이 GC000268 목록을 117 → 209 로 늘려 겹친 116 대 고름이 GC000269 로 뒤집힘(116 대 모두 원 단위 같음) · 같은 로직 줄만 대던 시험 → 같은 갈래 식(부착 · 손료만 · 조종원) 줄 모두 댐
- 진짜 흠(브레인 갈래 가) — GC000272 「조종원 없는 기종」 목록에 기계표 조종원 1 인 9 대가 겹쳐 목록 길이 차례로 고름 → `logic_of` = 기계표 조종원 > 0 이면 조종원 호표 든 식 먼저 · 불러오기 `hourly_link` 도 같은 길 · 바뀐 4 대 = 진동롤러(핸드가이드식) 0.7ton 6,557 → 41,969 · 래머 80kg 1,959 → 23,708 · 플레이트 콤팩터 1.5ton 2,855 → 24,604 · 발전기 100kW 45,535 → 67,284(M01 문서 금액 그대로) · 시험에 9 대 굳힘
- 778_5 기계 표 = 진짜 흠 — 812-9 찾기가 호표 기계 글 「운 반 : 덤프트럭(4.5Ton)」 · 「…[hr] : 90%」 · 「(90%)」 머리 꼬리를 안 뗌 → `Hopyo_Fixed._eqs` 쌍점 조각 · 율 꼬리 뗌 · 이어짐 187 · 고를 것 1 · 못 이음 2(조합 한 줄 · 괄호 깨짐) · 모두 이음 블록 161
- 721 단추 자리 = 낡은 기대(808-1 [단가 갱신][재생성] tools 더함)
- 815-1 보고 요약에 「합친 뒤」 열 — 기계 이음 95.7 % · 우리 값 원파일 1,891 · 0 원 줄 블록 147

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 16:32:28 +09:00

541 lines
24 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 802-4b-1 · 요구 664 · 665) — 서버 단독.
경쟁사 산근 블록도 같은 꼴(식 + 값)로 들어옴 — 엑셀 단가 그대로 엔진 줄(`가져온.로직` + SK 고침)을
`master_doc_convert` 로 문서로 옮기고(손 값 = 숫자 연결 · 중간 = 식 줄 · 덧줄 = 금액 줄 식 · 끝수 ·
단계 · 조정 · 합밖 · 자리 그대로) 이은 것을 연결로 얹음 · 연결마다 엑셀 값을 기억(`엑셀` 칸):
인력 · 재료 · 유가 · 전력 · 환율 이음 = 요소 {키} · 기계 이음(EQ) = 기계경비 단가산출
{키 GC, 입력 기계 …, 칸 = 그 줄 비목}(시간당 자료 없음 = 숫자 그대로 · 설명) ·
로직 이음 「로직(GC…)」 = 단가산출 {키, 입력} · 산근 속 산근(797-1 참조) = 단가산출
{키 형제 SK, 참조 {층, 주인}, 칸?} ·
소요량 · 계수 고른 것(797-2 `가져온.연결.고름`) = 그 수 자리에 표 연결 기호(pct · kgt = ÷ 배).
문서 머리 `가져온` = {파일, 시트, 행, 갈래, 코드?, 표지(797-1a · 못 읽음 까닭 그대로), 알림?}.
셈(`run`) — 단가기준 엑셀(기본) = 엑셀 값 기억 연결을 숫자로 보고 셈 · 우리 = 연결 값으로 셈 ·
형제 SK 는 늘 연결(부르는 쪽 단가기준으로 · 안쪽이 바뀌면 따라감).
"""
from __future__ import annotations
import copy as cp
from decimal import Decimal
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Import_Shape as shape
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Imported as imp
XL = "엑셀" # 연결 칸 — 엑셀 값 기억
HEAD = (
"파일",
"시트",
"행",
"갈래",
"코드",
"원파일",
"원코드",
"규격",
"확정",
) # 뒤 셋 = 원파일(810-3)
GUESS = "추정" # 원파일 품셈 조건 글 표지(810-2 `M02_Doc_NativeEst.GUESS`)
SUB = "원파일 소계" # 원파일 소계 덧줄(810-2 `M02_Doc_NativeEst`) — 합 밖 · 비목 칸 비움
PICK = "소요량" # 고른 표 칸 기호 이름 머리
_SCALE = {"eq": (1,), "pct": (100, 0.01), "kgt": (1000, 0.001)}
def is_imported(sk: Any) -> bool:
return imp.is_imported(sk)
def basis_of(sk: dict) -> str:
return "우리" if (sk.get(imp.KEY) or {}).get("단가기준") == "우리" else "엑셀"
# ── 엑셀 단가 그대로 엔진 줄(옛 셈 길 · SK 고침 얹은 것) ───────────────────
def _ctx(sk: dict, whole: Any) -> tuple:
"""엑셀 단가 기준 고친 줄 — 옛 셈 길(`OLD`)로 돌려 엔진에 넣은 (색인, 키, 입력, 답, 재료)."""
from M02_MasterTemplete import M02_Doc_Sk as dsk
from M02_MasterTemplete import M02_Sangeun_Calc as calc
raw = {**sk, imp.KEY: {**sk[imp.KEY], "단가기준": "엑셀"}}
box: list = []
token = dsk.OLD.set(True)
try:
calc.run(sd.clean(raw, None, whole), whole, catch=box)
finally:
dsk.OLD.reset(token)
return box[-1]
# ── 소요량 · 계수 고른 칸 → 수 자리에 기호 ──────────────────────────────
def _swap_nth(node: Any, n: int, new: tuple, seen: list) -> Any:
"""`M02_Sangeun_QtyLinks._literals` 차례의 n 번째 수 → `new` 마디(같은 걸음 규칙)."""
mf = dc.cv.mf
if not isinstance(node, tuple):
return node
kind = node[0]
if kind == "num":
seen[0] += 1
return new if seen[0] - 1 == n else node
if kind == "bin":
cmp_ = node[1] in mf._OPS
sides = [
side
if cmp_ and isinstance(side, tuple) and side[0] == "num"
else _swap_nth(side, n, new, seen)
for side in node[2:]
]
return (kind, node[1], *sides)
if kind in ("neg", "field"):
return (kind, _swap_nth(node[1], n, new, seen), *node[2:])
if kind == "call":
return ("call", node[1], [_swap_nth(a, n, new, seen) for a in node[2]])
return node
def _conds(table: dict, at: int) -> dict | None:
"""표 줄 차례 → 그 줄 하나만 맞는 조건 {조건: 값}(범위 = 위 끝 › 아래 끝) · 못 가림 = None."""
rows = table.get("줄") or []
if not (0 <= at < len(rows)):
return None
row, out = rows[at], {}
for cond, how in (table.get("조건") or {}).items():
if cond not in row:
continue
have = row[cond]
if how == "범위":
low, high = have
have = high if high is not None else low
if have is None:
return None
out[cond] = have
try:
hit = dc.ml.find(table, out)
except Exception: # noqa: BLE001 — 못 가림 = 수 그대로
return None
return out if hit is row else None
def _picks(row: dict, ids: list[str], base: list[str], meta: dict, master: Any) -> dict:
"""고른 칸(`가져온.연결.고름`) → 중간 줄(표 찾기 하나 · 연결 기호) · 수 자리에 그 기호 ÷ 배 ·
돌려줌 = {중간 이름: 엑셀 칸 값}. 자리 줄이 SK 고침으로 바뀌었으면 건너뜀."""
mf, mee = dc.cv.mf, dc.cv.mee
src = meta["로직"]
picked = ((meta.get("연결") or {}).get("고름")) or []
out: dict[str, Any] = {}
mids = row.setdefault("중간", [])
taken = {str(x.get("이름")) for x in [*(row.get("입력") or []), *mids]}
for one in picked:
part, i, n = str(one.get("자리") or "::").split(":")[:3]
field = "수량" if part == "호표" else "식"
lines = src.get(part) or []
if not (i.isdigit() and n.isdigit() and int(i) < len(lines)):
continue
cid = base[int(i) + (len(src.get("호표") or []) if part == "덧줄" else 0)]
if cid not in ids:
continue
at = ids.index(cid)
mine = (row["호표"] if at < len(row["호표"]) else row.get("덧줄") or [])[
at if at < len(row["호표"]) else at - len(row["호표"])
]
if str(mine.get(field)) != str(lines[int(i)].get(field)):
continue # SK 덮개가 그 식을 바꿈 — 수 자리 어긋남
try:
table = master.get(str(one.get("표")))
conds = _conds(table, int(one.get("줄")))
node = mf.parse(str(mine[field]))
except Exception: # noqa: BLE001 — 표 · 식 못 읽음 = 수 그대로
continue
if conds is None:
continue
name = next(f"{PICK}{k}" for k in range(1, 999) if f"{PICK}{k}" not in taken)
taken.add(name)
scale = next(
(m for m in _SCALE.get(str(one.get("꼴")), (1,)) if _near(one, m)), None
) # 엑셀 수 = 칸 값 ÷ 배
if scale is None:
continue
ref = (
("name", name)
if scale == 1
else ("bin", "/", ("name", name), ("num", Decimal(str(scale))))
)
mine[field] = mee.raw_text(_swap_nth(node, int(n), ref, [0]))
args = ", ".join(f"{k}={_lit(v)}" for k, v in conds.items())
mids.append(
{"이름": name, "식": f"찾기({one['표']}{', ' if args else ''}{args}).{_q(one['칸'])}"}
)
out[name] = Decimal(str(one.get("값")))
return out
def _near(one: dict, m: float) -> bool:
cell, v = float(one.get("값") or 0), float(one.get("수") or 0)
return abs(cell - v * m) <= 1e-9 * max(1.0, abs(cell))
def _lit(v: Any) -> str:
if isinstance(v, (int, float, Decimal)) and not isinstance(v, bool):
return dc.cv.mf._plain(Decimal(str(v)))
return "'" + str(v).replace("'", "") + "'"
def _q(name: Any) -> str:
return dc.cv.mee._q(str(name))
# ── 이음 → 연결 ────────────────────────────────────────────────────────
def hourly_link(eq: str, part: str) -> dict | None:
"""기계(EQ) 시간당 그 비목 = 기계경비 단가산출 부름(`M02_Machine_Hourly` 와 같은 짝 고름) ·
자료 없음 = None."""
from M02_MasterTemplete import M02_Machine_Hourly as machine
if part not in imp.COSTS or not imp.hourly_of(eq).get("ok"):
return None
code = str(imp._whole().get(eq).get("원문번호") or "")
logic = machine.logic_of(code)
if not logic:
return None
given = {"기계": _lit(code)}
for spec in logic.get("입력") or []:
if spec.get("이름") != "기계":
given[spec["이름"]] = _lit((spec.get("고르기") or [""])[0])
return {"갈래": "단가산출", "키": str(logic["키"]), "입력": given, "칸": part}
def _logic_link(text: str) -> dict | None:
"""「로직(GC…, 이름='값')」 → 단가산출 연결 · 인자가 수 · 글 아니면 None."""
try:
node = dc.cv.mf.parse(text)
except Exception: # noqa: BLE001
return None
if node[0] != "logic" or any(v[0] not in ("num", "str") for _, v in node[2]):
return None
return {"갈래": "단가산출", "키": node[1], "입력": {k: _lit(v[1]) for k, v in node[2]}}
def _link_of(item: dict) -> tuple[dict | None, str]:
"""가져온 줄 하나의 연결(엑셀 값 기억 없이) · 설명(못 이은 까닭)."""
if item.get("참조"):
ref = item["참조"]
link = {
"갈래": "단가산출",
"키": ref["키"],
"참조": {"층": ref["층"], "주인": ref.get("주인")},
}
return link | ({"칸": item["참조비목"]} if item.get("참조비목") else {}), ""
key = str(item.get("이음") or "")
if not key and (item.get("자원") or {}).get("조합식"):
return None, f"우리 조합 식 {item['자원']['조합식']} 에 듦(첫 줄) — 이 줄 값 0"
if not key:
return None, ""
if item.get("종류") == "로직":
return _logic_link(key), ""
if key.startswith("EQ"):
got = hourly_link(key, str(item.get("비목") or ""))
return got, "" if got else f"기계 {key} 시간당 경비 자료 없음 — 엑셀 단가 그대로"
if key.startswith("MO") and (got := _oil(key)):
return got, ""
link = {"갈래": "요소", "키": key}
if key[:2] == dc.cv.mm.LINKED: # 품셈재료 = 줄 단위로 값이 갈림(M01 변환과 같음)
link["단위"] = str(item.get("단위") or "")
return link, ""
OPTION = "_옵션" # 연결이 부르는 옵션 변수(문서에 없으면 `_attach` 가 더함)
def _oil(key: str) -> dict | None:
"""유가 · 전력 이음(814-1 · 요구 748) — 우리 기초 꼴: 유가 = `MO:경유:{S_지역}` + 지역 옵션(기본
전국평균) · 전력 = 전력량요금 계약종별 고르기 + 계약종별 옵션(기본 일반용(갑)Ⅰ 저압)."""
whole = imp._whole()
try:
row = whole.get(key)
except Exception: # noqa: BLE001 — 없는 키 = 보통 요소 연결
return None
rows = list(whole.index.get("MO", {}).values())
if row.get("구분") == "전력":
kinds = list(dict.fromkeys(str(r.get("상세구분")) for r in rows if r.get("구분") == "전력" and r.get("이름") == row.get("이름"))) # fmt: skip
var = _choice("전력 계약종별", "전력계약종별", kinds, str(row.get("상세구분")))
link = {
"갈래": "요소",
"재료": {"이름": row.get("이름"), "계약종별": "입력"},
"단위": "㎾h",
}
return link | {"계약종별": dc.md.var_id(var), OPTION: var}
head = str(row.get("원문번호") or "").split(":")[0]
if not head or row.get("구분") != "유류":
return None
places = [
str(r.get("상세구분")) for r in rows if str(r.get("원문번호") or "").split(":")[0] == head
]
places.sort(key=lambda x: x != "전국평균")
var = _choice("지역", "지역", places, str(row.get("상세구분")))
return {"갈래": "요소", "키": f"MO:{head}:{{{dc.md.var_id(var)}}}", OPTION: var}
def _choice(key: str, below: str, items: list[str], value: str) -> dict:
link = {"갈래": "선택", "목록": items, "값": value}
return {
"열쇠": key,
"기호": "S",
"아래": below,
"위": "",
"이름": key,
"단위": "",
"연결": link,
}
MACHINES = "_기계" # 문서 셈 동안 잠깐 — `derive` 가 머리 `가져온.기계` 로 옮김
def _machine_entry(item: dict, sym: str) -> dict:
"""기계 줄 찾기 판정 한 칸 — {기호, 원래 {이름, 규격, 단위}, 판정, 키, 후보, 찾기어, 까닭, 조합?}."""
res = item["자원"]
got = res["찾기"]
one = {
"기호": sym,
"원래": {"이름": got["이름"], "규격": got["규격"], "단위": item.get("단위")},
}
one |= {k: got.get(k) for k in ("판정", "키", "후보", "찾기어", "까닭")}
return one | {k: True for k in ("조합", "조합첫") if res.get(k)}
def _excel(item: dict) -> Any:
if item.get("값묶음"):
return {k: Decimal(str(item["값묶음"].get(k) or 0)) for k in imp.COSTS}
return Decimal(str(item["단가"])) if item.get("단가") is not None else None
def _attach(doc: dict, row: dict, ids: list[str], src: dict[str, dict]) -> None:
"""금액 줄 단가 숫자(엑셀 값) → 이은 연결 + 엑셀 값 기억(SK 고침으로 바뀐 값 = 숫자 그대로) ·
기계 찾기 판정(812-9) = 문서 `가져온.기계`(단가 기호마다 · 화면 고르기 · 저장 때 별칭)."""
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
found: list[dict] = doc.setdefault(MACHINES, [])
for at, line in enumerate((doc.get("금액") or [])[: len(row["호표"])]):
item = src.get(ids[at]) if at < len(ids) else None
price = str(line.get("단가") or "")
var = by_id.get(price.removesuffix(".계"))
if item is not None and var is not None and (item.get("자원") or {}).get("찾기"):
found.append(_machine_entry(item, dc.md.var_id(var)))
if item is None or var is None or (var.get("연결") or {}).get("갈래") != "숫자":
continue
excel = _excel(item)
if not item.get("참조") and var["연결"].get("값") != excel:
continue # SK 덮개 단가 = 사용자 값
link, note = _link_of(item)
if note:
var["설명"] = note
if link is None:
if not note and not item.get("이음"): # 못 이은 까닭(814-1 — 흠 아님 · 우리 정보 없음)
from M02_MasterTemplete import M02_San_Link as san_link
why = san_link.why(item)
var["설명"] = (
why if why == san_link.QUOTE else f"이음 없음 — {why} · 엑셀 단가 그대로"
)
continue
if (opt := link.pop(OPTION, None)) and opt["열쇠"] not in {
v.get("열쇠") for v in doc["변수"]
}:
doc["변수"].append(opt) # 유가 지역 · 전력 계약종별 옵션(문서에 하나)
if item.get("참조비목") and isinstance(excel, dict):
excel = excel[item["참조비목"]]
line["단가"], line["비목"] = dc.md.var_id(var), item["참조비목"]
var["연결"] = {**link, XL: excel}
def _numbers(doc: dict, keep: dict) -> None:
"""수 하나뿐인 식 줄(EST 얼린 변수) → 숫자 연결 · 고른 칸 중간 → 엑셀 칸 값 기억."""
by_id = {dc.md.var_id(v): v for v in doc.get("변수") or []}
lines = []
for one in doc.get("식") or []:
var = by_id.get(str(one.get("기호")))
try:
node = dc.md.parse(one.get("식"))
except dc.md.DocError:
node = None
if var is not None and node is not None and node[0] == "num":
var["연결"] = {"갈래": "숫자", "값": node[1]}
continue
lines.append(one)
doc["식"] = lines
for var in doc.get("변수") or []:
if var.get("열쇠") in keep and (var.get("연결") or {}).get("갈래") == "표":
var["연결"][XL] = keep[var["열쇠"]]
def derive(sk: dict, ctx: tuple | None = None, whole: Any = None) -> dict:
"""가져온 식 SK → 수식 문서(키 = SK 키) — `ctx` = 엑셀 단가 기준 옛 셈 길이 엔진에 넣은 것."""
meta = sk[imp.KEY]
if ctx is None:
ctx = _ctx(sk, whole or imp._whole())
master, key, values, got, _ = ctx
row = cp.deepcopy(master.logic(key))
ids = list(got.get("정체") or [])
bare = imp.row_of({**sk, imp.KEY: {**meta, "단가기준": "엑셀"}}) or {}
base = sd.line_ids(bare)
src = dict(zip(base, meta["로직"].get("호표") or []))
keep = _picks(row, ids, base, meta, master)
given = dc.given_of(master, key, values)
doc, notes = dc.cv.convert(_Row(master, key, row), row, given)
_attach(doc, row, ids, src)
_numbers(doc, keep)
native = bool((meta.get("원본") or {}).get("원파일"))
if native or (meta.get("원본") or {}).get(
"엑셀"
): # 810-2 · 812-1 — 소계 비목 칸 비움 · 값 변수 출처
_native(doc, native)
head = {k: (meta.get("원본") or {}).get(k) for k in HEAD if (meta.get("원본") or {}).get(k)}
marks = list((meta.get("원본") or {}).get("표지") or [])
if native or (meta.get("원본") or {}).get(
"엑셀"
): # 810-3 — 품셈 조건 글(값 없음) = 「추정」 칸
head["추정"] = [
{"행": x.get("행"), "글": " ".join(str(x.get("글") or "").split())}
for x in meta["원본"].get("글줄") or []
if GUESS in str(x.get("표지") or "")
]
if not (doc.get("금액") or doc.get("결과") or row["호표"] or row.get("덧줄")):
_empty(doc) # 빈 산근 = 엑셀 0 원(줄 없음 합) — 「—」 아님
marks.append(EMPTY)
found = doc.pop(MACHINES, None)
head |= {"기계": found} if found else {} # 812-9 기계 찾기 판정(줄 쌓기 고르기 · 저장 때 별칭)
doc["가져온"] = head | {"표지": marks} | ({"알림": notes} if notes else {})
shape.shape(doc, meta) # 809-1 · 요구 704 — 지은 이름 → 엑셀 글 머리 · EST 기계 세 줄 = 한 줄
if found: # 세 줄을 한 줄로 합쳐 사라진 기호의 기계 판정은 뺌
alive = {dc.md.var_id(v) for v in doc.get("변수") or []}
doc["가져온"]["기계"] = [x for x in found if x["기호"] in alive]
return {**doc, "키": str(sk.get("키") or key), "이름": str(sk.get("이름") or doc.get("이름"))}
def _native(doc: dict, native: bool = True) -> None:
"""원파일 · 엑셀 읽기기 문서 — 소계 덧줄(합 밖) 비목 칸 비움(금액만) · 수 하나 변수(옵션 칸) 출처
「원파일」(엑셀 = 출처 없음 → 화면 꼬리표 「엑셀」)."""
for line in doc.get("금액") or []:
if line.get("출처") == SUB and line.get("합밖"):
line.pop("비목", None)
for var in doc.get("변수") or []:
if native and (var.get("연결") or {}).get("갈래") == "숫자" and XL not in var["연결"]:
var["출처"] = "원파일"
EMPTY = "참고 · 빈 산근 — 엑셀 줄 없음 · 금액 0 그대로"
def _empty(doc: dict) -> None:
"""줄 없는 엑셀 산근 — 0 묶음 줄 하나(엑셀 금액 0 = 줄 없는 합 · 지은 값 아님)."""
doc["변수"].append(
{"열쇠": "P_빈", "기호": "P", "아래": "빈", "위": "", "이름": "빈 산근", "단위": "",
"연결": {"갈래": "숫자", "값": dict.fromkeys(imp.COSTS, Decimal(0))}}
) # fmt: skip
doc["금액"] = [{"이름": "빈 산근", "단위": "식", "비목": "묶음", "단가": "P_빈"}]
class _Row:
"""변환 색인 — 이 SK 줄만 고친 줄(소요량 중간 더함)로 · 그 밖 그대로."""
def __init__(self, master: Any, key: str, row: dict) -> None:
self._m, self._k, self._r = master, key, row
def logic(self, ref: str) -> dict:
return self._r if ref == self._k else self._m.logic(ref)
def __getattr__(self, name: str) -> Any:
return getattr(self._m, name)
# ── 셈 ────────────────────────────────────────────────────────────────
def view(doc: dict, basis: str) -> dict:
"""단가기준 엑셀 = 엑셀 값 기억 연결(형제 SK 빼고)을 숫자로 · 우리 = 그대로 ·
TS 짝 = `common_util/common_util_formula_doc_import.ts` `importView`(거울 시험 test_802_4b1)."""
if basis == "우리":
return doc
out = cp.deepcopy(doc)
for var in out.get("변수") or []:
link = var.get("연결") or {}
if XL in link and not str(link.get("키") or "").startswith("SK-"):
var["연결"] = {"갈래": "숫자", "값": link[XL]}
return out
class Links(dc.ml.Links):
"""M01 연결(지금 M01 · 수식 문서 폴더) + 형제 가져온 식 SK(부르는 쪽 단가기준)."""
def __init__(self, master: Any, basis: str, trail: tuple = ()) -> None:
super().__init__(master, dc.folder())
self.basis, self.trail, self.mine = basis, trail, {}
def doc(self, key: str) -> dict | None:
if not str(key).startswith("SK-"):
return super().doc(key)
return self.mine.get(key)
def learn(self, doc: dict) -> None:
"""문서의 형제 SK 연결 → 그 SK 문서를 읽어 둠(깊이 · 순환 = 못 읽음 · 표지는 계산기)."""
for var in doc.get("변수") or []:
link = var.get("연결") or {}
key, ref = str(link.get("키") or ""), link.get("참조")
if not key.startswith("SK-") or key in self.mine or not isinstance(ref, dict):
continue
full = {**ref, "키": key}
rid = refs.ref_id(full)
if rid in self.trail or len(self.trail) >= refs.DEPTH:
continue
sib = refs.read_ref(full)
mine = ((sib or {}).get(dc.KEY) or {}).get(
"문서"
) # 우리 기초 사본으로 바꾼 형제(810-4)
if not is_imported(sib) and not isinstance(mine, dict):
continue
self.mine[key] = None # 순환 막음
try:
sub = view(of(sib), self.basis) if is_imported(sib) else {**mine, "키": key}
except Exception: # noqa: BLE001 — 못 지음 = 없는 단가산출(연결 끊김 표지)
continue
self.mine[key] = sub
self.trail = (*self.trail, rid)
self.learn(sub)
def of(sk: dict) -> dict:
"""SK 의 문서 — 수식 길로 고친 것(따로) = 저장본 · 아니면 지금 줄에서 지음."""
got = sk.get(dc.KEY) if dc.has(sk) else None
if got and got.get("따로") and isinstance(got.get("문서"), dict):
return got["문서"]
return derive(sk)
def record(doc: dict, sk: dict) -> dict[str, Any]:
"""수식 탭 화면 캐시(`M02_Doc_Screen.record` 짝) — 단가기준 보기 문서 셈이 쓴 연결 · 형제 SK 문서."""
import master_doc_record as mr # `M02_Doc_Copy` 가 scripts 를 길에 둔 뒤
basis = basis_of(sk)
links = Links(imp._whole(), basis)
mine = view(doc, basis)
rec = mr.Rec(links)
try:
links.learn(mine)
rec.prefetch(mine)
dc.mc.calc(mine, rec)
except Exception: # noqa: BLE001 — 셈 틀림 = 셈 답 표지 · 기록은 모은 데까지
pass
return rec.out()
def run(doc: dict, sk: dict, master: Any = None) -> dict:
"""가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc`."""
basis = basis_of(sk)
links = Links(master or imp._whole(), basis)
mine = view(doc, basis)
try:
links.learn(mine)
return dc.mc.calc(mine, links)
except Exception as e: # noqa: BLE001 — 계산기 예외 없음이 원칙 · 남은 틈도 「—」
return {
"셈됨": False,
"표지": [{"갈래": "셈 틀림", "글": str(e)}],
**dict.fromkeys(dc.COSTS),
}