Merge remote-tracking branch 'origin/dev' into main_desktop_1

# Conflicts:
#	M01_MasterData/M01_MasterData_UI_LogicLab.ts
#	M01_MasterData/M01_MasterData_UI_LogicLab_Style.css
#	M01_MasterData/M01_MasterData_UI_Logic_Api.ts
#	resources/master_data/ref/_검증_별칭_복사.md
This commit is contained in:
2026-09-21 21:37:11 +09:00
18 changed files with 1300 additions and 99 deletions
+6 -4
View File
@@ -142,16 +142,18 @@
`POST /logic/draft` 받음 `{text, decided, head?, save?, owner?}` — 줄로 적은 식을 받아 변수를 뽑고 검사한 뒤 로직 한 줄로 옮김(해석기 `scripts/master_draft.py`).
- `text` = 줄마다 「이름 = 식」 · 빈 줄과 `#` 는 건너뜀 · **마지막 줄 왼쪽이 결과 변수**. 식 글자는 `_틀.md` 8장 범위만.
- `text` = 줄마다 「이름 = 식」 **또는 「식 = 이름」**(`A * B = C`) · 한쪽이 값 이름 하나면 그쪽이 이름 · 양쪽이 다 식이면 그 줄의 등호 자리를 짚어 `못읽음`. 괄호 안 등호(`찾기(…, 뒷길이=높이)`)와 비교 기호(`==` `<=` `>=` `!=`)는 안 가름. 빈 줄과 `#` 는 건너뜀 · **마지막 줄의 값 이름이 결과 변수**. 식 글자는 `_틀.md` 8장 범위만.
- `decided` = 변수마다 정한 것 `{이름: {무엇, 단위, 비목, 요소 | 찾기 | 값, 설명 · 고르기 · 범위}}`.
- `무엇` — `마스터요소`(키 또는 재료 고르기 조건을 `요소` 에 · `비목` 도 정함) · `설계입력`(입력 칸이 됨) · `고정값`(`값` 을 중간으로) · `표찾기`(`찾기` = `{표, 값칸, 조건}` → `찾기(…).값칸` 을 중간으로).
- `단위` — 마스터요소는 그 줄의 단위(「인」 · 「포」)를 적음. 식에서는 단가라 `원/단위` 로 셈.
- **단위 없는 맨 수**(횟수 · 배율 · 비율)는 단위 칸에 `없음`(또는 `맨수` · `-` · `%` · 빈 글)을 골라 무차원으로 둠 — 「전용 4회」 를 「회」 로 두면 `원/회` 가 남아 덧셈 검사에 걸림. 칸 자체가 없으면 아직 안 정한 것(`정할것` 에 `단위`).
- 비목 합 이름(`노무비` · `재료비` · `경비` · `계`)은 정하지 않음 — 덧줄에서만 쓰는 이름.
- `head` = 로직 머리(`이름` · `결과단위` · `원문번호` · `출처` · `비고` · `끝수`). 없으면 검사만 함.
- 줌 `{줄, 변수, 결과, 문제, 다됨, logic}` — `logic` = 옮긴 로직 한 줄(`head` 가 있을 때만 · 저장 전).
- `변수` = `{이름, 처음, 갈래, 정의줄, 쓰임, 무엇, 단위, 비목, 정할것}` · `갈래` = `정의됨`(왼쪽에 나옴) · `미정`(쓰기만 함) · `비목합` · `정할것` = 아직 안 정한 칸 이름들.
- `문제` = `{줄, 자리, 갈래, 말}` · `자리` = 그 줄의 글자 자리(모르면 null) · `갈래` = `못읽음` · `이름` · `두번정의` · `돌고돎` · `안쓰임`(= 결과가 하나인지) · `단위` · `덜정함` · `항` · `비목합`.
- 단위 검사 — 곱셈은 단위를 곱하고 나눗셈은 나눔 · 덧셈·뺄셈과 `만약`·`최소`·`최대`·비교는 단위가 같아야 함. 「m \* m」 과 「㎡」 는 같게 봄. 모르는 단위는 안 따짐.
- `변수` = `{이름, 처음, 갈래, 정의줄, 쓰임, 무엇, 단위, 비목, 정할것}` · `갈래` = `정의됨`(식이 있음) · `미정`(쓰기만 함) · `비목합` · `정할것` = 아직 안 정한 칸 이름들. `단위` 는 그 값이 **식에서** 가지는 단위(무차원은 빈 글).
- `문제` = `{줄, 자리, 갈래, 말}` · `자리` = 그 줄의 글자 자리(모르면 null) · `갈래` = `못읽음` · `이름` · `두번정의` · `돌고돎` · `안쓰임`(= 결과가 하나인지) · `단위` · `덜정함` · `항` · `비목` · `비목합`.
- 단위 검사 — 곱셈은 단위를 곱하고 나눗셈은 나눔(같은 단위끼리 나누면 맨 수) · 덧셈·뺄셈과 `만약`·`최소`·`최대`는 단위가 같아야 함 · 비교는 한쪽이 맨 수면 안 따짐(`높이 <= 3`). 「m \* m」 과 「㎡」 는 같게 봄. 모르는 단위는 안 따짐.
- **덧줄이 될 값**(결과 식의 항 가운데 마스터요소가 없는 것)은 막지 않고 그 값의 `정할것` 에 `비목` 을 얹음 — 화면은 「비목을 골라 주세요」 로 보임.
- `save` = true 면 `POST /logic/new` 와 같은 자리에 저장(키 `GX` 는 서버 · 소유 `owner`) → 답에 `{file, version, key, logic}` 이 더 옴. 문제가 하나라도 있으면 422 `{detail: {errors, 문제}}` 로 아무것도 안 씀.
- 옮기는 법 — 돈 로직(`결과단위` 가 「원」 으로 시작)은 결과 식을 `+` 로 갈라 항마다: 마스터요소 변수 하나면 **호표 줄**(나머지 곱이 `수량`) · 비목만 정한 변수면 **덧줄** · 그 밖은 문제 `항`. 나머지 정의 줄은 `중간`(쓰는 차례대로). 돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
- 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 `무엇` 을 `표찾기` 로 정할 때 모달이 `찾기` 묶음을 채움.
+41 -39
View File
@@ -4,6 +4,8 @@
판정: 걷음 = 부르는 곳 없음 · 바꿈 = 부르는 곳 있음(새 마스터 읽기로 바꾼 뒤 걷음).
「새 마스터 후보」= 이름만 대응 확인 · 구조 비교 안 함 · 실제 코드는 안 고침.
## 걷음 (부르는 곳 없음)
| 파일 | 읽는 것 | 부르는 곳 | 판정 |
@@ -14,54 +16,54 @@
## 바꿈 — Z01
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 |
|---|---|---|
| Z01_MasterData/Z01_MasterData_Tables.py | old/ 화면 이름표 | Z01 BasePrices · BasePrices_Machine · Router · WorkItems · test_z01_master_data |
| Z01_MasterData/Z01_MasterData_BasePrices_Machine.py | old/ 기계 파일 | Z01 BasePrices |
| Z01_MasterData/Z01_MasterData_WorkItems.py | old/ 공종 마스터 · 연결 | Z01 BasePrices · Router · test_z01_work_items · test_z01_base_tables |
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 | 새 마스터 후보 |
|---|---|---|---|
| Z01_MasterData/Z01_MasterData_Tables.py | old/ 화면 이름표(4_화면_이름표) | Z01 BasePrices · BasePrices_Machine · Router · WorkItems · test_z01_master_data | 없음(공종 계열) |
| Z01_MasterData/Z01_MasterData_BasePrices_Machine.py | old/ 기계 파일(B09 MachineCost·MachineOperating 통해) | Z01 BasePrices | B09 MachineCost·MachineOperating 판단 따름 |
| Z01_MasterData/Z01_MasterData_WorkItems.py | old/ 공종 마스터 · 열쇠대장 · 연결 | Z01 BasePrices · Router · test_z01_work_items · test_z01_base_tables | 없음(공종 마스터 계열 자체가 새 마스터에 아직 없음) |
## 바꿈 — B08
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 |
|---|---|---|
| B08_Quantity_Build_WorkItemMaster.py | old/3_품셈_산림_표전체 (읽고 old/ 에 씀) | WorkItemMaster_Const · _Table · 시험 6 |
| B08_Quantity_Build_WorkItemMaster_Const.py | old/3_품셈_건설_표전체 · work_item_keys_const.json | WorkItemMaster · test_work_item_master_const |
| B08_Quantity_Build_WorkItemMaster_Keys.py | old/ 열쇠대장 | WorkItemMaster · _Const · Z01 WorkItems · 시험 3 |
| B08_Quantity_Engine_BasisUnit.py | old/ 기준 원단위 | Handoff · Handoff_Rows · test_b08_basis_unit |
| B08_Quantity_Engine_Formwork.py | old/ 거푸집 사용횟수 | Handoff_Mapping · UnitQuantity · 시험 3 |
| B08_Quantity_Engine_Handoff_Mapping.py | ref/ 수량 연결 · old/ 철근 · 돌쌓기 · 목재 갈래 | Handoff 계열 7 · RockSplit · Provenance |
| B08_Quantity_Engine_MaterialSummary.py | ref/ 재료 할증 | Handoff_Boundaries · Provenance · Router_Material · 시험 다수 |
| B08_Quantity_Engine_ObservedUnit.py | ref/ 구조물 원단위 | StructureFigure · UnitQuantity · UnitQuantity_Base · 시험 다수 |
| B08_Quantity_Engine_Pipe.py | ref/ 사방 기슭막이 치수 | StructureFigure · StructureSummary · Router_Material · B06_Section_Structure_Layouts.ts · 시험 다수 |
| B08_Quantity_Engine_UnitQuantity_StoneSpec.py | ref/ 돌 종류 · old/ 돌쌓기 경사 | ObservedUnit · StructureTemplate · UnitQuantity · _Masonry · common_util_masonry_slope.ts · 시험 3 |
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 | 새 마스터 후보 |
|---|---|---|---|
| B08_Quantity_Build_WorkItemMaster.py | old/3_품셈_산림_표전체 (읽고 old/ 에 씀) | WorkItemMaster_Const · _Table · 시험 6 | 로직_산림품셈_*·계수_산림품셈_*·소요량_산림품셈_* 전 장 — 짜맞추기 새로 필요, 단순 대응 아님 |
| B08_Quantity_Build_WorkItemMaster_Const.py | old/3_품셈_건설_표전체 · work_item_keys_const.json | WorkItemMaster · test_work_item_master_const | 로직_건설품셈_*·계수_건설품셈_*·소요량_건설품셈_* 전 장 — 짜맞추기 새로 필요, 단순 대응 아님 |
| B08_Quantity_Build_WorkItemMaster_Keys.py | old/ 열쇠대장 | WorkItemMaster · _Const · Z01 WorkItems · 시험 3 | 없음(공종 마스터 계열) |
| B08_Quantity_Engine_BasisUnit.py | old/ 기준 원단위 | Handoff · Handoff_Rows · test_b08_basis_unit | 후보 낱개 확인 필요(소요량_*.json 장 미확정) |
| B08_Quantity_Engine_Formwork.py | old/ 거푸집 사용횟수(1-7-1) | Handoff_Mapping · UnitQuantity · 시험 3 | 계수_산림품셈_01장_적용기준.json |
| B08_Quantity_Engine_Handoff_Mapping.py | ref/ 수량 연결 · old/ 철근 · 돌쌓기 · 목재 갈래 | Handoff 계열 7 · RockSplit · Provenance | old 부분: 계수_산림품셈_12·13장·로직_산림품셈_12·13장 / ref 수량연결은 공종_수량연결_실무 그대로 |
| B08_Quantity_Engine_MaterialSummary.py | ref/ 재료 할증 | Handoff_Boundaries · Provenance · Router_Material · 시험 다수 | 없음(재료_품셈재료.json 구조 확인 안 됨) |
| B08_Quantity_Engine_ObservedUnit.py | ref/ 구조물 원단위 | StructureFigure · UnitQuantity · UnitQuantity_Base · 시험 다수 | 없음(기술기준 원자료 — 별도 마스터 미확보) |
| B08_Quantity_Engine_Pipe.py | ref/ 사방 기슭막이 치수 | StructureFigure · StructureSummary · Router_Material · B06_Section_Structure_Layouts.ts · 시험 다수 | 없음(사방기술교본 원자료 — 별도 마스터 미확보) |
| B08_Quantity_Engine_UnitQuantity_StoneSpec.py | ref/ 돌 종류 · old/ 돌쌓기 경사 | ObservedUnit · StructureTemplate · UnitQuantity · _Masonry · common_util_masonry_slope.ts · 시험 3 | old 부분: 계수_산림품셈_13장_구조물.json / 돌 종류(ref)는 후보 없음 |
## 바꿈 — B09
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 |
|---|---|---|
| B09_Estimation_BasisSheet.py | old/_manifest.json · 마스터 | Provenance_Sources · Router · 시험 3 |
| B09_Estimation_LaborSurcharge.py | old/3_품셈_산림_계수 | BasisSheet · Router · Router_Factors · UnitPrice · 시험 2 |
| B09_Estimation_MachineCost.py | old/ 기계 카탈로그 | Lists · Lists_Sources · MachineExpenseSheet · MachineOperating · MachineProductivity 계열 |
| B09_Estimation_MachineExpenseSheet.py | old/ 기계 카탈로그 | Provenance · Router · 시험 4 |
| B09_Estimation_MachineOperating.py | old/ 기계 카탈로그 (읽고 old/ 에 씀) | Consumables · Lists_Sources · UnitPrice · UnitPrice_View · WoodChipping · 시험 다수 |
| B09_Estimation_MachineProductivity_Data.py | old/ 작업량 | MachineProductivity_Dozer · _Dump |
| B09_Estimation_MaterialCatalog.py | old/ 재료 카탈로그 | MaterialSheet · ResourceAxis_Sources · UnitPrice_View |
| B09_Estimation_MaterialPrices.py | old/재료_나라장터자재.json | BillOfQuantities 계열 · Router 계열 · UnitPrice · test_b09_material_prices |
| B09_Estimation_Rates.py | old/ 제비율 | CostSheet · Engine_Cost · Provenance · RateOverride · RateTable · Router 계열 |
| B09_Estimation_ResourceAxis.py | old/ 공종 마스터 · 카탈로그 | B08 Variants · BillOfQuantities · 공종 계산 모듈 다수 |
| B09_Estimation_ResourceAxis_Join.py | old/ 자원줄 | MaterialPrices · ResourceAxis · _ChooseOne · _JudgedTable · _Sources · 시험 2 |
| B09_Estimation_ResourceAxis_Sources.py | old/ 마스터 · 카탈로그 | B08 Variants · ResourceAxis · _Join · 시험 2 |
| B09_Estimation_UnitPrice.py | old/ 각종 (파일명 인자) | B08 Variants · StructurePriceLink · Router 계열 · BillOfQuantities 계열 |
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 | 새 마스터 후보 |
|---|---|---|---|
| B09_Estimation_BasisSheet.py | old/_manifest.json · 마스터 | Provenance_Sources · Router · 시험 3 | 후보 낱개 확인 필요(_원문목록.json·_키대장.json 구조 다름) |
| B09_Estimation_LaborSurcharge.py | old/3_품셈_산림_계수 | BasisSheet · Router · Router_Factors · UnitPrice · 시험 2 | 계수_산림품셈_*.json(장별) |
| B09_Estimation_MachineCost.py | old/ 기계경비기준(3_품셈_건설_기계경비기준_8장) | Lists · Lists_Sources · MachineExpenseSheet · MachineOperating · MachineProductivity 계열 | 계수_건설품셈_08장_건설기계.json |
| B09_Estimation_MachineExpenseSheet.py | old/ 기계 카탈로그(MachineCost 통해) | Provenance · Router · 시험 4 | MachineCost 판단 따름 |
| B09_Estimation_MachineOperating.py | old/ 기계운전경비(3_품셈_건설_기계운전경비_8-4, 읽고 old/ 에 씀) | Consumables · Lists_Sources · UnitPrice · UnitPrice_View · WoodChipping · 시험 다수 | 로직_건설품셈_08장_건설기계.json |
| B09_Estimation_MachineProductivity_Data.py | old/ 작업량(3_품셈_산림건설_기계작업량) | MachineProductivity_Dozer · _Dump | 후보 없음(장별 미확정) |
| B09_Estimation_MaterialCatalog.py | old/ 재료 카탈로그 | MaterialSheet · ResourceAxis_Sources · UnitPrice_View | 재료_자재품목.json·재료_품셈재료.json |
| B09_Estimation_MaterialPrices.py | old/재료_나라장터자재.json | BillOfQuantities 계열 · Router 계열 · UnitPrice · test_b09_material_prices | 재료_자재품목.json(이름 직접 대응) |
| B09_Estimation_Rates.py | old/ 제비율 | CostSheet · Engine_Cost · Provenance · RateOverride · RateTable · Router 계열 | 요율_조달청제비율.json(이름 직접 대응) |
| B09_Estimation_ResourceAxis.py | old/ 공종 마스터 · 카탈로그 | B08 Variants · BillOfQuantities · 공종 계산 모듈 다수 | 공종 부분 후보 없음 — 절반 블록 |
| B09_Estimation_ResourceAxis_Join.py | old/ 자원줄 | MaterialPrices · ResourceAxis · _ChooseOne · _JudgedTable · _Sources · 시험 2 | 없음(공종 마스터 계열) |
| B09_Estimation_ResourceAxis_Sources.py | old/ 마스터 · 카탈로그 | B08 Variants · ResourceAxis · _Join · 시험 2 | ResourceAxis.py 판단 따름 |
| B09_Estimation_UnitPrice.py | old/ 각종 (파일명 인자) | B08 Variants · StructurePriceLink · Router 계열 · BillOfQuantities 계열 | 공용 로더 — 낱개 판단 필요(특정 불가) |
## 바꿈 — common_util · 화면
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 |
|---|---|---|
| common_util/common_util_node_bundle.py | old/ 통째(SOURCE_DATA_DIRS) | B05 Corridor_Prebuild · B06 Router_HaulPlan · B06 Server_Calc_Prebuild · B08 Engine_Formula |
| common_util/common_util_work_item_key.py | old/ 열쇠대장 | B08 Handoff · B09 BillOfQuantities_Input · 시험 2 |
| common_util/common_util_work_item_link.py | old/ 연결 | B09 BillOfQuantities · _Input · test_work_item_link_choice |
| B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts | ref/수량_사방기슭막이치수 (import) | B05 화면 |
| B06_Section/B06_Section_UI_Cross_Lean.ts | old/3_품셈_산림_돌쌓기경사 (import) | B06 화면 |
| 파일 | old/ref 에서 읽는 것 | 부르는 곳 | 새 마스터 후보 |
|---|---|---|---|
| common_util/common_util_node_bundle.py | old/ 통째(SOURCE_DATA_DIRS) | B05 Corridor_Prebuild · B06 Router_HaulPlan · B06 Server_Calc_Prebuild · B08 Engine_Formula | 없음 — 전체 재설계 필요(개별 대응 불가) |
| common_util/common_util_work_item_key.py | old/ 열쇠대장 | B08 Handoff · B09 BillOfQuantities_Input · 시험 2 | 없음(공종 마스터 계열) |
| common_util/common_util_work_item_link.py | old/ 연결 | B09 BillOfQuantities · _Input · test_work_item_link_choice | 없음(공종 마스터 계열) |
| B05_Profile/B05_Profile_UI_Drainage_Facility_Fields.ts | ref/수량_사방기슭막이치수 (import) | B05 화면 | B08 Engine_Pipe.py 판단 따름(후보 없음) |
| B06_Section/B06_Section_UI_Cross_Lean.ts | old/3_품셈_산림_돌쌓기경사 (import) | B06 화면 | 계수_산림품셈_13장_구조물.json(StoneSpec.py 와 같음) |
## 바꿈 — 시험
@@ -0,0 +1,35 @@
# 로직 개선 시험 — 사용자 경로 검증 (PLAN 3 마감)
ORCA · 포트 8006/5179 · 넓은 화면 928px · 좁은 화면 390px. 고치지 않고 적기만 함.
## ① 정본 로직 다섯
컨테이너 넷(기본정보 · 호표 · 텍스트 수식 · 시험 계산) 모두 있음. 기본정보는 읽기 전용, 비고만 고쳐짐. 호표에 [줄 더하기] 없음.
| 로직 | 호표 소계 합 | 계 | 모달 |
|---|---|---|---|
| 13-4-1 GF000219 (뒷길이 45 · 호박돌 · 켜쌓기 · 높이 2) | 인력 100,672.2 | 100,672.2 | 표 2개 |
| 12-4 GF000160 | 인력 82,948.3992 + 자재 36,163.252 | 119,111.6512 | 표 + 노란 줄 |
| 12-17-1 GF000182 | 6,615.989 + 438.8951 + 6,400.8296 | 13,455.7136 (합 13,455.7137) | 표 |
| 9-16-1 GF000119 | 그 밖 4,522.9569 | 4,522.9569 | 「다른 로직을 부름」 글만 · 표 없음 |
| 10-4 GF000137 | 그 밖 85,970.6407 | 85,970.6407 | 「다른 로직을 부름」 글만 · 표 없음 |
## ② 만들기
- [식으로 새로 만들기] : 보기 글 → [분석] 8개 변수 → 보통인부 마스터 요소 · 길이·폭 설계 입력 · 품 고정값 · 잡재료비 덧줄 비목 → 이름·단위 → 시험 계산 길이 2 · 폭 3 → 계 162,604.26 → 저장 → 자체 로직 GX000001 상세가 열림(저장 뒤 약 10초). 지움까지 됨.
- 막힘 셋 걸림 : 「길이 + 넓이」 단위 · 물고 돎 · 「가」 두 번 정의. 자리 표시 · 저장 단추 막힘.
- [새로 만들기] · [본떠 만들기] : 「로직 개선 시험」 화면에 [식으로 새로 만들기]만 있고 [본떠 만들기] 단추가 없음(글 `Edit_Copy` 만 있고 단추 코드 없음) → 정본을 본떠 한 줄 고쳐 저장하는 길은 못 걸어 봄.
## ③ 막히거나 어색한 자리
1. 본떠 만들기 단추 없음(PLAN 3-4 미착수) — 정본에서 자체 로직으로 가는 길이 화면에 없음.
2. 13-4-1 시험 계산 기본 선택(25 · 견치돌 · 골쌓기)이 「맞는 줄 0개」로 멈춤. 비고에 「원문에 없는 조합」 안내가 있으나 계산 화면에서는 안 보임. 첫 번째 선택으로 바로 계산되는 조합을 기본으로 두거나 멈춘 글에 안내 필요.
3. 13-4-1 모달 : 시험 계산 뒤에도 「걸린 줄을 아직 못 맞힘」 안내가 뜨고 강조 줄이 0개(석공 줄 · 계산 후 눌렀을 때).
4. 호표 줄 「표 찾기 찾기(QF000421 …」 — 「찾기」가 겹쳐 보임.
5. 호표 묶음 소계와 텍스트 수식 소계가 다름 : 12-17-1 호표 인력 6,615.989 · 경비 438.8951 ↔ 텍스트 노무비 소계 8,777.9014 · 경비 3,098.2357(호표는 줄 종류별 · 텍스트는 비목별). 합(계)은 같음. 같은 「소계」 이름이라 헷갈림.
6. 호표 묶음이 인력·자재·경비에 「그 밖」이 더해져 네 묶음 : 12-17-1 · 9-16-1 · 10-4. 9-16-1 · 10-4 는 인력·자재·경비 세 묶음이 모두 「줄 없음」이고 기계 줄이 「그 밖」 한 줄.
7. 자체 로직(GX) 상세는 옛 편집 꼴(설계에서 받을 값 · 호표 · 중간 값 · 덧줄 · [줄 더하기])이고 네 컨테이너 꼴이 아님 — 정본과 화면이 다름.
8. 새로 만들기 보기 글은 변수 8개를 다 골라야 저장됨(첫 사용자에게 많음). 저장에 약 10초 걸림 · 불러오는 표시만 뜸.
9. 좁은 화면 390px : 상세 화면 폭 안(가로 스크롤 없음). 모달은 iframe 조작으로 열리지 않아 폭을 못 잼.
시험 흔적 : GX000001 지움 · 키 대장 되돌림 · 자체 로직 파일 지움.
+96 -29
View File
@@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
"""줄로 적은 식 → 변수 뽑기 · 검사 · 단위 맞춤 · 로직 틀로 옮기기 (`_화면_계약.md` 7장).
"""줄로 적은 식 → 변수 뽑기 · 검사 · 단위 맞춤 · 로직 틀로 옮기기 (`_화면_계약.md` 8장).
수량 = 길이 * 폭
자재비 = 수량 * 시멘트
· 줄마다 「이름 = 식」 · 빈 줄과 `#` 는 건너뜀 · 마지막 줄 왼쪽이 결과 변수
· 줄마다 「이름 = 식」 또는 「식 = 이름」 · 빈 줄과 `#` 는 건너뜀 · 마지막 줄의 값 이름이 결과 변수
· 식 글자는 `_틀.md` 8장 범위만 — 읽기는 `master_formula` 가 함
· 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 「무엇」 을 `표찾기` 로 정할 때 이음
· 값은 안 씀 — 검사만 하고 `옮기기` 가 로직 한 줄을 만듦(저장은 `Store_Make.logic_new`)
@@ -32,8 +32,9 @@ ALIAS = {
"M": ("m", 1),
}
SQUARED = {2: "㎡", 3: "㎥"}
DIMLESS = ("%", "할") # 비율은 단위가 없음 — 「1 + 증가율 / 100」 이 안 걸리게
_ASSIGN = re.compile(r"^\s*([\w가-힣]+)\s*=(?!=)\s*(.*)$")
# 단위가 없는 맨 수 — 횟수 · 배율 · 비율. 빈 글도 같음(「없음」 을 고른 것과 같게 봄)
DIMLESS = ("%", "할", "없음", "맨수", "맨 수", "-")
_NAME_ONLY = re.compile(r"^[\w가-힣]+$")
# ── 자리를 적어 두는 읽기 ──────────────────────────────────────────────
@@ -224,18 +225,51 @@ def find_text(pick: dict) -> str:
# ── 읽기 · 검사 ───────────────────────────────────────────────────────
def _is_name(text: str) -> bool:
"""값 이름 하나 — 맨 수는 이름이 아님."""
return bool(_NAME_ONLY.fullmatch(text)) and not mf._NUMBER.fullmatch(text)
def _split(raw: str) -> int:
"""줄을 가를 등호 자리 — 괄호 안과 비교 기호(`==` `<=` `>=` `!=`)는 건너뜀 · 없으면 -1."""
depth = 0
for i, ch in enumerate(raw):
if ch in "([{":
depth += 1
elif ch in ")]}":
depth -= 1
elif ch == "=" and depth == 0:
if raw[i - 1 : i] in ("<", ">", "!", "=") or raw[i + 1 : i + 2] == "=":
continue
return i
return -1
def _cut(text: str):
"""(줄 번호, 왼쪽 이름, 식, 식이 시작하는 자리) — 빈 줄과 `#` 는 건너뜀."""
"""(줄 번호, 이름, 식, 식이 시작하는 자리, 까닭) — 빈 줄과 `#` 는 건너뜀.
「이름 = 식」 과 「식 = 이름」 을 다 받음 — 한쪽이 값 이름 하나면 그쪽이 이름.
"""
for at, raw in enumerate(str(text).split("\n"), 1):
if not raw.strip() or raw.lstrip().startswith("#"):
continue
m = _ASSIGN.match(raw)
yield (
at,
(m.group(1) if m else ""),
(m.group(2).strip() if m else raw.strip()),
(m.start(2) if m else len(raw) - len(raw.lstrip())),
)
cut = _split(raw)
if cut < 0:
yield (
at,
"",
raw.strip(),
len(raw) - len(raw.lstrip()),
"「이름 = 식」 이 아님 — 등호가 없음",
)
continue
left, right = raw[:cut], raw[cut + 1 :]
if _is_name(left.strip()):
yield at, left.strip(), right.strip(), cut + 1 + len(right) - len(right.lstrip()), ""
elif _is_name(right.strip()): # 「A * B = C」 — 오른쪽이 이름
yield at, right.strip(), left.strip(), len(left) - len(left.lstrip()), ""
else:
yield at, "", raw.strip(), cut, "양쪽이 다 식 — 한쪽은 값 이름 하나여야 함"
def _order(defined: dict, deps: dict) -> list[str]:
@@ -255,14 +289,14 @@ def _order(defined: dict, deps: dict) -> list[str]:
return out
def _scan(text: str, 정함: dict | None = None) -> dict:
def _scan(text: str, 정함: dict | None = None, 머리: dict | None = None) -> dict:
"""줄 가르기 · 변수 · 검사 · 단위 — `read` 와 `옮기기` 가 같이 씀(나무를 들고 있음)."""
정함 = 정함 or {}
lines, 문제 = [], []
for at, name, body, head in _cut(text):
for at, name, body, head, why in _cut(text):
one = {"줄": at, "이름": name, "식": body, "앞": head}
if not name:
문제.append(_trouble(at, head, "못읽음", "「이름 = 식」 이 아님"))
if why:
문제.append(_trouble(at, head, "못읽음", why))
elif not body:
문제.append(_trouble(at, head, "못읽음", f"「{name}」 오른쪽이 빔"))
else:
@@ -343,6 +377,7 @@ def _scan(text: str, 정함: dict | None = None) -> dict:
got = _unit(one["나무"], units, one["줄"], one["앞"], 문제)
units[name] = pinned if pinned is not None else got
덧줄후보 = _extra_names(by, 결과, 정함, 머리) if 결과 in by and 결과 not in loops else set()
변수 = []
for name in sorted(
set(쓰임) | set(정의), key=lambda n: (min([쓰임.get(n, [10**9])[0], 정의.get(n, 10**9)]), n)
@@ -359,16 +394,18 @@ def _scan(text: str, 정함: dict | None = None) -> dict:
"무엇": spec.get("무엇") if 갈래 == "미정" else None,
"단위": unit_text(units.get(name)),
"비목": spec.get("비목"),
"정할것": _todo(갈래, spec, units.get(name)),
"정할것": _todo(갈래, spec, units.get(name), name in 덧줄후보),
}
)
return {"줄": lines, "변수": 변수, "결과": 결과, "문제": 문제, "차례": order, "정의": 정의}
def _todo(갈래: str, spec: dict, unit) -> list[str]:
def _todo(갈래: str, spec: dict, unit, 덧줄: bool = False) -> list[str]:
"""그 변수에 아직 안 정한 것 — 무엇 · 요소·찾기·값 · 단위 · 비목."""
if 갈래 != "미정":
if 갈래 == "비목합":
return []
if 갈래 == "정의됨": # 식이 있는 값 — 덧줄이 될 항이면 비목만 고르면 됨
return ["비목"] if 덧줄 and spec.get("비목") not in COST_ITEMS else []
what, need = spec.get("무엇"), []
if what not in WHAT:
need.append("무엇")
@@ -386,9 +423,9 @@ def _todo(갈래: str, spec: dict, unit) -> list[str]:
return need
def read(text: str, 정함: dict | None = None) -> dict:
def read(text: str, 정함: dict | None = None, 머리: dict | None = None) -> dict:
"""줄로 적은 식 → `{줄, 변수, 결과, 문제, 다됨}` — 화면이 그대로 받는 모양(나무는 뺌)."""
got = _scan(text, 정함)
got = _scan(text, 정함, 머리)
다됨 = not got["문제"] and not any(v["정할것"] for v in got["변수"])
return {
"줄": [{k: v for k, v in one.items() if k != "나무"} for one in got["줄"]],
@@ -408,22 +445,40 @@ def _factors(node) -> list:
return _factors(node[2]) + _factors(node[3]) if node[0] == "bin" and node[1] == "*" else [node]
def _spread(node, by: dict, 정함: dict, 편: set) -> list:
"""결과 항 — 항이 이름 하나이고 비목을 안 정했으면 그 식으로 펼침.
def _spread(node, by: dict, 정함: dict, 편: set, 뿌리: str = "") -> list:
"""결과 항 — (항, 펼치기 전 이름). 항이 이름 하나이고 비목을 안 정했으면 그 식으로 펼침.
호표 줄을 항 안에서 찾으려는 것.
호표 줄을 항 안에서 찾으려는 것 · 못 펼친 항의 `뿌리` 가 덧줄이 될 값 이름.
"""
out = []
for term in _terms(node):
name = term[1] if term[0] == "name" else ""
if name in by and name not in 편 and not (정함.get(name) or {}).get("비목"):
편.add(name)
out += _spread(by[name]["나무"], by, 정함, 편)
out += _spread(by[name]["나무"], by, 정함, 편, name)
else:
out.append(term)
out.append((term, 뿌리))
return out
def _has_element(term, 정함: dict) -> bool:
return any(
f[0] == "name" and (정함.get(f[1]) or {}).get("무엇") == "마스터요소"
for f in _factors(term)
)
def _extra_names(by: dict, 결과: str, 정함: dict, 머리: dict | None) -> set[str]:
"""결과 식의 항 가운데 마스터요소가 없어 덧줄이 될 값 이름 — 비목을 골라야 함."""
if 머리 is not None and not str(머리.get("결과단위") or "").startswith("원"):
return set() # 돈 아닌 로직은 덧줄이 없음
return {
뿌리
for term, 뿌리 in _spread(by[결과]["나무"], by, 정함, set())
if 뿌리 and not _has_element(term, 정함)
}
def _kind_of(요소) -> str:
if isinstance(요소, dict):
return "재료"
@@ -447,13 +502,16 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
"""
정함, 머리 = 정함 or {}, dict(머리 or {})
got = _scan(text, 정함)
got = _scan(text, 정함, 머리)
문제 = list(got["문제"])
for v in got["변수"]:
if v["정할것"]:
문제.append(
_trouble(
v["처음"], 0, "덜정함", f"「{v['이름']}」 에 {' · '.join(v['정할것'])} 안 정함"
v["처음"],
0,
"덜정함",
f"「{v['이름']}」 에 아직 안 정한 것 — {' · '.join(v['정할것'])}",
)
)
if 문제:
@@ -475,7 +533,7 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
덧줄이름, 편 = set(), set()
if 돈:
for term in _spread(by[결과]["나무"], by, 정함, 편):
for term, 뿌리 in _spread(by[결과]["나무"], by, 정함, 편):
facts = _factors(term)
picks = [
f
@@ -520,6 +578,15 @@ def 옮기기(text: str, 정함: dict | None = None, 머리: dict | None = None)
{"이름": name, "식": _expr(by[name]["나무"]), "비목": 정함[name]["비목"]}
)
덧줄이름.add(name)
elif 뿌리 in by: # 덧줄이 될 값 — 막지 않고 비목만 짚음
문제.append(
_trouble(
by[뿌리]["줄"],
0,
"비목",
f"덧줄 「{뿌리}」 의 비목을 골라 주세요 — {' · '.join(COST_ITEMS)}",
)
)
else:
문제.append(
_trouble(