diff --git a/M02_MasterTemplete/DataFlow/M02_DF_Sample.ts b/M02_MasterTemplete/DataFlow/M02_DF_Sample.ts index 73367bcaf..96bda504e 100644 --- a/M02_MasterTemplete/DataFlow/M02_DF_Sample.ts +++ b/M02_MasterTemplete/DataFlow/M02_DF_Sample.ts @@ -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 = { "변수 · 품표 · 증가율표 · 인력 → 표 찾기 · 높이 할증 → 품 × 노임 → 노무비", "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 = { }; /** 보여 줄 차례 */ -const ORDER = ["masonry", "rate", "value"]; +const ORDER = ["masonry", "ground", "hume", "linked", "value"]; export interface CfSampleItem { /** 파일 이름(확장자 뺌) */ diff --git a/M02_MasterTemplete/DataFlow/_sample/ground.json b/M02_MasterTemplete/DataFlow/_sample/ground.json new file mode 100644 index 000000000..8e1be6bfb --- /dev/null +++ b/M02_MasterTemplete/DataFlow/_sample/ground.json @@ -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": [] +} diff --git a/M02_MasterTemplete/DataFlow/_sample/hume.json b/M02_MasterTemplete/DataFlow/_sample/hume.json new file mode 100644 index 000000000..a9e9efadb --- /dev/null +++ b/M02_MasterTemplete/DataFlow/_sample/hume.json @@ -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": [] +} diff --git a/M02_MasterTemplete/DataFlow/_sample/linked.json b/M02_MasterTemplete/DataFlow/_sample/linked.json new file mode 100644 index 000000000..1538c4f5d --- /dev/null +++ b/M02_MasterTemplete/DataFlow/_sample/linked.json @@ -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": [] +} diff --git a/M02_MasterTemplete/DataFlow/_sample/rate.json b/M02_MasterTemplete/DataFlow/_sample/rate.json deleted file mode 100644 index cf1ff8428..000000000 --- a/M02_MasterTemplete/DataFlow/_sample/rate.json +++ /dev/null @@ -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": [] -} diff --git a/resources/tester/test_m02_df_sample.py b/resources/tester/test_m02_df_sample.py index a5c12d17b..e74d11578 100644 --- a/resources/tester/test_m02_df_sample.py +++ b/resources/tester/test_m02_df_sample.py @@ -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": "버림"} + )