feat(793-2): 가져온 식 단가산출 SK 담기 — 바탕 갈래 · 원본 메타 · 검산 · 우리 단가로 · 이음 고름 · 우리 기초로 바꾸기 · 만들기 길

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 17:44:31 +09:00
co-authored by Claude Opus 5.5
parent c0604b917f
commit e393d67d98
6 changed files with 864 additions and 26 deletions
+3 -1
View File
@@ -22,6 +22,7 @@ from typing import Any
from common_util.common_util_derived_store import derived_key
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Sangeun_Imported as imp
DROP = {"키", "이름", "구분", "상세구분", "본뜬", "메모", "별칭", "바탕판", "원값", "계산값", "열"}
DROP |= {"일위대가번", "종류", "수정일", "출처", "처음수량", "뺀더한줄"} # 744-1 되돌림 기억
@@ -85,7 +86,7 @@ class Same:
doc = flat.flat_doc(kind, doc)
body = self._sk(doc, trail) if kind == "sangeun" else self._sc(doc, trail)
if kind == "sangeun" and not body.get("바탕") and not body.get("더한줄"):
if kind == "sangeun" and not any(body.get(k) for k in ("바탕", "더한줄", "가져온")):
return None
if kind != "sangeun" and not body.get("담은로직") and not body.get("자재"):
return None
@@ -133,6 +134,7 @@ class Same:
"뺀줄": sorted(map(str, doc.get("뺀줄") or [])),
# 값 바꾸는 새 칸(790-2 · 792-1) — 없으면 빠짐 = 옛 지문 그대로
**{k: norm(doc.get(k)) for k in ("상수덮개", "표값덮개", "식덮개", "변수줄")},
"가져온": norm(imp.same_part(doc)), # 가져온 식(793-2) = 로직 · 단가기준
}
return {k: v for k, v in body.items() if v not in EMPTY}
+7 -2
View File
@@ -31,6 +31,7 @@ from typing import Any
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Formula as fx
from M02_MasterTemplete import M02_Sangeun_Imported as imp
NEW = ("상수덮개", "표값덮개", "글줄", "까닭")
_SPOT = re.compile(r"^(mid:.+|(labor|material|machine|logic|pct):.*#\d+)$")
@@ -160,6 +161,8 @@ def no_flat(doc: Any) -> None:
"""펼침 막음 — 상수 · 표값 덮개 SK(펼친 줄에 못 옮김 · 옛 참조 그대로 = 값 그대로)."""
if isinstance(doc, dict) and (doc.get("상수덮개") or doc.get("표값덮개") or fx.has(doc)):
raise sd.DocError("상수 · 표값 덮개 · 식 칸 단가산출은 펼치지 않음(참조 그대로)")
if imp.is_imported(doc): # 가져온 식(793-2) = M01 기초 없음
raise sd.DocError("가져온 식 단가산출은 펼치지 않음(참조 그대로)")
def covers_n(doc: dict) -> int:
@@ -226,7 +229,7 @@ def clean_more(raw: Any, out: dict) -> dict:
why = {_key(k, "까닭", _KEY): _words(v, f"까닭 「{k}」") for k, v in why.items()}
if why := {k: v for k, v in why.items() if v}:
out["까닭"] = why
return fx.clean(raw, out) # 식 칸(792-1)
return imp.clean(raw, fx.clean(raw, out)) # 식 칸(792-1) · 가져온 식(793-2)
def _print(one: Any) -> str:
@@ -242,6 +245,8 @@ def stamp(row: dict, doc: dict | None = None) -> dict[str, Any]:
if doc is not None and fx.has(doc):
fixed = fx.settle(row, doc, strict=True)[2]
doc.update({k: fixed[k] for k in ("더한줄", *fx.FIELDS) if k in fixed})
if doc is not None and imp.is_imported(doc): # 가져온 식 검산(793-2) = 서버가 다시
doc[imp.KEY]["검산"] = imp.check(doc)
lines = [*(row.get("중간") or []), *(row.get("호표") or []), *(row.get("덧줄") or [])]
spots = [f"mid:{s.get('이름')}" for s in row.get("중간") or []] + sd.line_ids(row)
@@ -325,7 +330,7 @@ def answer(got: dict, clean: dict, raw: Any, given: dict | None, trail: tuple) -
from M02_MasterTemplete import M02_Sangeun_Calc as calc
out = {**calc.views.finish(calc.per_unit(got, clean)), "머리": calc.head_of(clean)}
return calc.labor._json(out | sheet(out, clean, raw, given, trail))
return calc.labor._json(imp.more(out | sheet(out, clean, raw, given, trail), clean))
def _bare(doc: dict) -> dict:
+4 -10
View File
@@ -511,13 +511,7 @@ def patched(
def empty_row(doc: dict) -> dict[str, Any]:
"""바탕 없는 단가산출의 바탕 줄 — 입력 · 줄 없음(더한줄만 셈)."""
return {
"키": doc.get("키"),
"이름": doc.get("이름"),
"결과단위": f"원/{doc.get('단위') or ''}",
"입력": [],
"중간": [],
"호표": [],
"덧줄": [],
}
"""바탕 없는 단가산출의 바탕 줄 — 입력 · 줄 없음(더한줄만 셈) · 가져온 식(793-2) = 그 줄."""
from M02_MasterTemplete import M02_Sangeun_Imported as imp
return imp.base_row(doc)
+430 -12
View File
@@ -6,18 +6,23 @@
가져온 로직 줄(만들기 몸 `로직`) = {이름, 결과단위 「원/단위」, 입력?, 중간? [{이름, 식, 표시?}],
호표 [줄], 덧줄? [{이름, 식, 비목, 번지?, 합밖?, 자리?}], 단계?, 끝전?, 조정?, 끝수?}
호표 줄 = {종류 인력|재료|기계|로직, 이름, 규격?, 단위, 수량(엔진 식 글), 비목(로직 줄은 없으면
노 · 재 · 경 나눠 얹음), 단가?(엑셀 단가 · 인력 · 재료 · 기계) | 값묶음? {노무비, 재료비, 경비}
(엑셀 · 로직 줄), 이음?(M01 요소 키 LB · MT · EQ … | 로직 줄 = 「로직(GC…, 이름='값')」 글 | null),
번지?, 합밖?, 자리?} · `요소` 칸은 서버가 채움(이음 | {이름}) — 줄 정체 cid 머리 = 이음 키 | 이름.
원본(만들기 몸 `원본`) = {파일, 시트, 행 [처음, 끝], 갈래 STmate|EST, 엑셀금액 {노무비, 재료비, 경비, 계}
(1 단위), 글줄 [{행, 글, 자리?(셈 줄 cid | mid:이름), 값?(그 줄 엑셀 금액)}]}.
SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않음), 단가기준 엑셀|우리, 원본, 검산 {같음,
셈 {노..계}, 엑셀 {노..계}, 줄 [{자리, 행, 셈, 엑셀, 같음, 까닭?}]}, 근거후보 [{기초, 옵션, 품검산,
단 확정|근접|사람}](793-3 이 채움), 바꿈?(우리 기초로 바꾼 뒤 = 그때 로직 · 단가기준 · 전 · 후 · 차이)} ·
바탕 = null · 가져온 식 SK = `가져온.로직` 있음.
단가기준 엑셀 = 줄 단가 · 값묶음 그대로 / 우리 = 이음 있는 줄만 단가 · 값묶음 떼고 M01(B09 = 사본 단가)
지금 값 · 이음 없는 줄 = 엑셀 단가 그대로 + 표지(`이음없음`).
호표 줄 = {종류 인력|재료|기계|로직, 이름, 규격?, 단위, 수량(엔진 식 글),
비목(로직 줄은 없으면 노 · 재 · 경 나눠 얹음),
단가?(엑셀 단가 · 인력 · 재료 · 기계) | 값묶음? {노무비, 재료비, 경비}(엑셀 · 로직 줄),
이음?(M01 요소 키 LB · MT · EQ … | 로직 줄 = 「로직(GC…, 이름='값')」 글 | null),
번지?, 합밖?, 자리?} · `요소` 칸은 서버가 채움(이음 | {이름}) ·
줄 정체 cid 머리 = 이음 키 | 이름.
원본(만들기 몸 `원본`) = {파일, 시트, 행 [처음, 끝], 갈래 STmate|EST,
엑셀금액 {노무비, 재료비, 경비, 계}(1 단위),
글줄 [{행, 글, 자리?(셈 줄 cid | mid:이름), 값?(그 줄 엑셀 금액)}]}.
SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않음), 단가기준 엑셀|우리, 원본,
검산 {같음, 까닭, 셈 {노..계}, 엑셀 {노..계}, 줄 [{자리, 행, 셈, 엑셀, 같음, 까닭?}]},
근거후보 [{기초, 옵션, 품검산, 단 확정|근접|사람}](793-3 이 채움),
바꿈?(우리 기초로 바꾼 뒤 = 그때 로직 · 단가기준 · 전 · 후 · 차이)} ·
바탕 = null · 가져온 식 SK = `가져온.로직` 있음 · 검산 = 저장 때 서버가 다시.
단가기준 엑셀 = 줄 단가 · 값묶음 그대로 /
우리 = 이음 있는 줄만 단가 · 값묶음 떼고 M01(B09 = 사본 단가) 지금 값 ·
이음 없는 줄 = 엑셀 단가 그대로 + 표지(`이음없음`).
길(`M02_Sangeun_Router` · 저장 없음 = 화면 캐시 · 만들기만 층 쓰기):
POST …/sangeun/imported 몸 {로직, 원본, 이름?, 단위?, 구분?, 상세구분?, 새로?, 씀?}
→ {이름(SK 키), 검산, 단가기준}(서버가 다시 셈해 엑셀과 맞댐 · 같은 내용 409 = 기존 길)
@@ -27,3 +32,416 @@ SK 문서 덧칸 `가져온` = {로직(엑셀 단가 그대로 · 고치지 않
셈 답(`…/sangeun/calc`) = 옛 꼴 그대로 · `원래` = 엑셀 금액(「M01 원래」 자리) · 덧칸 `가져온`
{단가기준, 파일, 시트, 행, 갈래, 엑셀금액, 글줄 [{행, 글, 자리, 엑셀, 지금, 같음}], 이음없음}.
"""
from __future__ import annotations
import json
import re
from decimal import Decimal
from typing import Any
from M02_MasterTemplete import M02_Sangeun_Doc as sd
KEY = "가져온"
BASIS = ("엑셀", "우리")
COSTS = ("노무비", "재료비", "경비")
KINDS = ("인력", "재료", "기계", "로직")
ROW = ("이름", "결과단위", "입력", "중간", "호표", "덧줄", "단계", "끝전", "조정", "끝수")
LINE = ("종류", "이름", "규격", "단위", "수량", "비목", "단가", "값묶음", "이음", "번지", "합밖")
LINE += ("자리", "출처", "비고")
EXTRA = ("이름", "식", "비목", "번지", "합밖", "자리", "출처", "비고")
META = ("원본", "검산", "근거후보", "바꿈") # 검산 = 저장 때 서버가 다시 적음(`stamp`)
MANY = 300 # 호표 · 덧줄 · 중간 줄 수
BIG = 400_000 # 메타 글 크기(바이트)
_KEYED = ("덮개", "고름", "자재표시", "상수덮개", "표값덮개", "식덮개", "까닭", "M01판")
def _mf() -> Any:
from M02_MasterTemplete import M02_Labor_Doc as labor
return labor.m01.cm.mf
def _whole() -> Any:
from M01_MasterData import M01_MasterData_Store as m01
return m01.loaded()[1]
def is_imported(doc: Any) -> bool:
return isinstance(doc, dict) and isinstance((doc.get(KEY) or {}).get("로직"), dict)
# ── 바탕 줄 ───────────────────────────────────────────────────────────
def row_of(doc: Any) -> dict | None:
"""가져온 식 바탕 줄(엔진 로직 줄) — 단가기준 우리 = 이음 있는 줄의 단가 · 값묶음 뗌 · 요소 =
이음 | {이름}(로직 줄 = 이음 | None) · 가져온 식 아님 = None."""
if not is_imported(doc):
return None
meta = doc[KEY]
ours = meta.get("단가기준") == "우리"
items = []
for one in meta["로직"].get("호표") or []:
link = one.get("이음")
item = {k: v for k, v in one.items() if k != "이음"}
item["요소"] = link or (None if one.get("종류") == "로직" else {"이름": one.get("이름")})
if ours and link:
item.pop("단가", None)
item.pop("값묶음", None)
items.append(item)
row = {"입력": [], "중간": [], "덧줄": [], **meta["로직"]}
return {**row, "키": doc.get("키"), "호표": items}
def base_row(doc: dict) -> dict[str, Any]:
"""바탕 없는 SK 바탕 줄(`sd.empty_row`) — 가져온 식 = 그 줄 · 아니면 입력 · 줄 없음."""
return row_of(doc) or {
"키": doc.get("키"),
"이름": doc.get("이름"),
"결과단위": f"원/{doc.get('단위') or ''}",
**{k: [] for k in ("입력", "중간", "호표", "덧줄")},
}
# ── 꼴 검사 ───────────────────────────────────────────────────────────
def _formula(raw: Any, where: str) -> str:
from M02_MasterTemplete import M02_Sangeun_Formula as fx
text = str(raw if raw is not None else "").strip()
if not text:
raise sd.DocError(f"{where} · 식이 빔")
if bad := fx.limits(text):
raise sd.DocError(f"{where} · {bad[0]}")
try:
_mf().parse(text)
except Exception as e: # noqa: BLE001 — 엔진 FormulaError
raise sd.DocError(f"{where} · {e}") from e
return text
def _link(raw: Any, kind: str, where: str, whole: Any) -> str | None:
"""이음 — 로직 줄 = 「로직(GC…)」 글(부르는 로직 있음) · 그 밖 = M01 요소 키(있음)."""
if raw in (None, ""):
return None
text = str(raw).strip()
try:
if kind == "로직":
node = _mf().parse(text)
if node[0] != "logic":
raise ValueError("로직(…) 꼴 아님")
whole.logic(node[1])
else:
whole.get(text)
except Exception as e: # noqa: BLE001
raise sd.DocError(f"{where} · 없는 이음 「{text}」 ({e})") from e
return text
def _line(raw: Any, where: str, whole: Any) -> dict:
if not isinstance(raw, dict):
raise sd.DocError(f"{where} · 묶음 아님")
kind = raw.get("종류")
if kind not in KINDS:
raise sd.DocError(f"{where} · 종류는 {' · '.join(KINDS)}")
out = {k: raw[k] for k in LINE if raw.get(k) not in (None, "")}
out["이름"] = str(raw.get("이름") or "").strip() or where
out["단위"] = str(raw.get("단위") or "")
out["수량"] = _formula(raw.get("수량"), f"{where} 수량")
out["이음"] = _link(raw.get("이음"), kind, where, whole)
if kind != "로직" and out.get("비목") not in sd.PARTS.values():
raise sd.DocError(f"{where} · 비목은 노무비 · 재료비 · 경비")
if "단가" in out:
out["단가"] = sd.plain(sd._num(out["단가"], f"{where} 단가"))
if "값묶음" in out:
pack = out["값묶음"] if isinstance(out["값묶음"], dict) else {}
out["값묶음"] = {k: sd.plain(sd._num(pack.get(k, 0), f"{where} {k}")) for k in COSTS}
if kind == "로직" and "단가" in out:
raise sd.DocError(f"{where} · 로직 줄 엑셀 값은 값묶음")
if not any(k in out for k in ("단가", "값묶음")) and not out["이음"]:
raise sd.DocError(f"{where} · 엑셀 단가 · 이음 둘 다 없음")
return out
def clean_row(raw: Any, whole: Any = None) -> dict[str, Any]:
"""가져온 로직 줄 꼴 — 줄 수 · 식(엔진 파서 · 상한) · 이음 키 · 엑셀 값 · 틀리면 `DocError`."""
whole = whole or _whole()
if not isinstance(raw, dict):
raise sd.DocError("가져온 로직 · 묶음 아님")
out = {k: raw[k] for k in ROW if raw.get(k) not in (None, "", [], {})}
out["이름"] = str(raw.get("이름") or "").strip() or "가져온 식"
if not re.match(r"^원/\S+$", str(raw.get("결과단위") or "").strip()):
raise sd.DocError("가져온 로직 · 결과단위는 「원/단위」")
items, extras, mids = raw.get("호표") or [], raw.get("덧줄") or [], raw.get("중간") or []
if not all(isinstance(x, list) for x in (items, extras, mids)) or not items:
raise sd.DocError("가져온 로직 · 호표(한 줄 이상) · 덧줄 · 중간은 목록")
if max(len(items), len(extras), len(mids)) > MANY:
raise sd.DocError(f"가져온 로직 · 줄은 {MANY} 까지")
out["호표"] = [_line(x, f"호표 {n}째", whole) for n, x in enumerate(items, 1)]
if extras:
out["덧줄"] = [_extra(x, f"덧줄 {n}째") for n, x in enumerate(extras, 1)]
if mids:
out["중간"] = [
{**x, "식": _formula(x.get("식"), f"중간 「{x.get('이름')}」")}
for x in mids
if isinstance(x, dict) and str(x.get("이름") or "").strip()
]
return out
def _extra(raw: Any, where: str) -> dict:
if not isinstance(raw, dict) or raw.get("비목") not in sd.PARTS.values():
raise sd.DocError(f"{where} · 묶음 · 비목(노무비 · 재료비 · 경비)")
out = {k: raw[k] for k in EXTRA if raw.get(k) not in (None, "")}
return {**out, "이름": str(raw.get("이름") or where), "식": _formula(raw.get("식"), where)}
def _meta(raw: Any, name: str) -> Any:
try:
size = len(json.dumps(raw, ensure_ascii=False, default=str).encode("utf-8"))
except (TypeError, ValueError) as e:
raise sd.DocError(f"가져온 {name} · 글로 못 담음") from e
if size > BIG:
raise sd.DocError(f"가져온 {name} · {BIG} 바이트를 넘음")
want = list if name == "근거후보" else dict
if not isinstance(raw, want):
raise sd.DocError(f"가져온 {name} · {'목록' if want is list else '묶음'} 아님")
return raw
def clean(raw: Any, out: dict) -> dict:
"""`sd.clean` 끝(`const.clean_more`) — `가져온` 칸 · 바탕과 같이 못 둠."""
meta = raw.get(KEY) if isinstance(raw, dict) else None
if meta in (None, {}):
return out
if not isinstance(meta, dict):
raise sd.DocError("가져온 · 묶음 아님")
got = {k: _meta(meta[k], k) for k in META if meta.get(k) not in (None, {}, [])}
if meta.get("로직") is not None:
if out.get("바탕"):
raise sd.DocError("가져온 식 단가산출은 바탕 없음")
basis = meta.get("단가기준") or "엑셀"
if basis not in BASIS:
raise sd.DocError("단가기준은 엑셀 · 우리")
got |= {"로직": clean_row(meta["로직"]), "단가기준": basis}
return out | ({KEY: got} if got else {})
def same_part(doc: Any) -> dict | None:
"""같은 내용 지문 몫(`M02_Library_Same`) — 로직 · 단가기준(원본 · 검산 · 근거후보 뺌)."""
if not is_imported(doc):
return None
return {k: doc[KEY].get(k) for k in ("로직", "단가기준")}
def unlinked(doc: dict) -> list[dict]:
"""이음 없는 자원 줄(표지 · 사람이 고름) `[{자리, 이름}]`."""
row = row_of(doc) or {}
ids = sd.line_ids(row)
return [
{"자리": ids[n], "이름": one.get("이름") or ""}
for n, one in enumerate(doc[KEY]["로직"].get("호표") or [])
if not one.get("이음")
]
# ── 셈 · 검산 ─────────────────────────────────────────────────────────
def _json(value: Any) -> Any:
from M02_MasterTemplete import M02_Labor_Doc as labor
return labor._json(value)
def _run(doc: dict, whole: Any) -> dict:
from M02_MasterTemplete import M02_Sangeun_Calc as calc
return calc.run(sd.clean(doc, None, whole), whole)
def _money(got: dict) -> dict | None:
unit = got.get("1단위")
if not unit:
return None
return {**{k: unit[k] for k in COSTS}, "계": unit["계"]}
def _same(mine: Any, excel: Any) -> bool:
"""셈 ≈ 엑셀 — 엑셀 값 끝자리 한 칸 안(엑셀 보이는 값 = 끊은 값)."""
if mine is None or excel is None:
return False
want = Decimal(str(excel))
step = Decimal(1).scaleb(min(want.normalize().as_tuple().exponent, 0))
return abs(Decimal(str(mine)) - want) < step
def check(doc: dict, whole: Any = None) -> dict[str, Any]:
"""검산 — 가져온 줄(엑셀 단가 · 고침 없음)을 엔진으로 다시 셈 → 엑셀 금액 · 줄마다 맞댐 ·
못 셈 = {같음 False, 까닭}."""
whole = whole or _whole()
meta = doc[KEY]
bare = {k: doc.get(k) for k in ("키", "이름", "단위", "입력값", "환산")}
bare |= {"바탕": None, KEY: {"로직": meta["로직"], "단가기준": "엑셀"}}
src = meta.get("원본") or {}
excel = src.get("엑셀금액") or {}
try:
got = _run(bare, whole)
except sd.DocError as e:
return {"같음": False, "까닭": str(e), "셈": None, "엑셀": excel, "줄": []}
mine = _money(got)
lines = _lines(src, {x.get("cid"): x.get("금액") for x in got.get("줄") or []})
ok = bool(mine) and all(_same(mine.get(k), excel.get(k)) for k in excel) and excel != {}
why = None if ok else (got.get("까닭") or ("엑셀 금액 없음" if not excel else "금액 다름"))
ok = ok and all(x["같음"] for x in lines)
return _json({"같음": ok, "까닭": why, "셈": mine, "엑셀": excel, "줄": lines})
def _lines(src: dict, now: dict) -> list[dict]:
out = []
for one in src.get("글줄") or []:
spot, want = one.get("자리"), one.get("값")
if not spot or want is None:
continue
got = now.get(spot)
same = _same(got, want)
why = None if same else ("짝 줄 없음" if spot not in now else f"셈 {got} · 엑셀 {want}")
out.append({"자리": spot, "행": one.get("행"), "셈": got, "엑셀": want, "같음": same})
out[-1] |= {"까닭": why} if why else {}
return out
def more(out: dict, clean_: dict) -> dict:
"""셈 답 덧칸(`const.answer`) — 가져온 식: `원래` = 엑셀 금액 · `가져온` 원본 글 보기."""
if not is_imported(clean_):
return out
meta = clean_[KEY]
src = meta.get("원본") or {}
spots = (out.get("시트") or {}).get("자리") or {}
now = {k: v.get("지금") for k, v in spots.items()}
lines = {x["자리"]: x for x in _lines(src, now)}
texts = [
{k: one.get(k) for k in ("행", "글", "자리")}
| ({"엑셀": one["값"]} if one.get("값") is not None else {})
| ({"지금": lines[one["자리"]]["셈"], "같음": lines[one["자리"]]["같음"]}
if one.get("자리") in lines else {})
for one in src.get("글줄") or []
] # fmt: skip
head = {k: src.get(k) for k in ("파일", "시트", "행", "갈래", "엑셀금액")}
view = {"단가기준": meta.get("단가기준"), **head, "글줄": texts, "이음없음": unlinked(clean_)}
view |= {k: meta[k] for k in ("검산", "근거후보", "바꿈") if meta.get(k)}
excel = src.get("엑셀금액")
return out | {KEY: view} | ({"원래": excel} if excel else {})
# ── 고치기(화면 캐시 · 저장 없음) ───────────────────────────────────────
def _need(doc: Any) -> dict:
if not is_imported(doc):
raise sd.DocError("가져온 식 단가산출 아님")
return doc
def _with(doc: dict, **meta: Any) -> dict:
return {**doc, KEY: {**doc[KEY], **meta}}
def to_price(doc: Any, basis: str) -> dict[str, Any]:
"""[우리 단가로] · [엑셀 단가로] — 단가기준만 바꾼 문서 · 바꾸기 전후 1 단위 금액 · 이음없음."""
doc = _need(doc)
if basis not in BASIS:
raise sd.DocError("단가기준은 엑셀 · 우리")
whole = _whole()
after = _with(doc, 단가기준=basis)
before = _money(_run(doc, whole))
return {
"문서": after,
"전": before,
"후": _money(_run(after, whole)),
"이음없음": unlinked(doc),
}
def link(doc: Any, spot: str, key: Any) -> dict[str, Any]:
"""자원 줄 이음 고름 — 그 줄 `이음` · 줄 정체가 바뀐 만큼 덮개 · 뺀줄 · 글줄 · 까닭 열쇠
옮김."""
doc = _need(doc)
whole = _whole()
old = sd.line_ids(row_of(doc))
if spot not in old[: len(doc[KEY]["로직"]["호표"])]:
raise sd.DocError(f"없는 자원 줄 「{spot}」")
items = [dict(x) for x in doc[KEY]["로직"]["호표"]]
at = old.index(spot)
items[at]["이음"] = _link(key, items[at]["종류"], f"「{spot}」", whole)
after = _with(doc, 로직={**doc[KEY]["로직"], "호표": items})
move = dict(zip(old, sd.line_ids(row_of(after))))
return {"문서": _rekey(after, {a: b for a, b in move.items() if a != b})}
def _rekey(doc: dict, move: dict) -> dict:
if not move:
return doc
out = dict(doc)
for name in _KEYED:
if isinstance(doc.get(name), dict):
out[name] = {move.get(c, c): v for c, v in doc[name].items()}
if isinstance(doc.get("뺀줄"), list):
out["뺀줄"] = [move.get(c, c) for c in doc["뺀줄"]]
if isinstance(doc.get("글줄"), list):
out["글줄"] = [{**t, "자리": move.get(t.get("자리"), t.get("자리"))} for t in doc["글줄"]]
out["더한줄"] = [_rate_rekey(x, move) for x in doc.get("더한줄") or []]
src = doc[KEY].get("원본") or {}
if src.get("글줄"):
lines = [{**t, "자리": move.get(t.get("자리"), t.get("자리"))} for t in src["글줄"]]
out[KEY] = {**out[KEY], "원본": {**src, "글줄": lines}}
return out
def _rate_rekey(one: Any, move: dict) -> Any:
"""율 더한줄 기준 줄 목록(778-4 · `열쇠@비목`) 열쇠 옮김."""
rate = ((one.get("고정") or {}).get("비율") or {}) if isinstance(one, dict) else {}
if not isinstance(rate.get("기준"), list):
return one
keys = []
for k in rate["기준"]:
head, _, part = str(k).partition("@")
keys.append(move.get(head, head) + (f"@{part}" if part else ""))
return {**one, "고정": {**one["고정"], "비율": {**rate, "기준": keys}}}
def to_base(doc: Any, code: str, given: dict | None = None) -> dict[str, Any]:
"""[우리 기초로 바꾸기] — 가져온 식 SK → M01 기초(바탕 로직) + 옵션 SK · 고침 걷힘 ·
원본 · 검산 · 근거후보 보존 + `바꿈` {로직, 단가기준, 전, 후, 차이}(1 단위)."""
doc = _need(doc)
whole = _whole()
before = _money(_run(doc, whole))
head = {k: doc[k] for k in ("키", "이름", "단위", "구분", "상세구분") if k in doc}
new = {**head, "바탕": {"로직": str(code or "").strip()}, "입력값": dict(given or {})}
new |= {"열린입력": [], "고름": {}, "자재표시": {}, "덮개": {}, "뺀줄": [], "더한줄": []}
after = _money(_run(new, whole))
gap = {k: after[k] - before[k] for k in after} if before and after else None
keep = {k: doc[KEY][k] for k in ("원본", "검산", "근거후보") if doc[KEY].get(k)}
was = {k: doc[KEY].get(k) for k in ("로직", "단가기준")}
new[KEY] = {**keep, "바꿈": {**was, "전": before, "후": after, "차이": gap}}
return {"문서": new, "전": before, "후": after, "차이": gap}
# ── 만들기(개인 층) ───────────────────────────────────────────────────
def create(
logic: Any, source: Any, name: str = "", unit: str = "", head: dict | None = None,
new: bool = False, use: str | None = None,
) -> dict[str, Any]: # fmt: skip
"""엔진 로직 줄 + 원본 → 지금 층 새 가져온 식 SK — 서버가 다시 셈해 엑셀과 맞댐(검산 ·
다르면 `같음` False 로 표지 · 막지 않음) · 같은 층 같은 내용 = 409(`ask` · 733-2)."""
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_Library_Ask as ask
whole = _whole()
row = clean_row(logic, whole)
unit = str(unit or "").strip() or row["결과단위"].split("/", 1)[1]
doc = {"키": "SK-000000", "이름": str(name or "").strip() or row["이름"], "단위": unit}
doc |= {k: str((head or {}).get(k) or "") for k in ("구분", "상세구분")}
doc |= {"바탕": None, "입력값": {}, "덮개": {}, "뺀줄": [], "더한줄": []}
meta = {"로직": row, "단가기준": "엑셀"}
if source not in (None, {}):
meta["원본"] = _meta(source, "원본")
doc[KEY] = meta
got = check(sd.clean(doc, None, whole), whole)
key = ask.ask(sd.KIND, doc, new, use) or refs.create(sd.KIND, doc)["이름"]
return {"이름": key, "검산": got, "단가기준": "엑셀"}
+73 -1
View File
@@ -1,6 +1,8 @@
"""M02 단가산출(SK) 셈 길(PLAN 715-1b) — `POST /api/m02/layers/{층}/sangeun/calc` ·
[단가산출로 저장](715-3) `POST /api/m02/layers/{층}/sangeun/from-line`(층 쓰기 권한) ·
식 칸 검사(792-1) `POST /api/m02/layers/{층}/sangeun/formula-check`(셈 없음 · 층 읽기 권한).
식 칸 검사(792-1) `POST /api/m02/layers/{층}/sangeun/formula-check`(셈 없음 · 층 읽기 권한) ·
가져온 식(793-2 · `M02_Sangeun_Imported`) `…/sangeun/imported`(만들기 · 층 쓰기) · `/price` ·
`/link` · `/to-base`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기).
저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만.
몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값) ·
@@ -21,6 +23,7 @@ from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Formula as fx
from M02_MasterTemplete import M02_Sangeun_Imported as imp
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root
router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Sangeun"])
@@ -90,3 +93,72 @@ def check_formula(
) -> dict:
"""식 칸 검사(792-1) — {되나, 까닭, 알림, 식(엔진), 글(이름 꼴)} · 셈 · 저장 없음."""
return _call(_root(session, layer, owner), fx.check_doc, body.문서, body.자리, body.식)
class ImportedBody(BaseModel):
로직: dict[str, Any]
원본: dict[str, Any] | None = None
이름: str = ""
단위: str = ""
구분: str = ""
상세구분: str = ""
새로: bool = False
씀: str | None = None
@router.post("/layers/{layer}/sangeun/imported")
def sangeun_imported(
layer: Layer,
body: ImportedBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""가져온 식 단가산출 만들기(793-2) — 답 = {이름, 검산, 단가기준} · 같은 내용 409."""
folder = _root(session, layer, owner, write=True)
head = {"구분": body.구분, "상세구분": body.상세구분}
args = (body.로직, body.원본, body.이름, body.단위, head, body.새로, body.씀)
return _call(folder, _sd(imp.create), *args)
class ImportedEditBody(BaseModel):
문서: dict[str, Any]
기준: str | None = None # price
자리: str | None = None # link
이음: str | None = None # link
로직: str | None = None # to-base
입력값: dict[str, Any] | None = None # to-base
def _sd(fn: Any) -> Any:
"""`sd.DocError` → 400 · 422(`store.StoreError`)."""
def run(*args: Any) -> Any:
try:
return calc.labor._json(fn(*args))
except sd.DocError as e:
raise calc._fail(e) from e
return run
_EDIT = {
"price": lambda b: (imp.to_price, b.문서, b.기준),
"link": lambda b: (imp.link, b.문서, b.자리, b.이음),
"to-base": lambda b: (imp.to_base, b.문서, b.로직, b.입력값),
}
@router.post("/layers/{layer}/sangeun/imported/{how}")
def sangeun_imported_edit(
layer: Layer,
how: str,
body: ImportedEditBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""가져온 식 고치기(793-2) — price [우리 · 엑셀 단가로] · link 이음 고름 · to-base [우리 기초로
바꾸기] · 답 = 고친 문서(화면 캐시 · 저장 없음) + 전 · 후 금액."""
if how not in _EDIT:
raise HTTPException(status_code=404, detail="price · link · to-base")
fn, *args = _EDIT[how](body)
return _call(_root(session, layer, owner), _sd(fn), *args)