feat(m02-cf): 단가산출 다이어그램 견본 셋 · M02_CF_Sample(목록 · 복사해서 열기)
- _sample/masonry.json — 돌쌓기 메쌓기(인력): 변수 · 품표(QF000421) · 증가율표(QF000422) · 인력 → 증가율 찾기 → 품(둘) → 노무비(둘) → 합 → 출력 노무비 - _sample/rate.json — 공구손료(비율): 노무비 × 율 → 출력 재료비 - _sample/value.json — 덤프트럭 시간당 작업량: q · f · E · Cm → 출력 값 - 시험 test_m02_cf_sample.py — 모양 · 알약 이름 규칙 · 견본 ① 노무비 148,470.504 = /calc GF000219 · ③ = GF000133 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
/* =============================================================================
|
||||
* M02_CF_Sample.ts
|
||||
* 단가산출 다이어그램 견본 — 처음 열었을 때 바로 보고 따라 만들 견본 셋(`_sample/*.json` · 손으로 짬)
|
||||
* ① 돌쌓기 메쌓기(인력) — 변수 · 품표 · 증가율표 · 인력 → 함수(표 찾기 · 높이 할증 · 직렬 · 병렬) → 출력 노무비
|
||||
* ② 공구손료(비율) — 노무비 × 율 → 출력 재료비
|
||||
* ③ 덤프트럭 시간당 작업량(값) — 변수 넷 → 함수 하나 → 출력 값
|
||||
* 견본은 읽기만 — `sampleLoad` 는 「복사해서 열기」 사본(key 없음 · 저장하면 `_data` 에 새 번호).
|
||||
* 입력 값(노임 · 표)은 견본에 안 들고 열 때 · 저장 때 마스터에서 받음(정함 「계산」).
|
||||
* 모양 = `M02_CF_Model` CfLogic · 알약 이름 규칙(입력 = 요소 이름 · 「표」 · 변수 이름 / 함수 = inputs[].port ·
|
||||
* output.name / 출력 = 「값」) — 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` 11. 이 폴더째 지워도 됨.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import type { CfLogic } from "./M02_CF_Model";
|
||||
|
||||
const FILES = import.meta.glob<CfLogic>("./_sample/*.json", { eager: true, import: "default" });
|
||||
|
||||
/** 견본마다 한 줄 설명 — [한, 영] */
|
||||
const NOTE: Record<string, readonly [string, string]> = {
|
||||
masonry: [
|
||||
"변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증 → 품 × 노임 → 노무비",
|
||||
"Variables, tables and labor → lookup, height surcharge → crew × wage → labor cost",
|
||||
],
|
||||
rate: ["노무비 × 공구손료율 → 재료비", "Labor cost × small-tools rate → material cost"],
|
||||
value: [
|
||||
"q · f · E · Cm → 60 × q × f × E ÷ Cm → 값(시간당 작업량)",
|
||||
"q, f, E, Cm → 60 × q × f × E ÷ Cm → value (output per hour)",
|
||||
],
|
||||
};
|
||||
|
||||
/** 보여 줄 차례 */
|
||||
const ORDER = ["masonry", "rate", "value"];
|
||||
|
||||
export interface CfSampleItem {
|
||||
/** 파일 이름(확장자 뺌) */
|
||||
id: string;
|
||||
name: string;
|
||||
resultUnit: string;
|
||||
/** 쉬운 말 한 줄 */
|
||||
note: string;
|
||||
}
|
||||
|
||||
const idOf = (path: string): string => path.replace(/^.*\//, "").replace(/\.json$/, "");
|
||||
const byId = (): Map<string, CfLogic> =>
|
||||
new Map(Object.entries(FILES).map(([path, lg]) => [idOf(path), lg]));
|
||||
|
||||
/** 견본 목록 — 정한 차례 먼저 · 나머지는 이름 차례 */
|
||||
export function sampleList(): CfSampleItem[] {
|
||||
const all = byId();
|
||||
const rank = (id: string): number => (ORDER.includes(id) ? ORDER.indexOf(id) : ORDER.length);
|
||||
return [...all.entries()]
|
||||
.sort(([a, x], [b, y]) => rank(a) - rank(b) || x.name.localeCompare(y.name))
|
||||
.map(([id, lg]) => ({
|
||||
id,
|
||||
name: lg.name,
|
||||
resultUnit: lg.resultUnit,
|
||||
note: NOTE[id]?.[currentLanguageIndex === 1 ? 1 : 0] ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
/** 견본 하나 「복사해서 열기」 — 깊은 사본 · key 없음(저장하면 서버가 새 번호) · 없으면 null */
|
||||
export function sampleLoad(id: string): CfLogic | null {
|
||||
const lg = byId().get(id);
|
||||
return lg ? { ...(JSON.parse(JSON.stringify(lg)) as CfLogic), key: null } : null;
|
||||
}
|
||||
@@ -0,0 +1,785 @@
|
||||
{
|
||||
"key": "SAMPLE-masonry",
|
||||
"name": "견본 · 돌쌓기 메쌓기(인력)",
|
||||
"resultUnit": "원/㎡",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "쌓기 조건",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 40,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "뒷길이",
|
||||
"type": "choice",
|
||||
"unit": "㎝",
|
||||
"example": 30,
|
||||
"choices": [25, 30, 35, 45, 55, 60, 75]
|
||||
},
|
||||
{
|
||||
"name": "돌",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "깬돌",
|
||||
"choices": ["깬잡석", "견치돌", "깬돌", "호박돌 및 야면석"]
|
||||
},
|
||||
{
|
||||
"name": "쌓기",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "골쌓기",
|
||||
"choices": ["골쌓기", "켜쌓기"]
|
||||
},
|
||||
{
|
||||
"name": "높이",
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"example": 4.5
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-poom",
|
||||
"kind": "input",
|
||||
"name": "메쌓기 품표",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "소요량",
|
||||
"elements": [],
|
||||
"table": {
|
||||
"key": "QF000421"
|
||||
},
|
||||
"variables": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-rate",
|
||||
"kind": "input",
|
||||
"name": "높이 증가율표",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 460,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "계수",
|
||||
"elements": [],
|
||||
"table": {
|
||||
"key": "QF000422"
|
||||
},
|
||||
"variables": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-labor",
|
||||
"kind": "input",
|
||||
"name": "인력",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 620,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "인력",
|
||||
"variables": [],
|
||||
"elements": [
|
||||
{
|
||||
"ref": "LB000033",
|
||||
"name": "석공",
|
||||
"spec": "",
|
||||
"unit": "원/일"
|
||||
},
|
||||
{
|
||||
"ref": "LB000002",
|
||||
"name": "보통인부",
|
||||
"spec": "",
|
||||
"unit": "원/일"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-rate",
|
||||
"kind": "function",
|
||||
"name": "증가율 찾기",
|
||||
"color": "#f59e0b",
|
||||
"unit": "%",
|
||||
"x": 340,
|
||||
"y": 420,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-rate",
|
||||
"이름": "증가율 찾기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "높이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · m"
|
||||
},
|
||||
"A3": {
|
||||
"값": "표에서 찾은 증가율"
|
||||
},
|
||||
"B3": {
|
||||
"식": "SUMIFS($G$2:$G$100,$E$2:$E$100,\"<\"&B2,$F$2:$F$100,\">=\"&B2)"
|
||||
},
|
||||
"C3": {
|
||||
"값": "높이가 「아래 < 높이 ≤ 위」 인 줄"
|
||||
},
|
||||
"A4": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"B4": {
|
||||
"식": "IF(B2<=3,0,B3)"
|
||||
},
|
||||
"C4": {
|
||||
"값": "3m 까지는 할증 없음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "높이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "증가율표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "증가율",
|
||||
"unit": "%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-poom0",
|
||||
"kind": "function",
|
||||
"name": "석공 품",
|
||||
"color": "#f59e0b",
|
||||
"unit": "인/㎡",
|
||||
"x": 640,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-poom0",
|
||||
"이름": "석공 품",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "뒷길이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · ㎝"
|
||||
},
|
||||
"A3": {
|
||||
"값": "돌"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "쌓기"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A5": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"C5": {
|
||||
"값": "받는 값 · %"
|
||||
},
|
||||
"A6": {
|
||||
"값": "표에서 찾은 품"
|
||||
},
|
||||
"B6": {
|
||||
"식": "SUMIFS($H$2:$H$200,$E$2:$E$200,B2,$F$2:$F$200,B3,$G$2:$G$200,B4)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "세 조건이 맞는 줄의 품"
|
||||
},
|
||||
"A7": {
|
||||
"값": "석공 품"
|
||||
},
|
||||
"B7": {
|
||||
"식": "B6*(1+B5/100)"
|
||||
},
|
||||
"C7": {
|
||||
"값": "높이 할증을 얹음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "뒷길이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "돌",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "쌓기",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "증가율",
|
||||
"cell": "B5"
|
||||
},
|
||||
{
|
||||
"port": "품표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B7",
|
||||
"name": "석공 품",
|
||||
"unit": "인/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-poom1",
|
||||
"kind": "function",
|
||||
"name": "보통인부 품",
|
||||
"color": "#f59e0b",
|
||||
"unit": "인/㎡",
|
||||
"x": 640,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-poom1",
|
||||
"이름": "보통인부 품",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "뒷길이"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · ㎝"
|
||||
},
|
||||
"A3": {
|
||||
"값": "돌"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "쌓기"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A5": {
|
||||
"값": "증가율"
|
||||
},
|
||||
"C5": {
|
||||
"값": "받는 값 · %"
|
||||
},
|
||||
"A6": {
|
||||
"값": "표에서 찾은 품"
|
||||
},
|
||||
"B6": {
|
||||
"식": "SUMIFS($I$2:$I$200,$E$2:$E$200,B2,$F$2:$F$200,B3,$G$2:$G$200,B4)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "세 조건이 맞는 줄의 품"
|
||||
},
|
||||
"A7": {
|
||||
"값": "보통인부 품"
|
||||
},
|
||||
"B7": {
|
||||
"식": "B6*(1+B5/100)"
|
||||
},
|
||||
"C7": {
|
||||
"값": "높이 할증을 얹음"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "뒷길이",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "돌",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "쌓기",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "증가율",
|
||||
"cell": "B5"
|
||||
},
|
||||
{
|
||||
"port": "품표",
|
||||
"cell": "E1"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B7",
|
||||
"name": "보통인부 품",
|
||||
"unit": "인/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pay0",
|
||||
"kind": "function",
|
||||
"name": "석공 노무비",
|
||||
"color": "#f59e0b",
|
||||
"unit": "원/㎡",
|
||||
"x": 940,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-pay0",
|
||||
"이름": "석공 노무비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "석공 품"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 인/㎡"
|
||||
},
|
||||
"A3": {
|
||||
"값": "석공 노임"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/인"
|
||||
},
|
||||
"A4": {
|
||||
"값": "석공 노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2*B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "품 × 노임"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "석공 품",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "석공 노임",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "석공 노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pay1",
|
||||
"kind": "function",
|
||||
"name": "보통인부 노무비",
|
||||
"color": "#f59e0b",
|
||||
"unit": "원/㎡",
|
||||
"x": 940,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-pay1",
|
||||
"이름": "보통인부 노무비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "보통인부 품"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 인/㎡"
|
||||
},
|
||||
"A3": {
|
||||
"값": "보통인부 노임"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/인"
|
||||
},
|
||||
"A4": {
|
||||
"값": "보통인부 노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2*B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "품 × 노임"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "보통인부 품",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "보통인부 노임",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "보통인부 노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-sum",
|
||||
"kind": "function",
|
||||
"name": "노무비 합",
|
||||
"color": "#f59e0b",
|
||||
"unit": "원/㎡",
|
||||
"x": 1240,
|
||||
"y": 190,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-sum",
|
||||
"이름": "노무비 합",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "석공 노무비"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A3": {
|
||||
"값": "보통인부 노무비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값"
|
||||
},
|
||||
"A4": {
|
||||
"값": "노무비"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2+B3"
|
||||
},
|
||||
"C4": {
|
||||
"값": "둘을 더함"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "석공 노무비",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "보통인부 노무비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-pay",
|
||||
"kind": "output",
|
||||
"name": "노무비",
|
||||
"color": "#10b981",
|
||||
"unit": "원/㎡",
|
||||
"x": 1520,
|
||||
"y": 190,
|
||||
"output": {
|
||||
"cost": "노무비",
|
||||
"unit": "원/㎡"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "높이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "높이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-rate",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "뒷길이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "뒷길이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "돌"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "돌"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "쌓기"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "쌓기"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "증가율"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l7",
|
||||
"from": {
|
||||
"box": "in-poom",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom0",
|
||||
"name": "품표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l8",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "뒷길이"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "뒷길이"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l9",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "돌"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "돌"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l10",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "쌓기"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "쌓기"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l11",
|
||||
"from": {
|
||||
"box": "fn-rate",
|
||||
"name": "증가율"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "증가율"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l12",
|
||||
"from": {
|
||||
"box": "in-poom",
|
||||
"name": "표"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-poom1",
|
||||
"name": "품표"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l13",
|
||||
"from": {
|
||||
"box": "fn-poom0",
|
||||
"name": "석공 품"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 품"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l14",
|
||||
"from": {
|
||||
"box": "in-labor",
|
||||
"name": "석공"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 노임"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l15",
|
||||
"from": {
|
||||
"box": "fn-poom1",
|
||||
"name": "보통인부 품"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 품"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l16",
|
||||
"from": {
|
||||
"box": "in-labor",
|
||||
"name": "보통인부"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 노임"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l17",
|
||||
"from": {
|
||||
"box": "fn-pay0",
|
||||
"name": "석공 노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "석공 노무비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l18",
|
||||
"from": {
|
||||
"box": "fn-pay1",
|
||||
"name": "보통인부 노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "보통인부 노무비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l19",
|
||||
"from": {
|
||||
"box": "fn-sum",
|
||||
"name": "노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-pay",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
{
|
||||
"key": "SAMPLE-rate",
|
||||
"name": "견본 · 공구손료(비율)",
|
||||
"resultUnit": "원",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "바탕 값",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 80,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "노무비",
|
||||
"type": "number",
|
||||
"unit": "원",
|
||||
"example": 100000
|
||||
},
|
||||
{
|
||||
"name": "공구손료율",
|
||||
"type": "number",
|
||||
"unit": "%",
|
||||
"example": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-rate",
|
||||
"kind": "function",
|
||||
"name": "공구손료",
|
||||
"color": "#f59e0b",
|
||||
"unit": "원",
|
||||
"x": 340,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-tool",
|
||||
"이름": "공구손료",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "노무비"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 원"
|
||||
},
|
||||
"A3": {
|
||||
"값": "공구손료율"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · %"
|
||||
},
|
||||
"A4": {
|
||||
"값": "공구손료"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B2*B3/100"
|
||||
},
|
||||
"C4": {
|
||||
"값": "노무비 × 율 ÷ 100"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "노무비",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "공구손료율",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "공구손료",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-mat",
|
||||
"kind": "output",
|
||||
"name": "재료비",
|
||||
"color": "#10b981",
|
||||
"unit": "원",
|
||||
"x": 640,
|
||||
"y": 80,
|
||||
"output": {
|
||||
"cost": "재료비",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "노무비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "노무비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "공구손료율"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-rate",
|
||||
"name": "공구손료율"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "fn-rate",
|
||||
"name": "공구손료"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-mat",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"key": "SAMPLE-value",
|
||||
"name": "견본 · 덤프트럭 시간당 작업량(값)",
|
||||
"resultUnit": "㎥/시간",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "덤프트럭 조건",
|
||||
"color": "#3b82f6",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 80,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "q",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 6
|
||||
},
|
||||
{
|
||||
"name": "f",
|
||||
"type": "number",
|
||||
"unit": "-",
|
||||
"example": 1.2
|
||||
},
|
||||
{
|
||||
"name": "E",
|
||||
"type": "number",
|
||||
"unit": "-",
|
||||
"example": 0.9
|
||||
},
|
||||
{
|
||||
"name": "Cm",
|
||||
"type": "number",
|
||||
"unit": "분",
|
||||
"example": 25
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-q",
|
||||
"kind": "function",
|
||||
"name": "시간당 작업량",
|
||||
"color": "#f59e0b",
|
||||
"unit": "㎥/시간",
|
||||
"x": 340,
|
||||
"y": 80,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "cf-q",
|
||||
"이름": "시간당 작업량",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "q"
|
||||
},
|
||||
"C2": {
|
||||
"값": "적재량 · ㎥"
|
||||
},
|
||||
"A3": {
|
||||
"값": "f"
|
||||
},
|
||||
"C3": {
|
||||
"값": "토량환산계수"
|
||||
},
|
||||
"A4": {
|
||||
"값": "E"
|
||||
},
|
||||
"C4": {
|
||||
"값": "작업효율"
|
||||
},
|
||||
"A5": {
|
||||
"값": "Cm"
|
||||
},
|
||||
"C5": {
|
||||
"값": "1회 사이클 시간 · 분"
|
||||
},
|
||||
"A6": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B6": {
|
||||
"식": "ROUND(60*B2*B3*B4/B5,3)"
|
||||
},
|
||||
"C6": {
|
||||
"값": "60 × q × f × E ÷ Cm · 소수 셋째 자리"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "q",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "f",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "E",
|
||||
"cell": "B4"
|
||||
},
|
||||
{
|
||||
"port": "Cm",
|
||||
"cell": "B5"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "시간당 작업량",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-q",
|
||||
"kind": "output",
|
||||
"name": "시간당 작업량",
|
||||
"color": "#10b981",
|
||||
"unit": "㎥/시간",
|
||||
"x": 640,
|
||||
"y": 80,
|
||||
"output": {
|
||||
"cost": "값",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "q"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "q"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "f"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "E"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "E"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "Cm"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-q",
|
||||
"name": "Cm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "fn-q",
|
||||
"name": "시간당 작업량"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-q",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
"""M02 단가산출 다이어그램 견본(35-7) 시험 — `M02_MasterTemplete/CostFlow/_sample/*.json` · `M02_CF_Sample.ts`.
|
||||
|
||||
· 견본 셋 모양 — 상자(입력 · 함수 · 출력) · 알약 이름 규칙 · 선(받는 자리마다 하나 · 출력에서 안 나감 · 순환 없음)
|
||||
· 견본 ① 원가 = 지금 `/calc`(GF000219 · 같은 예시값 · 끝수 원 미만) — 함수 상자 시트를 우리 스프레드시트 엔진
|
||||
(Node 번들)으로 차례대로 셈 · 입력 값(노임 · 품셈 표)은 마스터에서 읽기만
|
||||
· ③ 값 = `/calc`(GF000133) · ② = 노무비 × 율 / 100
|
||||
· `sampleList` · `sampleLoad`(사본 · key 없음) — Node 로 바로 돌림 · 파일 안 씀
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FOLDER = ROOT / "M02_MasterTemplete" / "CostFlow"
|
||||
SAMPLE_TS = FOLDER / "M02_CF_Sample.ts"
|
||||
SAMPLES = {p.stem: json.loads(p.read_text(encoding="utf-8")) for p in sorted((FOLDER / "_sample").glob("*.json"))} # fmt: skip
|
||||
|
||||
HOOKS = r"""
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { stripTypeScriptTypes } from "node:module";
|
||||
export async function load(url, ctx, next) {
|
||||
if (!url.endsWith(".ts")) return next(url, ctx);
|
||||
let src = readFileSync(fileURLToPath(url), "utf8");
|
||||
src = src.replace(/import\.meta\.glob(?:<[\s\S]*?>)?\([^)]*\)/g, "globalThis.__samples");
|
||||
src = src.replace(/import\s*\{([^}]*)\}\s*from\s*"([^"]+)";/g, (m, names) => {
|
||||
const list = names.split(",").map((x) => x.trim()).filter((x) => x && !x.startsWith("type "));
|
||||
return list.length ? `const { ${list.join(", ")} } = globalThis.__stub;` : "";
|
||||
});
|
||||
return { format: "module", source: stripTypeScriptTypes(src, { mode: "transform" }), shortCircuit: true };
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def _node(body: str) -> object:
|
||||
files = {f"./_sample/{k}.json": v for k, v in SAMPLES.items()}
|
||||
script = (
|
||||
"import { register } from 'node:module';\n"
|
||||
f"register('data:text/javascript,' + encodeURIComponent({json.dumps(HOOKS)}));\n"
|
||||
"globalThis.__stub = { currentLanguageIndex: 0 };\n"
|
||||
f"globalThis.__samples = {json.dumps(files, ensure_ascii=False)};\n"
|
||||
f"const S = await import({json.dumps(SAMPLE_TS.as_uri())});\n"
|
||||
"const out = (x) => process.stdout.write(JSON.stringify(x));\n" + body
|
||||
)
|
||||
result = subprocess.run(
|
||||
["node", "--no-warnings", "--input-type=module", "-e", script],
|
||||
capture_output=True, text=True, encoding="utf-8", timeout=120, cwd=ROOT,
|
||||
) # fmt: skip
|
||||
assert result.returncode == 0, result.stderr[-2000:]
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
# ── 모양 ─────────────────────────────────────────────────────────────────
|
||||
def _ports_out(box: dict) -> list[str]:
|
||||
if box["kind"] == "function":
|
||||
return [box["func"]["output"]["name"]]
|
||||
if box["kind"] == "output":
|
||||
return []
|
||||
inp = box["input"]
|
||||
return (
|
||||
[e["name"] for e in inp.get("elements", [])]
|
||||
+ (["표"] if inp.get("table") else [])
|
||||
+ [v["name"] for v in inp.get("variables", [])]
|
||||
)
|
||||
|
||||
|
||||
def _ports_in(box: dict) -> list[str]:
|
||||
if box["kind"] == "function":
|
||||
return [i["port"] for i in box["func"]["inputs"]]
|
||||
return ["값"] if box["kind"] == "output" else []
|
||||
|
||||
|
||||
def _order(lg: dict) -> list[dict]:
|
||||
"""함수 상자 차례(앞 값을 받는 것이 뒤) — 순환이면 실패"""
|
||||
boxes = {b["id"]: b for b in lg["boxes"]}
|
||||
need = {b["id"]: {link["from"]["box"] for link in lg["links"] if link["to"]["box"] == b["id"]} for b in lg["boxes"]} # fmt: skip
|
||||
done: list[str] = []
|
||||
while len(done) < len(boxes):
|
||||
ready = [i for i in boxes if i not in done and need[i] <= set(done)]
|
||||
assert ready, "선이 돌고 돎"
|
||||
done += ready
|
||||
return [boxes[i] for i in done]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["masonry", "rate", "value"])
|
||||
def test_견본_모양(name: str) -> None:
|
||||
lg = SAMPLES[name]
|
||||
assert lg["key"] and lg["name"] and lg["resultUnit"] and lg["userElements"] == []
|
||||
boxes = {b["id"]: b for b in lg["boxes"]}
|
||||
assert len(boxes) == len(lg["boxes"])
|
||||
sheets = [b["func"]["sheet"]["id"] for b in lg["boxes"] if b["kind"] == "function"]
|
||||
assert len(set(sheets)) == len(sheets)
|
||||
for b in lg["boxes"]:
|
||||
assert b["kind"] in ("input", "function", "output")
|
||||
assert (
|
||||
b["color"].startswith("#")
|
||||
and b["unit"]
|
||||
and isinstance(b["x"], int)
|
||||
and isinstance(b["y"], int)
|
||||
)
|
||||
if b["kind"] == "function":
|
||||
fn = b["func"]
|
||||
cells = fn["sheet"]["칸"]
|
||||
assert "식" in cells[fn["output"]["cell"]] # 출력 하나 = 수식 칸
|
||||
assert len({i["cell"] for i in fn["inputs"]}) == len(fn["inputs"])
|
||||
if b["kind"] == "output":
|
||||
assert b["output"]["cost"] in ("노무비", "재료비", "경비", "값")
|
||||
for link in lg["links"]:
|
||||
a, z = boxes[link["from"]["box"]], boxes[link["to"]["box"]]
|
||||
assert link["from"]["name"] in _ports_out(a), link
|
||||
assert link["to"]["name"] in _ports_in(z), link
|
||||
# 받는 자리마다 선 하나 · 출력에서 나가는 선 없음 · 왼 → 오
|
||||
for b in lg["boxes"]:
|
||||
for port in _ports_in(b):
|
||||
got = [x for x in lg["links"] if x["to"]["box"] == b["id"] and x["to"]["name"] == port]
|
||||
assert len(got) == 1, (b["id"], port)
|
||||
for link in lg["links"]:
|
||||
assert boxes[link["from"]["box"]]["kind"] != "output"
|
||||
assert boxes[link["from"]["box"]]["x"] < boxes[link["to"]["box"]]["x"]
|
||||
_order(lg)
|
||||
|
||||
|
||||
def test_견본_목록_열기() -> None:
|
||||
got = _node(
|
||||
"out({ list: S.sampleList(), one: S.sampleLoad('masonry'), none: S.sampleLoad('없음') });"
|
||||
)
|
||||
assert [x["id"] for x in got["list"]] == ["masonry", "rate", "value"]
|
||||
assert all(x["note"] for x in got["list"])
|
||||
assert got["one"]["key"] is None and got["one"]["boxes"] == SAMPLES["masonry"]["boxes"]
|
||||
assert got["none"] is None
|
||||
|
||||
|
||||
# ── 값 ───────────────────────────────────────────────────────────────────
|
||||
@pytest.fixture(scope="module")
|
||||
def master():
|
||||
from M01_MasterData import M01_MasterData_Store as store
|
||||
|
||||
cm = store.cm
|
||||
files = cm.load(folder=store.FOLDER)
|
||||
return cm, files
|
||||
|
||||
|
||||
def _table_rows(table: dict) -> list[list]:
|
||||
"""품셈 표 → 블록(머리 줄 + 줄) — 서버 「표」 와 같은 규칙(범위 = 「이름·아래」 「이름·위」 · 숫자 하나면 둘 다)"""
|
||||
conds, lines = table.get("조건") or {}, table.get("줄") or []
|
||||
names = [*conds, *(table.get("값칸") or {})]
|
||||
split = {
|
||||
n for n in names if conds.get(n) == "범위" or any(isinstance(r.get(n), list) for r in lines)
|
||||
}
|
||||
head = [h for n in names for h in ((f"{n}·아래", f"{n}·위") if n in split else (n,))]
|
||||
rows = []
|
||||
for r in lines:
|
||||
line: list = []
|
||||
for n in names:
|
||||
v = r.get(n)
|
||||
if n in split:
|
||||
line += list(v) if isinstance(v, list) else [v, v]
|
||||
else:
|
||||
line.append(v)
|
||||
rows.append(line)
|
||||
return [head, *rows]
|
||||
|
||||
|
||||
def _plain(v):
|
||||
return float(v) if isinstance(v, Decimal) else v
|
||||
|
||||
|
||||
def _run(lg: dict, master) -> dict:
|
||||
"""상자 차례대로 — 입력 값(예시값 · 노임 · 표) → 함수 상자 시트에 넣고 엔진으로 셈 → 출력 상자 값"""
|
||||
from common_util.common_util_spreadsheet import recalc_workbooks
|
||||
|
||||
cm, files = master
|
||||
m = cm.mf.Master(files)
|
||||
tables = {str(t["키"]): t for d in files.values() if d.get("그룹") in cm.mf.TABLE_GROUPS for t in d.get("표") or []} # fmt: skip
|
||||
ports: dict[str, dict] = {}
|
||||
for b in _order(lg):
|
||||
into = {x["to"]["name"]: ports[x["from"]["box"]][x["from"]["name"]] for x in lg["links"] if x["to"]["box"] == b["id"]} # fmt: skip
|
||||
if b["kind"] == "input":
|
||||
inp = b["input"]
|
||||
got = {v["name"]: v["example"] for v in inp.get("variables", [])}
|
||||
got |= {
|
||||
e["name"]: _plain(cm.mf.element(m, e["ref"], {})[0])
|
||||
for e in inp.get("elements", [])
|
||||
}
|
||||
if inp.get("table"):
|
||||
got["표"] = _table_rows(tables[inp["table"]["key"]])
|
||||
ports[b["id"]] = got
|
||||
elif b["kind"] == "function":
|
||||
fn = b["func"]
|
||||
sheet = json.loads(json.dumps(fn["sheet"]))
|
||||
for i in fn["inputs"]:
|
||||
value = into[i["port"]]
|
||||
col, row = ord(i["cell"][0]) - 65, int(i["cell"][1:])
|
||||
block = value if isinstance(value, list) else [[value]]
|
||||
for r, line in enumerate(block):
|
||||
for c, x in enumerate(line):
|
||||
if x is not None:
|
||||
sheet["칸"][f"{chr(65 + col + c)}{row + r}"] = {"값": _plain(x)}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "cf", "서식": [{}], "시트": [sheet]}
|
||||
out = recalc_workbooks([book])[0][sheet["id"]][fn["output"]["cell"]]
|
||||
assert "수" in out, out
|
||||
ports[b["id"]] = {fn["output"]["name"]: float(out["수"])}
|
||||
else:
|
||||
ports[b["id"]] = {"값": into["값"]}
|
||||
return {b["id"]: ports[b["id"]]["값"] for b in lg["boxes"] if b["kind"] == "output"}
|
||||
|
||||
|
||||
def test_견본1_원가_calc_와_같음(master) -> None:
|
||||
from M01_MasterData.LogicBuilder import M01_LB_Calc as lb
|
||||
|
||||
cm, files = master
|
||||
row = next(
|
||||
r
|
||||
for d in files.values()
|
||||
if d.get("그룹") == "로직"
|
||||
for r in d["줄"]
|
||||
if r["키"] == "GF000219"
|
||||
)
|
||||
given = {v["name"]: v["example"] for v in SAMPLES["masonry"]["boxes"][0]["input"]["variables"]}
|
||||
want = lb.calc(json.loads(json.dumps(row, default=_plain)), {**given, "초과증가율": 80}, {})
|
||||
assert want["ok"], want
|
||||
got = _run(SAMPLES["masonry"], master)["out-pay"]
|
||||
print(f"\n견본 ① 노무비 {got} · /calc {want['sums']['계']}")
|
||||
assert abs(got - float(want["sums"]["계"])) < 1
|
||||
|
||||
|
||||
def test_견본3_값_calc_와_같음_견본2_비율(master) -> None:
|
||||
from M01_MasterData.LogicBuilder import M01_LB_Calc as lb
|
||||
|
||||
cm, files = master
|
||||
row = next(
|
||||
r
|
||||
for d in files.values()
|
||||
if d.get("그룹") == "로직"
|
||||
for r in d["줄"]
|
||||
if r["키"] == "GF000133"
|
||||
)
|
||||
given = {v["name"]: v["example"] for v in SAMPLES["value"]["boxes"][0]["input"]["variables"]}
|
||||
given["qt"] = given.pop("q") # 견본은 정함대로 「q」 · 기존 로직 입력 이름은 「qt」
|
||||
want = lb.calc(json.loads(json.dumps(row, default=_plain)), given, {})
|
||||
assert _run(SAMPLES["value"], master)["out-q"] == float(want["result"])
|
||||
assert _run(SAMPLES["rate"], master)["out-mat"] == 100000 * 3 / 100
|
||||
Reference in New Issue
Block a user