fix(m01): 흐름 합계 끝수 = 비용 출력 상자에 실린 로직 자신의 끝수 · 새 로직만 기본 1원 미만 버림

왼쪽 흐름 보기 소계 · 계와 오른쪽 원가 합계가 같은 끝수를 씀(Round.totalRoundOf)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EauA2ATmqNRbeV48s9CcBr
This commit is contained in:
2026-10-02 00:34:17 +09:00
co-authored by Claude Sonnet 5.5
parent fa33abb5f0
commit a5edf43d6e
4 changed files with 70 additions and 9 deletions
@@ -21,7 +21,12 @@ import {
type FormulaLine,
} from "../M02_MasterTemplete/DataFlow/M02_DF_FormulaView";
import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
import { itemRoundNow, totalRoundNow } from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
import {
itemRoundNow,
roundText,
totalRoundNow,
totalRoundOf,
} from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
import {
flowRows,
type FlowFunc,
@@ -42,7 +47,7 @@ const TEXT = {
HValue: ["값", "Value"],
Sub: ["소계", "Subtotal"],
Total: ["계", "Total"],
TotalNote: ["합계 끝수 기본 = 1원 미만 버림", "Total rounding default: floor to 1 won"],
TotalNote: ["합계 끝수", "Total rounding"],
None: ["출력 상자가 없음", "No output box"],
NoFunc: ["함수 상자가 없음", "No function box"],
} as const;
@@ -158,7 +163,8 @@ export function buildFlowView(
);
return;
}
const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRoundNow() });
const totalRound = totalRoundOf(lg, totalRoundNow());
const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRound });
const detail = section(tf("Detail"), [
...(rows.groups.length
? rows.groups.map(groupTable)
@@ -168,7 +174,10 @@ export function buildFlowView(
attrs: { "data-sum": String(rows.total) },
text: `${tf("Total")} ${rows.totalText}`,
}),
el("p", { className: "m01-logic__muted", text: tf("TotalNote") }),
el("p", {
className: "m01-logic__muted",
text: `${tf("TotalNote")}: ${roundText(totalRound)}`,
}),
...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })),
]);
/** 함수 상자마다 식 두 줄(sub2 식 보기 · 받는 알약 값을 칸에 넣고 셈) — 못 그리면 이름 = 값만 */
@@ -107,3 +107,15 @@ export function showRounded(v: number, r?: CfRound): string {
let totalNow: CfRound = { places: 0, method: "버림" };
void defaultTotalRound().then((r) => (totalNow = r));
export const totalRoundNow = (): CfRound => totalNow;
/** 그 로직의 합계 끝수 — 비용 출력 상자에 끝수가 실려 있으면 그것(옛 로직을 옮긴 흐름은 자신의 끝수) ·
* 없으면 기본(새 로직 = 일위대가표 계금 1원 미만 버림) */
export function totalRoundOf(
lg: { boxes: { kind: string; output?: { cost: string; round?: CfRound } }[] },
fallback: CfRound,
): CfRound {
const hit = lg.boxes.find(
(b) => b.kind === "output" && b.output?.cost !== "값" && b.output?.round,
);
return hit?.output?.round ?? fallback;
}
+13 -4
View File
@@ -12,7 +12,14 @@ import { formatMoney } from "../../M01_MasterData/M01_MasterData_UI_Logic_Money"
import type { CfEvalOptions, CfEvalResult, CfValues } from "./M02_DF_Eval";
import type { CfBox, CfLogic, CfRound } from "./M02_DF_Model";
import { showValue } from "./M02_DF_OutputPanel";
import { defaultTotalRound, itemRoundNow, roundBy, roundText, showRounded } from "./M02_DF_Round";
import {
defaultTotalRound,
itemRoundNow,
roundBy,
roundText,
showRounded,
totalRoundOf,
} from "./M02_DF_Round";
import { renderVerify, type CfSel } from "./M02_DF_Verify";
import "./M02_DF_Summary.css";
@@ -69,18 +76,20 @@ export function renderSummary(
? ts("Total")
: ts("Per", { per: /^\d/.test(perUnit) ? perUnit : `1${perUnit}` });
const { cost } = result;
/** 합계 끝수 — 비용 출력 상자에 실린 끝수(옮긴 로직의 자신 끝수) · 없으면 기본 1원 미만 버림 */
const sumRound = totalRoundOf(lg, totalRound);
const big = el("p", {
className: "m02-dfsum__big",
children: [
el("span", { text: money ? perText : ts("Total") }),
el("strong", { text: `${formatMoney(roundBy(totalRound, cost.계))} ${ts("Won")}` }),
el("strong", { text: `${formatMoney(roundBy(sumRound, cost.계))} ${ts("Won")}` }),
],
});
const costs = el("dl", {
className: "m02-dfsum__costs",
children: COSTS.flatMap((c) => [
el("dt", { text: ts(COST_KEY[c]) }),
el("dd", { text: formatMoney(roundBy(totalRound, cost[c])) }),
el("dd", { text: formatMoney(roundBy(sumRound, cost[c])) }),
]),
});
@@ -161,7 +170,7 @@ export function renderSummary(
: []),
el("p", {
className: "m02-dfsum__muted",
text: `${ts("RoundTotal")}: ${roundText(totalRound)}`,
text: `${ts("RoundTotal")}: ${roundText(sumRound)}`,
}),
verifyHost,
);
+32 -1
View File
@@ -37,8 +37,9 @@ function load(f) {
return box.exports;
}
const m = load(file);
const r = load(path.join(root, "M02_MasterTemplete", "DataFlow", "M02_DF_Round.ts"));
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
const out = jobs.map((job) => m.flowRows(job.lg, job.ev, job.round));
const out = jobs.map((job) => job.op === "totalRound" ? r.totalRoundOf(job.lg, job.fallback) : m.flowRows(job.lg, job.ev, job.round));
process.stdout.write(JSON.stringify(out));
setTimeout(() => process.exit(0), 50);
"""
@@ -182,3 +183,33 @@ def test_missing_value_shows_dash():
rows = run([{"lg": lg, "ev": ev, "round": ROUND}])[0]
assert group(rows, "labor")["lines"][0]["text"] == "—"
assert rows["errors"] == ["끊김"]
def test_total_round_follows_converted_logic_own_round():
"""옮긴 로직 = 비용 출력 상자에 실린 끝수 · 새 로직(끝수 없음) = 기본."""
default = {"places": 0, "method": "버림"}
own = {"places": 1, "method": "반올림"}
converted = logic(
[out_box("o1", "인건비", "노무비", round_=own), out_box("o2", "작업량", "값")]
)
fresh = logic([out_box("o1", "인건비", "노무비")])
only_plain = logic([out_box("o3", "작업량", "값", round_={"places": 3, "method": "버림"})])
got = run(
[
{"op": "totalRound", "lg": converted, "fallback": default},
{"op": "totalRound", "lg": fresh, "fallback": default},
{"op": "totalRound", "lg": only_plain, "fallback": default},
]
)
assert got == [own, default, default] # 값(비용 아님)의 끝수는 합계 끝수가 아님
def test_subtotal_and_total_use_converted_round():
own = {"places": 1, "method": "반올림"}
lg = logic([out_box("o1", "인건비", "노무비", round_=own)])
ev = result(
{"o1": 3216.7}, {"노무비": 3216.7, "재료비": 0, "경비": 0, "값들": {}, "계": 3216.7}
)
rows = run([{"lg": lg, "ev": ev, "round": {"item": ROUND["item"], "total": own}}])[0]
assert group(rows, "labor")["subText"] == "3,216.7"
assert rows["totalText"] == "3,216.7"