feat(m02-df): 흐름도 견본 넷 — 지반 가르기(조건 통과 선 둘) · 흄관 조건 찾기(재료 자리) · 다른 로직 잇기 · 메쌓기
- _sample/ground.json · hume.json · linked.json 새로 · rate.json 뺌(견본 넷에 없음) · value.json 은 linked 가 부르는 작업량 로직 - M02_DF_Sample.ts 목록 차례 · 쉬운 말 한 줄 - 시험 test_m02_df_sample.py — 모양 · 선 규칙 · 재료 방식 · 다른 로직 입력 · 출처 사슬 · 값(내부 확인) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
/* =============================================================================
|
||||
* M02_DF_Sample.ts
|
||||
* 단가산출 다이어그램 견본 — 처음 열었을 때 바로 보고 따라 만들 견본 셋(`_sample/*.json` · 손으로 짬)
|
||||
* ① 돌쌓기 메쌓기(인력) — 변수 · 품표 · 증가율표 · 인력 → 함수(표 찾기 · 높이 할증 · 직렬 · 병렬) → 출력 노무비
|
||||
* ② 공구손료(비율) — 노무비 × 율 → 출력 재료비
|
||||
* ③ 덤프트럭 시간당 작업량(값) — 변수 넷 → 함수 하나 → 출력 값
|
||||
* 단가산출 흐름도 견본 — 처음 열었을 때 바로 보고 따라 만들 견본(`_sample/*.json` · 손으로 짬)
|
||||
* ① 돌쌓기 메쌓기(인력) — 변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증(직렬) · 품 × 노임(병렬) → 노무비
|
||||
* ② 지반 가르기 — 같은 굴착량이 「조건 통과」 선 둘(지반 = 토사 / ≠ 토사)로 굴삭기 · 브레이커에 감 → 경비
|
||||
* ③ 흄관 관경 조건 찾기 — 재료 자리(조건 찾기 · 규격에 관경 포함) → 본수 × 단가 → 재료비
|
||||
* ④ 다른 로직 잇기 — 작업량 로직(⑤) 출력 → 장비 시간 → 경비
|
||||
* ⑤ 덤프트럭 시간당 작업량(값) — ④ 가 부르는 다른 로직
|
||||
* 견본은 읽기만 — `sampleLoad` 는 「복사해서 열기」 사본(key 없음 · 저장하면 `_data` 에 새 번호).
|
||||
* 입력 값(노임 · 표)은 견본에 안 들고 열 때 · 저장 때 마스터에서 받음(정함 「계산」).
|
||||
* 모양 = `M02_DF_Model` CfLogic · 알약 이름 규칙(입력 = 요소 이름 · 「표」 · 변수 이름 / 함수 = inputs[].port ·
|
||||
* output.name / 출력 = 「값」) — 정함 `docs/raw/M01_쉬운단가/원가다이어그램_정함.md` 11. 이 폴더째 지워도 됨.
|
||||
* 입력 값(노임 · 표 · 재료)은 견본에 안 들고 열 때 · 저장 때 마스터에서 받음.
|
||||
* 모양 = `M02_DF_Model` CfLogic(선 규칙 · 재료 방식 · 다른 로직 입력 포함) · 알약 이름 규칙 — 정함
|
||||
* `docs/raw/M01_쉬운단가/흐름도_정함.md` 1 · 4~7 · 12. 이 폴더째 지워도 됨.
|
||||
* ========================================================================== */
|
||||
|
||||
import { currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
@@ -21,7 +23,18 @@ const NOTE: Record<string, readonly [string, string]> = {
|
||||
"변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증 → 품 × 노임 → 노무비",
|
||||
"Variables, tables and labor → lookup, height surcharge → crew × wage → labor cost",
|
||||
],
|
||||
rate: ["노무비 × 공구손료율 → 재료비", "Labor cost × small-tools rate → material cost"],
|
||||
ground: [
|
||||
"지반이 토사면 굴삭기로 · 아니면 브레이커로 — 조건 통과 선 둘 → 경비",
|
||||
"Soil goes to the excavator, rock to the breaker — two gate lines → expense",
|
||||
],
|
||||
hume: [
|
||||
"관경으로 흄관 품목을 찾음(재료 자리 · 규격에 관경 포함) → 본수 × 단가 → 재료비",
|
||||
"Pipe size picks the hume pipe item (spec contains size) → count × price → material cost",
|
||||
],
|
||||
linked: [
|
||||
"작업량 로직의 출력을 받아 장비 시간 → 경비",
|
||||
"Takes the output of a productivity logic → machine hours → expense",
|
||||
],
|
||||
value: [
|
||||
"q · f · E · Cm → 60 × q × f × E ÷ Cm → 값(시간당 작업량)",
|
||||
"q, f, E, Cm → 60 × q × f × E ÷ Cm → value (output per hour)",
|
||||
@@ -29,7 +42,7 @@ const NOTE: Record<string, readonly [string, string]> = {
|
||||
};
|
||||
|
||||
/** 보여 줄 차례 */
|
||||
const ORDER = ["masonry", "rate", "value"];
|
||||
const ORDER = ["masonry", "ground", "hume", "linked", "value"];
|
||||
|
||||
export interface CfSampleItem {
|
||||
/** 파일 이름(확장자 뺌) */
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
{
|
||||
"key": "SAMPLE-ground",
|
||||
"name": "견본 · 지반에 따라 굴삭기 / 브레이커",
|
||||
"resultUnit": "원/㎥",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-ground",
|
||||
"kind": "input",
|
||||
"name": "지반 · 굴착량",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "지반",
|
||||
"type": "choice",
|
||||
"unit": "-",
|
||||
"example": "토사",
|
||||
"choices": ["토사", "암"]
|
||||
},
|
||||
{
|
||||
"name": "굴착량",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 100
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-cost",
|
||||
"kind": "input",
|
||||
"name": "장비 시간당 경비",
|
||||
"color": "#2f80ed",
|
||||
"unit": "원/시간",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "굴삭기 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 60000
|
||||
},
|
||||
{
|
||||
"name": "브레이커 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 80000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-exc",
|
||||
"kind": "function",
|
||||
"name": "굴삭기로 파기",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원",
|
||||
"x": 380,
|
||||
"y": 40,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-exc",
|
||||
"이름": "굴삭기로 파기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 지반이 토사일 때만 옴"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B4": {
|
||||
"값": 30
|
||||
},
|
||||
"C4": {
|
||||
"값": "㎥/시간 · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "작업 시간"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간"
|
||||
},
|
||||
"A6": {
|
||||
"값": "굴삭기 경비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3"
|
||||
},
|
||||
"C6": {
|
||||
"값": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "굴삭기 경비",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-brk",
|
||||
"kind": "function",
|
||||
"name": "브레이커로 깨기",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원",
|
||||
"x": 380,
|
||||
"y": 300,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-brk",
|
||||
"이름": "브레이커로 깨기",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 지반이 토사가 아닐 때만 옴"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"B4": {
|
||||
"값": 8
|
||||
},
|
||||
"C4": {
|
||||
"값": "㎥/시간 · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "작업 시간"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간"
|
||||
},
|
||||
"A6": {
|
||||
"값": "브레이커 경비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3"
|
||||
},
|
||||
"C6": {
|
||||
"값": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "브레이커 경비",
|
||||
"unit": "원"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-sum",
|
||||
"kind": "function",
|
||||
"name": "1㎥ 경비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎥",
|
||||
"x": 720,
|
||||
"y": 170,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-sum",
|
||||
"이름": "1㎥ 경비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "굴삭기 경비"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 안 오면 빈 칸 = 0"
|
||||
},
|
||||
"A3": {
|
||||
"값": "브레이커 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 안 오면 빈 칸 = 0"
|
||||
},
|
||||
"A4": {
|
||||
"값": "굴착량"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A5": {
|
||||
"값": "1㎥ 경비"
|
||||
},
|
||||
"B5": {
|
||||
"식": "(B2+B3)/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "둘 중 온 것 ÷ 굴착량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "굴삭기 경비",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "브레이커 경비",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "굴착량",
|
||||
"cell": "B4"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B5",
|
||||
"name": "1㎥ 경비",
|
||||
"unit": "원/㎥"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-exp",
|
||||
"kind": "output",
|
||||
"name": "경비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/㎥",
|
||||
"x": 1040,
|
||||
"y": 170,
|
||||
"output": {
|
||||
"cost": "경비",
|
||||
"unit": "원/㎥",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-exc",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"rule": {
|
||||
"kind": "gate",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "지반"
|
||||
},
|
||||
"op": "=",
|
||||
"value": "토사"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-brk",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"rule": {
|
||||
"kind": "gate",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "지반"
|
||||
},
|
||||
"op": "≠",
|
||||
"value": "토사"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "in-cost",
|
||||
"name": "굴삭기 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-exc",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-cost",
|
||||
"name": "브레이커 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-brk",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "fn-exc",
|
||||
"name": "굴삭기 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "굴삭기 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-brk",
|
||||
"name": "브레이커 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "브레이커 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l7",
|
||||
"from": {
|
||||
"box": "in-ground",
|
||||
"name": "굴착량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-sum",
|
||||
"name": "굴착량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l8",
|
||||
"from": {
|
||||
"box": "fn-sum",
|
||||
"name": "1㎥ 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-exp",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"key": "SAMPLE-hume",
|
||||
"name": "견본 · 흄관 관경 조건 찾기",
|
||||
"resultUnit": "원/m",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-size",
|
||||
"kind": "input",
|
||||
"name": "관경 · 연장",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "관경",
|
||||
"type": "choice",
|
||||
"unit": "mm",
|
||||
"example": 600,
|
||||
"choices": [400, 450, 500, 600, 700, 800, 900, 1000, 1100, 1200]
|
||||
},
|
||||
{
|
||||
"name": "연장",
|
||||
"type": "number",
|
||||
"unit": "m",
|
||||
"example": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-pipe",
|
||||
"kind": "input",
|
||||
"name": "흄관",
|
||||
"color": "#2f80ed",
|
||||
"unit": "본",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "group",
|
||||
"group": "재료",
|
||||
"variables": [],
|
||||
"elements": [
|
||||
{
|
||||
"ref": "MT014991",
|
||||
"name": "흄관",
|
||||
"spec": "Φ600mm",
|
||||
"unit": "본",
|
||||
"mode": "rule",
|
||||
"scope": {
|
||||
"구분": "관·이음관",
|
||||
"상세구분": "흄관",
|
||||
"찾는말": "접속흄관"
|
||||
},
|
||||
"rule": {
|
||||
"field": "규격",
|
||||
"op": "포함",
|
||||
"from": {
|
||||
"box": "in-size",
|
||||
"name": "관경"
|
||||
}
|
||||
},
|
||||
"default": "MT014991",
|
||||
"slot": "흄관"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-pipe",
|
||||
"kind": "function",
|
||||
"name": "흄관 재료비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/m",
|
||||
"x": 380,
|
||||
"y": 160,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-pipe",
|
||||
"이름": "흄관 재료비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "연장"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · m"
|
||||
},
|
||||
"A3": {
|
||||
"값": "흄관 단가"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/본(관경으로 찾은 품목)"
|
||||
},
|
||||
"A4": {
|
||||
"값": "1본 길이"
|
||||
},
|
||||
"B4": {
|
||||
"값": 2.5
|
||||
},
|
||||
"C4": {
|
||||
"값": "m · 고침"
|
||||
},
|
||||
"A5": {
|
||||
"값": "본수"
|
||||
},
|
||||
"B5": {
|
||||
"식": "ROUNDUP(B2/B4,0)"
|
||||
},
|
||||
"C5": {
|
||||
"값": "올림"
|
||||
},
|
||||
"A6": {
|
||||
"값": "1m 재료비"
|
||||
},
|
||||
"B6": {
|
||||
"식": "B5*B3/B2"
|
||||
},
|
||||
"C6": {
|
||||
"값": "본수 × 단가 ÷ 연장"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "연장",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "흄관 단가",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B6",
|
||||
"name": "1m 재료비",
|
||||
"unit": "원/m"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-mat",
|
||||
"kind": "output",
|
||||
"name": "재료비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/m",
|
||||
"x": 720,
|
||||
"y": 160,
|
||||
"output": {
|
||||
"cost": "재료비",
|
||||
"unit": "원/m",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-size",
|
||||
"name": "연장"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pipe",
|
||||
"name": "연장"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-pipe",
|
||||
"name": "흄관"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-pipe",
|
||||
"name": "흄관 단가"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "fn-pipe",
|
||||
"name": "1m 재료비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-mat",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"key": "SAMPLE-linked",
|
||||
"name": "견본 · 다른 로직 잇기(작업량 → 장비 시간)",
|
||||
"resultUnit": "원/㎥",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-logic",
|
||||
"kind": "input",
|
||||
"name": "덤프트럭 시간당 작업량",
|
||||
"color": "#2f80ed",
|
||||
"unit": "㎥/시간",
|
||||
"x": 40,
|
||||
"y": 60,
|
||||
"input": {
|
||||
"mode": "logic",
|
||||
"elements": [],
|
||||
"variables": [],
|
||||
"logic": {
|
||||
"key": "value",
|
||||
"name": "견본 · 덤프트럭 시간당 작업량(값)",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "시간당 작업량",
|
||||
"unit": "㎥/시간"
|
||||
}
|
||||
],
|
||||
"designs": [
|
||||
{
|
||||
"name": "q",
|
||||
"unit": "㎥"
|
||||
},
|
||||
{
|
||||
"name": "f",
|
||||
"unit": "-"
|
||||
},
|
||||
{
|
||||
"name": "E",
|
||||
"unit": "-"
|
||||
},
|
||||
{
|
||||
"name": "Cm",
|
||||
"unit": "분"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "운반량 · 경비",
|
||||
"color": "#2f80ed",
|
||||
"unit": "-",
|
||||
"x": 40,
|
||||
"y": 300,
|
||||
"input": {
|
||||
"mode": "variable",
|
||||
"elements": [],
|
||||
"variables": [
|
||||
{
|
||||
"name": "운반량",
|
||||
"type": "number",
|
||||
"unit": "㎥",
|
||||
"example": 120
|
||||
},
|
||||
{
|
||||
"name": "덤프트럭 시간당 경비",
|
||||
"type": "number",
|
||||
"unit": "원/시간",
|
||||
"example": 50000
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-hours",
|
||||
"kind": "function",
|
||||
"name": "운반 시간",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "시간",
|
||||
"x": 380,
|
||||
"y": 60,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-hours",
|
||||
"이름": "운반 시간",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "시간당 작업량"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 다른 로직 출력"
|
||||
},
|
||||
"A3": {
|
||||
"값": "운반량"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A4": {
|
||||
"값": "운반 시간"
|
||||
},
|
||||
"B4": {
|
||||
"식": "B3/B2"
|
||||
},
|
||||
"C4": {
|
||||
"값": "운반량 ÷ 시간당 작업량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "시간당 작업량",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "운반량",
|
||||
"cell": "B3"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B4",
|
||||
"name": "운반 시간",
|
||||
"unit": "시간",
|
||||
"round": {
|
||||
"places": 2,
|
||||
"method": "반올림"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fn-cost",
|
||||
"kind": "function",
|
||||
"name": "1㎥ 운반 경비",
|
||||
"color": "#7b4fd6",
|
||||
"unit": "원/㎥",
|
||||
"x": 700,
|
||||
"y": 180,
|
||||
"func": {
|
||||
"sheet": {
|
||||
"id": "s-fn-cost",
|
||||
"이름": "1㎥ 운반 경비",
|
||||
"칸": {
|
||||
"A1": {
|
||||
"값": "이름"
|
||||
},
|
||||
"B1": {
|
||||
"값": "값"
|
||||
},
|
||||
"C1": {
|
||||
"값": "설명"
|
||||
},
|
||||
"A2": {
|
||||
"값": "운반 시간"
|
||||
},
|
||||
"C2": {
|
||||
"값": "받는 값 · 시간"
|
||||
},
|
||||
"A3": {
|
||||
"값": "시간당 경비"
|
||||
},
|
||||
"C3": {
|
||||
"값": "받는 값 · 원/시간"
|
||||
},
|
||||
"A4": {
|
||||
"값": "운반량"
|
||||
},
|
||||
"C4": {
|
||||
"값": "받는 값 · ㎥"
|
||||
},
|
||||
"A5": {
|
||||
"값": "1㎥ 운반 경비"
|
||||
},
|
||||
"B5": {
|
||||
"식": "B2*B3/B4"
|
||||
},
|
||||
"C5": {
|
||||
"값": "시간 × 경비 ÷ 운반량"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"port": "운반 시간",
|
||||
"cell": "B2"
|
||||
},
|
||||
{
|
||||
"port": "시간당 경비",
|
||||
"cell": "B3"
|
||||
},
|
||||
{
|
||||
"port": "운반량",
|
||||
"cell": "B4"
|
||||
}
|
||||
],
|
||||
"variables": [],
|
||||
"output": {
|
||||
"cell": "B5",
|
||||
"name": "1㎥ 운반 경비",
|
||||
"unit": "원/㎥"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "out-exp",
|
||||
"kind": "output",
|
||||
"name": "경비",
|
||||
"color": "#27ae60",
|
||||
"unit": "원/㎥",
|
||||
"x": 1020,
|
||||
"y": 180,
|
||||
"output": {
|
||||
"cost": "경비",
|
||||
"unit": "원/㎥",
|
||||
"round": {
|
||||
"places": 1,
|
||||
"method": "버림"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
{
|
||||
"id": "l1",
|
||||
"from": {
|
||||
"box": "in-logic",
|
||||
"name": "시간당 작업량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-hours",
|
||||
"name": "시간당 작업량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l2",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "운반량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-hours",
|
||||
"name": "운반량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l3",
|
||||
"from": {
|
||||
"box": "fn-hours",
|
||||
"name": "운반 시간"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "운반 시간"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l4",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "덤프트럭 시간당 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "시간당 경비"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l5",
|
||||
"from": {
|
||||
"box": "in-var",
|
||||
"name": "운반량"
|
||||
},
|
||||
"to": {
|
||||
"box": "fn-cost",
|
||||
"name": "운반량"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "l6",
|
||||
"from": {
|
||||
"box": "fn-cost",
|
||||
"name": "1㎥ 운반 경비"
|
||||
},
|
||||
"to": {
|
||||
"box": "out-exp",
|
||||
"name": "값"
|
||||
}
|
||||
}
|
||||
],
|
||||
"userElements": []
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
{
|
||||
"key": "SAMPLE-rate",
|
||||
"name": "견본 · 공구손료(비율)",
|
||||
"resultUnit": "원",
|
||||
"boxes": [
|
||||
{
|
||||
"id": "in-var",
|
||||
"kind": "input",
|
||||
"name": "바탕 값",
|
||||
"color": "#2f80ed",
|
||||
"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": "#7b4fd6",
|
||||
"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": "#27ae60",
|
||||
"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": []
|
||||
}
|
||||
@@ -1,17 +1,19 @@
|
||||
"""M02 단가산출 다이어그램 견본(35-7) 시험 — `M02_MasterTemplete/DataFlow/_sample/*.json` · `M02_DF_Sample.ts`.
|
||||
"""M02 단가산출 흐름도 견본(35-9) 시험 — `M02_MasterTemplete/DataFlow/_sample/*.json` · `M02_DF_Sample.ts`.
|
||||
|
||||
· 견본 셋 모양 — 상자(입력 · 함수 · 출력) · 알약 이름 규칙 · 선(받는 자리마다 하나 · 출력에서 안 나감 · 순환 없음)
|
||||
· 견본 ① 원가 = 지금 `/calc`(GF000219 · 같은 예시값 · 끝수 원 미만) — 함수 상자 시트를 우리 스프레드시트 엔진
|
||||
(Node 번들)으로 차례대로 셈 · 입력 값(노임 · 품셈 표)은 마스터에서 읽기만
|
||||
· ③ 값 = `/calc`(GF000133) · ② = 노무비 × 율 / 100
|
||||
견본 넷(메쌓기 · 지반 가르기 · 흄관 조건 찾기 · 다른 로직 잇기) + 넷째가 부르는 작업량 로직(value)
|
||||
· 모양 — 상자(입력 · 함수 · 출력) · 알약 이름 규칙 · 기본 색 · 선(받는 자리마다 하나 · 출력에서 안 나감 · 순환 없음)
|
||||
· 선 규칙(조건 통과 · 끝수) · 재료 방식(조건 찾기) · 다른 로직 입력의 알약이 있는 자리를 가리킴
|
||||
· 흐름 — 출력에서 거꾸로 따라가면 닿는 출처(선 · 선 규칙의 조건 알약 · 재료 조건 알약 포함)가 견본 뜻과 같음
|
||||
· 값(내부 확인) — 함수 상자 시트를 우리 스프레드시트 엔진(Node 번들)으로 차례대로 셈 · 선 규칙 · 끝수를 지남 ·
|
||||
입력 값(노임 · 품셈 표 · 재료 단가)은 마스터에서 읽기만 · 메쌓기 = /calc GF000219 · value = /calc GF000133
|
||||
· `sampleList` · `sampleLoad`(사본 · key 없음) — Node 로 바로 돌림 · 파일 안 씀
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
@@ -22,6 +24,15 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
FOLDER = ROOT / "M02_MasterTemplete" / "DataFlow"
|
||||
SAMPLE_TS = FOLDER / "M02_DF_Sample.ts"
|
||||
SAMPLES = {p.stem: json.loads(p.read_text(encoding="utf-8")) for p in sorted((FOLDER / "_sample").glob("*.json"))} # fmt: skip
|
||||
NAMES = ["masonry", "ground", "hume", "linked", "value"]
|
||||
BOX_COLOR = dict(
|
||||
re.findall(
|
||||
r'(\w+): "(#[0-9a-fA-F]{6})"',
|
||||
re.search(
|
||||
r"BOX_COLOR[^{]*\{([^}]*)\}", (FOLDER / "M02_DF_Model.ts").read_text(encoding="utf-8")
|
||||
).group(1),
|
||||
)
|
||||
)
|
||||
|
||||
HOOKS = r"""
|
||||
import { readFileSync } from "node:fs";
|
||||
@@ -58,13 +69,15 @@ def _node(body: str) -> object:
|
||||
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"]
|
||||
if inp["mode"] == "logic":
|
||||
return [p["name"] for p in inp["logic"]["outputs"]]
|
||||
return (
|
||||
[e["name"] for e in inp.get("elements", [])]
|
||||
+ (["표"] if inp.get("table") else [])
|
||||
@@ -78,10 +91,28 @@ def _ports_in(box: dict) -> list[str]:
|
||||
return ["값"] if box["kind"] == "output" else []
|
||||
|
||||
|
||||
def _deps(lg: dict, box_id: str) -> list[dict]:
|
||||
"""그 상자가 기대는 알약 — 들어온 선 · 그 선 규칙의 조건 알약 · 재료 조건 알약"""
|
||||
out = []
|
||||
for link in lg["links"]:
|
||||
if link["to"]["box"] != box_id:
|
||||
continue
|
||||
out.append(link["from"])
|
||||
rule = link.get("rule") or {}
|
||||
if rule.get("kind") == "gate":
|
||||
out.append(rule["from"])
|
||||
out += [c["from"] for c in rule.get("conds", [])]
|
||||
box = next(b for b in lg["boxes"] if b["id"] == box_id)
|
||||
for e in (box.get("input") or {}).get("elements", []):
|
||||
if e.get("rule"):
|
||||
out.append(e["rule"]["from"])
|
||||
return out
|
||||
|
||||
|
||||
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
|
||||
need = {i: {p["box"] for p in _deps(lg, i)} for i in boxes}
|
||||
done: list[str] = []
|
||||
while len(done) < len(boxes):
|
||||
ready = [i for i in boxes if i not in done and need[i] <= set(done)]
|
||||
@@ -90,18 +121,24 @@ def _order(lg: dict) -> list[dict]:
|
||||
return [boxes[i] for i in done]
|
||||
|
||||
|
||||
# Model 기본 색(`M02_DF_Model.ts` BOX_COLOR) — 견본 색은 이것과 같아야 함
|
||||
BOX_COLOR = dict(
|
||||
re.findall(
|
||||
r'(\w+): "(#[0-9a-fA-F]{6})"',
|
||||
re.search(
|
||||
r"BOX_COLOR[^{]*\{([^}]*)\}", (FOLDER / "M02_DF_Model.ts").read_text(encoding="utf-8")
|
||||
).group(1),
|
||||
)
|
||||
)
|
||||
def _trace_back(lg: dict, box_id: str) -> set[tuple[str, str]]:
|
||||
"""그 상자 값이 온 길의 출처 알약(입력 상자) — 선 · 규칙 조건 · 재료 조건을 거꾸로 끝까지"""
|
||||
boxes = {b["id"]: b for b in lg["boxes"]}
|
||||
seen: set[tuple[str, str]] = set()
|
||||
todo = [box_id]
|
||||
while todo:
|
||||
cur = todo.pop()
|
||||
for p in _deps(lg, cur):
|
||||
if boxes[p["box"]]["kind"] == "input":
|
||||
seen.add((p["box"], p["name"]))
|
||||
todo.append(p["box"]) # 재료 조건처럼 입력 상자도 다른 알약에 기댐
|
||||
else:
|
||||
todo.append(p["box"])
|
||||
return seen
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["masonry", "rate", "value"])
|
||||
# ── 모양 ─────────────────────────────────────────────────────────────────
|
||||
@pytest.mark.parametrize("name", NAMES)
|
||||
def test_견본_모양(name: str) -> None:
|
||||
lg = SAMPLES[name]
|
||||
assert lg["key"] and lg["name"] and lg["resultUnit"] and lg["userElements"] == []
|
||||
@@ -111,59 +148,98 @@ def test_견본_모양(name: str) -> None:
|
||||
assert len(set(sheets)) == len(sheets)
|
||||
for b in lg["boxes"]:
|
||||
assert b["kind"] in ("input", "function", "output")
|
||||
assert (
|
||||
b["color"] == BOX_COLOR[b["kind"]]
|
||||
and b["unit"]
|
||||
and isinstance(b["x"], int)
|
||||
and isinstance(b["y"], int)
|
||||
)
|
||||
assert b["color"] == BOX_COLOR[b["kind"]] and b["unit"]
|
||||
assert 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 "식" in fn["sheet"]["칸"][fn["output"]["cell"]] # 출력 하나 = 수식 칸
|
||||
assert len({i["cell"] for i in fn["inputs"]}) == len(fn["inputs"])
|
||||
if b["kind"] == "output":
|
||||
assert b["output"]["cost"] in ("노무비", "재료비", "경비", "값")
|
||||
units = {e["unit"] for e in (b.get("input") or {}).get("elements") or []}
|
||||
if len(units) == 1: # 요소 단위가 모두 같으면 상자 단위도 그것(공통 단위 규칙)
|
||||
if len(units) == 1:
|
||||
assert b["unit"] == next(iter(units)), b["id"]
|
||||
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
|
||||
# 받는 자리마다 선 하나 · 출력에서 나가는 선 없음 · 왼 → 오
|
||||
assert a["kind"] != "output" and a["x"] < z["x"]
|
||||
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"]
|
||||
# 규칙 · 재료 조건이 가리키는 알약은 있는 자리
|
||||
for b in lg["boxes"]:
|
||||
for p in _deps(lg, b["id"]):
|
||||
assert p["name"] in _ports_out(boxes[p["box"]]), p
|
||||
_order(lg)
|
||||
|
||||
|
||||
def test_견본_흐름_규칙_방식() -> None:
|
||||
"""견본마다 보여 주려는 흐름 — 조건 통과 선 둘 · 재료 조건 찾기 · 다른 로직 입력 · 출처 사슬"""
|
||||
g = SAMPLES["ground"]
|
||||
gates = [x for x in g["links"] if (x.get("rule") or {}).get("kind") == "gate"]
|
||||
assert len(gates) == 2 and {x["from"]["name"] for x in gates} == {"굴착량"}
|
||||
assert {x["rule"]["op"] for x in gates} == {"=", "≠"} and len(
|
||||
{x["rule"]["value"] for x in gates}
|
||||
) == 1
|
||||
assert ("in-ground", "지반") in _trace_back(g, "out-exp") # 조건 알약도 출처 사슬에
|
||||
|
||||
h = SAMPLES["hume"]
|
||||
pipe = next(b for b in h["boxes"] if b["id"] == "in-pipe")["input"]["elements"][0]
|
||||
assert pipe["mode"] == "rule" and pipe["slot"] == pipe["name"] and pipe["rule"]["op"] == "포함"
|
||||
assert pipe["scope"]["상세구분"] == "흄관" and pipe["default"] == pipe["ref"]
|
||||
assert _trace_back(h, "out-mat") == {
|
||||
("in-size", "연장"),
|
||||
("in-pipe", "흄관"),
|
||||
("in-size", "관경"),
|
||||
}
|
||||
|
||||
lk = SAMPLES["linked"]
|
||||
logic = next(b for b in lk["boxes"] if b["id"] == "in-logic")["input"]["logic"]
|
||||
called = SAMPLES[logic["key"]]
|
||||
assert [p["name"] for p in logic["outputs"]] == [
|
||||
b["name"] for b in called["boxes"] if b["kind"] == "output"
|
||||
]
|
||||
assert [p["name"] for p in logic["designs"]] == [
|
||||
v["name"] for b in called["boxes"] if b["kind"] == "input" for v in b["input"]["variables"]
|
||||
]
|
||||
assert ("in-logic", "시간당 작업량") in _trace_back(lk, "out-exp")
|
||||
|
||||
m = SAMPLES["masonry"]
|
||||
assert _trace_back(m, "out-pay") >= {
|
||||
("in-var", "높이"),
|
||||
("in-poom", "표"),
|
||||
("in-rate", "표"),
|
||||
("in-labor", "석공"),
|
||||
}
|
||||
|
||||
|
||||
def test_견본_목록_열기() -> None:
|
||||
got = _node(
|
||||
"out({ list: S.sampleList(), one: S.sampleLoad('masonry'), none: S.sampleLoad('없음') });"
|
||||
"out({ list: S.sampleList(), one: S.sampleLoad('ground'), none: S.sampleLoad('없음') });"
|
||||
)
|
||||
assert [x["id"] for x in got["list"]] == ["masonry", "rate", "value"]
|
||||
assert [x["id"] for x in got["list"]] == NAMES
|
||||
assert all(x["note"] for x in got["list"])
|
||||
assert got["one"]["key"] is None and got["one"]["boxes"] == SAMPLES["masonry"]["boxes"]
|
||||
assert got["one"]["key"] is None and got["one"]["links"] == SAMPLES["ground"]["links"]
|
||||
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
|
||||
return cm, cm.load(folder=store.FOLDER)
|
||||
|
||||
|
||||
def _plain(v):
|
||||
return float(v) if isinstance(v, Decimal) else v
|
||||
|
||||
|
||||
def _table_rows(table: dict) -> list[list]:
|
||||
"""품셈 표 → 블록(머리 줄 + 줄) — 서버 「표」 와 같은 규칙(범위 = 「이름·아래」 「이름·위」 · 숫자 하나면 둘 다)"""
|
||||
"""품셈 표 → 블록(머리 줄 + 줄) — 서버 「표」 와 같은 규칙"""
|
||||
conds, lines = table.get("조건") or {}, table.get("줄") or []
|
||||
names = [*conds, *(table.get("값칸") or {})]
|
||||
split = {
|
||||
@@ -175,37 +251,69 @@ def _table_rows(table: dict) -> list[list]:
|
||||
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)
|
||||
line += (list(v) if isinstance(v, list) else [v, v]) if n in split else [v]
|
||||
rows.append(line)
|
||||
return [head, *rows]
|
||||
|
||||
|
||||
def _plain(v):
|
||||
return float(v) if isinstance(v, Decimal) else v
|
||||
def _cut(value, rnd):
|
||||
if value is None or not rnd:
|
||||
return value
|
||||
k = 10 ** rnd["places"]
|
||||
way = {"반올림": lambda x: math.floor(x + 0.5), "올림": math.ceil, "버림": math.floor}[
|
||||
rnd["method"]
|
||||
]
|
||||
return way(round(value * k, 9)) / k
|
||||
|
||||
|
||||
def _run(lg: dict, master) -> dict:
|
||||
"""상자 차례대로 — 입력 값(예시값 · 노임 · 표) → 함수 상자 시트에 넣고 엔진으로 셈 → 출력 상자 값"""
|
||||
def _gate(rule, value, ports):
|
||||
have = ports[rule["from"]["box"]][rule["from"]["name"]]
|
||||
want = rule["value"]
|
||||
ok = {"=": have == want, "≠": have != want, "≥": have >= want, "≤": have <= want, ">": have > want, "<": have < want} # fmt: skip
|
||||
return value if ok[rule["op"]] else None
|
||||
|
||||
|
||||
def _run(lg: dict, master, examples: dict | None = None) -> 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
|
||||
items = next(d for d in files.values() if d.get("그룹") == "재료" and any("흄관" in str(r.get("이름")) for r in d["줄"]))["줄"] # 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
|
||||
into = {}
|
||||
for x in lg["links"]:
|
||||
if x["to"]["box"] == b["id"]:
|
||||
value, rule = ports[x["from"]["box"]][x["from"]["name"]], x.get("rule") or {}
|
||||
if rule.get("kind") == "gate":
|
||||
value = _gate(rule, value, ports)
|
||||
elif rule.get("kind") == "round":
|
||||
value = _cut(value, rule)
|
||||
into[x["to"]["name"]] = value
|
||||
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", [])
|
||||
got = {
|
||||
v["name"]: (examples or {}).get(v["name"], v["example"])
|
||||
for v in inp.get("variables", [])
|
||||
}
|
||||
for e in inp.get("elements", []):
|
||||
key = e["ref"]
|
||||
if (
|
||||
e.get("mode") == "rule"
|
||||
): # 범위 안 품목 중 규격 / 이름에 조건 알약 값이 든 것 하나
|
||||
want = str(ports[e["rule"]["from"]["box"]][e["rule"]["from"]["name"]])
|
||||
sc = e["scope"]
|
||||
hits = [r for r in items if r.get("구분") == sc["구분"] and r.get("상세구분") == sc["상세구분"]
|
||||
and sc["찾는말"] in r.get("이름", "") and want in str(r.get(e["rule"]["field"], ""))] # fmt: skip
|
||||
assert len(hits) == 1, (want, [h["키"] for h in hits])
|
||||
key = hits[0]["키"]
|
||||
got[e["name"]] = _plain(cm.mf.element(m, key, {})[0])
|
||||
if inp.get("table"):
|
||||
got["표"] = _table_rows(tables[inp["table"]["key"]])
|
||||
if inp["mode"] == "logic":
|
||||
got = _run(SAMPLES[inp["logic"]["key"]], master) # 설계 값은 그 로직의 예시값
|
||||
ports[b["id"]] = got
|
||||
elif b["kind"] == "function":
|
||||
fn = b["func"]
|
||||
@@ -213,63 +321,48 @@ def _run(lg: dict, master) -> dict:
|
||||
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 r, line in enumerate(value if isinstance(value, list) else [[value]]):
|
||||
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["수"])}
|
||||
book = {"종류": "통합문서", "판": 1, "열": "df", "서식": [{}], "시트": [sheet]}
|
||||
got = recalc_workbooks([book])[0][sheet["id"]][fn["output"]["cell"]]
|
||||
assert "수" in got, (b["id"], got)
|
||||
ports[b["id"]] = {
|
||||
fn["output"]["name"]: _cut(float(got["수"]), fn["output"].get("round"))
|
||||
}
|
||||
else:
|
||||
ports[b["id"]] = {"값": into["값"]}
|
||||
return {b["id"]: ports[b["id"]]["값"] for b in lg["boxes"] if b["kind"] == "output"}
|
||||
ports[b["id"]] = {"값": _cut(into["값"], b["output"].get("round"))}
|
||||
return {b["name"]: ports[b["id"]]["값"] for b in lg["boxes"] if b["kind"] == "output"}
|
||||
|
||||
|
||||
def test_견본_표_그룹이_마스터와_같음(master) -> None:
|
||||
"""표 입력 상자의 group = 그 표가 든 마스터 파일 그룹(소요량 · 계수)"""
|
||||
cm, files = master
|
||||
group = {str(t["키"]): d["그룹"] for d in files.values() if d.get("그룹") in cm.mf.TABLE_GROUPS for t in d.get("표") or []} # fmt: skip
|
||||
for lg in SAMPLES.values():
|
||||
for b in lg["boxes"]:
|
||||
table = b.get("input", {}).get("table")
|
||||
if table:
|
||||
assert b["input"]["group"] == group[table["key"]], (b["id"], table["key"])
|
||||
|
||||
|
||||
def test_견본1_원가_calc_와_같음(master) -> None:
|
||||
def test_값_메쌓기_value_는_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"
|
||||
)
|
||||
_, files = master
|
||||
rows = {r["키"]: r for d in files.values() if d.get("그룹") == "로직" for r in d["줄"]}
|
||||
raw = lambda k: json.loads(json.dumps(rows[k], default=_plain)) # noqa: E731
|
||||
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"
|
||||
)
|
||||
want = lb.calc(raw("GF000219"), {**given, "초과증가율": 80}, {})
|
||||
assert abs(_run(SAMPLES["masonry"], master)["노무비"] - float(want["sums"]["계"])) < 1
|
||||
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
|
||||
given["qt"] = given.pop("q")
|
||||
assert _run(SAMPLES["value"], master)["시간당 작업량"] == float(
|
||||
lb.calc(raw("GF000133"), given, {})["result"]
|
||||
)
|
||||
|
||||
|
||||
def test_값_지반_흄관_다른로직(master) -> None:
|
||||
# 지반 = 토사 → 굴삭기 길만 · 암 → 브레이커 길만(조건 통과 안 된 선은 빈 칸 = 0)
|
||||
assert _run(SAMPLES["ground"], master)["경비"] == 100 / 30 * 60000 / 100
|
||||
assert _run(SAMPLES["ground"], master, {"지반": "암"})["경비"] == 100 / 8 * 80000 / 100
|
||||
# 관경 600 → Φ600mm 품목 · 4 본 × 단가 ÷ 10 m · 0.1 원 미만 버림
|
||||
cm, files = master
|
||||
price = float(cm.mf.element(cm.mf.Master(files), "MT014991", {})[0])
|
||||
assert _run(SAMPLES["hume"], master)["재료비"] == _cut(
|
||||
4 * price / 10, {"places": 1, "method": "버림"}
|
||||
)
|
||||
# 다른 로직(작업량 15.552) → 운반 시간 120 ÷ 15.552 = 7.72(소수 둘째 반올림) → 7.72 × 50,000 ÷ 120
|
||||
assert _run(SAMPLES["linked"], master)["경비"] == _cut(
|
||||
7.72 * 50000 / 120, {"places": 1, "method": "버림"}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user