Files
Aislo/M02_MasterTemplete/M02_Sangeun_Doc.py
T
eomsangdonandClaude Opus 5.5 6c0211af1f feat(744-1): 둘째 기초 세부 원문 품 · 되돌리기 기억(처음수량 · 뺀더한줄) — 서버
- 둘째 기초 펼침 줄 덧칸 원문 {수량, 금액, 비목} = 그 기초 1 기준단위 몫(M01 원문 품 · 머리 수량 안 곱함) · 줄 수량 · 금액 = 머리 수량 곱한 몫(상자 합) 그대로
- 더한줄 항목 처음수량 = 넣을 때 수량(없으면 저장 때 지금 수량) · SK · 일위대가 줄 패치 모두
- 뺀더한줄 [{차례, 줄}] = [기초 빼기] · 더한 항목 빼기 기억(셈 안 함 · 못 읽는 줄 버림) · 일위대가 줄 패치도
- 733 지문에서 처음수량 · 뺀더한줄 뺌 · Labor_Patch.as_doc = 셈 칸(CALC)만(화면 짝 꼴 그대로)
- 이음 = PLAN 744 아래 줄(화면 = sub2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 20:17:06 +09:00

497 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 단가산출(SK) 문서 — 꼴 · 검사 · 줄 정체 · 고친 줄(PLAN 715-1a · 요구서 내역12 4장 12 · 13).
문서 `{층}/sangeun/SK-######.json` = M01 로직에 연결 + 바꾼 것만 —
{종류: "단가산출", 키, 이름, 단위(필수), 구분, 상세구분, 본뜬,
바탕: {로직: GF…} | {참조: {층, 주인, 키: SK-…}} | null(바탕 없음 = 더한줄만),
입력값 {이름: 값}, 열린입력 [이름](설계 때 정함 · 그 SK 품 덮개 = 배율만), 켜쌓기율?,
고름 {cid: MT키}, 자재표시 {cid: {세 칸}},
덮개 {cid: {qty | 배율, labor, material, expense, 원값}},
더한줄 [{종류: 인력|재료|기계, 요소, 이름, 단위, 수량, 비목?, 단가?,
별도계상?(재료 · 없음 = 계 안)}
| {종류: 로직, 로직, 입력값, 수량, 덮개?, 뺀줄?, 바탕판?,
고름? · 자재표시? · 켜쌓기율? · 더한줄?(넣은 SK 몸 · 740-1) · 본뜬?(출처)}
| {참조 | 고정, 이름?, 단위?, 수량}],
뺀줄 [cid], 바탕판, 환산?}
SK 단위 환산(728-2) = `환산` {계수: 양수}(성격 다름 · 바탕 앞 수 뗀 1 단위 = SK 몇 단위) |
{단위중량: {종별, 형상}}(부피 ↔ 무게 · 표 CC000337) — 셈 = `M02_Sangeun_Calc.ratio_of`.
둘째 기초(727-2) = 더한줄 로직 — 수량 = 그 기초 기준단위 몇 개 · `덮개` {세부 cid: 덮개} ·
`뺀줄` [세부 cid](세부 cid = 그 기초 줄 `kind:키#n` · 화면 열쇠 `add:#k/세부 cid`) ·
`바탕판` = 저장 때 서버가 그 기초 지문.
SK `입력값` = 바탕 입력 + 더한줄 로직 입력(727-3 · 넣은 뒤 고른 옵션).
줄 정체 cid = `kind:키#n`(B09 702 덮개 꼴) — kind = labor · material · machine · logic · pct(덧줄) ·
키 = 요소 키(로직 줄 = 부르는 GC 키 · 조건 재료 = 이름 · 덧줄 = 이름 · 더한줄 참조 = 그 키) ·
n = 같은 머리 차례 · 바탕 줄(M01 로직 줄 · 바탕 SK 의 고친 줄)에서 셈 — 뺀줄 · 더한줄 뒤 같은 줄.
고름 · 자재표시도 cid 열쇠(호표 차례 열쇠면 뺀줄 뒤 엉뚱한 줄에 붙음) → `patched` 가 차례로 바꿈.
덮개 = 숫자만(식 안 받음 · 4-1 확정 4) — qty = 품 수 · 배율 = 품 × 율 · labor · material · expense =
줄 단가(로직 줄 = 노 · 재 · 경 묶음) · 덧줄 = qty(율 %) · 배율만 · `원값` · `바탕판` = 저장 때
서버가 셈(브라우저 값 안 받음). 짝 잃은 덮개(M01 개정으로 줄이 사라짐) = 지우지 않고 `짝잃음` 알림.
엔진 입력 덧칸(715-1b · `master_pick`) = 줄 `단가`(수) · 로직 줄 `값묶음{노무비 · 재료비 · 경비}`
"""
from __future__ import annotations
import re
from decimal import Decimal, InvalidOperation
from typing import Any
KIND = "sangeun"
TITLE = "단가산출"
PREFIX = "SK"
KEY = re.compile(r"^SK-\d{6}$")
PARTS = {"labor": "노무비", "material": "재료비", "expense": "경비"}
COVER_KEYS = ("qty", "배율", *PARTS)
KINDS = {"인력": "labor", "재료": "material", "기계": "machine", "로직": "logic"}
ADD_PART = {"인력": "노무비", "재료": "재료비", "기계": "경비"}
_CID = re.compile(r"^(labor|material|machine|logic|pct):.*#\d+$")
_CALL = re.compile(r"^\s*로직\(\s*([A-Z]{2}\d{6})")
_PCT = re.compile(r"^(?P<x>.+)\*(?P<y>[^*]+)/\s*100\s*$", re.S) # 「X * Y / 100」
_NAME = re.compile(r"^[\w가-힣]+$")
class DocError(ValueError):
def __init__(self, text: str, status: int = 400) -> None:
super().__init__(text)
self.status = status
def _num(raw: Any, where: str) -> Decimal:
try:
got = Decimal(str(raw).replace(",", "").strip())
except (InvalidOperation, TypeError):
got = None
if raw is None or isinstance(raw, bool) or got is None or not got.is_finite():
raise DocError(f"{where} · 수가 아님 「{raw}」")
return got
def plain(value: Decimal) -> int | float:
return int(value) if value == value.to_integral_value() else float(value)
def text(value: Any) -> str:
"""식 안에 넣을 수 글 — 지수 꼴 없이."""
got = value if isinstance(value, Decimal) else Decimal(str(value))
return format(got.normalize(), "f")
# ── 줄 정체 ───────────────────────────────────────────────────────────
def _head(item: dict, extra: bool) -> str:
if extra:
return f"pct:{item.get('이름') or ''}"
kind = KINDS.get(str(item.get("종류")), "logic")
elem = item.get("요소")
if isinstance(item.get("참조"), dict):
key = str(item["참조"].get("키") or "")
elif isinstance(item.get("고정"), dict):
key = str(item["고정"].get("이름") or "")
elif kind == "logic":
found = _CALL.match(str(elem or ""))
key = found.group(1) if found else str(item.get("이름") or "")
else:
key = elem if isinstance(elem, str) else str(item.get("이름") or "")
return f"{kind}:{key}"
def line_ids(row: dict) -> list[str]:
"""로직 줄(호표 → 덧줄 차례)마다 cid — 같은 머리 n번째 = `#n`."""
seen: dict[str, int] = {}
out = []
pairs = [(x, False) for x in row.get("호표") or []] + [(x, True) for x in row.get("덧줄") or []]
for item, extra in pairs:
head = _head(item, extra)
seen[head] = seen.get(head, -1) + 1
out.append(f"{head}#{seen[head]}")
return out
# ── 정리 · 검사 ───────────────────────────────────────────────────────
def _cid(raw: Any, where: str) -> str:
if not isinstance(raw, str) or not _CID.match(raw):
raise DocError(f"{where} · 줄 정체는 kind:키#n 「{raw}」")
return raw
def _base(raw: Any) -> dict | None:
if raw is None:
return None
if isinstance(raw, dict) and isinstance(raw.get("로직"), str) and raw["로직"].strip():
return {"로직": raw["로직"].strip()}
if isinstance(raw, dict) and isinstance(raw.get("참조"), dict):
ref = raw["참조"]
if not KEY.match(str(ref.get("키") or "")):
raise DocError("바탕 참조 키는 SK-######")
return {"참조": {"층": ref.get("층"), "주인": ref.get("주인"), "키": str(ref["키"])}}
raise DocError("바탕은 {로직} · {참조} · null")
def _given(raw: Any, where: str) -> dict:
if raw is None:
return {}
if not isinstance(raw, dict):
raise DocError(f"{where} · 입력값이 묶음 아님")
return {str(k): v for k, v in raw.items() if v not in (None, "")}
def _cover(raw: Any, where: str, opened: bool) -> dict:
if not isinstance(raw, dict):
raise DocError(f"{where} · 덮개는 묶음")
unknown = set(raw) - {*COVER_KEYS, "원값"}
if unknown:
raise DocError(f"{where} · 모르는 덮개 칸 {sorted(unknown)}")
out = {k: plain(_num(raw[k], f"{where} {k}")) for k in COVER_KEYS if raw.get(k) is not None}
if "qty" in out and "배율" in out:
raise DocError(f"{where} · 품은 qty · 배율 중 하나")
if opened and "qty" in out:
raise DocError(f"{where} · 옵션을 열어 둔 단가산출은 품 배율만")
if any(v < 0 for v in out.values()):
raise DocError(f"{where} · 음수 덮개")
if not out:
raise DocError(f"{where} · 빈 덮개")
return out
def _added(raw: Any, where: str, whole: Any) -> dict:
"""더한줄 하나 — 수량은 숫자(식 안 받음)."""
if not isinstance(raw, dict):
raise DocError(f"{where} · 묶음 아님")
qty = plain(_num(raw.get("수량"), f"{where} 수량"))
name, unit = str(raw.get("이름") or "").strip(), str(raw.get("단위") or "").strip()
if isinstance(raw.get("참조"), dict):
ref = raw["참조"]
if not re.match(r"^(SK|SC)-\d{6}$", str(ref.get("키") or "")):
raise DocError(f"{where} · 참조 키는 SK · SC")
head: dict[str, Any] = {"참조": {k: ref.get(k) for k in ("층", "주인", "키")}}
elif isinstance(raw.get("고정"), dict):
from M02_MasterTemplete import M02_Labor_Fixed as fixed
try:
head = {"고정": fixed.clean(raw["고정"], where, whole) if whole else raw["고정"]}
except Exception as e: # noqa: BLE001 — 고정 줄 검사 = StoreError(400)
raise DocError(str(getattr(e, "detail", e))) from e
if head["고정"].get("비율"):
raise DocError(f"{where} · 비율 고정 줄은 단가산출에 못 담음 — 덧줄 율로")
elif raw.get("종류") == "로직":
code = str(raw.get("로직") or "")
row = _logic(whole, code, where)
names = {str(s.get("이름")) for s in (row or {}).get("입력") or []}
given = _given(raw.get("입력값"), where)
head = {"종류": "로직", "로직": code, "입력값": given}
if row is not None:
head["입력값"] = {k: v for k, v in given.items() if k in names}
name = name or str(row.get("이름") or code)
head |= _sub_patch(raw, where) | _sub_body(raw, where, whole)
elif raw.get("종류") in ADD_PART:
key = str(raw.get("요소") or "")
got = _element(whole, key, where)
part = str(raw.get("비목") or ADD_PART[raw["종류"]])
if part not in PARTS.values():
raise DocError(f"{where} · 비목은 노무비 · 재료비 · 경비")
head = {"종류": raw["종류"], "요소": key, "비목": part}
if raw.get("단가") is not None:
head["단가"] = plain(_num(raw["단가"], f"{where} 단가"))
if raw["종류"] == "재료" and raw.get("별도계상") is True:
head["별도계상"] = True # 더한 재료 기본 = 계 안 · 이 칸만 별도계상(`patched` 자재표시)
name = name or str((got or {}).get("이름") or key)
unit = unit or str((got or {}).get("단위") or "")
else:
raise DocError(f"{where} · 종류는 인력 · 재료 · 기계 · 로직 · 참조 · 고정")
mark = {"본뜬": raw["본뜬"]} if isinstance(raw.get("본뜬"), dict) else {} # 출처(740-1)
if raw.get("처음수량") is not None: # 넣을 때 수량(744-1 [되돌림] 값)
mark["처음수량"] = plain(_num(raw["처음수량"], f"{where} 처음수량"))
return {**head, "이름": name, "단위": unit, "수량": qty, **mark}
def _gone_adds(raw: Any, whole: Any) -> list[dict]:
"""뺀 더한줄 기억(744-1 · [되살림]) `[{차례, 줄}]` — 셈 안 함 · 못 읽는 줄은 버림."""
out = []
for one in raw if isinstance(raw, list) else []:
at = one.get("차례") if isinstance(one, dict) else None
if not isinstance(at, int) or isinstance(at, bool) or at < 0:
continue
try:
out.append({"차례": at, "줄": _added(one.get("줄"), "뺀 더한줄", whole)})
except DocError:
continue
return out
def _sub_patch(raw: dict, where: str) -> dict:
"""둘째 기초 세부 고침(727-2) — `덮개` {세부 cid: 덮개} · `뺀줄` [세부 cid] · 빈 칸은 안 둠 ·
`원값` · `바탕판` = 서버 몫(버림)."""
covers = {
_cid(c, f"{where} 덮개"): _cover(one, f"{where} 덮개 「{c}」", False)
for c, one in (raw.get("덮개") or {}).items()
}
drop = [_cid(x, f"{where} 뺀줄") for x in raw.get("뺀줄") or []]
if len(set(drop)) != len(drop):
raise DocError(f"{where} · 뺀줄에 같은 줄 두 번")
return {**({"덮개": covers} if covers else {}), **({"뺀줄": drop} if drop else {})}
BODY = ("고름", "자재표시", "켜쌓기율", "더한줄") # 넣은 SK 몸(740-1 · `M02_Library_Flat`)
def _sub_body(raw: dict, where: str, whole: Any) -> dict:
"""넣은 SK 한 줄(740-1) — 그 SK 의 고름 · 자재표시 · 켜쌓기율 · 더한줄(M01 키만) · `본뜬`(출처
정보 · 셈에 안 씀) · 빈 칸은 안 둠."""
out: dict[str, Any] = {}
if isinstance(raw.get("본뜬"), dict):
out["본뜬"] = raw["본뜬"]
if not any(raw.get(k) not in (None, {}, []) for k in BODY):
return out
body = {k: raw.get(k) for k in BODY if raw.get(k) not in (None, {}, [])}
doc = {"키": "SK-000000", "이름": "줄", "단위": "식", "바탕": {"로직": raw.get("로직")}, **body}
try:
got = clean(doc, None, whole)
except DocError as e:
raise DocError(f"{where} · {e}") from e
return out | {k: got[k] for k in BODY if got.get(k) not in (None, {}, [])}
def sub_patched(one: dict) -> bool:
"""고친 둘째 기초 — 더한줄 로직에 세부 덮개 · 뺀줄 · 넣은 SK 몸(740-1)."""
return one.get("종류") == "로직" and any(one.get(k) for k in ("덮개", "뺀줄", *BODY))
def _added_names(whole: Any, adds: Any) -> set[str]:
"""더한줄 로직 입력 이름(727-3 · SK 입력값에 남길 옵션) — 못 읽는 로직은 뺌
(검사가 따로 막음)."""
out: set[str] = set()
if whole is None:
return out
for one in adds or []:
if isinstance(one, dict) and one.get("종류") == "로직":
try:
out |= {
str(s.get("이름")) for s in whole.logic(str(one.get("로직"))).get("입력") or []
}
except Exception: # noqa: BLE001
continue
return out
def _logic(whole: Any, code: str, where: str) -> dict | None:
if whole is None:
return None
try:
return whole.logic(code)
except Exception as e: # noqa: BLE001 — 엔진 FormulaError(가져오기 둘레 피함)
raise DocError(f"{where} · {e}") from e
def _element(whole: Any, key: str, where: str) -> dict | None:
if not key:
raise DocError(f"{where} · 요소 키가 빔")
if whole is None:
return None
try:
return whole.get(key)
except Exception as e: # noqa: BLE001
raise DocError(f"{where} · 없는 요소 「{key}」") from e
def clean(doc: Any, base_row: dict | None = None, whole: Any = None) -> dict[str, Any]:
"""서버가 받는 꼴로 — 틀리면 `DocError`(400). `base_row` = 패치를 얹을 바탕 줄(입력 이름 ·
열린입력 검사) · `whole` = M01(요소 · 로직 · 자재 키 검사). 짝 없는 cid 는 받음(셈 때 알림) ·
`원값` · `바탕판` 은 서버 몫이라 버림."""
if not isinstance(doc, dict):
raise DocError("단가산출 문서가 묶음 아님")
key = str(doc.get("키") or "")
if not KEY.match(key):
raise DocError("단가산출 키는 SK-######")
name, unit = str(doc.get("이름") or "").strip(), str(doc.get("단위") or "").strip()
if not name:
raise DocError("단가산출 이름이 빔")
if not unit:
raise DocError("단가산출 단위가 빔 — 1 단위 값의 단위")
base = _base(doc.get("바탕"))
if base and "로직" in base and whole is not None and base_row is None:
base_row = _logic(whole, base["로직"], "바탕")
names = {str(s.get("이름")) for s in (base_row or {}).get("입력") or []}
given = _given(doc.get("입력값"), "입력값")
opened = [str(x) for x in doc.get("열린입력") or []]
if base_row is not None:
keep = names | _added_names(whole, doc.get("더한줄"))
given = {k: v for k, v in given.items() if k in keep}
bad = [x for x in opened if x not in names or x in given]
if bad:
raise DocError(f"열린입력은 바탕 입력 중 값 안 넣은 것만 {bad}")
out: dict[str, Any] = {"종류": TITLE, "키": key, "이름": name, "단위": unit, "바탕": base}
out.update({k: str(doc.get(k) or "").strip() for k in ("구분", "상세구분")})
out["본뜬"] = doc["본뜬"] if isinstance(doc.get("본뜬"), dict) else None
out["입력값"], out["열린입력"] = given, opened
if doc.get("켜쌓기율") is not None:
out["켜쌓기율"] = plain(_num(doc["켜쌓기율"], "켜쌓기율"))
if doc.get("환산"):
out["환산"] = _swap_how(doc["환산"])
picks = {}
for cid, mt in (doc.get("고름") or {}).items():
mt = str(mt or "")
if not mt.startswith("MT") or (whole is not None and _element(whole, mt, "고름") is None):
raise DocError(f"고름 「{cid}」 · M01 자재품목 키 아님 「{mt}」")
picks[_cid(cid, "고름")] = mt
out["고름"] = picks
out["자재표시"] = _marks(doc.get("자재표시"))
out["덮개"] = {
_cid(cid, "덮개"): _cover(one, f"덮개 「{cid}」", bool(opened))
for cid, one in (doc.get("덮개") or {}).items()
}
out["더한줄"] = [
_added(one, f"더한줄 {at}째", whole) for at, one in enumerate(doc.get("더한줄") or [], 1)
]
drop = [_cid(x, "뺀줄") for x in doc.get("뺀줄") or []]
if len(set(drop)) != len(drop):
raise DocError("뺀줄에 같은 줄 두 번")
out["뺀줄"] = drop
if gone := _gone_adds(doc.get("뺀더한줄"), whole):
out["뺀더한줄"] = gone
return out
def _swap_how(raw: Any) -> dict[str, Any]:
"""단위 환산 칸(728-2) — 계수(양수) · 단위중량 {종별, 형상} 중 있는 것만."""
if not isinstance(raw, dict):
raise DocError("환산이 묶음 아님")
out: dict[str, Any] = {}
if raw.get("계수") is not None:
coef = _num(raw["계수"], "환산 계수")
if coef <= 0:
raise DocError("환산 계수는 0 보다 커야 함")
out["계수"] = plain(coef)
pick = raw.get("단위중량")
if isinstance(pick, dict) and pick.get("종별") and pick.get("형상"):
out["단위중량"] = {k: str(pick[k]) for k in ("종별", "형상")}
return out
def _marks(raw: Any) -> dict[str, dict[str, str]]:
from M02_MasterTemplete import M02_Labor_Doc as labor
out = {}
for cid, one in (raw or {}).items() if isinstance(raw, dict) else ():
got = labor.marks(one)
if got:
out[_cid(cid, "자재표시")] = got
return out
# ── 고친 줄 ───────────────────────────────────────────────────────────
def _covered(item: dict, cover: dict, extra: bool) -> tuple[dict, str | None]:
"""덮개 한 줄을 로직 줄에 — (고친 줄, 못 덮은 까닭)."""
item = dict(item)
if extra: # 덧줄 = 율(qty) · 배율만
if any(k in cover for k in PARTS):
return item, "덧줄은 율만 고침"
formula = str(item.get("식") or "")
if "배율" in cover:
item["식"] = f"({formula}) * {text(cover['배율'])}"
return item, None
found = _PCT.match(formula)
if not found:
return item, "덧줄 식이 「X * 율 / 100」 꼴 아님"
item["식"] = f"{found.group('x').strip()} * {text(cover['qty'])} / 100"
return item, None
if "qty" in cover:
item["수량"] = text(cover["qty"])
elif "배율" in cover:
item["수량"] = f"({item.get('수량')}) * {text(cover['배율'])}"
prices = {PARTS[k]: Decimal(str(cover[k])) for k in PARTS if k in cover}
if not prices:
return item, None
if item.get("종류") == "로직":
item["값묶음"] = {**dict(item.get("값묶음") or {}), **prices}
return item, None
part = str(item.get("비목") or "")
if set(prices) != {part}:
return item, f"비목 다름 — 이 줄은 {part or '비목 없음'}"
item["단가"] = prices[part]
return item, None
def _add_item(one: dict) -> dict:
"""더한줄 → 엔진 호표 줄 — 참조 · 고정 줄 `값묶음` 은 셈(`M02_Sangeun_Calc`)이 채움."""
head = {"이름": one.get("이름") or "", "단위": one.get("단위") or ""}
qty = text(one["수량"])
if "참조" in one or "고정" in one:
kept = {k: one[k] for k in ("참조", "고정") if k in one}
return {**head, "종류": "로직", "요소": None, "수량": qty, **kept, "값묶음": None}
if one.get("종류") == "로직":
args = "".join(
f", {k if _NAME.match(k) else repr(k)}="
+ (text(v) if isinstance(v, (int, float, Decimal)) else f"'{v}'")
for k, v in (one.get("입력값") or {}).items()
)
return {**head, "종류": "로직", "요소": f"로직({one['로직']}{args})", "수량": qty}
item = {**head, "종류": one["종류"], "요소": one["요소"], "수량": qty, "비목": one["비목"]}
return {**item, **({"단가": Decimal(str(one["단가"]))} if "단가" in one else {})}
def patched(
row: dict, doc: dict, picks: dict | None = None, shown: dict | None = None
) -> dict[str, Any]:
"""바탕 줄(M01 로직 줄 · 바탕 SK 의 고친 줄) + SK 패치 → 고친 줄(순수 · 엔진 안 부름).
`picks` · `shown` = 바탕 쪽 차례 열쇠 고름 · 자재표시(바탕 SK 사슬) → 새 차례로 옮김.
돌려줌 = {줄, 고름 {차례: MT}, 자재표시 {차례: 칸}, 정체 [새 줄 cid · 더한줄 = `add:#k`],
짝잃음 [{cid, 까닭}]}."""
items, extras = list(row.get("호표") or []), list(row.get("덧줄") or [])
ids = line_ids(row)
covers, drop = doc.get("덮개") or {}, set(doc.get("뺀줄") or [])
mine = doc.get("고름") or {}
marks = doc.get("자재표시") or {}
lost = [
{"cid": c, "까닭": "뺀줄 — 바탕에 없는 줄"} for c in doc.get("뺀줄") or [] if c not in ids
]
new_items, new_extras, cids, out_picks, out_marks = [], [], [], {}, {}
for at, cid in enumerate(ids):
if cid in drop:
continue
extra = at >= len(items)
item = extras[at - len(items)] if extra else items[at]
if cid in covers:
item, why = _covered(item, covers[cid], extra)
if why:
lost.append({"cid": cid, "까닭": why})
if extra:
new_extras.append(item)
continue
n = str(len(new_items))
old = (picks or {}).get(str(at))
if cid in mine and item.get("종류") != "재료":
lost.append({"cid": cid, "까닭": "고름 — 재료 줄 아님"})
elif cid in mine or old:
out_picks[n] = mine.get(cid) or old
if (shown or {}).get(str(at)) or cid in marks:
out_marks[n] = marks.get(cid) or shown[str(at)]
new_items.append(item)
cids.append(cid)
for k, one in enumerate(doc.get("더한줄") or []):
if one.get("종류") == "재료": # 더한 재료 = 기본 계 안(직접계산) · `별도계상` 칸만 밖
out_marks[str(len(new_items))] = {
"별도계상": "별도계상" if one.get("별도계상") else "직접계산"
}
new_items.append(_add_item(one))
cids.append(f"add:#{k}")
cids += [c for at, c in enumerate(ids) if at >= len(items) and c not in drop]
for name, keys in (("덮개", covers), ("고름", mine), ("자재표시", marks)):
lost += [{"cid": c, "까닭": f"{name} — 바탕에 없는 줄"} for c in keys if c not in ids]
return {
"줄": {**row, "호표": new_items, "덧줄": new_extras},
"고름": out_picks,
"자재표시": out_marks,
"정체": cids,
"짝잃음": lost,
}
def empty_row(doc: dict) -> dict[str, Any]:
"""바탕 없는 단가산출의 바탕 줄 — 입력 · 줄 없음(더한줄만 셈)."""
return {
"키": doc.get("키"),
"이름": doc.get("이름"),
"결과단위": f"원/{doc.get('단위') or ''}",
"입력": [],
"중간": [],
"호표": [],
"덧줄": [],
}