feat(B09): 설계내역서 구조물 줄 = 대가 블록마다 나눔 — B08 열 owners 잇기(145-3)

- 항목 짜기 = B08 구조물 집계표 열 owners {사본 주인 id: 값}(sub8 덧칸) 를 싣고
- 짝 셈 split_by_block / splitByBlock = 주인마다 대가 단가(price_of = 145-2 daega_price 자리) → 같은 대가 블록끼리 한 줄(수량 합 ROUND 3) · 대가 없는 주인 = 한 줄 「연결 없음」
- price_of 없으면 열 하나 그대로(단가 빈칸)
- 시험 = test_145_3_b09_bill 더함(대가17 × 20 = 2,491,880 · 대가24 × 12.5 = 2,078,274 · 거울)

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 15:24:46 +09:00
co-authored by Claude Opus 5.5
parent e359bef714
commit 03fdde07fb
4 changed files with 141 additions and 7 deletions
@@ -105,6 +105,8 @@ def structure_items(summary: dict[str, Any] | None) -> list[dict[str, Any]]:
"qty": total.get(cid),
"note": " · ".join(combos),
"ref": {"tab": "dg_list", "key": cid},
# 사본 주인마다 값(B08 145-3 덧칸) — 대가 블록마다 줄을 나눌 때 씀
"owners": dict(column.get("owners") or {}),
"why": WHY_STRUCTURE,
}
)
+42 -4
View File
@@ -18,9 +18,10 @@
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, gap, num, rounddown
from B09_Estimation.B09_Estimation_Calc_Base import compute_base, gap, num, round_half, rounddown
#: 공종 나무 — (번호, 이름, 부모) · 엑셀 기번3 공통 1공구 꼴(공구 안 나눔)
SECTIONS: list[tuple[str, str, str | None]] = [
@@ -103,13 +104,48 @@ def _material_items(inputs: dict[str, Any], compare: list[dict[str, Any]]) -> li
return out
PriceOf = Callable[[str], dict[str, Any] | None]
def split_by_block(item: dict[str, Any], price_of: PriceOf) -> list[tuple[dict, dict | None]]:
"""구조물 열 → 대가 블록마다 한 줄 — 수량 = 그 블록 사본 주인 값 합(ROUND 3) ·
대가 없는 주인은 한 줄로 모음(단가 빈칸 「연결 없음」) · 차례 = 주인이 처음 나온 차례."""
groups: dict[str, dict[str, Any]] = {}
for owner, value in (item.get("owners") or {}).items():
price = price_of(owner)
tag = str(price.get("no") or "") if price else ""
group = groups.setdefault(tag, {"qty": 0.0, "price": price, "owners": []})
group["qty"] += num(value) or 0
group["owners"].append(owner)
out = []
for tag, group in groups.items():
part = {
**item,
"key": f"{item['key']}#{tag}" if tag else item["key"],
"qty": round_half(group["qty"], 3),
"note": " · ".join(t for t in (tag, item.get("note") or "") if t),
"owners": {o: item["owners"][o] for o in group["owners"]},
}
out.append((part, group["price"]))
return out
def compute_bill(
inputs: dict[str, Any], user: dict[str, Any], base: dict[str, Any]
inputs: dict[str, Any],
user: dict[str, Any],
base: dict[str, Any],
price_of: PriceOf | None = None,
) -> dict[str, Any]:
"""`price_of(사본 주인 id)` = 대가 1 단위 단가(145-2 `daega_price`) —
주면 구조물 열을 블록마다 나눔."""
gaps: list[dict[str, str]] = []
hopyo = inputs.get("hopyo") or {}
items: list[dict[str, Any]] = []
for item in (inputs.get("bill") or {}).get("items") or []:
if price_of is not None and item.get("owners"):
for part, prices in split_by_block(item, price_of):
items.append(_item_row(part, prices, gaps))
continue
ref = item.get("ref") or {}
prices = hopyo.get(ref.get("key")) if ref.get("key") else None
items.append(_item_row(item, prices, gaps))
@@ -201,8 +237,10 @@ def compute_bill(
return {"rows": rows, "totals": totals, "sums": sums_out, "gaps": gaps}
def compute_estimation(inputs: dict[str, Any], user: dict[str, Any]) -> dict[str, Any]:
def compute_estimation(
inputs: dict[str, Any], user: dict[str, Any], price_of: PriceOf | None = None
) -> dict[str, Any]:
"""144 바탕 + 145-3 설계내역서 — 「연결 없음」 목록은 둘을 이음."""
base = compute_base(inputs, user)
bill = compute_bill(inputs, user, base)
bill = compute_bill(inputs, user, base, price_of)
return {**base, "bill": bill, "gaps": base["gaps"] + bill["gaps"]}
+42 -3
View File
@@ -15,6 +15,7 @@ import {
computeBase,
gap,
num,
roundHalf,
rounddown,
type Gap,
type Num,
@@ -95,11 +96,49 @@ function materialItems(inputs: Row, compare: Row[]): Row[] {
return out;
}
export function computeBill(inputs: Row, user: Row, base: Row): Row {
/** 사본 주인 id → 대가 1 단위 단가(145-2 `daegaPrice`) */
export type PriceOf = (owner: string) => Row | null;
/** 구조물 열 → 대가 블록마다 한 줄(수량 = 그 블록 주인 값 합 ROUND 3 · 대가 없는 주인 = 한 줄) */
export function splitByBlock(item: Row, priceOf: PriceOf): Array<[Row, Row | null]> {
const groups = new Map<string, Row>();
for (const [owner, value] of Object.entries(item.owners || {})) {
const price = priceOf(owner);
const tag = price ? String(price.no || "") : "";
if (!groups.has(tag)) groups.set(tag, { qty: 0, price, owners: [] });
const group = groups.get(tag)!;
group.qty += num(value) || 0;
group.owners.push(owner);
}
const out: Array<[Row, Row | null]> = [];
for (const [tag, group] of groups) {
const part: Row = {
...item,
key: tag ? `${item.key}#${tag}` : item.key,
qty: roundHalf(group.qty, 3),
note: [tag, item.note || ""].filter((t) => t).join(" · "),
owners: Object.fromEntries(group.owners.map((o: string) => [o, item.owners[o]])),
};
out.push([part, group.price]);
}
return out;
}
export function computeBill(
inputs: Row,
user: Row,
base: Row,
priceOf: PriceOf | null = null,
): Row {
const gaps: Gap[] = [];
const hopyo: Row = inputs.hopyo || {};
const items: Row[] = [];
for (const item of (inputs.bill || {}).items || []) {
if (priceOf !== null && item.owners && Object.keys(item.owners).length) {
for (const [part, prices] of splitByBlock(item, priceOf))
items.push(itemRow(part, prices, gaps));
continue;
}
const ref: Row = item.ref || {};
const prices = ref.key ? (hopyo[ref.key] ?? null) : null;
items.push(itemRow(item, prices, gaps));
@@ -190,8 +229,8 @@ export function computeBill(inputs: Row, user: Row, base: Row): Row {
}
/** 144 바탕 + 145-3 설계내역서 */
export function computeEstimation(inputs: Row, user: Row): Row {
export function computeEstimation(inputs: Row, user: Row, priceOf: PriceOf | null = null): Row {
const base = computeBase(inputs, user);
const bill = computeBill(inputs, user, base);
const bill = computeBill(inputs, user, base, priceOf);
return { ...base, bill, gaps: [...base.gaps, ...bill.gaps] };
}