feat(b09): 호표 갈래 뒤집기 — 일위대가화 줄 = 대가 · 단가산출 줄 = 산근(155-3 · 155-4)

- 일위대가목록 = 구조물 조합 + 사전조합 + 일위대가화(승격 · 옛 SP) 줄 · 단가산출 줄 = 산근 줄
- 산근 줄 수량 = 수량 그대로(배 ≠ 1 로직 금액 1/배 흠) · 대가 줄만 ÷ 배
- 품셈 블록 로직 줄 판정 = 산근 풀이와 같은 call_of(기계경비 = 중기 · 그 밖 = 하위 산근)
- 비고 「산근 n호표」(Calc_Labor 파이썬 · TS 짝) · 산근 줄 누름 = 단가산출근거 탭
- 산근목록 원천 = 단가산출 줄 + 사전조합 안 줄 + 일위대가화 줄 하위 로직(노무 · 자재 · 중기 줄 제외)
- 산근 자재 자리표시 키(MO:경유:…) = 자재단가대비표 키로
- 시험: 145-1 다섯 · 145-2 셋 새 갈래로(머리에 근거 한 줄) · 새 test_155_b09_sources

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
This commit is contained in:
2026-10-09 19:20:39 +09:00
co-authored by Claude Opus 5.5
parent 4ebee1197a
commit 82a3cb5e7d
8 changed files with 328 additions and 119 deletions
@@ -9,6 +9,7 @@
노무 줄 = 노무비목록표 단가 · 재료 줄 = 재료비목록표 단가 ·
중기 줄 = 중기목록표(노무 · 재료 · 경비)
대가 줄 = 부른 블록 합계(노무 · 재료 · 경비) · 산근 줄 = 145-1 산근 단가(`sangeun` {키: 값})
비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
% 줄 = 밑수(그 블록 노무 줄 금액 합) × % / 100
끝수 = 엑셀 자리 그대로 — 줄 금액 = ROUNDDOWN(단가 × 수량, 1) ·
% 줄 = ROUNDDOWN(밑수 × % / 100, 1) · 관급 · 별도계상 자재 = 대가 안 0원 ·
@@ -159,6 +160,9 @@ def compute_labor(
for line in got["lines"]:
if line["kind"] == "daega" and line.get("key") in no:
line["ref_no"] = f"대가{no[line['key']]}"
sg_no = (books["sangeun"].get(line.get("key")) or {}).get("no")
if line["kind"] == "sangeun" and sg_no is not None:
line["ref_no"] = f"산근 {sg_no}호표" # 155-3 비고(엑셀 꼴)
out_blocks.append(got)
listing = [
{
@@ -8,6 +8,7 @@
*
* 노무 줄 = 노무비목록표 · 재료 줄 = 재료비목록표 · 중기 줄 = 중기목록표 · 대가 줄 = 부른 블록 합계 ·
* 산근 줄 = 145-1 산근 단가 · % 줄 = 밑수(노무 줄 금액 합) × % / 100
* 비고 = 대가 줄 「대가n」 · 산근 줄 「산근 n호표」(산근 목록 번호)
* 줄 금액 ROUNDDOWN(단가 × 수량, 1) · 블록 비목 합 ROUNDDOWN(Σ, 0) · 합계 = 세 비목 합
* 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈)
* 빈칸 = 원천이 없으면 null · 그 칸을 쓰는 합도 null · 「연결 없음」 목록(gaps)
@@ -147,6 +148,10 @@ export function computeLabor(daega: Row | null | undefined, sheets: Row, sangeun
const got: Row = { ...done[b.id], no: no[b.id] };
for (const line of got.lines)
if (line.kind === "daega" && line.key in no) line.ref_no = `대가${no[line.key]}`;
for (const line of got.lines) {
const sgNo = (books.sangeun[line.key] || {}).no;
if (line.kind === "sangeun" && sgNo != null) line.ref_no = `산근 ${sgNo}호표`; // 155-3 비고
}
return got;
});
const listing = outBlocks.map((b) => ({
+39 -36
View File
@@ -5,11 +5,14 @@
조합 구조물 조합 사본 하나 = 블록(엑셀 「조합」 꼴) · 줄 = 담은 로직 줄 · 자재 줄
사전조합 참조 SC = 겹 대가(굳힌 `참조사본`) · 줄 = 그 안 로직 · 참조 줄(수량 = 손)
품셈 승격 아닌 로직 줄 = 로직마다 따로(노무 · 재료 · 중기 · % 줄) — 조합이 수량만큼 부름
산근 승격 줄 · 옛 참조 SP = 산근 호표 참조(키 = 145-1 `sangeun_key` · 없으면 자리만)
품셈 일위대가화(승격) 줄 · 옛 참조 SP = 로직마다 따로(노무 · 재료 · 중기 · 산근 · % 줄)
— 조합이 수량만큼 부름 · 기계 = 기계경비 로직 줄 · 다른 돈 로직 줄 = 하위 산근
산근 단가산출 줄(일위대가화 아닌 로직 줄) = 산근 호표 참조(키 = 145-1 `sangeun_key`)
(155-3 · 사용자 뜻 = 일위대가화 줄 = 대가 · 단가산출 줄 = 산근 · 145 정함 뒤집음)
같은 블록 = 같은 꼴 → 한 호표(id = 내용 해시 · 같은 원본 + 같은 셈 단가 = 하나) ·
줄 수량 = 사본 셈 수량(손 · 면적 알약 칸) ÷ 배 = 부르는 호표 1 단위 몫.
줄 수량 = 사본 셈 수량(손 · 면적 알약 칸) — 대가 줄만 ÷ 배(부르는 호표 1 단위 몫 ·
산근 단가는 이미 1 단위).
단가 · 금액 · 끝수는 셈 짝(`B09_Estimation_Calc_Labor`)이 B09 프로젝트 한 벌 단가로 냄.
"""
@@ -22,6 +25,7 @@ from decimal import Decimal
from pathlib import Path
from typing import Any
from B09_Estimation import B09_Estimation_Sangeun as sg
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
from M02_MasterTemplete import M02_Labor_Ref as refs
@@ -38,8 +42,6 @@ mf = item_prices.mf
#: 엔진 비목 → 블록 비목
PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
#: 기계 로직 줄 요소 — `로직(GC…, 기계='0201-0070', …)` 의 원문번호
MACHINE = re.compile(r"기계\s*=\s*['\"]([^'\"]+)['\"]")
#: % 덧줄 밑수 — `노무비 * …` · `줄.'이름' * …` · `(석공 * LB… + …) * …`(인력 줄)
BASE_PART = re.compile(r"^\s*(노무비|재료비|경비)\s*\*")
BASE_LINE = re.compile(r"^\s*줄\.'([^']+)'\s*\*")
@@ -62,12 +64,8 @@ def _digest(value: Any) -> str:
def sangeun_ref(code: str, given: Any, rate: Any, picks: Any) -> str:
"""산근 호표 키 — 145-1(`B09_Estimation_Sangeun.sangeun_key`)이 들어오면 그 키 · 전엔 자리만."""
try:
from B09_Estimation.B09_Estimation_Sangeun import sangeun_key
except ImportError:
return f"SG:{code}:{_digest([given or {}, rate, picks or {}])}"
return sangeun_key(code, given or {}, rate, picks or {})
"""산근 호표 키 — 145-1 `B09_Estimation_Sangeun.sangeun_key`."""
return sg.sangeun_key(code, given or {}, rate, picks or {})
class Builder:
@@ -84,7 +82,7 @@ class Builder:
return bid
# ── 품셈(로직 하나) ─────────────────────────────────────────────────
def poomsem(self, whole: Any, one: dict[str, Any]) -> str | None:
def poomsem(self, whole: Any, one: dict[str, Any], title: str = "") -> str | None:
code = str(one.get("로직") or "")
given, rate, picks = one.get("입력값") or {}, one.get("켜쌓기율"), one.get("고름")
try:
@@ -98,15 +96,20 @@ class Builder:
lines = []
engine = got.get("줄") or []
bases = _engine_bases(engine[: len(items)], items)
calls = sg.line_calls(whole, code, given, picks)
for n, x in enumerate(engine):
item = items[n] if n < len(items) else None
lines.append(self._engine_line(whole, item, x, logic, n - len(items), bases))
lines.append(self._engine_line(item, x, logic, n - len(items), bases, calls.get(n)))
eq = {str(r.get("키")): r for r in (whole.numbers.get("EQ") or {}).values()}
for line in lines:
if line["kind"] == "machine":
line["spec"] = (eq.get(line["key"]) or {}).get("규격") or ""
unit = str(logic.get("결과단위") or "").replace("원/", "")
spec = " · ".join(f"{k} {v}" for k, v in sorted(given.items()))
return self.put(
{
"kind": "품셈",
"name": logic.get("이름") or code,
"name": title or logic.get("이름") or code,
"spec": spec,
"unit": unit,
"cite": logic.get("출처") or "",
@@ -116,7 +119,7 @@ class Builder:
)
def _engine_line(
self, whole: Any, item: dict | None, x: dict, logic: dict, extra: int, bases: dict
self, item: dict | None, x: dict, logic: dict, extra: int, bases: dict, call: dict | None
) -> dict[str, Any]:
head = {"name": x.get("이름") or "", "spec": "", "unit": x.get("단위") or ""}
qty = _f(x.get("수량"))
@@ -140,24 +143,20 @@ class Builder:
return {**head, "kind": "blank", "qty": qty, "why": x.get("까닭") or "품목 없음"}
return {**head, "kind": "material", "key": goods["키"], "spec": goods.get("규격") or "",
"qty": qty} # fmt: skip
hit = MACHINE.search(str(item.get("요소") or ""))
row = whole.numbers.get("EQ", {}).get(hit.group(1)) if hit else None
if row is None:
# ponytail: 기계 아닌 하위 로직 줄은 B09 단가로 못 펼침 — 줄만 두고 빈칸
return {
**head,
"kind": "blank",
"qty": qty,
"why": "하위 로직 줄 — 대가 단가로 못 펼침",
}
return {**head, "kind": "machine", "key": row["키"], "spec": row.get("규격") or "",
"qty": qty} # fmt: skip
# 로직 줄 — `Sangeun.call_of` 판정: 기계경비 로직 = 중기 · 그 밖 = 하위 산근
if call and call.get("machine"):
return {**head, "kind": "machine", "key": call["machine"], "qty": qty}
if call and call.get("code"):
key = sg.sangeun_key(call["code"], call["given"])
return {**head, "kind": "sangeun", "key": key, "qty": qty}
return {**head, "kind": "blank", "qty": qty, "why": "하위 로직 줄 — 못 펼침"}
# ── 조합 · 사전조합 줄 ──────────────────────────────────────────────
def line_of(
self, whole: Any, one: dict, qty: Any, times: Any, name: str, copies: dict, depth: int = 0
) -> dict[str, Any]:
"""담은 로직 줄 하나 → 블록 줄 — 수량 = 수량 ÷ 배(부르는 호표 1 단위 몫)."""
"""담은 로직 줄 하나 → 블록 줄 — 대가 줄 수량 = 수량 ÷ 배(부르는 호표 1 단위 몫) ·
산근 줄 수량 = 수량 그대로(산근 단가 = 결과단위 1 단위)."""
q = _f(qty)
t = _f(times) or 1.0
each = None if q is None else q / t
@@ -171,20 +170,24 @@ class Builder:
"qty": each,
"why": f"없는 참조 「{ref.get('키')}」",
}
if refs.kind_of(ref.get("키")) == "promote": # 옛 단가승격 참조 = 산근
if refs.kind_of(ref.get("키")) == "promote": # 옛 단가승격 참조 = 일위대가화 = 대가
first = (doc.get("담은로직") or [{}])[0]
key = sangeun_ref(str(first.get("로직") or ""), first.get("입력값"),
first.get("켜쌓기율"), first.get("고름")) # fmt: skip
return {"kind": "sangeun", "key": key, "name": doc.get("이름") or name,
bid = self.poomsem(whole, first, doc.get("이름") or name)
if bid is None:
return {"kind": "blank", "name": name, "qty": each, "why": "돈 아닌 로직"}
return {"kind": "daega", "key": bid, "name": self.blocks[bid]["name"],
"unit": doc.get("단위") or "", "qty": each} # fmt: skip
bid = self.precombo(whole, ref, doc, copies, depth + 1)
return {"kind": "daega", "key": bid, "name": doc.get("이름") or name,
"unit": doc.get("단위") or "", "qty": each} # fmt: skip
code = str(one.get("로직") or "")
if one.get("승격") is True:
if one.get("승격") is not True: # 단가산출 줄 = 산근
if not sg.is_money(whole, code):
return {"kind": "blank", "name": name or code, "qty": q, "why": "돈 아닌 로직"}
key = sangeun_ref(code, one.get("입력값"), one.get("켜쌓기율"), one.get("고름"))
return {"kind": "sangeun", "key": key, "name": name, "qty": each}
bid = self.poomsem(whole, one)
return {"kind": "sangeun", "key": key, "name": name or code,
"unit": sg.unit_of(code), "qty": q} # fmt: skip
bid = self.poomsem(whole, one, str(one.get("별칭") or "") or name)
if bid is None:
return {
"kind": "blank",
@@ -312,7 +315,7 @@ def _plain(value: Any) -> Any:
def daega_inputs(project_root: str | Path) -> dict[str, Any]:
"""대가 셈 입력 — `{blocks: [...], owners: {구조물 id: 블록 id}}` · 사본 없으면 빈 것.
차례 = 조합(구조물 id 차례) → 사전조합 → 품셈(처음 본 차례) — 호표 번호 = 이 차례.
차례 = 조합(구조물 id 차례) → 사전조합 → 일위대가화(품셈 · 처음 본 차례) — 호표 번호 = 이 차례.
"""
base = structures_dir(project_root)
build = Builder()
+111 -46
View File
@@ -1,8 +1,10 @@
"""B09 단가산출근거 · 단가산출근거목록표(PLAN 145-1) — 서버 단독(③ · 화면 짝 없음 · 저장 안 함).
산근 호표 = 연결된 사본(`linked_snapshots` · 143-1)의 일위대가 로직 줄 중
`승격: true` 인 줄(134-1 · 입력값 = 사본 `입력값.json` · 빈 입력 = 자동값) +
옛 단가승격 참조 줄(134-2 ③ 로 남은 SP · 입력값 = 굳힌 `참조사본`)
산근 호표(155-4 · 엑셀 꼴 · 145 정함 뒤집음) = 연결된 사본(`linked_snapshots` · 143-1)의
단가산출 줄(일위대가화 아닌 돈 로직 줄 · 입력값 = 사본 `입력값.json` · 빈 입력 = 자동값) +
사전조합(SC 참조 · 굳힌 `참조사본`) 안 단가산출 줄(입력값 = 그 문서 값) +
일위대가화 줄(승격 · 옛 SP)의 하위 로직 줄(기계경비 밖 · 입력 = 부르는 식 값)
— 노무 · 자재 · 중기 줄은 각 목록표(산근 아님) · 일위대가화 줄 자체 = 대가(145-2 블록)
같은 로직 + 같은 입력값(+ 켜쌓기율 · 고른 자재) = 산근 하나(`sangeun_key`) ·
번호 = 첫 등장 차례 ·
로직 줄이 부르는 다른 돈 로직(기계경비 밖) = 하위 산근(그 산근도 목록에 · 산근 → 산근).
@@ -47,7 +49,7 @@ COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"}
DEPTH = 4 # 하위 산근 깊이 상한
_Q = re.compile(r"Q\d*") # 작업량 중간값 이름(산림품셈 09장 토공 · 07장 집재 …)
NO_SOURCE = "조합 밖 토공 · 운반 · 사토 산근 · 준비공 = 원천 없음(연결 없음 · 만들지 않음)"
EMPTY = "연결된 구조물 조합 사본에 단가승격 줄 없음 — 연결 없음"
EMPTY = "연결된 구조물 조합 사본에 단가산출 줄 없음 — 연결 없음"
# ── 산근 정체 ─────────────────────────────────────────────────────────
@@ -81,42 +83,57 @@ def _values(values: dict[str, Any], code: str) -> dict[str, Any]:
return {name: (one or {}).get("값") for name, one in (values.get(code) or {}).items()}
def rows_of(folder: Path) -> list[dict[str, Any]]:
"""사본 하나의 산근 원천 줄 — 승격 로직 줄 · 옛 SP 참조 줄(차례대로)."""
def rows_of(folder: Path, whole: Any = None) -> list[dict[str, Any]]:
"""사본 하나의 산근 원천 줄(차례대로) — 단가산출 줄 · 사전조합 안 단가산출 줄 ·
일위대가화 줄(승격 · 옛 SP)의 하위 로직 줄. 키 = 대가 블록 산근 줄 키(`Labor_Source`)와 같음."""
from M02_MasterTemplete import M02_Labor_Ref as refs
labor = read_json(folder / LABOR_FILE)
if not isinstance(labor, dict):
return []
if whole is None:
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
values = read_json(folder / VALUES_FILE) or {}
picks = read_json(folder / PICKS_FILE) or {}
copies = labor.get("참조사본") or {}
out = []
for one in (labor.get("줄") or {}).get("담은로직") or []:
if not isinstance(one, dict):
continue
ref = one.get("참조")
if one.get("승격") is True and one.get("로직"):
code = str(one["로직"])
got = {
"code": code,
"given": _values(values, code),
"rate": one.get("켜쌓기율"),
"picks": picks.get(code) or one.get("고름"),
}
elif isinstance(ref, dict) and refs.kind_of(ref.get("키")) == "promote":
line = ((copies.get(refs.ref_id(ref)) or {}).get("담은로직") or [None])[0]
if not isinstance(line, dict) or not line.get("로직"):
out: list[dict[str, Any]] = []
def logic_line(code: str, given: Any, rate: Any, chosen: Any, promoted: bool) -> None:
if not is_money(whole, code):
return
if not promoted:
out.append({"code": code, "given": _clean_given(given), "rate": rate, "picks": chosen})
return
for call in line_calls(whole, code, given, chosen).values():
if call and call.get("code"):
out.append({"code": call["code"], "given": call["given"], "rate": None,
"picks": None}) # fmt: skip
def walk(lines: list, inner: bool, depth: int) -> None:
for one in lines:
if not isinstance(one, dict):
continue
got = {
"code": str(line["로직"]),
"given": line.get("입력값") or {},
"rate": line.get("켜쌓기율"),
"picks": line.get("고름"),
}
else:
continue
out.append({**got, "given": _clean_given(got["given"])})
ref = one.get("참조")
if isinstance(ref, dict):
doc = copies.get(refs.ref_id(ref))
if not isinstance(doc, dict) or depth >= refs.DEPTH:
continue
if refs.kind_of(ref.get("키")) == "promote": # 옛 SP = 일위대가화
first = (doc.get("담은로직") or [None])[0]
if isinstance(first, dict) and first.get("로직"):
logic_line(str(first["로직"]), first.get("입력값"),
first.get("켜쌓기율"), first.get("고름"), True) # fmt: skip
continue
walk(doc.get("담은로직") or [], True, depth + 1) # 사전조합 = 굳힌 값
continue
if not one.get("로직"):
continue
code = str(one["로직"])
given = one.get("입력값") if inner else _values(values, code)
chosen = one.get("고름") if inner else (picks.get(code) or one.get("고름"))
logic_line(code, given, one.get("켜쌓기율"), chosen, one.get("승격") is True)
walk((labor.get("줄") or {}).get("담은로직") or [], False, 0)
return out
@@ -136,6 +153,11 @@ def _unit(code: str) -> tuple[str, Decimal]:
return str(pill.get("단위") or ""), Decimal(str(pill.get("배") or 1))
def unit_of(code: str) -> str:
"""산근 1 단위 이름(「원/10㎡」 = ㎡)."""
return _unit(code)[0]
def _env(whole: Any, row: dict, given: dict) -> dict:
"""입력(빈 입력 = 자동값) + 중간값 · 작업량 Q = ROUND(식, 2)(엑셀 자리)."""
auto = {k: v for k, v in m01.mau.values(whole, row).items() if v is not None}
@@ -152,6 +174,53 @@ def _env(whole: Any, row: dict, given: dict) -> dict:
return env
def is_money(whole: Any, code: str) -> bool:
"""돈 로직(호표 줄 꼴)인지 — 없는 로직 · 흐름 · 결과 로직 = 아님."""
try:
row = whole.logic(code)
except mf.FormulaError:
return False
return not (isinstance(row.get("흐름"), dict) or "결과" in row)
def call_of(whole: Any, item: dict, env: dict) -> dict[str, Any] | None:
"""로직 호표 줄(종류 로직) → 부른 것 — 기계경비 로직 `{machine: EQ 키 | None}` ·
다른 돈 로직 `{code, given}`(하위 산근) · 못 펼치면 None.
대가 품셈 줄(`B09_Estimation_Labor_Source`)과 산근 풀이가 같은 판정을 씀."""
node = mf.parse(item["요소"])
called = node[1] if node and node[0] == "logic" else None
try:
target = whole.logic(called) if called else None
except mf.FormulaError:
target = None
if not target:
return None
if any(s.get("이름") == "기계" for s in target.get("입력", [])):
keys = _captured(whole, node, env)
return {"machine": keys[0] if keys else None}
given = {name: mf.evaluate(expr, env, whole, 1) for name, expr in node[2]}
return {"code": called, "given": _clean_given(given)}
def line_calls(whole: Any, code: str, given: Any, picks: Any = None) -> dict[int, dict | None]:
"""로직 하나의 로직 호표 줄마다 `call_of`(호표 차례 n → 부른 것) — 못 셈 줄 = None."""
try:
row = whole.logic(code)
env = _env(whole, row, _clean_given(given))
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
return {}
out: dict[int, dict | None] = {}
with mf.mp.choosing(picks):
for n, item in enumerate(row.get("호표", [])):
if item.get("종류") != "로직":
continue
try:
out[n] = call_of(whole, item, env)
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError):
out[n] = None
return out
def _captured(whole: Any, node: Any, env: dict) -> list[str]:
"""식을 돌리며 부른 기계 키(EQ) — 기계경비 로직 줄의 기계."""
seen: list[str] = []
@@ -278,6 +347,11 @@ class Solver:
key = str(
goods.get("키") or (source or "").split(".")[0] or (ref if isinstance(ref, str) else "")
)
if key not in self.prices.material: # 자리표시 글(「MO:경유:전국평균」) = 대비표 키로
try:
key = str((whole.get(key) or {}).get("키") or key)
except (mf.FormulaError, KeyError, TypeError, ValueError):
pass
part = COST_PART.get(item.get("비목"), "material")
price = self.prices.material.get(key)
if q == 0:
@@ -289,14 +363,9 @@ class Solver:
"price": {part: price}, part: _money(price, q), "why": why} # fmt: skip
def _logic_line(self, whole, item, env, q, base, zero, depth) -> dict[str, Any]:
node = mf.parse(item["요소"])
called = node[1] if node and node[0] == "logic" else None
try:
target = whole.logic(called) if called else None
except mf.FormulaError:
target = None
if target and any(s.get("이름") == "기계" for s in target.get("입력", [])):
keys = _captured(whole, node, env)
call = call_of(whole, item, env)
if call and "machine" in call:
keys = [call["machine"]] if call["machine"] else []
m = self.prices.machine.get(keys[0]) if keys else None
price = {p: num((m or {}).get(p)) for p in PARTS}
why = None if m else f"중기목록표에 없음 「{keys[0] if keys else item.get('이름')}」"
@@ -304,10 +373,9 @@ class Solver:
ref = {"tab": "machine", "key": keys[0] if keys else ""}
return {**base, "kind": "machine", "ref": ref,
"price": price, **amounts, "why": why} # fmt: skip
if not target or depth >= DEPTH:
if not call or depth >= DEPTH:
return {**self._blank(item, "하위 로직을 못 펼침"), **base}
given = {name: mf.evaluate(expr, env, whole, 1) for name, expr in node[2]}
sub = self.add(whole, {"code": called, "given": _clean_given(given)}, None, depth + 1)
sub = self.add(whole, {"code": call["code"], "given": call["given"]}, None, depth + 1)
price = {p: sub.get(p) for p in PARTS}
why = None if sub.get("sum") is not None else f"하위 산근 {sub['no']} 빈칸"
return {**base, "kind": "sangeun", "ref": {"tab": "sg", "key": sub["key"]},
@@ -342,11 +410,8 @@ def collect(entries: list[tuple[str, Path]], sheets: dict[str, Any]) -> dict[str
"""`entries` = [(구조물 id, 사본 폴더)] → 블록 · 목록 · 「연결 없음」."""
solver = Solver(sheets)
for owner, folder in entries:
rows = rows_of(folder)
if not rows:
continue
whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {})
for src in rows:
for src in rows_of(folder, whole):
solver.add(whole, src, owner)
blocks = sorted(solver.blocks.values(), key=lambda b: b["no"])
listing = [
@@ -32,7 +32,7 @@ const LINE_TAB: Record<string, TabId> = {
material: "material",
machine: "machine",
daega: "dg",
sangeun: "sg_list",
sangeun: "sg",
};
export const buildDaegaList: TabBuild = ({ sheets }): TabTable | string => {