diff --git a/M02_MasterTemplete/M02_Sangeun_Equation_Full.py b/M02_MasterTemplete/M02_Sangeun_Equation_Full.py index 58d305186..1a9f8ca7d 100644 --- a/M02_MasterTemplete/M02_Sangeun_Equation_Full.py +++ b/M02_MasterTemplete/M02_Sangeun_Equation_Full.py @@ -4,10 +4,10 @@ (`M02_Sangeun_Calc.calc_doc` = 화면 · 저장 · B09 가 쓰는 한 벌)을 얹어 화면 금액 = SK 금액 (셈은 안 바꿈 · 보이기만): 합계 = 셈 답 `기준`(노 · 재 · 경 · 계 · 1 단위) — 바탕 줄만 셈한 합계 대신 - 소계 = 셈 답 `줄` 한 줄마다 한 행 {cid, 갈래(자원 | 블록 | 덧줄 | 율), 이름, 상태, 수량, - 노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?, 합밖?, 밖금액?} + - 끝수(합계 − 행 합 · 까닭 글) - 합밖 줄(엔진이 합계에서 뺌) = 행 금액 0 + 밖금액 + 소계 = 셈 답 `줄` → 돈 묶음(801-3 · `master_equation_group`) — 더한 · 부른 단가산출(갈래 블록) 하나씩 + {cid, 이름, 상태, 수량, 노 · 재 · 경 · 계, 덮개?, 원래?, 까닭?, 나무?} + 직접 줄(인력 · 재료 · 기계 · + 덧줄 · 율) 한 묶음 {갈래 직접, 이름, 노 · 재 · 경 · 계, 낱줄?(더함 · 덮음 줄)} + + 끝수(합계 − 묶음 합 · 까닭 글) · 밖(합계 밖 줄 — 엔진이 합계에서 뺌 · 이름 · 밖금액) 상태 = 바탕(그대로) · 덮음(품 · 단가 · 상수 · 표값 · 식 덮개 · 변수줄) · 더함(더한줄) · 뺌(뺀줄 · 금액 0) 나무 = 더한 기초(더한줄 로직)의 수식(세부 덮개 · 뺀줄 얹은 줄) — 화면은 접힘 블록 @@ -31,6 +31,7 @@ SUMS = (*COSTS, "계") _ADD = re.compile(r"^add:#(\d+)$") _HEAD = {"labor": "자원", "material": "자원", "machine": "자원", "logic": "블록", "pct": "덧줄"} _ADD_KIND = {"인력": "자원", "재료": "자원", "기계": "자원"} +_WORD = {"labor": "인력", "material": "재료", "machine": "기계"} # 직접 묶음 이름(801-3) def _d(value: Any) -> Decimal: @@ -69,6 +70,15 @@ def _kind(cid: str, adds: list) -> str: return _ADD_KIND.get(str(one.get("종류")), "블록") +def _word(cid: str, adds: list) -> str: + """직접 줄의 자원 갈래 말 — 인력 · 재료 · 기계(그 밖 = 빈 글).""" + hit = _ADD.match(cid) + if not hit: + return _WORD.get(cid.split(":", 1)[0], "") + one = adds[int(hit[1])] if int(hit[1]) < len(adds) else {} + return str(one.get("종류")) if one.get("종류") in _ADD_KIND else "" + + 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 { @@ -112,6 +122,7 @@ def rows(doc: dict, ans: dict, tree: Any, out_ids: set[str] = frozenset()) -> li if outside else None, 수량=line.get("수량"), + 종류=_word(cid, adds), 덮개=cover, 원래=info.get("원래") if state == "덮음" else None, 까닭=line.get("까닭") or info.get("까닭"), @@ -209,6 +220,8 @@ def attach( eq: dict, doc: dict, whole: Any, base: dict, inputs: dict | None, ans: dict | None ) -> dict: """수식 `eq` 에 SK 셈 답 합계 · 소계 · 표지를 얹음(제자리) — 답 없음 = `답까닭` 만.""" + import master_equation_group as grp # M01 이 길을 둔 뒤 + if ans is None: return eq | {"답까닭": "단가산출 셈 답이 서지 않음 — 바탕 줄 합계를 보임"} _mark(eq, base, ans, doc) @@ -219,8 +232,9 @@ def attach( lambda k: _basic_tree(whole, doc, adds[k], inputs) if k >= 0 else None, _outside(base), ) + got = grp.bundles(table) # 801-3 묶음 — 부른 · 더한 단가산출 하나씩 + 직접 줄 한 묶음 now = total(ans) or {} - rest = {k: now[k] - sum((r[k] for r in table), Decimal(0)) for k in SUMS} + rest = {k: now[k] - sum((r[k] for r in got["줄"]), Decimal(0)) for k in SUMS} eq["합계"] = now - eq["소계"] = {"줄": table, "끝수": rest, "끝수까닭": _why(ans)} + eq["소계"] = {"줄": got["줄"], "밖": got["밖"], "끝수": rest, "끝수까닭": _why(ans)} return eq diff --git a/resources/master_data/scripts/master_equation.py b/resources/master_data/scripts/master_equation.py index 0c5184e18..386a19c7f 100644 --- a/resources/master_data/scripts/master_equation.py +++ b/resources/master_data/scripts/master_equation.py @@ -28,6 +28,7 @@ from pathlib import Path import master_auto as ma import master_cut as mcut +import master_equation_group as grp import master_equation_view as view import master_formula as mf import master_material as mm @@ -401,36 +402,30 @@ def _line(ctx: _Ctx, kind: str, key: str, text: str, value, **more) -> dict: def subtotals(logic: dict, cut: dict, total: dict) -> dict: - """호표 줄 → 소계(799-1 · 보이기만) — 하위 로직 줄 = 블록마다 · 그 밖 자원 · 덧줄은 한 줄씩 + 끝수. + """호표 줄 → 소계 묶음(799-1 · 801-3 · 보이기만) — 부른 단가산출 = 묶음 하나씩 · 직접 줄 = 한 묶음 + 끝수. 값 = `master_cut.engine` 이 버린 줄 비목(0.1 원) 그대로 · 합계 = 엔진 비목 합(1 원 버림) · - 끝수 = 합계 − 줄 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함). + 끝수 = 합계 − 묶음 합(금액 셈은 안 바꿈 · 사용자 확정 뒤에 정함). 묶음 꼴 = `master_equation_group`. """ - items, adds = logic.get("호표") or [], logic.get("덧줄") or [] + items = logic.get("호표") or [] skip = mcut.ms.skipped(logic) if mcut.ms.uses(logic) else set() rows: list[dict] = [] - pool: dict[str, dict] = {} for n, line in enumerate(cut.get("줄") or []): if n in skip: continue split = {k: line.get("비목", {}).get(k) or Decimal(0) for k in mf.COST_ITEMS} - if n < len(items) and items[n].get("종류") == "로직": - rows.append({"갈래": "블록", "이름": str(items[n].get("이름") or ""), **split}) - continue - kind = "덧줄" if n >= len(items) else "자원" - one = pool.setdefault( - kind, {"갈래": kind, "이름": kind, **dict.fromkeys(mf.COST_ITEMS, Decimal(0))} + item = items[n] if n < len(items) else {} + kind = "블록" if item.get("종류") == "로직" else "자원" if item else "덧줄" + rows.append( + {"갈래": kind, "이름": str(item.get("이름") or ""), "종류": item.get("종류"), **split} + | {"계": sum(split.values(), Decimal(0))} ) - for k in mf.COST_ITEMS: - one[k] += split[k] - rows += [pool[k] for k in ("자원", "덧줄") if k in pool] - for one in rows: - one["계"] = sum((one[k] for k in mf.COST_ITEMS), Decimal(0)) + got = grp.bundles(rows) rest = { - k: (total.get(k) or Decimal(0)) - sum((one[k] for one in rows), Decimal(0)) + k: (total.get(k) or Decimal(0)) - sum((one[k] for one in got["줄"]), Decimal(0)) for k in mf.BUNDLE } - return {"줄": rows, "끝수": rest} + return {"줄": got["줄"], "끝수": rest} def export(master, key: str, given: dict | None = None, depth: int = 0) -> dict: diff --git a/resources/master_data/scripts/master_equation_group.py b/resources/master_data/scripts/master_equation_group.py new file mode 100644 index 000000000..427f4c860 --- /dev/null +++ b/resources/master_data/scripts/master_equation_group.py @@ -0,0 +1,68 @@ +"""수식 탭 소계 묶음(PLAN 801-3 · 요구 662-3) — 돈 묶음 한 곳 · M01(`master_equation`) · M02(`M02_Sangeun_Equation_Full`) 같은 꼴. + +묶음 = 부른 · 더한 단가산출(갈래 블록) 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄 · 율) 전부 한 묶음(갈래 직접). +돈 없는 줄(계 0 · 바탕) · 뺀 줄 = 묶음 아님(식 줄로만 보임) · 합계 밖 줄 = `밖` 으로만. +머리 · 소계 · 합계 · 계 글자 판정 = 화면 부품(`ui_template_equation_sum.ts`) — 묶음 둘 이상 = 머리 · 소계 · +단가산출(블록) 둘 이상 = 합계 · 아니면 계. +시험 `resources/tester/test_801_3_groups.py`. +""" + +from __future__ import annotations + +from decimal import Decimal + +COSTS = ("노무비", "재료비", "경비") +_WORDS = ("인력", "재료", "기계") # 직접 묶음 이름 차례 + + +def _zero(row: dict) -> bool: + """돈 없는 바탕 줄 · 뺀 줄 — 묶음 아님.""" + if row.get("상태") == "뺌": + return True + return row["계"] == 0 and row.get("상태") in (None, "바탕") + + +def _name(direct: list[dict]) -> str: + """직접 묶음 이름 — 자원 갈래 말(인력 · 재료 · 기계) · 율 줄 = 이름 (율 %) · 덧줄뿐 = 비목 말.""" + words = [w for w in _WORDS if any(r.get("종류") == w for r in direct)] + words += [ + f"{r['이름']} ({r['수량']}%)" if r.get("수량") not in (None, "") else str(r["이름"]) + for r in direct + if r["갈래"] == "율" + ] + if not words: + words = [k for k in COSTS if any(r[k] != 0 for r in direct)] + return " · ".join(words) + + +def bundles(rows: list[dict]) -> dict: + """줄(갈래 블록 | 자원 | 덧줄 | 율 · 비목 칸 · 계) → `{줄: 묶음들, 밖: 합계 밖 줄들}` — 금액은 더하기만(셈 안 바꿈).""" + blocks: list[dict] = [] + direct: list[dict] = [] + out: list[dict] = [] + for row in rows: + if row.get("합밖"): + out.append({"이름": row["이름"], "밖금액": row.get("밖금액")}) + elif _zero(row): + continue + elif row["갈래"] == "블록": + blocks.append(row) + else: + direct.append(row) + lines = list(blocks) + if direct: + one = { + "갈래": "직접", + "이름": _name(direct), + **{k: sum((r[k] for r in direct), Decimal(0)) for k in COSTS}, + } + one["계"] = sum((one[k] for k in COSTS), Decimal(0)) + mine = [ # 더한 · 덮은 줄 — 묶음 속에 한 줄씩(고친 줄을 숨기지 않음) + {k: r[k] for k in ("이름", "상태", "계", "원래") if r.get(k) is not None} + for r in direct + if r.get("상태") in ("더함", "덮음") + ] + if mine: + one["낱줄"] = mine + lines.append(one) + return {"줄": lines, "밖": out} diff --git a/resources/tester/test_796_2c_m02_equation_full.py b/resources/tester/test_796_2c_m02_equation_full.py index 41a9eb897..0375c2a8d 100644 --- a/resources/tester/test_796_2c_m02_equation_full.py +++ b/resources/tester/test_796_2c_m02_equation_full.py @@ -41,7 +41,11 @@ def _read(folder, doc, inputs=None): def _rows(eq) -> dict: - return {r["cid"]: r for r in eq["소계"]["줄"]} + """묶음(801-3) — 더한 · 부른 단가산출 = cid 로 · 직접 줄 묶음 속 더함 · 덮음 줄 = 이름으로.""" + got = {r["cid"]: r for r in eq["소계"]["줄"] if "cid" in r} + for r in eq["소계"]["줄"]: + got |= {x["이름"]: x for x in r.get("낱줄", [])} + return got def _closes(eq, ans) -> None: @@ -80,21 +84,19 @@ def test_A_added_removed_covered_total_equals_sk(made): _closes(eq, ans) rows = _rows(eq) # 덮음 — 형틀목공 품 덮개 · 원래 = 고침 없는 SK 의 같은 줄 금액 - cover = rows["labor:LB000007#0"] + cover = rows["형틀목공"] # 직접 묶음 속 덮은 줄(이름) was = next(x for x in bare["줄"] if x["cid"] == "labor:LB000007#0")["금액"] - assert cover["상태"] == "덮음" and "품" in cover["덮개"] + assert 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 cover["계"] == D("137895") # 품 0.5 × 단가 275,790 + # 뺌 — 지운 줄 = 묶음 아님(식 줄 표지로만 · 아래 `out["뺌"]`) + assert "labor:LB000002#0" not in rows and "보통인부" not in rows + # 더함 — 기초(굴착기 641 × 2) = 묶음 · 인력 = 직접 묶음 속 더한 줄 · 기초는 수식 나무 + basic = rows["add:#0"] + man = next(x for r in eq["소계"]["줄"] for x in r.get("낱줄", []) if x["상태"] == "더함") + assert basic["상태"] == man["상태"] == "더함" and basic["갈래"] == "블록" + assert [r["갈래"] for r in eq["소계"]["줄"]] == ["블록", "직접"] + assert basic["계"] > 0 and man["계"] > 0 assert basic["나무"]["열쇠"] == "GC001148" and basic["나무"]["줄"] # 나무 합계 × 수량 = 행(같은 입력) assert abs(basic["나무"]["합계"]["계"] * 2 - basic["계"]) <= 2 @@ -106,7 +108,8 @@ def test_A_added_removed_covered_total_equals_sk(made): assert out["뺌"] is True and out["표지"] == [] # 고침 없는 SK(plain)는 표지도 더함도 없음 — 합계 = 같은 바탕 금액 flat = _read(me, _doc(me, plain)) - assert {r["상태"] for r in flat["소계"]["줄"]} == {"바탕"} + assert {r.get("상태", "바탕") for r in flat["소계"]["줄"]} == {"바탕"} + assert [r["갈래"] for r in flat["소계"]["줄"]] == ["직접"] # 801-3 한 묶음 · 낱줄 없음 assert flat["합계"]["계"] == _d(bare["기준"]["계"]) assert not any("표지" in x for x in flat["줄"]) @@ -151,9 +154,9 @@ def test_B_unit_cover_rate_and_reference(world): 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 + pool = next(r for r in eq["소계"]["줄"] if r["갈래"] == "직접") + price = next(x for x in pool["낱줄"] if x["상태"] == "덮음") + assert price["원래"] is not None and "sub2임시 율" in pool["이름"] and pool["경비"] > 0 assert rows["add:#1"]["갈래"] == "블록" and rows["add:#1"]["상태"] == "더함" assert _line(eq, "호표", price["이름"])["표지"] == ["단가"] @@ -171,7 +174,7 @@ def test_C_imported_sk_same_rule(world): eq = _read(me, doc) _closes(eq, ans) rows = _rows(eq) - assert rows["add:#0"]["상태"] == "더함" and rows[cid]["상태"] == "덮음" + assert [x["상태"] for x in eq["소계"]["줄"][-1]["낱줄"]] == ["덮음", "더함"], rows def test_plain_sk_has_answer_table(world): @@ -183,7 +186,7 @@ def test_plain_sk_has_answer_table(world): def test_D_imported_outside_total_line_not_in_sum(world): - """D 가져온 식 `합밖` 줄 = 합계에 안 듦 → 행 0 · 밖금액 · 끝수에 안 섞임.""" + """D 가져온 식 `합밖` 줄 = 합계에 안 듦 → 묶음 아님 · `밖` 글 · 끝수에 안 섞임.""" me = world["personal"] row = _imported_row() row["호표"][1] = {**row["호표"][1], "합밖": True} @@ -193,6 +196,6 @@ def test_D_imported_outside_total_line_not_in_sum(world): 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 + out = eq["소계"]["밖"] + assert len(out) == 1 and out[0]["밖금액"] > 0 assert abs(eq["소계"]["끝수"]["계"]) < 1 diff --git a/resources/tester/test_799_1_subtotals.py b/resources/tester/test_799_1_subtotals.py index 83870dfa7..7b012419a 100644 --- a/resources/tester/test_799_1_subtotals.py +++ b/resources/tester/test_799_1_subtotals.py @@ -62,10 +62,10 @@ def test_굴착기_블록_한_줄_끝수(): def test_거푸집_자원_덧줄_지게_자원(): eq = _export("GC000211") - assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원", "덧줄"] + assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"] # 801-3 직접 줄 한 묶음 _closes(eq) eq = _export("GC001140") - assert [r["갈래"] for r in eq["소계"]["줄"]] == ["자원"] + assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"] _closes(eq) @@ -96,7 +96,7 @@ def test_결과_로직은_소계_없음(): def test_화면_부품이_서버_칸을_읽음(): ts = Path("ui_template/ui_template_equation_sum.ts").read_text(encoding="utf-8") - for name in ("노무비", "재료비", "경비", "끝수", "합계", "자원", "덧줄"): + 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, open" in line # 796-2e 덧단추 자리 인자가 뒤에 붙음 diff --git a/resources/tester/test_801_3_groups.py b/resources/tester/test_801_3_groups.py new file mode 100644 index 000000000..16ece1cee --- /dev/null +++ b/resources/tester/test_801_3_groups.py @@ -0,0 +1,117 @@ +"""801-3 수식 탭 소계 묶음(요구 662-3) 지킴 — 금액 셈은 그대로 · 묶음만 다시. + +① 묶음 = 부른 단가산출(갈래 블록) 하나씩 + 직접 줄 한 묶음(갈래 직접) · 돈 없는 줄 = 묶음 아님 · 안쪽 말 0 +② M01 전수 — 묶음 합 + 끝수 = 합계(엔진 비목 합 그대로) · 직접 묶음 하나뿐 +③ 부품 — 묶음 하나 = 머리 없음 · 계 / 둘 이상 = 머리 · 소계 / 단가산출 둘 이상 = 합계 · 안쪽 말 글 0 +""" + +from __future__ import annotations + +import importlib +from decimal import Decimal as D +from pathlib import Path + +from M01_MasterData import M01_MasterData_Store as store + +mf = importlib.import_module("master_formula") +meq = importlib.import_module("master_equation") +grp = importlib.import_module("master_equation_group") + +COSTS = ("노무비", "재료비", "경비", "계") +BAD = ("자기", "자원", "덧줄", "블록") # 코드 안쪽 말 — 화면 글에 0 + + +def _w(): + return store.loaded()[1] + + +def _export(key: str) -> dict: + w = _w() + return meq.export(w, key, meq.given_of(w, w.logic(key), None)) + + +def _row(kind, name, cost=(0, 0, 0), **more): + got = dict(zip(COSTS[:3], (D(x) for x in cost), strict=True)) + return {"갈래": kind, "이름": name, **got, "계": sum(got.values(), D(0))} | more + + +def test_묶음_함수_하나뿐_직접_한_묶음(): + got = grp.bundles( + [ + _row("블록", "굴착기", (1, 2, 3)), + _row("블록", "쉬는 단가산출"), # 돈 0 = 묶음 아님 + _row("자원", "보통인부", (5, 0, 0), 종류="인력"), + _row("자원", "시멘트", (0, 7, 0), 종류="재료"), + _row("덧줄", "공구손료", (0, 0, 1)), + ] + ) + assert [r["갈래"] for r in got["줄"]] == ["블록", "직접"] + assert got["줄"][0]["이름"] == "굴착기" and got["줄"][1]["이름"] == "인력 · 재료" + assert (got["줄"][1]["노무비"], got["줄"][1]["재료비"], got["줄"][1]["경비"]) == (5, 7, 1) + only = grp.bundles([_row("덧줄", "공구손료", (0, 0, 1))])["줄"] + assert [r["이름"] for r in only] == ["경비"] and not grp.bundles([_row("블록", "빈")])["줄"] + + +def test_M02_상태_줄(): + got = grp.bundles( + [ + _row("자원", "보통인부", (5, 0, 0), 상태="더함", 종류="인력"), + _row("자원", "형틀목공", (4, 0, 0), 상태="덮음", 원래=D(9), 종류="인력"), + _row("자원", "지운 줄", 상태="뺌", 종류="인력"), + _row("자원", "밖 줄", 합밖=True, 밖금액=D(8)), + ] + ) + assert [x["이름"] for x in got["줄"][0]["낱줄"]] == ["보통인부", "형틀목공"] + assert got["밖"] == [{"이름": "밖 줄", "밖금액": D(8)}] and got["줄"][0]["계"] == 9 + + +def test_거푸집_한_묶음_굴착기_한_묶음(): + eq = _export("GC000211") + assert [r["갈래"] for r in eq["소계"]["줄"]] == ["직접"] + assert eq["소계"]["줄"][0]["이름"] == "인력 · 재료" + eq = _export("GC001148") + rows = eq["소계"]["줄"] + assert [(r["갈래"], r["이름"]) for r in rows] == [("블록", "굴착기(무한궤도)")] + assert eq["합계"]["계"] == D("641.0") and eq["소계"]["끝수"]["계"] == D("-0.3") + + +def test_M01_전수_묶음_합_끝수_합계_안쪽_말_0(): + w = _w() + many = seen = 0 + for tid, rows in sorted(w.index.items()): + if mf.mk.GROUP_OF.get(tid) != "로직": + continue + for logic in rows.values(): + if not logic.get("호표"): + continue + try: + eq = _export(logic["키"]) + except mf.FormulaError: + continue + sub = eq.get("소계") + if not sub: + continue + seen += 1 + lines = sub["줄"] + assert sum(r["갈래"] == "직접" for r in lines) <= 1, logic["키"] + assert all(r["계"] != 0 for r in lines), logic["키"] + for r in lines: + assert not any(b in r["이름"] for b in BAD), (logic["키"], r["이름"]) + for k in COSTS: + got = sum((r[k] for r in lines), D(0)) + sub["끝수"][k] + assert got == eq["합계"][k], (logic["키"], k) + many += sum(r["갈래"] == "블록" for r in lines) >= 2 + assert seen > 400 and many >= 10, (seen, many) + + +def test_부품_머리_소계_합계_규칙_안쪽_말_0(): + ts = Path("ui_template/ui_template_equation_sum.ts").read_text(encoding="utf-8") + assert "sub.줄.length >= 2" in ts # 묶음 하나 = 머리 · 소계 없음 + assert '"합계" : "계"' in ts and '=== "블록").length >= 2' in ts # 합계 = 단가산출 둘 이상 + ui = "".join( + line + for line in ts.splitlines(keepends=True) + if not line.lstrip().startswith(("*", "/*", "//", "/**")) + ) + for word in ("자기 자원", "덧줄", "KIND"): + assert word not in ui, word diff --git a/ui_template/ui_template_equation.ts b/ui_template/ui_template_equation.ts index b438eef97..777799ce6 100644 --- a/ui_template/ui_template_equation.ts +++ b/ui_template/ui_template_equation.ts @@ -45,7 +45,7 @@ export interface EqNode { 사슬?: boolean; } -/** 단가산출 소계 줄(799-1) — 갈래 블록 | 자원 | 덧줄 | 율 · M02 SK(796-2c) 는 셈 답 한 줄마다 + 상태 */ +/** 단가산출 소계 묶음(799-1 · 801-3) — 갈래 블록(부른 · 더한 단가산출) | 직접(인력 · 재료 · 기계 · 덧줄 한 묶음) · M02 SK 는 상태 */ export interface EqSumRow { 갈래: string; 이름: string; @@ -61,9 +61,8 @@ export interface EqSumRow { /** 덮음 · 뺌 — 고치기 전 금액 */ 원래?: unknown; 까닭?: string; - /** 합계 밖 줄(엔진이 비목 합에서 뺌) — 행 금액은 0 · 밖금액에 원래 금액 */ - 합밖?: boolean; - 밖금액?: unknown; + /** 직접 묶음 속 더한 · 덮은 줄(M02 SK) */ + 낱줄?: { 이름: string; 상태: string; 계: unknown; 원래?: unknown }[]; /** 더한 기초의 수식(접힘 블록) */ 나무?: EqTree | null; /** 더한 기초 차례(M02 SK 더한줄 · 연결 끊기 796-2e) */ @@ -86,8 +85,13 @@ export interface EqTree { 줄: EqLine[]; 변수표: EqVarRow[]; 합계: Record; - /** 799-1 — 하위 로직 블록마다 노 · 재 · 경 소계 + 끝수(합계 − 줄 합) · 결과 로직은 없음 */ - 소계?: { 줄: EqSumRow[]; 끝수: Record; 끝수까닭?: string }; + /** 799-1 · 801-3 — 묶음마다 노 · 재 · 경 소계 + 끝수(합계 − 묶음 합) · 합계 밖 줄 · 결과 로직은 없음 */ + 소계?: { + 줄: EqSumRow[]; + 끝수: Record; + 끝수까닭?: string; + 밖?: { 이름: string; 밖금액: unknown }[]; + }; /** M02 SK — 셈 답이 안 서서 바탕 줄 합계를 보일 때의 까닭 */ 답까닭?: string; /** 800-2 — 오른쪽 변수 정의 영역(고른 옵션 갈래만 · 같은 기호 한 줄 · 값 · 단위 · DB 열쇠 없음) */ diff --git a/ui_template/ui_template_equation_sum.ts b/ui_template/ui_template_equation_sum.ts index eab9950c9..38e65556d 100644 --- a/ui_template/ui_template_equation_sum.ts +++ b/ui_template/ui_template_equation_sum.ts @@ -1,11 +1,11 @@ /* ============================================================================= * ui_template_equation_sum.ts - * 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 요구 655-1 · 661 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로). - * 묶음 = 기준 단가산출마다(하위 로직 블록 · 자기 자원 · 덧줄) 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계(노 · 재 · 경 · 계 칸) - * 맨 끝 「소 계」 묶음 = 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) → 끝수(합계 − 묶음 합 · 0 이면 줄 없음) → 합계(엔진 비목 합) - * 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` = SK 셈 답) · 소계가 없으면(결과 로직) 아무것도 안 그림 - * M02 SK 묶음 = 상태 알약(더함 · 덮음 · 뺌) · 덮음 갈래 · 원래 금액 · 수량 · 더한 기초는 접힘 수식 블록 · 뺌 = 흐리게 · 금액 0 - * 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`inline` · 줄 열쇠 = 블록 이름 · `createEqChain` 의 `after`) · 나머지(자기 자원 · 덧줄 · 더한 기초) · 맨 끝 소 계 · 끝수 · 합계는 `tail` + * 부품 「수식」 셋째 파일(799-1 · 796-2c · 800-4 · 801-3 · 요구 655-1 · 661 · 662-3 · 요구서_655 2 장 4) — 단가산출 소계 · 합계(보이기만 · 금액 셈 그대로). + * 묶음 = 부른 · 더한 단가산출 하나씩 + 직접 줄(인력 · 재료 · 기계 · 덧줄) 한 묶음 — 값 = 서버 `소계`(M01 `master_equation.subtotals` · M02 `M02_Sangeun_Equation_Full` · 꼴 = `master_equation_group`) · 소계가 없으면(결과 로직) 아무것도 안 그림 + * 묶음 하나뿐 = 머리 없음 · 노무비 · 재료비 · 경비 줄 → 「계」 한 줄(끝수가 있으면 끝수 줄 먼저) + * 묶음 둘 이상 = 묶음마다 「가. 이름」 머리 → 노무비 · 재료비 · 경비 줄 → 소 계 · 맨 끝 묶음 소계를 더한 식 → 끝수 → 합계(단가산출이 둘 이상) 아니면 계 + * M02 SK = 상태 알약(더함 · 덮음) · 원래 금액 · 더한 기초는 접힘 수식 블록(더한 기초 하나뿐이어도 머리 줄은 둠 — 연결 끊기 단추 자리) · 합계 밖 줄은 글 한 줄 + * 자리 = 하위 로직 블록 묶음은 그 블록 식 줄 바로 아래(`inline` · 줄 열쇠 = 블록 이름 · `createEqChain` 의 `after`) · 나머지(직접 줄 · 더한 기초) · 맨 끝 합계 · 끝수는 `tail` * 쓰는 법 = `createEqSum(tree, open?, hooks?, lineKeys?)` → { inline, tail } | null — `lineKeys` = 수식 영역 호표 줄 열쇠들 · `open` = 펼친 블록 이름표 모음 · * `hooks` = 페이지가 더한 기초 줄 머리 단추(`head`) · 접힘 속(`body`)을 바꿔 끼움(796-2e 연결 끊기) * ========================================================================== */ @@ -18,7 +18,6 @@ import { createEqChain } from "./ui_template_equation_line"; const COLS = ["노무비", "재료비", "경비", "계"] as const; const PARTS = ["노무비", "재료비", "경비"] as const; type Col = (typeof COLS)[number]; -const KIND: Record = { 자원: "자기 자원", 덧줄: "덧줄" }; const TONE: Record = { 더함: "success", 덮음: "warning", 뺌: "neutral" }; const ORDER = "가나다라마바사아자차카타파하"; @@ -66,17 +65,23 @@ const part = (label: string, text: string): HTMLElement => { }; /** 기준 단가산출 묶음 하나 — 「가. 이름」 머리(+ 상태 · 접힘 수식) → 노 · 재 · 경 줄 → 소 계 */ -function group(r: EqSumRow, open: Set, at: number, hooks?: EqSumHooks): HTMLElement { +function group( + r: EqSumRow, + open: Set, + at: number, + heads: boolean, + hooks?: EqSumHooks, +): HTMLElement { const box = node("div", "ui-eq__grp"); box.dataset.eqSumItem = r.갈래; box.dataset.eqSumState = r.상태 ?? "바탕"; const name = node("div", "ui-eq__grp-head"); - const title = r.갈래 === r.이름 ? (KIND[r.갈래] ?? r.이름) : r.이름; + const title = r.이름; const tree = r.나무; const id = `sum:${at}:${r.이름}`; const body = node("div", "ui-eq__sum-tree"); body.hidden = true; - name.append(node("span", "ui-eq__grp-mark", `${mark(at)}.`)); + if (heads) name.append(node("span", "ui-eq__grp-mark", `${mark(at)}.`)); if (tree) { const head = createButton({ label: title, @@ -107,35 +112,32 @@ function group(r: EqSumRow, open: Set, at: number, hooks?: EqSumHooks): name.append(createBadge(`${r.상태}${detail}`, TONE[r.상태] ?? "neutral", { size: 18 })); } name.append(...(hooks?.head?.(r, at) ?? [])); - const out = !!r.합밖; - if (out) name.append(createBadge("합 밖", "neutral", { size: 18 })); const notes: string[] = []; - if (out) 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(" · "))); - box.append(name, body); + const shown = heads || !!tree || notes.length > 0 || (!!r.상태 && r.상태 !== "바탕"); + if (shown) box.append(name, body); for (const k of PARTS) if (num(r[k]) !== 0) box.append(part(k, eqText(r[k]))); - const total = row("소계", "소 계", (k) => r[k]); - total.classList.add("ui-eq__grp-total"); - if (out) total.dataset.eqSumOut = "1"; - box.append(total); + for (const x of r.낱줄 ?? []) { + const was = x.원래 !== undefined && x.원래 !== null ? ` · 원래 ${eqText(x.원래)}` : ""; + box.append(part(`${x.이름} · ${x.상태}`, `${eqText(x.계) || "0"}${was}`)); + } + if (heads) { + const total = row("소계", "소 계", (k) => r[k]); + total.classList.add("ui-eq__grp-total"); + box.append(total); + } return box; } -/** 맨 끝 「소 계」 묶음 — 묶음 소계를 더한 식 */ -function final(rows: EqSumRow[], at: number): HTMLElement { +/** 맨 끝 — 묶음 소계를 더한 식(노무비 : 2,299.4 + 5,162 = 7,461.4) */ +function final(rows: EqSumRow[]): HTMLElement { const box = node("div", "ui-eq__grp"); - box.dataset.eqSumItem = "소계"; - const head = node("div", "ui-eq__grp-head"); - head.append( - node("span", "ui-eq__grp-mark", `${mark(at)}.`), - node("span", "ui-eq__sum-label", "소 계"), - ); - box.append(head); + box.dataset.eqSumItem = "합"; for (const k of PARTS) { const adds = rows.map((r) => num(r[k])).filter((v) => v !== 0); if (!adds.length) continue; @@ -160,21 +162,27 @@ export function createEqSum( lineKeys: string[] = [], ): EqSumParts | null { const sub = tree.소계; - if (!sub?.줄.length) return null; + if (!sub) return null; const inline = new Map(); const box = node("div", "ui-eq__sum"); box.dataset.eqSum = ""; + const heads = sub.줄.length >= 2; // 묶음 하나뿐 = 머리 · 소계 없음 + const word = sub.줄.filter((r) => r.갈래 === "블록").length >= 2 ? "합계" : "계"; // 합계 = 단가산출 둘 이상 sub.줄.forEach((r, at) => { - const one = group(r, open, at, hooks); + const one = group(r, open, at, heads, hooks); if (r.갈래 === "블록" && lineKeys.includes(r.이름) && !inline.has(r.이름)) inline.set(r.이름, one); else box.append(one); }); - box.append(final(sub.줄, sub.줄.length)); + if (heads) box.append(final(sub.줄)); const rest = sub.끝수; const odd = COLS.some((k) => Number(rest[k] ?? 0) !== 0); if (odd) box.append(row("끝수", "끝수", (k) => rest[k])); - box.append(row("합계", "합계", (k) => tree.합계[k])); + box.append(row(word, word, (k) => tree.합계[k])); + for (const x of sub.밖 ?? []) + box.append( + node("div", "ui-eq__sum-note", `합계에 안 듦 · ${x.이름} ${eqText(x.밖금액) || "0"}`), + ); const note = [odd ? sub.끝수까닭 : "", tree.답까닭].filter(Boolean).join(" · "); if (note) box.append(node("div", "ui-eq__sum-note", note)); return { inline, tail: box };