feat(796-2c): M02 수식 탭이 SK 셈 답 전체를 그림 — 더한 기초 · 뺀줄 · 덮개 · 율 줄 · 합밖 소계표 · 합계 = SK 금액 · 고친 칸 표지
- 서버 M02_Sangeun_Equation_Full(새 파일) — 합계 = SK 셈 답 기준 · 소계 = 셈 답 줄마다 한 행(바탕 · 덮음 · 더함 · 뺌 · 합밖) + 끝수 · 더한 기초 수식 나무 · 줄 표지(식 · 상수 · 표값 · 품 · 단가 · 변수줄 · 뺌 + 원래 → 지금) - M02_Sangeun_Equation — read · preview 가 얹음(전 = 고침 없는 SK 금액) · 수식 입력 = 자동값(SK 셈과 같은 입력) - 부품 createEqSum(tree, open) 상태 알약 · 원래 · 접힘 기초 수식 · 줄 표지 · 흐림 CSS - 시험 test_796_2c(견본 넷) · 낡은 시험 정리 — test_792_1 TS 거울 시험(799-2 에 걷은 모델 파일 읽음) 지움 · test_799_1 호출 글 갱신 - SK 금액 전후 같음 · M01 수식 탭 그대로 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -458,7 +458,7 @@ export function createLabEquation(opt: LabEquationOptions) {
|
||||
return;
|
||||
}
|
||||
const edit = canEdit();
|
||||
const sum = createEqSum(eq);
|
||||
const sum = createEqSum(eq, open);
|
||||
body.replaceChildren(
|
||||
createEqChain(eq.줄, {
|
||||
path: "",
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
저장 = SK [저장](화면 캐시 `수식고침` → `clean_doc` 이 다시 얹어 검사 · 셈 ·
|
||||
브라우저 값 안 받음).
|
||||
바탕 참조 SK(`바탕.참조`) = 수식 없음(바탕 SK 의 고침은 그 SK 가 얹음).
|
||||
합계 · 소계(796-2c) = SK 셈 답 기준(`M02_Sangeun_Equation_Full` — 더한 기초 · 뺀줄 · 덮개 · 율 줄 ·
|
||||
합밖) · 줄 값 = 식 덮개 · 상수 · 표값 · 변수줄을 얹은 식(고침 주소는 바탕 차례 그대로) ·
|
||||
전 = 고침 없는 SK 금액.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -76,8 +79,12 @@ def _stand(row: dict) -> tuple[dict, str]:
|
||||
|
||||
|
||||
def export(whole: Any, row: dict, inputs: dict | None) -> dict:
|
||||
"""수식 읽기 — 입력 = 받은 값 › 자동값(SK 셈 `run_line` 과 같음 · 기본 제안값이 안 덮음)."""
|
||||
stand, key = _stand(row)
|
||||
return _eqn().export_row(whole, stand, inputs) | {"열쇠": key}
|
||||
eqn = _eqn()
|
||||
auto = eqn.meq.ma.values(eqn.me.swap(whole, stand), stand)
|
||||
given = {**{k: v for k, v in auto.items() if v is not None}, **(inputs or {})}
|
||||
return eqn.export_row(whole, stand, given) | {"열쇠": key}
|
||||
|
||||
|
||||
def apply(row: dict, edits: list, whole: Any) -> tuple[dict, list[str]]:
|
||||
@@ -130,28 +137,48 @@ def _plain(doc: Any) -> tuple[dict, Any, dict]:
|
||||
return doc, whole, row
|
||||
|
||||
|
||||
def _tree(doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None) -> dict:
|
||||
"""수식 = 바탕 줄(+ 고침 · 식 덮개 · 상수 · 표값 · 변수줄) + SK 셈 답 합계 · 소계(796-2c)."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Const as const
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
|
||||
|
||||
try:
|
||||
shown = const.formulas(base, doc)
|
||||
except Exception: # noqa: BLE001 — 틀린 식 덮개 = 바탕 그대로(짝잃음은 셈 길이 알림)
|
||||
shown = base
|
||||
return full.attach(export(whole, shown, inputs), doc, whole, base, inputs, ans)
|
||||
|
||||
|
||||
def read(doc: Any, inputs: dict | None = None) -> dict[str, Any]:
|
||||
"""{ok, 수식} — 저장본 · 화면 캐시 고침 얹은 수식(못 얹음 = 바탕 그대로 + 까닭)."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
|
||||
|
||||
eqn = _eqn()
|
||||
try:
|
||||
doc, whole, row = _plain(doc)
|
||||
now, lost = on(row, doc, whole)
|
||||
out = {"ok": True, "수식": export(whole, now, inputs)}
|
||||
out = {"ok": True, "수식": _tree(doc, whole, now, inputs, full.answer(doc, inputs))}
|
||||
except (sd.DocError, *eqn._ERR) as e:
|
||||
return {"ok": False, "reason": str(e) or type(e).__name__}
|
||||
return out | ({"reason": lost[0]["까닭"]} if lost else {})
|
||||
|
||||
|
||||
def preview(doc: Any, edits: list, inputs: dict | None = None) -> dict[str, Any]:
|
||||
"""고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(고침 없는 사본), 후}."""
|
||||
"""고침 통째 미리 셈(저장 없음) — {되나, 까닭, 줄, 수식, 전(고침 없는 사본 SK 금액), 후}."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
|
||||
|
||||
eqn = _eqn()
|
||||
try:
|
||||
doc, whole, row = _plain(doc)
|
||||
before = export(whole, row, inputs)["합계"]
|
||||
new, why = apply(row, edits, whole) if edits else (row, [])
|
||||
if why:
|
||||
return {"되나": False, "까닭": why}
|
||||
eq = export(whole, new, inputs)
|
||||
bare = {k: v for k, v in doc.items() if k != KEY}
|
||||
mine = {**bare, KEY: edits} if edits else bare
|
||||
ans = full.answer(mine, inputs)
|
||||
eq = _tree(mine, whole, new, inputs, ans)
|
||||
before = full.total(full.answer(bare, inputs) if edits else ans)
|
||||
before = before or export(whole, row, inputs)["합계"]
|
||||
except (sd.DocError, *eqn._ERR) as e:
|
||||
return {"되나": False, "까닭": [f"셈 막힘 — {e}"]}
|
||||
return {"되나": True, "까닭": [], "줄": new, "수식": eq, "전": before, "후": eq["합계"]}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""M02 단가산출(SK) 수식 탭 — SK 셈 답 기준 소계 · 합계 · 고친 칸 표지(PLAN 796-2c · 요구 657).
|
||||
|
||||
수식 탭 줄(`M02_Sangeun_Equation`) = 바탕 사본 줄(+ 수식고침). 그 위에 SK 셈 답
|
||||
(`M02_Sangeun_Calc.calc_doc` = 화면 · 저장 · B09 가 쓰는 한 벌)을 얹어 화면 금액 = SK 금액
|
||||
(셈은 안 바꿈 · 보이기만):
|
||||
합계 = 셈 답 `기준`(노 · 재 · 경 · 계 · 1 단위) — 바탕 줄만 셈한 합계 대신
|
||||
소계 = 셈 답 `줄` 한 줄마다 한 행 {cid, 갈래(자원 | 블록 | 덧줄 | 율), 이름, 상태, 수량,
|
||||
노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?, 합밖?, 밖금액?} +
|
||||
끝수(합계 − 행 합 · 까닭 글)
|
||||
합밖 줄(엔진이 합계에서 뺌) = 행 금액 0 + 밖금액
|
||||
상태 = 바탕(그대로) · 덮음(품 · 단가 · 상수 · 표값 · 식 덮개 · 변수줄) ·
|
||||
더함(더한줄) · 뺌(뺀줄 · 금액 0)
|
||||
나무 = 더한 기초(더한줄 로직)의 수식(세부 덮개 · 뺀줄 얹은 줄) — 화면은 접힘 블록
|
||||
표지 = 수식 탭 줄에 고친 칸 {표지[갈래], 원래, 지금, 뺌} — 줄 값 = 식 덮개 · 상수 · 표값 ·
|
||||
변수줄을 얹은 식(`M02_Sangeun_Const.formulas`) · 품 · 단가 덮개는 줄 값에 안 얹고
|
||||
`지금` 으로 보임(고침 주소 = 바탕 차례 그대로)
|
||||
셈 답이 안 서는 SK(틀린 문서 · 돈 아닌 로직) = 바탕 줄 합계 그대로 + `답까닭`.
|
||||
시험 `resources/tester/test_796_2c_m02_equation_full.py`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from typing import Any
|
||||
|
||||
from M02_MasterTemplete import M02_Sangeun_Doc as sd
|
||||
|
||||
COSTS = ("노무비", "재료비", "경비")
|
||||
SUMS = (*COSTS, "계")
|
||||
_ADD = re.compile(r"^add:#(\d+)$")
|
||||
_HEAD = {"labor": "자원", "material": "자원", "machine": "자원", "logic": "블록", "pct": "덧줄"}
|
||||
_ADD_KIND = {"인력": "자원", "재료": "자원", "기계": "자원"}
|
||||
|
||||
|
||||
def _d(value: Any) -> Decimal:
|
||||
return Decimal(str(value)) if value is not None else Decimal(0)
|
||||
|
||||
|
||||
def _errors() -> tuple:
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
|
||||
return (sd.DocError, store.StoreError, ArithmeticError, KeyError, TypeError, ValueError)
|
||||
|
||||
|
||||
def answer(doc: dict, inputs: dict | None) -> dict | None:
|
||||
"""SK 셈 답 — 못 셈(틀린 문서 · 돈 아닌 로직) = None."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Calc as calc
|
||||
|
||||
try:
|
||||
got = calc.calc_doc(doc, inputs or None, False)
|
||||
except _errors():
|
||||
return None
|
||||
return got if got.get("기준") else None
|
||||
|
||||
|
||||
def total(ans: dict | None) -> dict | None:
|
||||
return {k: _d(ans["기준"].get(k)) for k in SUMS} if ans else None
|
||||
|
||||
|
||||
# ── 행 ────────────────────────────────────────────────────────────────
|
||||
def _kind(cid: str, adds: list) -> str:
|
||||
hit = _ADD.match(cid)
|
||||
if not hit:
|
||||
return _HEAD.get(cid.split(":", 1)[0], "자원")
|
||||
one = adds[int(hit[1])] if int(hit[1]) < len(adds) else {}
|
||||
if sd.is_rate(one):
|
||||
return "율"
|
||||
return _ADD_KIND.get(str(one.get("종류")), "블록")
|
||||
|
||||
|
||||
def _row(cid: str, kind: str, name: str, state: str, parts: dict | None, **more: Any) -> dict:
|
||||
cost = {k: _d((parts or {}).get(k)) for k in COSTS}
|
||||
return {
|
||||
"cid": cid,
|
||||
"갈래": kind,
|
||||
"이름": name,
|
||||
"상태": state,
|
||||
**cost,
|
||||
"계": sum(cost.values(), Decimal(0)),
|
||||
**{k: v for k, v in more.items() if v not in (None, [], "")},
|
||||
}
|
||||
|
||||
|
||||
def rows(doc: dict, ans: dict, tree: Any, out_ids: set[str] = frozenset()) -> list[dict]:
|
||||
"""셈 답 줄 → 행(셈 답 순서) + 뺀줄(금액 0) — `tree(더한줄 차례)` = 더한 기초 수식."""
|
||||
adds = doc.get("더한줄") or []
|
||||
spots = (ans.get("시트") or {}).get("자리") or {}
|
||||
out = []
|
||||
for line in ans.get("줄") or []:
|
||||
cid = str(line.get("cid") or "")
|
||||
hit = _ADD.match(cid)
|
||||
info = spots.get(cid) or {}
|
||||
cover = [k for k in info.get("덮개") or [] if k != "뺌"] or [
|
||||
k for k in line.get("덮개") or []
|
||||
]
|
||||
k = int(hit[1]) if hit else -1
|
||||
state = "더함" if hit else "덮음" if cover else "바탕"
|
||||
one = adds[k] if 0 <= k < len(adds) else {}
|
||||
if hit and one.get("종류") == "로직" and sd.sub_patched(one):
|
||||
cover = cover or ["세부"]
|
||||
outside = cid in out_ids # 합계 밖 줄(가져온 식 `합밖` · 제외 단계) — 합에 안 듦
|
||||
out.append(
|
||||
_row(
|
||||
cid,
|
||||
_kind(cid, adds),
|
||||
str(line.get("이름") or info.get("이름") or ""),
|
||||
state,
|
||||
None if outside else line.get("비목"),
|
||||
합밖=outside or None,
|
||||
밖금액=sum((_d(v) for v in (line.get("비목") or {}).values()), Decimal(0))
|
||||
if outside
|
||||
else None,
|
||||
수량=line.get("수량"),
|
||||
덮개=cover,
|
||||
원래=info.get("원래") if state == "덮음" else None,
|
||||
까닭=line.get("까닭") or info.get("까닭"),
|
||||
나무=tree(k) if hit and one.get("종류") == "로직" else None,
|
||||
)
|
||||
)
|
||||
for cid in doc.get("뺀줄") or []: # 지운 줄 — 금액 0 · 원래 금액만 보임
|
||||
info = spots.get(cid) or {}
|
||||
out.append(
|
||||
_row(
|
||||
cid,
|
||||
_kind(cid, adds),
|
||||
str(info.get("이름") or ""),
|
||||
"뺌",
|
||||
None,
|
||||
원래=info.get("원래"),
|
||||
까닭=info.get("까닭"),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _why(ans: dict) -> str:
|
||||
"""끝수 까닭 — 합계는 엔진이 비목마다 합쳐 버린 값 · 행은 줄마다 버린 값."""
|
||||
text = ["줄마다 버린 값의 합 ≠ 엔진 비목 합(품셈 1-2-2 끝수)"]
|
||||
if _d(ans.get("화면배") or 1) != 1:
|
||||
text.append(f"단위 환산 ÷ {ans['화면배']}")
|
||||
rate = ans.get("켜쌓기율")
|
||||
if rate not in (None, 100):
|
||||
text.append(f"켜쌓기율 {rate} %")
|
||||
return " · ".join(text)
|
||||
|
||||
|
||||
# ── 더한 기초 수식 ─────────────────────────────────────────────────────
|
||||
def _basic_tree(whole: Any, doc: dict, one: dict, inputs: dict | None) -> dict | None:
|
||||
"""더한줄 로직 하나의 수식 — 세부 덮개 · 뺀줄 얹은 줄 · 입력 = 줄 값 › 공통 옵션 › 자동."""
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation as eqm
|
||||
|
||||
if any(one.get(k) not in (None, {}, []) for k in sd.BODY):
|
||||
return None
|
||||
code = str(one["로직"])
|
||||
mine = {k: v for k, v in (one.get("입력값") or {}).items() if str(v).strip()}
|
||||
given = {**(doc.get("입력값") or {}), **(inputs or {}), **mine}
|
||||
try:
|
||||
row = sd.patched(whole.logic(code), one)["줄"]
|
||||
return eqm.export(whole, {**row, "키": code}, given)
|
||||
except Exception: # noqa: BLE001 — 엔진 FormulaError · 기초 하나가 틀려도 나머지는 그림
|
||||
return None
|
||||
|
||||
|
||||
# ── 줄 표지 ────────────────────────────────────────────────────────────
|
||||
def _mark(eq: dict, base: dict, ans: dict, doc: dict) -> None:
|
||||
"""수식 탭 줄에 고친 칸 표지 — 자리 = 바탕 cid(호표 · 덧줄) · `mid:이름`."""
|
||||
spots = (ans.get("시트") or {}).get("자리") or {}
|
||||
ids = sd.line_ids(base)
|
||||
heads = len(base.get("호표") or [])
|
||||
for line in eq.get("줄") or []:
|
||||
if line["갈래"] == "중간":
|
||||
spot = f"mid:{line['열쇠']}"
|
||||
elif line["갈래"] == "호표":
|
||||
spot = ids[line["차례"]] if line.get("차례", 0) < len(ids) else ""
|
||||
elif line["갈래"] == "덧줄":
|
||||
at = heads + line.get("차례", 0)
|
||||
spot = ids[at] if at < len(ids) else ""
|
||||
else:
|
||||
continue
|
||||
info = spots.get(spot) or {}
|
||||
kinds = list(info.get("덮개") or [])
|
||||
if not kinds:
|
||||
continue
|
||||
line["표지"] = [k for k in kinds if k != "뺌"]
|
||||
line["원래"], line["지금"] = info.get("원래"), info.get("지금")
|
||||
if "뺌" in kinds:
|
||||
line["뺌"] = True
|
||||
|
||||
|
||||
def _outside(base: dict) -> set[str]:
|
||||
"""합계 밖 줄 cid — 엔진이 비목 합에서 빼는 줄(`master_formula_sum.skipped`)."""
|
||||
import master_cut as mcut # M01 이 길을 둔 뒤(수식 탭 export 가 먼저)
|
||||
|
||||
if not mcut.ms.uses(base):
|
||||
return set()
|
||||
ids = sd.line_ids(base)
|
||||
return {ids[i] for i in mcut.ms.skipped(base) if i < len(ids)}
|
||||
|
||||
|
||||
# ── 붙이기 ────────────────────────────────────────────────────────────
|
||||
def attach(
|
||||
eq: dict, doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None
|
||||
) -> dict:
|
||||
"""수식 `eq` 에 SK 셈 답 합계 · 소계 · 표지를 얹음(제자리) — 답 없음 = `답까닭` 만."""
|
||||
if ans is None:
|
||||
return eq | {"답까닭": "단가산출 셈 답이 서지 않음 — 바탕 줄 합계를 보임"}
|
||||
_mark(eq, base, ans, doc)
|
||||
adds = doc.get("더한줄") or []
|
||||
table = rows(
|
||||
doc,
|
||||
ans,
|
||||
lambda k: _basic_tree(whole, doc, adds[k], inputs) if k >= 0 else None,
|
||||
_outside(base),
|
||||
)
|
||||
now = total(ans) or {}
|
||||
rest = {k: now[k] - sum((r[k] for r in table), Decimal(0)) for k in SUMS}
|
||||
eq["합계"] = now
|
||||
eq["소계"] = {"줄": table, "끝수": rest, "끝수까닭": _why(ans)}
|
||||
return eq
|
||||
@@ -7,13 +7,12 @@
|
||||
- 검사 까닭 — 없는 이름 · 괄호 · 순환 · 0 나눔 · 길이 · 괄호 깊이 · 변수 줄 수 · 자리 겹침
|
||||
- 옛 SK(식 칸 없음) = 꼴 · 금액 그대로 · 저장 = 엔진 식 · 틀린 식 저장 400 · 셈 답 덧칸
|
||||
- B09 사본 굳힘(`GX9#####`) 줄 = 식 칸 그대로 · 다시 셈 같은 금액
|
||||
- TS `Formula_Model` · `Const_Model`(식덮개) 결과 = 서버가 받음 · [M01 원래로] 거울
|
||||
- 799-2 — TS 거울(`Formula_Model.ts` · `Const_Model.ts`)은 시트 탭과 함께 걷음 · 서버만 지킴
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@@ -274,46 +273,6 @@ def test_B09_굳힘_식_칸_그대로():
|
||||
assert again == live and float(live) > 51501
|
||||
|
||||
|
||||
# ── TS ───────────────────────────────────────────────────────────────
|
||||
def _ts(body: str):
|
||||
src = "".join(
|
||||
(ROOT / f"M02_MasterTemplete/logic/M02_Sangeun_{n}_Model.ts").read_text("utf-8")
|
||||
for n in ("Const", "Formula")
|
||||
)
|
||||
src = "\n".join(x for x in src.splitlines() if not x.startswith("import type"))
|
||||
got = subprocess.run(
|
||||
["node", "--experimental-strip-types", "--no-warnings",
|
||||
"--input-type=module-typescript", "-e", src + body],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=60,
|
||||
) # fmt: skip
|
||||
assert got.returncode == 0, got.stderr
|
||||
return json.loads(got.stdout)
|
||||
|
||||
|
||||
def test_TS_고치기_꼴_서버가_받음():
|
||||
ts = _ts(
|
||||
f"\nlet d = {json.dumps(_doc(CARRY, _carry()), ensure_ascii=False)};"
|
||||
"\nd = {...d, 더한줄: [{종류: '인력', 요소: 'LB000002', 수량: 1}]};"
|
||||
"\nd = addVar(d, {이름: '할증', 식: '10', 자리: ''});"
|
||||
"\nd = addVar(d, {이름: '덤', 식: '0', 자리: 'mid:할증'});"
|
||||
"\nd = editVar(d, '할증', {이름: '할증율', 식: '20'});"
|
||||
"\nd = setFormula(d, 'mid:할증율', '20'); d = dropVar(d, '덤');"
|
||||
f"\nd = setFormula(d, '{MAN}', '{PLUS}'); d = setFormula(d, 'mid:운반량', '25');"
|
||||
"\nd = setFormula(d, 'mid:운반량', null); d = setFormula(d, 'add:#0', '1 / 일운반회수');"
|
||||
"\nd = setReason(d, 'mid:할증율', '할증');"
|
||||
f"\nconst out = {{d, r: restoreM01(d, '{MAN}'), k: coverKinds(d, '{MAN}')}};"
|
||||
"\nconsole.log(JSON.stringify(out));"
|
||||
)
|
||||
d = ts["d"]
|
||||
assert d["변수줄"] == [{"이름": "할증율", "식": "20", "자리": ""}]
|
||||
assert d["식덮개"] == {MAN: PLUS} and d["까닭"] == {"mid:할증율": "할증"}
|
||||
got = sd.clean(d, None, _whole())
|
||||
assert {k: got[k] for k in fx.FIELDS} == {k: d[k] for k in fx.FIELDS}
|
||||
assert got["더한줄"][0]["수량식"] == "1 / 일운반회수"
|
||||
assert _sum(d) > 51501
|
||||
assert ts["r"] == const.restore(d, MAN) and ts["k"] == const.kinds(d, MAN) == ["식"]
|
||||
|
||||
|
||||
def test_같은_내용_지문_식_칸_가림():
|
||||
"""식 칸만 다른 SK = 다른 내용(409 같은 것 물음 안 함) · 옛 SK 지문 그대로."""
|
||||
from M02_MasterTemplete import M02_Library_Same as same
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
|
||||
"""796-2c M02 수식 탭이 SK 셈 답 전체를 그림 — 더한 기초 · 뺀줄 · 덮개 · 율 줄 · 참조 · 가져온 식.
|
||||
|
||||
견본 셋(임시 storage · M01 그대로):
|
||||
A 거푸집 + 더한 기초(굴착기) + 더한 인력 + 뺀줄 + 품 덮개 + 식 덮개
|
||||
B 포장 + 단가 덮개 + 율 줄 + 참조 SK
|
||||
C 가져온 식 + 더한 인력 + 품 덮개
|
||||
지킴: 수식 탭 합계 = SK 셈 답 `기준`(원 단위까지) · 행 합 + 끝수 = 합계(비목마다) ·
|
||||
상태(덮음 · 더함 · 뺌) · 고친 칸 표지(원래 · 지금) · 더한 기초 수식 나무 ·
|
||||
미리 셈 전/후 = SK 금액 · SK 금액 전후 같음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal as D
|
||||
|
||||
import pytest
|
||||
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Sangeun_Equation as eqm
|
||||
from M02_MasterTemplete import M02_Sangeun_Imported as imp
|
||||
from resources.tester.test_795_1_sk_copy import _calc, _doc, _imported_row, _save, _sk
|
||||
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
|
||||
|
||||
SUMS = ("노무비", "재료비", "경비", "계")
|
||||
BASIC = {"종류": "로직", "로직": "GC001148", "수량": 2, "입력값": {}} # 굴착기 시간당 641
|
||||
MAN = {"종류": "인력", "요소": "LB000002", "이름": "sub2임시 인부", "단위": "인", "수량": 1}
|
||||
FORM = "GC000211" # 합판거푸집 — 형틀목공 · 보통인부 · 중간 시공량
|
||||
MINE = {"층": "personal", "주인": "7/42"}
|
||||
|
||||
|
||||
def _d(v) -> D:
|
||||
return D(str(v))
|
||||
|
||||
|
||||
def _read(folder, doc, inputs=None):
|
||||
with store.at(folder):
|
||||
got = eqm.read(doc, inputs)
|
||||
assert got["ok"], got
|
||||
return got["수식"]
|
||||
|
||||
|
||||
def _rows(eq) -> dict:
|
||||
return {r["cid"]: r for r in eq["소계"]["줄"]}
|
||||
|
||||
|
||||
def _closes(eq, ans) -> None:
|
||||
"""합계 = SK 셈 답 원 단위까지 · 행 합 + 끝수 = 합계 · 끝수 작음."""
|
||||
sub = eq["소계"]
|
||||
for k in SUMS:
|
||||
assert eq["합계"][k] == _d(ans["기준"][k]), k
|
||||
assert sum((r[k] for r in sub["줄"]), D(0)) + sub["끝수"][k] == eq["합계"][k], k
|
||||
assert abs(sub["끝수"]["계"]) < 2, sub["끝수"]
|
||||
|
||||
|
||||
def _line(eq, kind, key):
|
||||
return next(x for x in eq["줄"] if x["갈래"] == kind and x["열쇠"] == key)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def made(world):
|
||||
me = world["personal"]
|
||||
plain = _sk(me, 바탕={"로직": FORM}) # 고침 없는 같은 바탕 — 원래 값 대조용
|
||||
key = _sk(
|
||||
me,
|
||||
바탕={"로직": FORM},
|
||||
더한줄=[dict(BASIC), dict(MAN)],
|
||||
뺀줄=["labor:LB000002#0"],
|
||||
덮개={"labor:LB000007#0": {"qty": 0.5}},
|
||||
식덮개={"mid:시공량": "50"},
|
||||
)
|
||||
return me, key, plain
|
||||
|
||||
|
||||
def test_A_added_removed_covered_total_equals_sk(made):
|
||||
me, key, plain = made
|
||||
doc, bare = _doc(me, key), _calc(me, _doc(me, plain))
|
||||
ans = _calc(me, doc)
|
||||
eq = _read(me, doc)
|
||||
_closes(eq, ans)
|
||||
rows = _rows(eq)
|
||||
# 덮음 — 형틀목공 품 덮개 · 원래 = 고침 없는 SK 의 같은 줄 금액
|
||||
cover = rows["labor:LB000007#0"]
|
||||
was = next(x for x in bare["줄"] if x["cid"] == "labor:LB000007#0")["금액"]
|
||||
assert cover["상태"] == "덮음" and "품" in cover["덮개"]
|
||||
assert _d(cover["원래"]) == _d(was)
|
||||
assert cover["노무비"] == D("137895") # 품 0.5 × 단가 275,790
|
||||
# 뺌 — 지운 줄 = 금액 0 · 원래 금액만
|
||||
gone = rows["labor:LB000002#0"]
|
||||
assert gone["상태"] == "뺌" and gone["계"] == 0
|
||||
assert _d(gone["원래"]) == _d(next(x for x in bare["줄"] if x["cid"] == gone["cid"])["금액"])
|
||||
# 더함 — 기초(굴착기 641 × 2) · 인력 · 기초는 수식 나무
|
||||
basic, man = rows["add:#0"], rows["add:#1"]
|
||||
assert (
|
||||
basic["상태"] == man["상태"] == "더함" and basic["갈래"] == "블록" and man["갈래"] == "자원"
|
||||
)
|
||||
assert basic["계"] > 0 and man["노무비"] > 0
|
||||
assert basic["나무"]["열쇠"] == "GC001148" and basic["나무"]["줄"]
|
||||
# 나무 합계 × 수량 = 행(같은 입력)
|
||||
assert abs(basic["나무"]["합계"]["계"] * 2 - basic["계"]) <= 2
|
||||
# 고친 칸 표지 — 수식 탭 줄(식 덮개 · 품 · 뺌)
|
||||
mid = _line(eq, "중간", "시공량")
|
||||
assert mid["표지"] == ["식"] and mid.get("원래") is not None and "50" in mid["식"]
|
||||
assert _line(eq, "호표", "형틀목공")["표지"] == ["품"]
|
||||
out = _line(eq, "호표", "보통인부")
|
||||
assert out["뺌"] is True and out["표지"] == []
|
||||
# 고침 없는 SK(plain)는 표지도 더함도 없음 — 합계 = 같은 바탕 금액
|
||||
flat = _read(me, _doc(me, plain))
|
||||
assert {r["상태"] for r in flat["소계"]["줄"]} == {"바탕"}
|
||||
assert flat["합계"]["계"] == _d(bare["기준"]["계"])
|
||||
assert not any("표지" in x for x in flat["줄"])
|
||||
|
||||
|
||||
def test_A_preview_before_after_and_save_same(made):
|
||||
"""고침 미리 셈 전/후 = SK 금액 · 저장 뒤 셈 = 미리 셈 · 더한 것도 합계에 듦."""
|
||||
me, key, _ = made
|
||||
doc = _doc(me, key)
|
||||
was = _calc(me, doc)["기준"]["계"]
|
||||
edit = [{"할일": "식", "자리": "호표:3", "식": "0.038 × 2", "까닭": "sub2임시 2c"}]
|
||||
with store.at(me):
|
||||
got = eqm.preview(doc, edit)
|
||||
assert got["되나"], got["까닭"]
|
||||
assert got["전"]["계"] == _d(was) and got["후"]["계"] != got["전"]["계"]
|
||||
_save(me, key, {**doc, eqm.KEY: edit})
|
||||
now = _calc(me, _doc(me, key))
|
||||
assert _d(now["기준"]["계"]) == got["후"]["계"]
|
||||
eq = _read(me, _doc(me, key))
|
||||
_closes(eq, now)
|
||||
|
||||
|
||||
def test_B_unit_cover_rate_and_reference(world):
|
||||
"""B 단가 덮개 · 율 줄 · 참조 SK 가 소계에 들고 합계 = SK 금액."""
|
||||
me = world["personal"]
|
||||
ref = _sk(me, 바탕={"로직": FORM})
|
||||
rate = {
|
||||
"고정": {
|
||||
"이름": "sub2임시 율",
|
||||
"단가": {},
|
||||
"이음": {},
|
||||
"비율": {"기준": "인력", "비목": "경비"},
|
||||
}
|
||||
}
|
||||
adds = [{**rate, "수량": 3}, {"참조": {**MINE, "키": ref}, "수량": 1}]
|
||||
key = _sk(
|
||||
me,
|
||||
바탕={"로직": "GC001109"},
|
||||
더한줄=adds,
|
||||
덮개={"labor:LB000002#0": {"labor": 200000}},
|
||||
)
|
||||
doc, ans = _doc(me, key), _calc(me, _doc(me, key))
|
||||
eq = _read(me, doc)
|
||||
_closes(eq, ans)
|
||||
rows = _rows(eq)
|
||||
price = rows["labor:LB000002#0"]
|
||||
assert price["상태"] == "덮음" and "단가" in price["덮개"] and price["원래"] is not None
|
||||
assert rows["add:#0"]["갈래"] == "율" and rows["add:#0"]["경비"] > 0
|
||||
assert rows["add:#1"]["갈래"] == "블록" and rows["add:#1"]["상태"] == "더함"
|
||||
assert _line(eq, "호표", price["이름"])["표지"] == ["단가"]
|
||||
|
||||
|
||||
def test_C_imported_sk_same_rule(world):
|
||||
"""C 가져온 식 SK — 더한 인력 · 품 덮개 · 합계 = SK 금액."""
|
||||
me = world["personal"]
|
||||
with store.at(me):
|
||||
key = imp.create(_imported_row(), None, "sub2임시 가져온")["이름"]
|
||||
doc = _doc(me, key)
|
||||
cid = next(x["cid"] for x in _calc(me, doc)["줄"] if x["cid"].startswith("labor:"))
|
||||
_save(me, key, {**doc, "더한줄": [dict(MAN)], "덮개": {cid: {"qty": 1}}})
|
||||
doc = _doc(me, key)
|
||||
ans = _calc(me, doc)
|
||||
eq = _read(me, doc)
|
||||
_closes(eq, ans)
|
||||
rows = _rows(eq)
|
||||
assert rows["add:#0"]["상태"] == "더함" and rows[cid]["상태"] == "덮음"
|
||||
|
||||
|
||||
def test_plain_sk_has_answer_table(world):
|
||||
"""고침 없는 SK = 답까닭 없음 · 소계 표 있음."""
|
||||
me = world["personal"]
|
||||
key = _sk(me, 바탕={"로직": FORM})
|
||||
eq = _read(me, _doc(me, key))
|
||||
assert "답까닭" not in eq and eq["소계"]["줄"]
|
||||
|
||||
|
||||
def test_D_imported_outside_total_line_not_in_sum(world):
|
||||
"""D 가져온 식 `합밖` 줄 = 합계에 안 듦 → 행 0 · 밖금액 · 끝수에 안 섞임."""
|
||||
me = world["personal"]
|
||||
row = _imported_row()
|
||||
row["호표"][1] = {**row["호표"][1], "합밖": True}
|
||||
with store.at(me):
|
||||
key = imp.create(row, None, "sub2임시 합밖")["이름"]
|
||||
doc = _doc(me, key)
|
||||
ans = _calc(me, doc)
|
||||
eq = _read(me, doc)
|
||||
_closes(eq, ans)
|
||||
out = [r for r in eq["소계"]["줄"] if r.get("합밖")]
|
||||
assert len(out) == 1 and out[0]["계"] == 0 and out[0]["밖금액"] > 0
|
||||
assert abs(eq["소계"]["끝수"]["계"]) < 1
|
||||
@@ -99,4 +99,4 @@ def test_화면_부품이_서버_칸을_읽음():
|
||||
for name in ("노무비", "재료비", "경비", "끝수", "합계", "자원", "덧줄"):
|
||||
assert name in ts
|
||||
line = Path("M01_MasterData/M01_MasterData_UI_LogicLab_Equation.ts").read_text(encoding="utf-8")
|
||||
assert "createEqSum(eq)" in line
|
||||
assert "createEqSum(eq, open)" in line
|
||||
|
||||
@@ -323,7 +323,28 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── 단가산출 소계 · 합계(799-1) ── */
|
||||
/* ── M02 SK 고친 칸 표지(796-2c) ── */
|
||||
.ui-eq__mark {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
}
|
||||
|
||||
.ui-eq__mark-was {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-eq__line[data-removed] > .ui-eq__main {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ui-eq__line[data-removed] .ui-eq__res {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
/* ── 단가산출 소계 · 합계(799-1 · 796-2c) ── */
|
||||
.ui-eq__sum {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -348,6 +369,41 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ui-eq__sum-name {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ui-eq__sum-was,
|
||||
.ui-eq__sum-note {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.ui-eq__sum-note {
|
||||
padding: var(--spacing-4) var(--spacing-8);
|
||||
}
|
||||
|
||||
.ui-eq__sum-row[data-eq-sum-state="뺌"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ui-eq__sum-row[data-eq-sum-state="뺌"] .ui-eq__sum-label {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.ui-eq__sum-tree {
|
||||
padding: var(--spacing-4) var(--spacing-8) var(--spacing-8) var(--spacing-16);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.ui-eq__sum-tree[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.ui-eq__sum-row[data-eq-sum-kind="끝수"] {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ export interface EqNode {
|
||||
사슬?: boolean;
|
||||
}
|
||||
|
||||
/** 단가산출 소계 줄(799-1) — 갈래 블록 | 자원 | 덧줄 */
|
||||
/** 단가산출 소계 줄(799-1) — 갈래 블록 | 자원 | 덧줄 | 율 · M02 SK(796-2c) 는 셈 답 한 줄마다 + 상태 */
|
||||
export interface EqSumRow {
|
||||
갈래: string;
|
||||
이름: string;
|
||||
@@ -52,6 +52,19 @@ export interface EqSumRow {
|
||||
재료비: unknown;
|
||||
경비: unknown;
|
||||
계: unknown;
|
||||
/** M02 SK — 바탕 | 덮음 | 더함 | 뺌 (M01 은 없음) */
|
||||
상태?: string;
|
||||
수량?: unknown;
|
||||
/** 덮음 갈래(품 · 단가 · 상수 · 표값 · 식) */
|
||||
덮개?: string[];
|
||||
/** 덮음 · 뺌 — 고치기 전 금액 */
|
||||
원래?: unknown;
|
||||
까닭?: string;
|
||||
/** 합계 밖 줄(엔진이 비목 합에서 뺌) — 행 금액은 0 · 밖금액에 원래 금액 */
|
||||
합밖?: boolean;
|
||||
밖금액?: unknown;
|
||||
/** 더한 기초의 수식(접힘 블록) */
|
||||
나무?: EqTree | null;
|
||||
}
|
||||
|
||||
/** 로직 하나의 수식(줄 · 변수표 · 합계 · 소계) */
|
||||
@@ -63,7 +76,9 @@ export interface EqTree {
|
||||
변수표: EqVarRow[];
|
||||
합계: Record<string, unknown>;
|
||||
/** 799-1 — 하위 로직 블록마다 노 · 재 · 경 소계 + 끝수(합계 − 줄 합) · 결과 로직은 없음 */
|
||||
소계?: { 줄: EqSumRow[]; 끝수: Record<string, unknown> };
|
||||
소계?: { 줄: EqSumRow[]; 끝수: Record<string, unknown>; 끝수까닭?: string };
|
||||
/** M02 SK — 셈 답이 안 서서 바탕 줄 합계를 보일 때의 까닭 */
|
||||
답까닭?: string;
|
||||
}
|
||||
|
||||
export interface EqVarRow {
|
||||
@@ -93,6 +108,11 @@ export interface EqLine {
|
||||
글?: string;
|
||||
표시?: number;
|
||||
비목?: unknown;
|
||||
/** M02 SK 고친 칸(796-2c) — 갈래(식 · 상수 · 표값 · 품 · 단가 · 변수줄) · 원래 → 지금 · 뺀줄 */
|
||||
표지?: string[];
|
||||
원래?: unknown;
|
||||
지금?: unknown;
|
||||
뺌?: boolean;
|
||||
}
|
||||
|
||||
export interface EqHandlers {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* 대신한 옛 변종 = 시트 줄(이름 | 식 | 값 한 줄 · 분수 · 풀이 없음 · 799-2 에 걷음)
|
||||
* ========================================================================== */
|
||||
|
||||
import { createBadge } from "./ui_template_badge";
|
||||
import { createButton } from "./ui_template_button";
|
||||
import {
|
||||
eqText,
|
||||
@@ -164,6 +165,23 @@ function blockPanel(node: EqNode, at: number, o: EqLineOptions): HTMLElement {
|
||||
return box;
|
||||
}
|
||||
|
||||
/** M02 SK 고친 칸(796-2c) — 갈래 알약 + 원래 → 지금 · 뺀줄 알약 */
|
||||
function markOf(line: EqLine): HTMLElement | null {
|
||||
const kinds = line.표지 ?? [];
|
||||
if (!kinds.length && !line.뺌) return null;
|
||||
const box = span("ui-eq__mark");
|
||||
if (kinds.length) box.append(createBadge(`고침 · ${kinds.join(" · ")}`, "warning", { size: 18 }));
|
||||
if (line.뺌) box.append(createBadge("뺌", "neutral", { size: 18 }));
|
||||
if (line.원래 !== undefined && line.원래 !== null)
|
||||
box.append(
|
||||
span(
|
||||
"ui-eq__mark-was",
|
||||
`원래 ${eqText(line.원래)}${line.뺌 ? "" : ` → 지금 ${eqText(line.지금) || "—"}`}`,
|
||||
),
|
||||
);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 줄 하나 */
|
||||
export function createEqLine(line: EqLine, index: number, o: EqLineOptions): HTMLElement {
|
||||
const id = lineId(line);
|
||||
@@ -224,6 +242,9 @@ export function createEqLine(line: EqLine, index: number, o: EqLineOptions): HTM
|
||||
span("ui-eq__bad", line.까닭 ?? ""),
|
||||
);
|
||||
}
|
||||
const mark = markOf(line);
|
||||
if (mark) body.append(mark);
|
||||
if (line.뺌) root.dataset.removed = "1";
|
||||
const solve = div("ui-eq__solve", ...vars.map((v) => solveRow(v, o)));
|
||||
const main = div("ui-eq__main", body, solve);
|
||||
if (o.onSelect) {
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
/* =============================================================================
|
||||
* ui_template_equation_sum.ts
|
||||
* 부품 「수식」 셋째 파일(799-1 · 요구 655-1 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
|
||||
* 부품 「수식」 셋째 파일(799-1 · 796-2c · 요구 655-1 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로).
|
||||
* 표 = 줄(하위 로직 블록마다 한 줄 · 자기 자원 · 덧줄) × 노무비 · 재료비 · 경비 · 계 → 합계(엔진 비목 합) → 끝수(합계 − 줄 합)
|
||||
* 값 = 서버 `소계`(`master_equation.subtotals` · 버린 줄 비목 0.1 원) · 끝수가 0 이면 줄을 안 그림 · 소계가 없으면(결과 로직) 아무것도 안 그림
|
||||
* 쓰는 법 = `createEqSum(tree)` → 요소 | null — 줄 이음(`createEqChain`) 아래에 붙임
|
||||
* 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` = SK 셈 답) · 끝수가 0 이면 줄을 안 그림 · 소계가 없으면(결과 로직) 아무것도 안 그림
|
||||
* M02 SK 줄 = 상태 알약(더함 · 덮음 · 뺌) · 덮음 갈래 · 원래 금액 · 수량 · 더한 기초는 접힘 수식 블록 · 뺌 = 흐리게 · 금액 0
|
||||
* 쓰는 법 = `createEqSum(tree, open?)` → 요소 | null — 줄 이음(`createEqChain`) 아래에 붙임 · `open` = 펼친 블록 이름표 모음
|
||||
* ========================================================================== */
|
||||
|
||||
import { eqText, type EqTree } from "./ui_template_equation";
|
||||
import { createBadge, type BadgeTone } from "./ui_template_badge";
|
||||
import { createButton } from "./ui_template_button";
|
||||
import { eqText, type EqSumRow, type EqTree } from "./ui_template_equation";
|
||||
import { createEqChain } from "./ui_template_equation_line";
|
||||
|
||||
const COLS = ["노무비", "재료비", "경비", "계"] as const;
|
||||
const KIND: Record<string, string> = { 자원: "자기 자원", 덧줄: "덧줄" };
|
||||
const TONE: Record<string, BadgeTone> = { 더함: "success", 덮음: "warning", 뺌: "neutral" };
|
||||
|
||||
const node = (tag: "div" | "span", className: string, text = ""): HTMLElement => {
|
||||
const out = document.createElement(tag);
|
||||
@@ -18,19 +23,89 @@ const node = (tag: "div" | "span", className: string, text = ""): HTMLElement =>
|
||||
return out;
|
||||
};
|
||||
|
||||
function row(kind: string, name: string, get: (k: (typeof COLS)[number]) => unknown): HTMLElement {
|
||||
const line = node("div", "ui-eq__sum-row");
|
||||
line.dataset.eqSumKind = kind;
|
||||
line.append(node("span", "ui-eq__sum-name", name));
|
||||
function cells(line: HTMLElement, get: (k: (typeof COLS)[number]) => unknown): void {
|
||||
for (const k of COLS) {
|
||||
const cell = node("span", "ui-eq__sum-cell", eqText(get(k)) || "0");
|
||||
cell.dataset.eqSumCol = k;
|
||||
line.append(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function row(kind: string, name: string, get: (k: (typeof COLS)[number]) => unknown): HTMLElement {
|
||||
const line = node("div", "ui-eq__sum-row");
|
||||
line.dataset.eqSumKind = kind;
|
||||
line.append(node("span", "ui-eq__sum-name", name));
|
||||
cells(line, get);
|
||||
return line;
|
||||
}
|
||||
|
||||
export function createEqSum(tree: Pick<EqTree, "합계" | "소계">): HTMLElement | null {
|
||||
/** M02 SK 줄 하나 — 이름 + 알약 + 원래 · 수량 / 더한 기초는 접힘 수식 */
|
||||
function itemRow(r: EqSumRow, open: Set<string>, at: number): HTMLElement {
|
||||
const item = node("div", "ui-eq__sum-item");
|
||||
const line = node("div", "ui-eq__sum-row");
|
||||
line.dataset.eqSumKind = r.갈래;
|
||||
const name = node("span", "ui-eq__sum-name");
|
||||
const tree = r.나무;
|
||||
const id = `sum:${at}:${r.이름}`;
|
||||
const body = node("div", "ui-eq__sum-tree");
|
||||
body.hidden = true;
|
||||
if (tree) {
|
||||
const head = createButton({
|
||||
label: r.이름,
|
||||
variant: "ghost",
|
||||
size: 22,
|
||||
iconStart: "chevronRight",
|
||||
onClick: () => toggle(),
|
||||
});
|
||||
head.classList.add("ui-eq__block-head");
|
||||
const toggle = (force?: boolean): void => {
|
||||
const on = force ?? !open.has(id);
|
||||
if (on) {
|
||||
open.add(id);
|
||||
if (!body.childElementCount)
|
||||
body.append(
|
||||
createEqChain(tree.줄, {
|
||||
path: id,
|
||||
open,
|
||||
resultUnit: tree.결과단위,
|
||||
numbered: false,
|
||||
}),
|
||||
);
|
||||
} else open.delete(id);
|
||||
item.dataset.open = on ? "1" : "";
|
||||
body.hidden = !on;
|
||||
};
|
||||
name.append(head);
|
||||
if (open.has(id)) toggle(true);
|
||||
} else name.append(node("span", "ui-eq__sum-label", r.이름));
|
||||
if (r.상태 && r.상태 !== "바탕") {
|
||||
const detail = r.덮개?.length ? ` · ${r.덮개.join(" · ")}` : "";
|
||||
name.append(createBadge(`${r.상태}${detail}`, TONE[r.상태] ?? "neutral", { size: 18 }));
|
||||
}
|
||||
if (r.합밖) {
|
||||
name.append(createBadge("합 밖", "neutral", { size: 18 }));
|
||||
line.dataset.eqSumOut = "1";
|
||||
}
|
||||
const notes: string[] = [];
|
||||
if (r.합밖) notes.push(`합계에 안 듦 ${eqText(r.밖금액) || "0"}`);
|
||||
if (r.상태 === "더함" && r.수량 !== undefined && r.수량 !== null)
|
||||
notes.push(`× ${eqText(r.수량)}`);
|
||||
if (r.원래 !== undefined && r.원래 !== null && r.상태 !== "더함")
|
||||
notes.push(`원래 ${eqText(r.원래)}`);
|
||||
if (r.까닭) notes.push(r.까닭);
|
||||
if (notes.length) name.append(node("span", "ui-eq__sum-was", notes.join(" · ")));
|
||||
line.dataset.eqSumState = r.상태 ?? "바탕";
|
||||
line.append(name);
|
||||
cells(line, (k) => r[k]);
|
||||
item.dataset.eqSumItem = r.갈래;
|
||||
item.append(line, body);
|
||||
return item;
|
||||
}
|
||||
|
||||
export function createEqSum(
|
||||
tree: Pick<EqTree, "합계" | "소계" | "답까닭">,
|
||||
open: Set<string> = new Set(),
|
||||
): HTMLElement | null {
|
||||
const sub = tree.소계;
|
||||
if (!sub?.줄.length) return null;
|
||||
const box = node("div", "ui-eq__sum");
|
||||
@@ -39,9 +114,16 @@ export function createEqSum(tree: Pick<EqTree, "합계" | "소계">): HTMLElemen
|
||||
head.append(node("span", "ui-eq__sum-name", "소계"));
|
||||
for (const k of COLS) head.append(node("span", "ui-eq__sum-cell", k));
|
||||
box.append(head);
|
||||
for (const r of sub.줄) box.append(row(r.갈래, KIND[r.갈래] ?? r.이름, (k) => r[k]));
|
||||
sub.줄.forEach((r, at) => {
|
||||
if (r.상태) box.append(itemRow(r, open, at));
|
||||
else
|
||||
box.append(row(r.갈래, r.이름 === r.갈래 ? (KIND[r.갈래] ?? r.이름) : r.이름, (k) => r[k]));
|
||||
});
|
||||
const rest = sub.끝수;
|
||||
if (COLS.some((k) => Number(rest[k] ?? 0) !== 0)) box.append(row("끝수", "끝수", (k) => rest[k]));
|
||||
const odd = COLS.some((k) => Number(rest[k] ?? 0) !== 0);
|
||||
if (odd) box.append(row("끝수", "끝수", (k) => rest[k]));
|
||||
box.append(row("합계", "합계", (k) => tree.합계[k]));
|
||||
const note = [odd ? sub.끝수까닭 : "", tree.답까닭].filter(Boolean).join(" · ");
|
||||
if (note) box.append(node("div", "ui-eq__sum-note", note));
|
||||
return box;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user