Merge remote-tracking branch 'origin/sub_laptop_5' into main_laptop_1
This commit is contained in:
@@ -72,6 +72,9 @@ export interface DetailContext {
|
||||
onChange: () => void;
|
||||
/** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */
|
||||
calcHost: HTMLElement;
|
||||
/** 흐름이 있는 로직 — 이 칸에 왼쪽 「지금 내용」(단가산출 상세 · 텍스트 수식 = `buildFlowView`)을 그림 ·
|
||||
* 있으면 호표 · 읽는 식 두 묶음 대신 이 칸을 놓음(기본정보 · 시험 계산은 그대로) */
|
||||
flowHost?: HTMLElement;
|
||||
}
|
||||
|
||||
type Group = "labor" | "material" | "cost" | "other";
|
||||
@@ -482,8 +485,7 @@ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void {
|
||||
: null;
|
||||
host.replaceChildren(
|
||||
infoBox(ctx),
|
||||
hoBox(ctx),
|
||||
section(tl("Formula_Title"), [formula]),
|
||||
...(ctx.flowHost ? [ctx.flowHost] : [hoBox(ctx), section(tl("Formula_Title"), [formula])]),
|
||||
section(tl("Calc_Title"), [ctx.calcHost]),
|
||||
);
|
||||
const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_FlowRows.ts
|
||||
* 흐름 로직의 왼쪽 「단가산출 상세」 줄 만들기(DOM 없음 · 순수) — 흐름도 출력 상자를 비용 종류별로 묶어
|
||||
* 줄(이름 · 단위 · 값) · 소계 · 계 · 함수 상자별 결과 값. 값은 DataFlow 계산 결과(`CfEvalResult`)를 읽기만 함.
|
||||
* 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7. 시험 = resources/tester/test_m01_logic_flowview.py
|
||||
* ========================================================================== */
|
||||
|
||||
import type { CfEvalResult } from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
|
||||
import type { CfBox, CfCost, CfLogic, CfRound } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
|
||||
import { roundBy, showRounded } from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
|
||||
|
||||
export type FlowGroupKey = "labor" | "material" | "cost" | "plain";
|
||||
|
||||
/** 비용 종류 → 묶음(인력 = 노무비 · 재료 = 재료비 · 경비) · 값(비용 아님)은 따로 */
|
||||
export const GROUP_OF: Record<CfCost, FlowGroupKey> = {
|
||||
노무비: "labor",
|
||||
재료비: "material",
|
||||
경비: "cost",
|
||||
값: "plain",
|
||||
};
|
||||
export const GROUP_ORDER: FlowGroupKey[] = ["labor", "material", "cost", "plain"];
|
||||
const COST_OF: Record<Exclude<FlowGroupKey, "plain">, "노무비" | "재료비" | "경비"> = {
|
||||
labor: "노무비",
|
||||
material: "재료비",
|
||||
cost: "경비",
|
||||
};
|
||||
|
||||
export interface FlowLine {
|
||||
box: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
value: number | string | boolean | null;
|
||||
/** 보이는 글 — 숫자는 그 출력의 끝수 자리대로 */
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface FlowGroup {
|
||||
key: FlowGroupKey;
|
||||
lines: FlowLine[];
|
||||
/** 비용 묶음만 — 합계 끝수를 적용한 소계 */
|
||||
sub: number | null;
|
||||
subText: string;
|
||||
}
|
||||
|
||||
export interface FlowFunc {
|
||||
box: string;
|
||||
name: string;
|
||||
unit: string;
|
||||
value: unknown;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface FlowRows {
|
||||
groups: FlowGroup[];
|
||||
/** 합계 끝수(기본 1원 미만 버림)를 적용한 계 */
|
||||
total: number;
|
||||
totalText: string;
|
||||
funcs: FlowFunc[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
const plain = (v: unknown): string =>
|
||||
v === null || v === undefined || Array.isArray(v)
|
||||
? "—"
|
||||
: typeof v === "number"
|
||||
? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 })
|
||||
: String(v);
|
||||
|
||||
/** 줄 모으기 — round.item = 출력 기본 끝수 · round.total = 합계 기본 끝수 */
|
||||
export function flowRows(
|
||||
lg: CfLogic,
|
||||
ev: CfEvalResult,
|
||||
round: { item: CfRound; total: CfRound },
|
||||
): FlowRows {
|
||||
const outs: CfBox[] = lg.boxes.filter((b) => b.kind === "output");
|
||||
const groups = GROUP_ORDER.map((key): FlowGroup => {
|
||||
const mine = outs.filter((b) => GROUP_OF[b.output?.cost ?? "값"] === key);
|
||||
const lines = mine.map((b): FlowLine => {
|
||||
const v = ev.outputs[b.id] ?? null;
|
||||
const r = b.output?.round ?? (key === "plain" ? undefined : round.item);
|
||||
return {
|
||||
box: b.id,
|
||||
name: b.name,
|
||||
unit: b.output?.unit ?? b.unit,
|
||||
value: v,
|
||||
text: typeof v === "number" ? showRounded(v, r) : plain(v),
|
||||
};
|
||||
});
|
||||
const raw = key === "plain" ? null : ev.cost[COST_OF[key]];
|
||||
const sub = raw === null ? null : roundBy(round.total, raw);
|
||||
return {
|
||||
key,
|
||||
lines,
|
||||
sub,
|
||||
subText: sub === null ? "" : showRounded(sub, round.total),
|
||||
};
|
||||
}).filter((g) => g.lines.length);
|
||||
const total = roundBy(round.total, ev.cost.계);
|
||||
const funcs = lg.boxes
|
||||
.filter((b) => b.kind === "function" && b.func)
|
||||
.map((b): FlowFunc => {
|
||||
const out = b.func!.output;
|
||||
const v = ev.ports[b.id]?.[out.name];
|
||||
return {
|
||||
box: b.id,
|
||||
name: b.name,
|
||||
unit: out.unit || b.unit,
|
||||
value: v,
|
||||
text:
|
||||
typeof v === "number"
|
||||
? v.toLocaleString("ko-KR", { maximumFractionDigits: 6 })
|
||||
: plain(v),
|
||||
};
|
||||
});
|
||||
return {
|
||||
groups,
|
||||
total,
|
||||
totalText: showRounded(total, round.total),
|
||||
funcs,
|
||||
errors: ev.errors,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
.m01flow__group {
|
||||
margin: 0 0 var(--spacing-12);
|
||||
}
|
||||
|
||||
.m01flow__block {
|
||||
margin: 0 0 var(--spacing-8);
|
||||
}
|
||||
|
||||
.m01flow__func {
|
||||
margin: 0 0 var(--spacing-4);
|
||||
color: var(--color-text-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m01flow__branch {
|
||||
margin: 0 0 0 var(--spacing-16);
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
|
||||
.m01flow__branch--now {
|
||||
color: var(--color-text-body);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.m01flow__warn {
|
||||
margin: var(--spacing-4) 0 0;
|
||||
color: var(--color-danger);
|
||||
font-size: var(--text-body-sm);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_FlowView.ts
|
||||
* 흐름 로직의 왼쪽 「지금 내용」 — ② 단가산출 상세(출력 상자를 비용 종류별 줄 · 소계 · 계) · ③ 텍스트 수식(함수 상자마다
|
||||
* 식 두 줄 — sub2 `formulaLines` 가 있으면 그것 · 없으면 함수 이름 · 결과 값만). 기본정보 · 시험 계산은 Detail 이 둠.
|
||||
* 값은 DataFlow 계산(Eval · 같은 코드)으로 바로 — 오른쪽 패널이 고치면 LogicLab 이 이 함수를 다시 부름.
|
||||
* 화면은 `buildFlowView(host, lg, values)` 하나. 이 파일은 DataFlow 를 정적으로 가져오므로 LogicLab 은 import() 로 늦게 부름.
|
||||
* 모양은 docs/raw/M01_쉬운단가/흐름도_적용_정함.md 7 · 8.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import {
|
||||
evalLogic,
|
||||
type CfEvalOptions,
|
||||
type CfEvalResult,
|
||||
type CfValues,
|
||||
} from "../M02_MasterTemplete/DataFlow/M02_DF_Eval";
|
||||
import type { CfBox, CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model";
|
||||
import { itemRoundNow, totalRoundNow } from "../M02_MasterTemplete/DataFlow/M02_DF_Round";
|
||||
import {
|
||||
flowRows,
|
||||
type FlowFunc,
|
||||
type FlowGroup,
|
||||
type FlowGroupKey,
|
||||
} from "./M01_MasterData_UI_LogicLab_FlowRows";
|
||||
import "./M01_MasterData_UI_LogicLab_FlowView.css";
|
||||
|
||||
const TEXT = {
|
||||
Detail: ["단가산출 상세", "Unit-price detail"],
|
||||
Formula: ["텍스트 수식", "Text formulas"],
|
||||
Labor: ["인력 · 노무비", "Labor · labor cost"],
|
||||
Material: ["재료 · 재료비", "Material · material cost"],
|
||||
Cost: ["경비", "Expense"],
|
||||
Plain: ["값(비용 아님)", "Values (not costs)"],
|
||||
HName: ["이름", "Name"],
|
||||
HUnit: ["단위", "Unit"],
|
||||
HValue: ["값", "Value"],
|
||||
Sub: ["소계", "Subtotal"],
|
||||
Total: ["계", "Total"],
|
||||
TotalNote: ["합계 끝수 기본 = 1원 미만 버림", "Total rounding default: floor to 1 won"],
|
||||
None: ["출력 상자가 없음", "No output box"],
|
||||
NoFunc: ["함수 상자가 없음", "No function box"],
|
||||
} as const;
|
||||
|
||||
type TextKey = keyof typeof TEXT;
|
||||
const tf = (key: TextKey): string => TEXT[key][currentLanguageIndex === 1 ? 1 : 0];
|
||||
const GROUP_TITLE: Record<FlowGroupKey, TextKey> = {
|
||||
labor: "Labor",
|
||||
material: "Material",
|
||||
cost: "Cost",
|
||||
plain: "Plain",
|
||||
};
|
||||
|
||||
/** sub2 의 식 보기(`M02_DF_FormulaView.ts` · 정함 8) — 아직 없으면 비어 있음 */
|
||||
interface FormulaLine {
|
||||
name: string;
|
||||
named: string;
|
||||
valued: string;
|
||||
branches?: { when: string; then: string; now: boolean }[];
|
||||
}
|
||||
type FormulaFn = (box: CfBox, values: CfValues) => FormulaLine[];
|
||||
const FORMULA = import.meta.glob<{ formulaLines?: FormulaFn }>(
|
||||
"../M02_MasterTemplete/DataFlow/M02_DF_FormulaView.ts",
|
||||
);
|
||||
const loadFormula = async (): Promise<FormulaFn | null> => {
|
||||
const load = Object.values(FORMULA)[0];
|
||||
return load ? ((await load()).formulaLines ?? null) : null;
|
||||
};
|
||||
|
||||
const section = (title: string, body: HTMLElement[]): HTMLElement =>
|
||||
el("section", {
|
||||
className: "m01-logic__section m01lab__box",
|
||||
children: [
|
||||
el("div", { className: "m01-logic__section-head", children: [el("h3", { text: title })] }),
|
||||
...body,
|
||||
],
|
||||
});
|
||||
|
||||
const groupTable = (g: FlowGroup): HTMLElement => {
|
||||
const sub =
|
||||
g.sub === null
|
||||
? []
|
||||
: [
|
||||
el("tr", {
|
||||
className: "m01lab__sub",
|
||||
children: [
|
||||
el("td", { text: tf("Sub"), attrs: { colspan: "2" } }),
|
||||
el("td", { className: "m01-logic__money", text: g.subText }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
return el("div", {
|
||||
className: "m01flow__group",
|
||||
children: [
|
||||
el("h4", { text: tf(GROUP_TITLE[g.key]) }),
|
||||
el("div", {
|
||||
className: "m01-logic__scroll",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "m01-logic__grid",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [
|
||||
el("tr", {
|
||||
children: [tf("HName"), tf("HUnit"), tf("HValue")].map((h) =>
|
||||
el("th", { text: h }),
|
||||
),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
el("tbody", {
|
||||
children: [
|
||||
...g.lines.map((l) =>
|
||||
el("tr", {
|
||||
children: [
|
||||
el("td", { text: l.name }),
|
||||
el("td", { text: l.unit }),
|
||||
el("td", { className: "m01-logic__money", text: l.text }),
|
||||
],
|
||||
}),
|
||||
),
|
||||
...sub,
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
/** 함수 상자 하나의 글 줄 — 식 보기가 있으면 두 줄(+ 갈래) · 없으면 이름 = 값 */
|
||||
function funcBlock(f: FlowFunc, lines: FormulaLine[] | null): HTMLElement {
|
||||
const unit = f.unit && f.unit !== "-" ? ` ${f.unit}` : "";
|
||||
const head = el("p", { className: "m01flow__func", text: `${f.name} = ${f.text}${unit}` });
|
||||
if (!lines?.length) return el("div", { className: "m01flow__block", children: [head] });
|
||||
return el("div", {
|
||||
className: "m01flow__block",
|
||||
children: [
|
||||
head,
|
||||
...lines.flatMap((l) => [
|
||||
el("p", { className: "m01lab__expr", text: `${l.name} = ${l.named}` }),
|
||||
el("p", { className: "m01lab__expr", text: `${l.name} = ${l.valued}` }),
|
||||
...(l.branches ?? []).map((b) =>
|
||||
el("p", {
|
||||
className: `m01flow__branch${b.now ? " m01flow__branch--now" : ""}`,
|
||||
text: `${b.now ? "▶ " : ""}${b.when} → ${b.then}`,
|
||||
}),
|
||||
),
|
||||
]),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
/** 왼쪽 「지금 내용」 — 단가산출 상세 + 텍스트 수식을 `host` 에 그림(`values` = 입력 값 · 다시 부르면 다시 그림) */
|
||||
export function buildFlowView(
|
||||
host: HTMLElement,
|
||||
lg: CfLogic,
|
||||
values: CfValues,
|
||||
opts: { logics?: CfEvalOptions["logics"]; result?: CfEvalResult } = {},
|
||||
): void {
|
||||
let ev: CfEvalResult;
|
||||
try {
|
||||
ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics });
|
||||
} catch (e) {
|
||||
host.replaceChildren(
|
||||
section(tf("Detail"), [el("p", { className: "m01-logic__muted", text: String(e) })]),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const rows = flowRows(lg, ev, { item: itemRoundNow(), total: totalRoundNow() });
|
||||
const detail = section(tf("Detail"), [
|
||||
...(rows.groups.length
|
||||
? rows.groups.map(groupTable)
|
||||
: [el("p", { className: "m01-logic__muted", text: tf("None") })]),
|
||||
el("p", {
|
||||
className: "m01lab__total",
|
||||
attrs: { "data-sum": String(rows.total) },
|
||||
text: `${tf("Total")} ${rows.totalText}`,
|
||||
}),
|
||||
el("p", { className: "m01-logic__muted", text: tf("TotalNote") }),
|
||||
...rows.errors.slice(0, 3).map((t) => el("p", { className: "m01flow__warn", text: t })),
|
||||
]);
|
||||
const bodies = rows.funcs.map((f) => funcBlock(f, null));
|
||||
const text = section(
|
||||
tf("Formula"),
|
||||
bodies.length ? bodies : [el("p", { className: "m01-logic__muted", text: tf("NoFunc") })],
|
||||
);
|
||||
host.replaceChildren(detail, text);
|
||||
// sub2 식 보기가 올라와 있으면 함수 상자마다 식 두 줄을 더함
|
||||
void loadFormula().then((fn) => {
|
||||
if (!fn || !bodies.length) return;
|
||||
const boxes = lg.boxes.filter((b) => b.kind === "function" && b.func);
|
||||
const merged = { ...ev.ports, ...ev.arrived } as CfValues;
|
||||
text.replaceChildren(
|
||||
text.firstElementChild!,
|
||||
...rows.funcs.map((f, i) => {
|
||||
try {
|
||||
return funcBlock(f, fn(boxes[i], merged));
|
||||
} catch {
|
||||
return bodies[i];
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -102,3 +102,8 @@ export function showRounded(v: number, r?: CfRound): string {
|
||||
maximumFractionDigits: Math.max(0, r.places),
|
||||
});
|
||||
}
|
||||
|
||||
/** 원가 합계 기본 끝수(표를 읽어 오기 전에는 계금 1원 미만 버림) — 화면이 바로 쓰는 값 */
|
||||
let totalNow: CfRound = { places: 0, method: "버림" };
|
||||
void defaultTotalRound().then((r) => (totalNow = r));
|
||||
export const totalRoundNow = (): CfRound => totalNow;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
"""흐름 로직 왼쪽 「단가산출 상세」 줄 만들기(`M01_MasterData_UI_LogicLab_FlowRows.ts`) — 순수 부분.
|
||||
|
||||
화면 TS 를 그 자리에서 트랜스파일해 node 로 돌림 — 상대 경로 TS(DataFlow Round …)는 이어 트랜스파일 ·
|
||||
화면 부품(@ui · @config)은 빈 흉내. 견본 로직 · 계산 결과는 이 시험 안에 둠(마스터 내용에 기대지 않음).
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = ROOT / "M01_MasterData" / "M01_MasterData_UI_LogicLab_FlowRows.ts"
|
||||
|
||||
RUNNER = r"""
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const [root, file] = process.argv.slice(1);
|
||||
const ts = require(path.join(root, "config", "node_modules", "typescript"));
|
||||
const stub = new Proxy({}, { get: (_t, k) => (k === "currentLanguageIndex" ? 0 : k === "API_BASE_URL" ? "http://127.0.0.1:1/api" : () => undefined) });
|
||||
const cache = {};
|
||||
function load(f) {
|
||||
if (cache[f]) return cache[f].exports;
|
||||
const js = ts.transpileModule(fs.readFileSync(f, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
const box = (cache[f] = { exports: {} });
|
||||
const req = (id) => {
|
||||
if (id.startsWith(".")) {
|
||||
const p = path.resolve(path.dirname(f), id);
|
||||
return fs.existsSync(p + ".ts") ? load(p + ".ts") : stub;
|
||||
}
|
||||
return stub;
|
||||
};
|
||||
new Function("exports", "module", "require", js)(box.exports, box, req);
|
||||
return box.exports;
|
||||
}
|
||||
const m = load(file);
|
||||
const jobs = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
const out = jobs.map((job) => m.flowRows(job.lg, job.ev, job.round));
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
setTimeout(() => process.exit(0), 50);
|
||||
"""
|
||||
|
||||
ROUND = {"item": {"places": 1, "method": "버림"}, "total": {"places": 0, "method": "버림"}}
|
||||
|
||||
|
||||
def box(id_, kind, name, unit="원", **extra):
|
||||
return {
|
||||
"id": id_,
|
||||
"kind": kind,
|
||||
"name": name,
|
||||
"color": "#000",
|
||||
"unit": unit,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def out_box(id_, name, cost, unit="원", round_=None):
|
||||
output = {"cost": cost, "unit": unit}
|
||||
if round_:
|
||||
output["round"] = round_
|
||||
return box(id_, "output", name, unit, output=output)
|
||||
|
||||
|
||||
def func_box(id_, name, out_name, unit="시간"):
|
||||
func = {
|
||||
"sheet": {"id": "s-" + id_, "이름": name, "칸": {}},
|
||||
"inputs": [],
|
||||
"variables": [],
|
||||
"output": {"cell": "B1", "name": out_name, "unit": unit},
|
||||
}
|
||||
return box(id_, "function", name, unit, func=func)
|
||||
|
||||
|
||||
def logic(boxes):
|
||||
return {
|
||||
"key": None,
|
||||
"name": "시험",
|
||||
"resultUnit": "원/㎡",
|
||||
"boxes": boxes,
|
||||
"links": [],
|
||||
"userElements": [],
|
||||
}
|
||||
|
||||
|
||||
def result(outputs, cost, ports=None, errors=None):
|
||||
return {"ports": ports or {}, "outputs": outputs, "cost": cost, "errors": errors or []}
|
||||
|
||||
|
||||
def run(jobs: list[dict]) -> list:
|
||||
node = "node"
|
||||
done = subprocess.run(
|
||||
[node, "-e", RUNNER, str(ROOT), str(SOURCE)],
|
||||
input=json.dumps(jobs),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
check=False,
|
||||
)
|
||||
assert done.returncode == 0, done.stderr
|
||||
return json.loads(done.stdout)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def sample():
|
||||
lg = logic(
|
||||
[
|
||||
out_box("o1", "인건비", "노무비"),
|
||||
out_box("o2", "재료값", "재료비"),
|
||||
out_box("o3", "시간당 작업량", "값", "㎥/시간"),
|
||||
out_box("o4", "특별 끝수", "경비", round_={"places": 2, "method": "반올림"}),
|
||||
func_box("f1", "운반 시간", "시간", "시간"),
|
||||
]
|
||||
)
|
||||
ev = result(
|
||||
{"o1": 3216.69, "o2": 100.04, "o3": 12.5, "o4": 1.239},
|
||||
{
|
||||
"노무비": 3216.69,
|
||||
"재료비": 100.04,
|
||||
"경비": 1.239,
|
||||
"값들": {"시간당 작업량": 12.5},
|
||||
"계": 3317.969,
|
||||
},
|
||||
ports={"f1": {"시간": 6.024}},
|
||||
)
|
||||
return run([{"lg": lg, "ev": ev, "round": ROUND}])[0]
|
||||
|
||||
|
||||
def group(rows, key):
|
||||
return next(g for g in rows["groups"] if g["key"] == key)
|
||||
|
||||
|
||||
def test_group_order_and_costs_only(sample):
|
||||
assert [g["key"] for g in sample["groups"]] == ["labor", "material", "cost", "plain"]
|
||||
|
||||
|
||||
def test_line_uses_output_round(sample):
|
||||
# 출력 끝수를 안 정한 비용 줄 = 기본 0.1원 버림 → 3,216.6 (정수로 반올림하지 않음)
|
||||
assert group(sample, "labor")["lines"][0]["text"] == "3,216.6"
|
||||
assert group(sample, "material")["lines"][0]["text"] == "100"
|
||||
# 그 출력의 끝수가 있으면 그 자리대로(소수 둘째 반올림)
|
||||
assert group(sample, "cost")["lines"][0]["text"] == "1.24"
|
||||
|
||||
|
||||
def test_plain_value_is_not_rounded_and_has_no_subtotal(sample):
|
||||
plain = group(sample, "plain")
|
||||
assert plain["lines"][0]["text"] == "12.5"
|
||||
assert plain["lines"][0]["unit"] == "㎥/시간"
|
||||
assert plain["sub"] is None
|
||||
|
||||
|
||||
def test_subtotal_and_total_floor_to_won(sample):
|
||||
assert group(sample, "labor")["sub"] == 3216
|
||||
assert group(sample, "labor")["subText"] == "3,216"
|
||||
assert sample["total"] == 3317
|
||||
assert sample["totalText"] == "3,317"
|
||||
|
||||
|
||||
def test_function_rows(sample):
|
||||
assert sample["funcs"] == [
|
||||
{"box": "f1", "name": "운반 시간", "unit": "시간", "value": 6.024, "text": "6.024"}
|
||||
]
|
||||
|
||||
|
||||
def test_empty_group_is_hidden():
|
||||
lg = logic([out_box("o1", "인건비", "노무비")])
|
||||
ev = result({"o1": 10}, {"노무비": 10, "재료비": 0, "경비": 0, "값들": {}, "계": 10})
|
||||
rows = run([{"lg": lg, "ev": ev, "round": ROUND}])[0]
|
||||
assert [g["key"] for g in rows["groups"]] == ["labor"]
|
||||
assert rows["total"] == 10
|
||||
|
||||
|
||||
def test_missing_value_shows_dash():
|
||||
lg = logic([out_box("o1", "인건비", "노무비")])
|
||||
ev = result(
|
||||
{"o1": None}, {"노무비": 0, "재료비": 0, "경비": 0, "값들": {}, "계": 0}, errors=["끊김"]
|
||||
)
|
||||
rows = run([{"lg": lg, "ev": ev, "round": ROUND}])[0]
|
||||
assert group(rows, "labor")["lines"][0]["text"] == "—"
|
||||
assert rows["errors"] == ["끊김"]
|
||||
Reference in New Issue
Block a user