feat(master_data): 키 개편 뒤따름 — _틀.md 새 규칙 · 빌더 잇기(adopt) · ESTX 비교 · 화면 계약
- _틀.md: 파일 이름(인력.json · 기계.json · NN장_장 제목) · 머리 부문·차례 · 키·테이블ID·대장 · 변수(키 · ID:원문번호 · 찾기(QF…) · 로직(GC…)) - master_keys: old_view(옛 이름으로 읽기) · adopt(빌더 옛 모양 → 키·한 테이블·장 이름 · 부문 앞말 · 참조 변환) · order_chapters(차례) - 빌더 7개 끝에서 adopt · 노임·시중물가 빌더 다시 돌려 결과 같음(시중물가 머리 비고 글 한 줄만 「키」) · 기계·기계설비 7장은 사본 폴더에서 같음 - compare_estx: 인력.json 조사 · 기계.json 세부분류 · 로직 GC:원문번호 · 네 갈래 모두 돌아감 - _화면_계약.md: key = 키 · 새 줄 키는 서버가 대장 다음 번호 · 키 못 고침 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
+64
-31
@@ -10,56 +10,87 @@
|
||||
|
||||
## 2. 파일
|
||||
|
||||
- 이름 = `그룹_원문[_장].json` — 날짜 없음 · 마스터는 항상 최신. 예: `인력_건설노임.json` · `소요량_산림품셈_13장.json` · `로직_산림품셈_13장.json`
|
||||
- 날짜 없음 · 마스터는 항상 최신. UTF-8 · LF · 들여쓰기 2칸 · 칸 이름은 한글.
|
||||
- 그룹 = 인력 · 재료 · 기계 · 소요량 · 계수 · 환율 · 요율 · 로직
|
||||
- 원문 이름 = 산림품셈 · 건설품셈 · 건설노임 · 제조노임 · 엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 · 준용대상 · 나라장터자재 · 오피넷유가 · 시중물가 · 한국은행환율 · 조달청제비율 · 품셈재료 · 자체
|
||||
- UTF-8 · LF · 들여쓰기 2칸 · 칸 이름은 한글.
|
||||
|
||||
## 3. 출처
|
||||
| 그룹 | 파일 이름 |
|
||||
|---|---|
|
||||
| 인력 | `인력.json` 하나 — 줄마다 `조사` |
|
||||
| 기계 | `기계.json` 하나 — 줄마다 `세부분류` |
|
||||
| 재료 | `재료_<원문>.json` — 시중물가 · 나라장터자재 · 오피넷유가 · 품셈재료 |
|
||||
| 환율 · 요율 | `환율_한국은행환율.json` · `요율_조달청제비율.json` |
|
||||
| 소요량 · 계수 · 로직 | `<그룹>_<원문>_<NN>장_<장 제목>.json` — 예 `소요량_건설품셈_10장_창호_및_유리공사.json` |
|
||||
|
||||
- 장 파일 — 장 번호는 두 자리 · 장 제목은 원문 본문 폴더 이름 그대로 · 부문은 이름에 넣지 않음.
|
||||
- 장 파일 머리 — `부문`(건설품셈만 · 공통 · 토목 · 건축 · 기계설비 · 유지관리) · `차례`(그룹 안 목록 차례: 산림 1~14장 → 건설 공통 → 토목 → 건축 → 기계설비 → 유지관리).
|
||||
- 원문 이름 = 산림품셈 · 건설품셈 · 건설노임 · 제조노임 · 엔지니어링노임 · 측량노임 · 건설사업관리노임 · SW노임 · 산림노임 · 준용대상 · 나라장터자재 · 오피넷유가 · 시중물가 · 한국은행환율 · 조달청제비율 · 품셈재료 · 자체
|
||||
|
||||
## 3. 키
|
||||
|
||||
- 모든 요소 줄 · 표 · 로직에 `키` = 테이블ID(영문 대문자 2) + 6자리 일련번호 — `LB000123`. 특수문자·한글 없음.
|
||||
- 한 번 준 키는 안 바뀜 · 지운 줄의 키는 다시 쓰지 않음. 키는 전체에서 하나.
|
||||
- 원문이 준 번호·옛 열쇠(직종 `1018` · 기계 `0201-0070` · `13-4-1 메쌓기` · 시중물가 코드)는 `원문번호` 칸 — 같은 테이블 안에서 하나.
|
||||
- 대장 `_키대장.json` = 키 ↔ 원문번호 ↔ 파일 · 테이블별 `다음` 번호. 새 줄은 대장의 다음 번호를 받음(M01 저장 · 빌더).
|
||||
|
||||
| ID | 테이블 | ID | 테이블 |
|
||||
|---|---|---|---|
|
||||
| LB | 인력 | QF · QC | 소요량 산림 · 건설 |
|
||||
| MT | 재료 시중물가 | CF · CC | 계수 산림 · 건설 |
|
||||
| MN | 재료 나라장터 | FX | 환율 |
|
||||
| MO | 재료 유가 | RT | 요율 |
|
||||
| MP | 재료 품셈재료 | GF · GC · GX | 로직 산림 · 건설 · 자체 |
|
||||
| EQ | 기계 | | |
|
||||
|
||||
- 빌더는 옛 모양(`열쇠` · 원문별 파일)으로 쓰고 끝에 `scripts/master_keys.py` 의 `adopt` 를 부름 — 키·원문번호·한 테이블·장 파일 이름으로 받음. 옛 이름으로 읽기는 `old_view`.
|
||||
|
||||
## 4. 출처
|
||||
|
||||
- 한 칸 · `원문 이름 + 번호` — 「산림품셈 13-4-1」 · 「건설품셈 공통 8-4-1」(건설은 부문 필수) · 「건설노임 1018」 · 「자체」
|
||||
- [주] 는 뒤에 붙임 — 「산림품셈 13-4-4 주⑩」
|
||||
- 판(기준일)은 파일 머리에 한 번.
|
||||
- 판(기준일)은 파일 머리에 한 번 · 인력·기계는 갈래 묶음 안에.
|
||||
|
||||
## 4. 요소 파일
|
||||
## 5. 요소 파일
|
||||
|
||||
```json
|
||||
{
|
||||
"그룹": "인력",
|
||||
"원문": "건설노임",
|
||||
"판": "2026-01-01",
|
||||
"조사": {
|
||||
"건설업": { "원문": "건설노임", "판": "2026-01-01", "비고": ["…"] }
|
||||
},
|
||||
"줄": [
|
||||
{ "열쇠": "1018", "이름": "포설공", "규격": "", "단위": "원/일", "값": 0, "출처": "건설노임 1018" }
|
||||
{ "키": "LB000018", "원문번호": "1018", "조사": "건설업", "이름": "포설공", "규격": "", "단위": "원/일", "값": 0, "출처": "건설노임 1018" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `열쇠` = 원문이 준 번호(직종 번호 · 물품 번호 · 기계 분류번호 `0201-0070`). 없으면 이름.
|
||||
- 인력 `조사` = 건설업 · 제조업 · 엔지니어링 · 측량 · 건설사업관리 · SW · 산림 · 준용대상 · 조사별 원문·판·비고는 머리 `조사` 묶음.
|
||||
- 기계 `세부분류` = 건설품셈 · 산림품셈 · 세부분류별 원문·판·주는 머리 `세부분류` 묶음.
|
||||
- 값이 여럿인 요소(기계)는 `값` 을 묶음으로 — `{ "가격": …, "상각비계수": …, "정비비계수": …, "관리비계수": …, "손료계수": …, "주연료": …, "주연료종류": …, "잡재료율": …, "조종원": …, "조종원직종": … }` · `손료계수` = 원문 「계」.
|
||||
- 묶음 요소는 `단위` · `출처` 도 같은 모양의 묶음.
|
||||
- 원문에 없는 값을 지어 넣지 않음. 원문 「-」 = 칸을 비움(`null`).
|
||||
- `준용` = 값 없는 요소에 관리자가 고르는 유사 요소 열쇠(같은 원문 열쇠 또는 `그룹:원문:열쇠`) — 준용 값을 미리 채우지 않음. 공표 노임에 이름이 온전히 같은 직종이 없으면 `인력_준용대상.json`.
|
||||
- `통합` = 건설노임 원문 「통합·명칭변경 직종」 목록의 새 직종 `그룹:원문:열쇠` · 갈리면 null + `후보`. `후보` = 비슷한 이름 글(값 안 씀).
|
||||
- `준용` = 값 없는 요소에 관리자가 고르는 유사 요소 키 — 준용 값을 미리 채우지 않음. 공표 노임에 이름이 온전히 같은 직종이 없으면 조사 「준용대상」.
|
||||
- `통합` = 건설노임 원문 「통합·명칭변경 직종」 목록의 새 직종 키 · 갈리면 null + `후보`. `후보` = 비슷한 이름 글(「키 이름」 · 값 안 씀).
|
||||
- `산정` = 건설노임 미공표 직종의 원문 4-라 값 `{ 값, 근거, 마지막공표[, 비고] }` — 원 미만 처리 안 함.
|
||||
- 엔진 차례 = 값 → 준용 → 통합 → 산정 · 호표 출처에 「준용: <이름>」 · 「통합: <이름>」 · 「산정: …」.
|
||||
- 원문 이름 대신 번호로 적은 값(`조종원직종` = 노임 번호 「1048」)은 원문 이름을 `비고` 칸에.
|
||||
- 원문 이름 대신 번호로 적은 값(`조종원직종` = 노임 원문번호 「1048」)은 원문 이름을 `비고` 칸에.
|
||||
|
||||
재료 값:
|
||||
|
||||
- `재료_시중물가` `값` = `{ "시중": { 가격정보 · 물가자료 · 유통물가 · 물가정보 · 거래가격, "면수": {…} }, "조달": { "값", "출처", "기준" } }` — 기본가·채택값 · 견적가 두지 않음. 조달 값이 없으면 비움.
|
||||
- 조달 `출처` = 「가격정보」(시중물가 원천의 조달청 관급 단가 · `기준` = 원천 면수 글) · 「나라장터:<열쇠>」(값 안 베낌 · 이름·규격·단위가 온전히 같은 짝만 · 애매하면 사람이 화면에서 이음).
|
||||
- 조달 `출처` = 「가격정보」(시중물가 원천의 조달청 관급 단가 · `기준` = 원천 면수 글) · 「나라장터:<키>」(값 안 베낌 · 이름·규격·단위가 온전히 같은 짝만 · 애매하면 사람이 화면에서 이음).
|
||||
- `재료_나라장터자재` = 조달 자료 — 머리 `자료` 에 밝힘.
|
||||
- `재료_품셈재료` = 품셈이 부르는 재료 목록 — `{ 열쇠, 이름, 규격, 단위, 요구절: [출처 모양], 연결 }` · 값 칸 없음.
|
||||
- `연결` = 시중물가 열쇠 하나 · 후보 조건 `{ 이름, 규격, 지역: "입력" }`(지역이 갈리는 레미콘·골재·아스콘) · `null`(아직 못 이음).
|
||||
- `재료_품셈재료` = 품셈이 부르는 재료 목록 — `{ 키, 원문번호, 이름, 규격, 단위, 요구절: [출처 모양], 연결 }` · 값 칸 없음.
|
||||
- `연결` = 시중물가 키 하나 · 후보 조건 `{ 이름, 규격, 지역: "입력" }`(지역이 갈리는 레미콘·골재·아스콘) · `null`(아직 못 이음).
|
||||
- 단가 = 연결된 줄의 시중·조달 값 가운데 낮은 값 · 호표 결과 줄에 `출처`(그 값의 칸). 후보 조건 = 품명 뿌리가 `이름` 으로 끝남 · 이름에 로직 입력 `자재지역` 글자 · 단위 같음 · 규격이 둘 이상 갈리면 멈춤 「후보 여럿」 · 그 지역 줄이 없으면 가까운 시군을 고르지 않고 멈춤 「지역 값 없음」.
|
||||
|
||||
## 5. 표형 요소 (소요량 · 계수)
|
||||
## 6. 표형 요소 (소요량 · 계수)
|
||||
|
||||
원문 표 하나 = 표 하나. 머리가 여러 단이어도 **조건 칸 + 값 칸의 긴 줄**로 폄. 표들은 파일 머리 아래 `표` 칸에 목록으로 둠.
|
||||
|
||||
```json
|
||||
{
|
||||
"열쇠": "13-4-1 메쌓기",
|
||||
"키": "QF000041",
|
||||
"원문번호": "13-4-1 메쌓기",
|
||||
"이름": "메쌓기(인력)",
|
||||
"기준": "1㎡",
|
||||
"출처": "산림품셈 13-4-1",
|
||||
@@ -72,8 +103,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
- 건설품셈 표 · 로직의 열쇠 앞에 부문 — 「건축 1-1-1 계수」 · 「공통 8 기계 시간당 경비」.
|
||||
- 열쇠는 같은 그룹 · 원문 안에서 파일 사이에도 하나.
|
||||
- 건설품셈 표 · 로직의 원문번호 앞에 부문 — 「건축 1-1-1 계수」 · 「공통 8 기계 시간당 경비」.
|
||||
- 조건 종류 — `수`(같은 값) · `고르기`(글자) · `범위`(줄에 `[아래, 위]` · 끝 열림은 `null` · 포함 여부는 `범위규칙` 에 「초과·이하」처럼).
|
||||
- `범위규칙` 첫 마디 = 엔진이 읽는 이름 — 「초과·이하」 · 「이상·미만」 · 「이하」(위 끝만) · 「이상·이하」(양 끝 포함). 뒤 마디(` · ` 뒤)는 원문 설명. 「이하」 · 「이상·이하」는 여러 줄이 맞으면 위 끝이 가장 작은 줄.
|
||||
- 원문 「-」 칸은 줄에서 그 값 칸을 뺌. 줄 전체가 「-」 면 줄을 뺌.
|
||||
@@ -86,15 +116,17 @@
|
||||
- 수는 원문 그대로(쉼표만 뺌). 단위 환산하지 않음.
|
||||
- `주` 는 원문 글 그대로 — 계산에 쓰는 수가 들어 있으면 따로 계수 표로도 올림.
|
||||
|
||||
## 6. 로직 파일
|
||||
## 7. 로직 파일
|
||||
|
||||
```json
|
||||
{
|
||||
"그룹": "로직",
|
||||
"원문": "산림품셈",
|
||||
"차례": 11,
|
||||
"줄": [
|
||||
{
|
||||
"열쇠": "13-4-1 메쌓기",
|
||||
"키": "GF000123",
|
||||
"원문번호": "13-4-1 메쌓기",
|
||||
"이름": "돌쌓기 메쌓기(인력)",
|
||||
"결과단위": "원/㎡",
|
||||
"출처": "산림품셈 13-4-1",
|
||||
@@ -105,7 +137,7 @@
|
||||
],
|
||||
"중간": [{ "이름": "증가율", "식": "…" }],
|
||||
"호표": [
|
||||
{ "종류": "인력", "요소": "인력:건설노임:…", "단위": "인", "수량": "…", "비목": "노무비" }
|
||||
{ "종류": "인력", "요소": "LB000002", "단위": "인", "수량": "…", "비목": "노무비" }
|
||||
],
|
||||
"덧줄": [{ "이름": "잡재료비", "식": "노무비 * 0.05", "비목": "재료비", "출처": "…" }],
|
||||
"끝수": "원 미만 버림"
|
||||
@@ -115,7 +147,7 @@
|
||||
```
|
||||
|
||||
- `호표` 줄 = 실무 일위대가표 한 줄(종류 · 이름·규격 · 단위 · 수량 · 단가 · 금액). 단가는 적지 않음 — `요소` 에서 옴. 금액 = 수량 × 단가.
|
||||
- `종류` = 인력 · 재료 · 기계 · 로직(하위 로직의 결과를 단가로 씀).
|
||||
- `종류` = 인력 · 재료 · 기계 · 로직(하위 로직의 결과를 단가로 씀) — 요소 키의 테이블ID 그룹과 같음.
|
||||
- `비목` = 노무비 · 재료비 · 경비. `덧줄` 식에서 비목 합을 이름으로 씀.
|
||||
- `결과단위` 가 돈이 아닌 로직(㎥/시간)은 `호표` 없이 `중간` + `결과` 식.
|
||||
- `종류` 가 로직이고 `비목` 칸이 없는 호표 줄은 불러온 결과를 비목별로 나눠 얹음.
|
||||
@@ -123,19 +155,20 @@
|
||||
- 입력에 원문 범위가 있으면 `범위: [아래, 위]` · 기본값 두지 않음.
|
||||
- `소유` = 공용 · 개인(뒤 단계 자리).
|
||||
|
||||
## 7. 변수 · 식
|
||||
## 8. 변수 · 식
|
||||
|
||||
- 요소 — `그룹:원문:열쇠` · 값 묶음은 `.칸` — `기계:건설품셈:0201-0070.주연료` · 둘째 쌍점 뒤는 모두 열쇠(`재료:오피넷유가:경유:전국평균`).
|
||||
- 열쇠에 `{이름}` — 입력·중간 값을 끼움 — `기계:건설품셈:{기계}.가격`
|
||||
- 표 찾기 — `찾기(소요량:산림품셈:13-4-1 메쌓기, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공`
|
||||
- 요소 — 키 `LB000123` · 값 묶음은 `.칸` — `EQ000045.주연료`.
|
||||
- 입력·중간 값으로 요소를 고르면 `ID:원문번호` 에 `{이름}` 을 끼움 — `EQ:{기계}.가격` · `MO:경유:{지역}` · `LB:{조종원직종}`.
|
||||
- 표 찾기 — `찾기(QF000041, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공`
|
||||
- 맞는 줄이 여럿이면 찾는 값 칸이 있는 줄만 봄. 값 칸·조건 이름에 괄호·빈칸이 있으면 작은따옴표 — `.'시공량(m)'` · `'규준틀 높이'=높이`
|
||||
- 찾기·로직 인자의 글자 값은 작은따옴표 — `구분='V(다짐속도,km/hr)'` · 따옴표 없으면 이름(입력·중간).
|
||||
- 로직 부르기 — `로직(건설품셈:공통 8 굴착기(무한궤도) 시간당 경비, 기계='0201-0070')` · 돈 로직의 결과는 비목 묶음 `.노무비` `.재료비` `.경비` `.계` · 돈 아닌 로직은 수 하나.
|
||||
- 로직 부르기 — `로직(GC000871, 기계='0201-0070')` · 돈 로직의 결과는 비목 묶음 `.노무비` `.재료비` `.경비` `.계` · 돈 아닌 로직은 수 하나.
|
||||
- 입력 · 중간 · 비목 합은 이름 그대로.
|
||||
- 후보 조건 품셈재료를 쓰는 로직(부르는 로직 포함)은 입력 `자재지역` 을 두고 하위 로직에 `자재지역=자재지역` 으로 넘김 — `지역` 은 유가 고르기.
|
||||
- 쓸 수 있는 것 — `+ - * /` · 괄호 · 비교 `< <= > >= == !=` · `그리고` `또는` · `찾기` · `로직` · `만약(조건, 참, 거짓)` · `올림` `버림` `반올림`(자리) · `최소` `최대`. 그 밖은 쓰지 않음.
|
||||
- 로직이 돌고 도는 참조를 만들지 않음.
|
||||
- 사람이 읽는 자리(M01 화면 · 검사 알림)는 키 옆에 이름.
|
||||
|
||||
## 8. 검사
|
||||
## 9. 검사
|
||||
|
||||
- 고친 뒤 `scripts/check_master.py` — 틀 · 본문 대조 · 로직 검사. 로직은 `계산` 으로 손 계산과 맞댐.
|
||||
- 고친 뒤 `scripts/check_master.py` — 틀 · 본문 대조 · 로직 검사 · 키(모양 · 겹침 · 원문번호 겹침 · 대장 · 끊긴 키). 로직은 `계산` 으로 손 계산과 맞댐.
|
||||
|
||||
@@ -8,32 +8,34 @@
|
||||
- 봉투 칸 이름은 영문 · 줄·표·로직 알맹이는 파일 그대로(한글 칸).
|
||||
- 수는 JSON 수로 옴.
|
||||
- `version` = 파일 판본(내용 해시). 읽을 때 받아 두고 저장 때 그대로 돌려줌.
|
||||
- `file` = 파일 이름(`소요량_건설품셈_기계설비7장.json`). 밑줄 파일·하위 폴더는 없음.
|
||||
- 없는 파일·열쇠 = 404 · 틀린 요청 = 400.
|
||||
- `file` = 파일 이름(`소요량_건설품셈_07장_위생기구설비공사.json` · `인력.json`). 밑줄 파일·하위 폴더는 없음.
|
||||
- `key` = 키(`LB000123`) — 전체에서 하나. 줄의 원문 번호는 `원문번호` 칸.
|
||||
- 없는 파일·키 = 404 · 틀린 요청 = 400.
|
||||
|
||||
## 2. 읽기
|
||||
|
||||
| 길 | 받음 | 줌 |
|
||||
|---|---|---|
|
||||
| `GET /groups` | | `{groups: [{group, files, rows}]}` — 그룹 차례는 `_틀.md` 2장 |
|
||||
| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version}]}` |
|
||||
| `GET /groups/{group}/files` | | `{files: [{file, book, chapter, edition, rows, version, order}]}` — 머리 `차례` 순 |
|
||||
| `GET /rows` | `file` · `page`(1) · `size`(50) · `q` | `{file, version, total, page, size, rows: [줄…]}` |
|
||||
| `GET /tables` | `file` · `q` | `{file, version, tables: [{열쇠, 이름, 기준, 출처, 조건, 값칸, count}]}` |
|
||||
| `GET /tables` | `file` · `q` | `{file, version, tables: [{키, 원문번호, 이름, 기준, 출처, 조건, 값칸, count}]}` |
|
||||
| `GET /table` | `file` · `key` | `{file, version, table: {표 하나 통째}}` |
|
||||
| `GET /logics` | `book` · `chapter` · `q` · `blocked`(0/1) | `{logics: [{file, book, chapter, 열쇠, 이름, 결과단위, 출처, blocked, reasons}]}` |
|
||||
| `GET /logic` | `book` · `key` | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` |
|
||||
| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 이름, 규격, 단위, 값, 값칸}]}` |
|
||||
| `GET /logics` | `book` · `chapter` · `q` · `blocked`(0/1) | `{logics: [{file, book, chapter, 키, 원문번호, 이름, 결과단위, 출처, blocked, reasons}]}` |
|
||||
| `GET /logic` | `key`(`book` 은 안 씀) | `{file, version, logic: {로직 한 줄 통째}, blocked, reasons, prices: {요소: 요약 \| null}}` |
|
||||
| `GET /elements` | `group` · `q` · `limit`(50 · 200까지) | `{total, items: [{ref, file, 원문번호, 이름, 규격, 단위, 값, 값칸}]}` |
|
||||
|
||||
- `rows` 는 요소·로직 파일은 `줄`, 표형 파일(소요량·계수)은 `표` 를 줄로 봄.
|
||||
- `q` = 이름 찾기 — `열쇠`·`이름` 에 든 글(대소문자 무시).
|
||||
- `chapter` = 파일 이름의 장(`공통3장` · `13장` · `01장`) · 없으면 "".
|
||||
- `q` = 찾기 — `키`·`원문번호`·`이름` 에 든 글(대소문자 무시).
|
||||
- `chapter` = 「부문 NN장 장 제목」(`공통 03장 토공사` · `13장 구조물`) · 장 파일 아니면 "".
|
||||
- `blocked` = 로직 검사(`check_master.py 로직`)에 걸림 · `reasons` = 걸린 까닭 글.
|
||||
- `prices` = 호표 `요소` → `{ref, 이름, 규격, 단위, 값}`(단가 자동 칸) · 없는 요소는 `null` · `{이름}` 이 낀 열쇠·하위 로직 줄은 빠짐(계산 때 정해짐). 품셈재료는 `값` = 연결을 따라간 낮은 값 · `출처` = 그 값의 칸(`재료:시중물가:<열쇠>.시중.<칸>` · `….조달` · `재료:나라장터자재:<열쇠>`) · 자재지역 후보는 `값` null(계산 때 정해짐).
|
||||
- `elements` = 요소 찾기 창 — 그룹 전체에서 `q` 찾기 · `ref` = 식에 넣는 `그룹:원문:열쇠` · 표형 그룹은 `값칸` 이 옴.
|
||||
- `prices` = 호표 `요소` → `{ref, 이름, 규격, 단위, 값}`(단가 자동 칸) · 없는 요소는 `null` · `{이름}` 이 낀 원문번호·하위 로직 줄은 빠짐(계산 때 정해짐). 품셈재료는 `값` = 연결을 따라간 낮은 값 · `출처` = 그 값의 칸(`<시중물가 키>.시중.<칸>` · `….조달` · `<나라장터자재 키>`) · 자재지역 후보는 `값` null(계산 때 정해짐).
|
||||
- `elements` = 요소 찾기 창 — 그룹 전체에서 `q` 찾기 · `ref` = 식에 넣는 키 · 표형 그룹은 `값칸` 이 옴.
|
||||
- `pick` = 고르기 창 — `procure` ref `나라장터:<키>` · `price` ref 키 · `job` = `인력.json` 값 있는 공표 직종(준용대상 빼고) ref 키.
|
||||
|
||||
## 3. 시험 계산
|
||||
|
||||
`POST /calc` 받음 `{book, key, inputs: {이름: 값}, row?, file?}`
|
||||
`POST /calc` 받음 `{key, inputs: {이름: 값}, row?, file?}` — 새 로직은 `key` ""
|
||||
|
||||
- 됨 — `{ok: true, lines: [{이름, 단위, 수량, 단가, 금액, 비목: {비목: 금액}, 출처?}], sums: {노무비, 재료비, 경비, 계}, middle: {이름: 값}}` · `출처` = 품셈재료 줄의 단가 칸
|
||||
- 돈 아닌 로직 — `{ok: true, result, middle}`
|
||||
@@ -44,7 +46,7 @@
|
||||
|
||||
`POST /save` 받음 `{files: [{file, version, changes: [{op, key, row}]}]}`
|
||||
|
||||
- `op` — `edit`(열쇠 `key` 줄을 `row` 로 바꿈) · `add`(`row` 더함 · `key` 안 씀) · `delete`(`key` 줄 지움).
|
||||
- `op` — `edit`(키 `key` 줄을 `row` 로 바꿈 · 키는 못 바꿈) · `add`(`row` 더함 · 키는 서버가 `_키대장.json` 의 다음 번호로 줌) · `delete`(`key` 줄 지움).
|
||||
- 요소 파일 = `줄` 한 줄 · 표형 파일 = `표` 하나 · 로직 파일 = 로직 한 줄. `row` 는 통째(칸 일부만 보내지 않음).
|
||||
- 화면은 조작을 캐시에 쌓고 [저장] 한 번에 보냄 — 서버는 이때만 파일에 씀.
|
||||
- 순서: 모든 `version` 대조 → 모두 적용(메모리) → 검사 → 모두 씀. 하나라도 걸리면 아무것도 안 씀.
|
||||
@@ -54,9 +56,9 @@
|
||||
| 200 | 씀 | `{files: [{file, version}]}` — 새 판본 |
|
||||
| 409 | 낡은 화면 — 그 사이 파일이 바뀜 | `{detail: {stale: [file…]}}` → 다시 읽고 다시 고침 |
|
||||
| 422 | 검사 걸림 | `{detail: {errors: [글…]}}` |
|
||||
| 404 | 없는 파일·열쇠 | `{detail}` |
|
||||
| 400 | 틀린 `op` · 더할 열쇠가 이미 있음 · 고친 줄의 열쇠가 딴 줄과 겹침 | `{detail}` |
|
||||
| 404 | 없는 파일·키 | `{detail}` |
|
||||
| 400 | 틀린 `op` · 고친 줄의 키를 바꿈 | `{detail}` |
|
||||
|
||||
- 검사 = `check_master.py` 의 틀(고친 파일) + 로직 변수(모든 로직 파일, 고친 뒤 마스터 기준). **고친 뒤 새로 생긴 걸림**만 막음 — 원래 있던 걸림은 막지 않음.
|
||||
- 검사 = `check_master.py` 의 틀(고친 파일) + 로직 변수(모든 로직 파일, 고친 뒤 마스터 기준). **고친 뒤 새로 생긴 걸림**만 막음 — 원래 있던 걸림은 막지 않음. 원문번호 겹침(테이블 안)도 걸림.
|
||||
- 요소를 지워 어떤 로직이 가리키던 것이 사라지면 로직 변수 검사에 걸림(422).
|
||||
- 쓰기는 UTF-8 · LF · 들여쓰기 2칸.
|
||||
|
||||
@@ -23,6 +23,7 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import master_keys as mk
|
||||
from _migrate_base import dump_master
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -57,7 +58,7 @@ def notice_day(row: dict) -> str:
|
||||
|
||||
def build() -> dict:
|
||||
raw = json.loads(RAW.read_text(encoding="utf-8"))
|
||||
master = json.loads(MASTER.read_text(encoding="utf-8"))
|
||||
master = mk.old_view(MASTER.name)
|
||||
records = master["줄"]
|
||||
|
||||
history: dict[str, list[dict]] = {}
|
||||
@@ -99,3 +100,4 @@ if __name__ == "__main__":
|
||||
master = result["master"]
|
||||
dump_master(MASTER, {k: v for k, v in master.items() if k != "줄"}, master["줄"])
|
||||
print(f"고친 줄 {result['repaired']} · 못 고친 줄 {result['unrecovered']}")
|
||||
print("새 틀로 받음", mk.adopt())
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
돌리기: `./venv/Scripts/python.exe resources/master_data/scripts/_migrate_base.py`
|
||||
옛 파일은 `old/` 에서 읽기만 함. 새 파일은 `resources/master_data/` 첫 층에 씀. 값은 바꾸지 않음.
|
||||
`dump_master` 는 `_build_mat_price.py` 도 씀.
|
||||
`dump_master` 는 `_build_mat_price.py` 도 씀. 한 번 돌린 옮기기 — 지금 틀(키 · 인력.json)은 뒤에 `_migrate_keys.py` 로 옮김.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,7 +14,7 @@ import re
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
TARGET = HERE.parent / "계수_산림품셈_01장.json"
|
||||
TARGET = HERE.parent / "계수_산림품셈_01장_적용기준.json" # 한 번 돌림 — 이미 들어감
|
||||
BODY = HERE.parents[2] / "resources/knowledge/original/원가계산/산림_표준품셈/본문/제01장_적용기준"
|
||||
SRC = "산림품셈 "
|
||||
|
||||
@@ -599,7 +599,7 @@ def main() -> None:
|
||||
text = TARGET.read_bytes().decode("utf-8")
|
||||
crlf = "\r\n" in text
|
||||
text = text.replace("\r\n", "\n")
|
||||
if '"열쇠": "1-2-2 ' in text:
|
||||
if '"원문번호": "1-2-2 ' in text:
|
||||
raise SystemExit("이미 추가됨")
|
||||
A, T = from_1_02(), from_text()
|
||||
front = [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
본문 md(확정) 표를 읽어 기계 한 줄로 합침. 값은 md 에서만 · 계산·추정 없음.
|
||||
실행: python resources/master_data/scripts/build_기계_건설품셈.py [--dry]
|
||||
옛 모양(열쇠)으로 쓰고 `master_keys.adopt` 가 기계.json(키 · 세부분류 건설품셈)으로 받음.
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -10,6 +11,9 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3]
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
|
||||
DIR = (
|
||||
REPO
|
||||
/ "resources/knowledge/original/원가계산/건설공사_표준품셈/본문/01_공통부문/제08장_건설기계"
|
||||
@@ -653,3 +657,4 @@ if __name__ == "__main__":
|
||||
with open(OUT, "w", encoding="utf8", newline="\n") as f:
|
||||
f.write(text)
|
||||
print("씀", OUT)
|
||||
print("새 틀로 받음", mk.adopt(OUT.parent))
|
||||
|
||||
@@ -12,6 +12,7 @@ import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import master_keys as mk
|
||||
from _계수_공통2장 import lift_table
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -681,6 +682,7 @@ if __name__ == "__main__":
|
||||
if dup:
|
||||
raise SystemExit(f"열쇠 겹침 {dup}")
|
||||
OUT.write_text(dump(tables), encoding="utf-8", newline="\n")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
for f, (a, b, _) in count.items():
|
||||
print(f"{f} · 옮긴 표 {a} · 계수 표 {b}")
|
||||
print("합계", sum(a for a, _, _ in count.values()), sum(b for _, b, _ in count.values()))
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
import master_keys as mk # noqa: E402
|
||||
from _md표_읽기 import ( # noqa: E402
|
||||
LOSE,
|
||||
PCT,
|
||||
@@ -607,3 +608,4 @@ if __name__ == "__main__":
|
||||
f()
|
||||
write()
|
||||
print(len(TABLES), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -9,6 +9,7 @@ import re
|
||||
import sys
|
||||
|
||||
import _토목_읽기 as base
|
||||
import master_keys as mk
|
||||
from _설비_계수 import rows_of
|
||||
from _설비_읽기 import DIV, read_chapter
|
||||
from _설비_표 import convert
|
||||
@@ -58,3 +59,4 @@ if __name__ == "__main__":
|
||||
for w in sys.argv[1:] or ["7", "8", "9", "10", "11", "12", "13"]:
|
||||
o = build(int(w), f"소요량_건설품셈_기계설비{w}장.json")
|
||||
print(w, len(o.tables), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
import master_keys as mk
|
||||
from _md표_읽기 import N, R, join
|
||||
from _토목_계수 import lift
|
||||
from _토목_손 import SPECIAL
|
||||
@@ -163,3 +164,4 @@ if __name__ == "__main__":
|
||||
}[w]
|
||||
o = build(ch[0], ch[1], SPECIAL.get(w, {}))
|
||||
print(w, len(o.tables), "표")
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 장 파일 이름
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
이음 = 원문 회차의 전체직종 평균을 그대로 이음(미공표직종_추적.md 4절) · 산정값은 같은 md 5절과 대조.
|
||||
|
||||
쓰기: ./venv/Scripts/python.exe resources/master_data/scripts/build_인력_노임.py
|
||||
옛 모양(원문별 파일 · 열쇠)으로 쓰고 끝에 `master_keys.adopt` 가 인력.json(키 · 조사)으로 받음.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
@@ -19,6 +20,7 @@ from decimal import ROUND_HALF_UP, Decimal
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -289,14 +291,14 @@ def main():
|
||||
assert {b: len(v[2]) for b, v in books.items()} == expect
|
||||
|
||||
# 이름 → ref (새 다섯 + 건설·제조 공표)
|
||||
wage = json.loads((MD / "인력_건설노임.json").read_text(encoding="utf-8"))
|
||||
make = json.loads((MD / "인력_제조노임.json").read_text(encoding="utf-8"))
|
||||
wage = mk.old_view("인력_건설노임.json")
|
||||
make = mk.old_view("인력_제조노임.json")
|
||||
pool = {r["이름"]: f"인력:{b}:{r['열쇠']}" for b, (_, _, rows) in books.items() for r in rows}
|
||||
old_pool = {r["이름"]: f"인력:{d['원문']}:{r['열쇠']}" for d in (wage, make) for r in d["줄"]}
|
||||
merges = merge_list()
|
||||
|
||||
alias_path = MD / "인력_준용대상.json"
|
||||
alias = json.loads(alias_path.read_text(encoding="utf-8"))
|
||||
alias = mk.old_view(alias_path.name)
|
||||
moved, kept = {}, []
|
||||
for r in alias["줄"]:
|
||||
name = r["이름"]
|
||||
@@ -318,7 +320,7 @@ def main():
|
||||
r["후보"] = "비슷한 이름: " + " · ".join(near)
|
||||
kept.append(r)
|
||||
alias["비고"] = (
|
||||
"값을 확인 못 한 직종(공표 노임에 이름이 온전히 같은 직종 없음) — 이름·단위만 · 준용 = 관리자가 고르는 공표 직종 열쇠(`인력:원문:열쇠`)"
|
||||
"값을 확인 못 한 직종(공표 노임에 이름이 온전히 같은 직종 없음) — 이름·단위만 · 준용 = 관리자가 고르는 공표 직종 키(`LB000123`)"
|
||||
" · 통합 = 건설노임 원문 「통합·명칭변경 직종」 목록의 새 직종(엔진이 그 값을 씀) · 후보 = 비슷한 이름(값 안 씀) · 비고 = 이 직종을 요구한 품셈 절"
|
||||
)
|
||||
head = {k: v for k, v in alias.items() if k != "줄"}
|
||||
@@ -345,6 +347,7 @@ def main():
|
||||
)
|
||||
|
||||
estimate(wage)
|
||||
print("새 틀로 받음", mk.adopt())
|
||||
|
||||
|
||||
# ── 건설노임 미공표 산정 ─────────────────────────────────────────────
|
||||
|
||||
@@ -23,6 +23,7 @@ import unicodedata
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import master_keys as mk # noqa: E402
|
||||
from _migrate_base import dump_master # noqa: E402
|
||||
from compare_estx import ESTX, ROOT, SLOTS, Estx, bare_spec, base_name # noqa: E402
|
||||
|
||||
@@ -169,12 +170,13 @@ def main():
|
||||
"비고": [
|
||||
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
||||
"값 = 시중(물가지 다섯 · 면수) · 조달(값 · 출처 · 기준) — 기본가·채택값 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<열쇠>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<키>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)",
|
||||
],
|
||||
}
|
||||
nara = json.loads(NARA.read_text(encoding="utf-8"))["줄"]
|
||||
nara = mk.old_view(NARA.name)["줄"]
|
||||
dump_master(OUT, head, build(rows, nara_links(rows, nara)))
|
||||
check(rows)
|
||||
print("새 틀로 받음", mk.adopt()) # 열쇠 → 키 · 나라장터 연결 → 키
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -122,9 +122,10 @@ def labor(estx: Estx, files: dict):
|
||||
heads = [r["Name"] for r in rows if r.get("Name") and not r.get("Unite")]
|
||||
print("## 노임 — ESTX 머리줄", heads)
|
||||
ours = {}
|
||||
for fname, tag in (("인력_건설노임.json", "건설"), ("인력_제조노임.json", "제조")):
|
||||
for r in files[fname]["줄"]:
|
||||
ours.setdefault((tag, norm(r["이름"])), r)
|
||||
for survey, tag in (("건설업", "건설"), ("제조업", "제조")):
|
||||
for r in files["인력.json"]["줄"]:
|
||||
if r["조사"] == survey:
|
||||
ours.setdefault((tag, norm(r["이름"])), r)
|
||||
theirs = {}
|
||||
for r in rows:
|
||||
if r.get("Unite") != "인":
|
||||
@@ -230,7 +231,7 @@ def material(estx: Estx, files: dict):
|
||||
for r in files["재료_품셈재료.json"]["줄"]:
|
||||
if r.get("연결"):
|
||||
continue
|
||||
mine = {base_name(r["이름"]), base_name(r["열쇠"])}
|
||||
mine = {base_name(r["이름"]), base_name(r["원문번호"])}
|
||||
hits = [e for m in mine for e in by_name.get(m, [])]
|
||||
if hits:
|
||||
have.append((r, hits))
|
||||
@@ -243,15 +244,15 @@ def material(estx: Estx, files: dict):
|
||||
f"### 재료_품셈재료 못 이음 {len(have) + len(near) + len(none)} — ESTX 품명 같음 {len(have)} · 품명 끝 같음 {len(near)} · 없음 {len(none)}"
|
||||
)
|
||||
for r, hits in near:
|
||||
print(f" 비슷 {r['열쇠']} | {sorted({e['Name'] for e in hits})[:4]}")
|
||||
print(f" 비슷 {r['원문번호']} | {sorted({e['Name'] for e in hits})[:4]}")
|
||||
for r, hits in have:
|
||||
s = bare_spec(r["이름"], r.get("규격"))
|
||||
same = [e for e in hits if s and spec_match(s, bare_spec(e["Name"], e.get("Size")))]
|
||||
h = (same or hits)[0]
|
||||
print(
|
||||
f" 있음 {r['열쇠']} [{r.get('단위')}] | ESTX {len(hits)}줄 규격같음 {len(same)} | 예 {h['Name']} {h.get('Size')} {h.get('Unite')} {h['TC']:.0f}"
|
||||
f" 있음 {r['원문번호']} [{r.get('단위')}] | ESTX {len(hits)}줄 규격같음 {len(same)} | 예 {h['Name']} {h.get('Size')} {h.get('Unite')} {h['TC']:.0f}"
|
||||
)
|
||||
print(" 없음", [r["열쇠"] for r, _ in none])
|
||||
print(" 없음", [r["원문번호"] for r, _ in none])
|
||||
|
||||
|
||||
# ── (3) 기계경비 ──────────────────────────────────────────────────────
|
||||
@@ -284,7 +285,7 @@ def mach_key(name, size) -> str:
|
||||
def machine_cost(master: mf.Master, mid: str, region="전국평균"):
|
||||
"""로직으로 계산 — 고르기 밖 기계는 메모리에서만 고르기에 더함."""
|
||||
for key in LOGICS:
|
||||
row = master.logic("건설품셈:" + key)
|
||||
row = master.logic("GC:" + key)
|
||||
spec = next(i for i in row["입력"] if i["이름"] == "기계")
|
||||
added = mid not in spec["고르기"]
|
||||
if added:
|
||||
@@ -292,7 +293,7 @@ def machine_cost(master: mf.Master, mid: str, region="전국평균"):
|
||||
try:
|
||||
given = {"기계": mid, "지역": region}
|
||||
given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row["입력"])}
|
||||
return key, mf.run(master, "건설품셈:" + key, given)
|
||||
return key, mf.run(master, "GC:" + key, given)
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError):
|
||||
continue
|
||||
finally:
|
||||
@@ -306,17 +307,17 @@ def machine(estx: Estx, files: dict, master: mf.Master):
|
||||
for r in estx.table("MachTitleDat"):
|
||||
if r.get("Unite") == "hr" and r.get("TC"):
|
||||
est.setdefault(mach_key(r.get("Name"), r.get("Size")), r)
|
||||
ours = files["기계_건설품셈.json"]["줄"]
|
||||
ours = [m for m in files["기계.json"]["줄"] if m["세부분류"] == "건설품셈"]
|
||||
pairs = [(m, est[k]) for m in ours if (k := mach_key(m["이름"], m["규격"])) in est]
|
||||
print(f"## 기계 — 우리 {len(ours)} · ESTX 시간당 {len(est)} · 기종·규격 짝 {len(pairs)}")
|
||||
out = []
|
||||
for m, e in pairs:
|
||||
key, res = machine_cost(master, m["열쇠"])
|
||||
key, res = machine_cost(master, m["원문번호"])
|
||||
if not res:
|
||||
continue
|
||||
out.append((m, e, key, res))
|
||||
print(f" 로직 계산됨 {len(out)}")
|
||||
first = [x for x in out if x[0]["열쇠"].startswith(FIRST)]
|
||||
first = [x for x in out if x[0]["원문번호"].startswith(FIRST)]
|
||||
for label, group in (("임도 기계", first), ("전체", out)):
|
||||
print(f"### {label}")
|
||||
print(" 계:", spread([rate(e["TC"], res["계"]) for m, e, k, res in group]))
|
||||
@@ -340,18 +341,18 @@ def machine(estx: Estx, files: dict, master: mf.Master):
|
||||
e.get("GC") and abs(rate(e["GC"], res["재료비"])) > 5
|
||||
):
|
||||
print(
|
||||
f" {m['열쇠']} {m['이름']} {m['규격']} {k} | ESTX 재 {e.get('GC', 0):.0f} 경 {e['JC']:.0f} | 우리 재 {res['재료비']:.0f} 경 {res['경비']:.0f}"
|
||||
f" {m['원문번호']} {m['이름']} {m['규격']} {k} | ESTX 재 {e.get('GC', 0):.0f} 경 {e['JC']:.0f} | 우리 재 {res['재료비']:.0f} 경 {res['경비']:.0f}"
|
||||
)
|
||||
for m, e, k, res in first:
|
||||
print(
|
||||
f" {m['열쇠']} {m['이름']} {m['규격']} | ESTX {e['TC']:.0f} (노 {e.get('NC', 0):.0f} 재 {e.get('GC', 0):.0f} 경 {e.get('JC', 0):.0f})"
|
||||
f" {m['원문번호']} {m['이름']} {m['규격']} | ESTX {e['TC']:.0f} (노 {e.get('NC', 0):.0f} 재 {e.get('GC', 0):.0f} 경 {e.get('JC', 0):.0f})"
|
||||
f" | 우리 {res['계']:.0f} (노 {res['노무비']:.0f} 재 {res['재료비']:.0f} 경 {res['경비']:.0f}) {rate(e['TC'], res['계']):+.1f}%"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── (4) 일위대가 ──────────────────────────────────────────────────────
|
||||
# (ESTX 제목 코드, 로직 열쇠, 입력[, 단위 환산 배수 — 우리 값 × 배수 = ESTX 단위])
|
||||
# (ESTX 제목 코드, 로직 원문번호, 입력[, 단위 환산 배수 — 우리 값 × 배수 = ESTX 단위])
|
||||
REGION = {"지역": "전국평균", "보정작업": "해당 없음"}
|
||||
PAIRS = [
|
||||
(
|
||||
@@ -456,14 +457,15 @@ def with_estx_materials(master: mf.Master, estx: Estx, code: str):
|
||||
"""ESTX 일위대가의 자재 단가를 품명으로 우리 빈 재료(값 없음)에만 임시로 넣은 사본 — 파일에 안 씀."""
|
||||
temp = copy.copy(master)
|
||||
temp.index = {k: dict(v) for k, v in master.index.items()}
|
||||
temp.numbers = {k: dict(v) for k, v in master.numbers.items()}
|
||||
items = [
|
||||
d
|
||||
for d in estx.details("HeadMainDat").get(code, [])
|
||||
if str(d.get("Code", "")).startswith("M")
|
||||
]
|
||||
put = []
|
||||
for book in (("재료", "자체"), ("재료", "나라장터자재")):
|
||||
rows = temp.index.get(book, {})
|
||||
for tid in ("MN",):
|
||||
rows = temp.index.get(tid, {})
|
||||
for key, row in list(rows.items()):
|
||||
if row.get("값") is not None:
|
||||
continue
|
||||
@@ -472,6 +474,7 @@ def with_estx_materials(master: mf.Master, estx: Estx, code: str):
|
||||
)
|
||||
if hit:
|
||||
rows[key] = {**row, "값": Decimal(str(hit["TU"]))}
|
||||
temp.numbers[tid][str(row["원문번호"])] = rows[key]
|
||||
put.append(f"{key}={hit['TU']:.0f}")
|
||||
return temp, put
|
||||
|
||||
@@ -484,11 +487,11 @@ def unit_price(estx: Estx, master: mf.Master):
|
||||
h = heads[code]
|
||||
k = Decimal(scale[0]) if scale else Decimal(1)
|
||||
temp, put = with_estx_materials(master, estx, code)
|
||||
row = temp.logic("건설품셈:" + key)
|
||||
row = temp.logic("GC:" + key)
|
||||
given = {k: v for k, v in given.items() if any(i["이름"] == k for i in row.get("입력", []))}
|
||||
given = {k: Decimal(str(v)) if isinstance(v, (int, float)) else v for k, v in given.items()}
|
||||
try:
|
||||
res = mf.run(temp, "건설품셈:" + key, given)
|
||||
res = mf.run(temp, "GC:" + key, given)
|
||||
res = {c: res[c] * k for c in ("계", *mf.COST_ITEMS)}
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError) as e:
|
||||
print(f" {code} {h['Name']} {h.get('Size')} | {key} | 멈춤: {str(e)[:120]}")
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
MASTER = Path(__file__).resolve().parent.parent
|
||||
@@ -62,12 +63,18 @@ def save_book(book: dict, path: Path = BOOK) -> None:
|
||||
path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def issue(book: dict, tid: str, number: str, file: str) -> str:
|
||||
"""(테이블ID, 원문번호) 의 키 — 대장에 있으면 그 키 · 없으면(원문번호 빈 새 줄 포함) 다음 번호를 줌."""
|
||||
def lookup(book: dict) -> dict:
|
||||
"""(테이블ID, 원문번호) → 키 — 대장에서 한 번 세워 둠(`finish` 가 걷음)."""
|
||||
index = book.setdefault("_찾기", {})
|
||||
if not index:
|
||||
for key, v in book["키"].items():
|
||||
index[(key[:2], v["원문번호"])] = key
|
||||
return index
|
||||
|
||||
|
||||
def issue(book: dict, tid: str, number: str, file: str) -> str:
|
||||
"""(테이블ID, 원문번호) 의 키 — 대장에 있으면 그 키 · 없으면 다음 번호(원문번호 빈 새 줄 포함)."""
|
||||
index = lookup(book)
|
||||
key = index.get((tid, number)) if number else None
|
||||
if key:
|
||||
book["키"][key]["파일"] = file
|
||||
@@ -97,3 +104,254 @@ def stamp(
|
||||
key = row.pop("키", None) or issue(book, tid, number, file)
|
||||
out.append({"키": key, "원문번호": number, **(extra or {}), **row})
|
||||
return out
|
||||
|
||||
|
||||
# ── 빌더 잇기 — 빌더는 옛 모양(「열쇠」 · 원문별 파일 이름)으로 쓰고 adopt 가 새 틀로 받음 ──
|
||||
LABOR = {
|
||||
"건설노임": "건설업",
|
||||
"제조노임": "제조업",
|
||||
"엔지니어링노임": "엔지니어링",
|
||||
"측량노임": "측량",
|
||||
"건설사업관리노임": "건설사업관리",
|
||||
"SW노임": "SW",
|
||||
"산림노임": "산림",
|
||||
"준용대상": "준용대상",
|
||||
}
|
||||
MERGED = {
|
||||
"인력": ("인력.json", "조사", LABOR),
|
||||
"기계": ("기계.json", "세부분류", {"건설품셈": "건설품셈", "산림품셈": "산림품셈"}),
|
||||
}
|
||||
DIVISIONS = ("공통", "토목", "건축", "기계설비", "유지관리")
|
||||
OLD_CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈)_(\D*)(\d+)장\.json$")
|
||||
CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈|자체)_(\d\d)장_.+\.json$")
|
||||
_BODY = MASTER.parents[1] / "resources/knowledge/original/원가계산"
|
||||
_OLDREF = re.compile(r"(인력|재료|기계|소요량|계수|로직|환율|요율):([^:\s·]+):(.+?)(?=\s·\s|$)")
|
||||
_STR = re.compile(r'"(?:[^"\\]|\\.)*"')
|
||||
|
||||
|
||||
def title(book: str, division: str, number: int) -> str:
|
||||
"""원문 장 제목 — 본문 폴더 이름 그대로(「제07장_지붕_및_홈통공사」 → 「지붕_및_홈통공사」)."""
|
||||
base = _BODY / "산림_표준품셈/본문"
|
||||
if book == "건설품셈":
|
||||
index = DIVISIONS.index(division) + 1
|
||||
base = _BODY / "건설공사_표준품셈/본문" / f"{index:02d}_{division}부문"
|
||||
(hit,) = base.glob(f"제{number:02d}장_*")
|
||||
return hit.name.split("_", 1)[1]
|
||||
|
||||
|
||||
def chapter_file(group: str, book: str, division: str, number: int) -> str:
|
||||
return f"{group}_{book}_{number:02d}장_{title(book, division, number)}.json"
|
||||
|
||||
|
||||
def _items(data: dict) -> str:
|
||||
return "표" if data.get("그룹") in ("소요량", "계수") else "줄"
|
||||
|
||||
|
||||
def _read(path: Path, exact: bool = True) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
return json.loads(text, parse_float=Decimal, parse_int=Decimal) if exact else json.loads(text)
|
||||
|
||||
|
||||
def old_view(name: str, folder: Path = MASTER) -> dict:
|
||||
"""옛 파일 이름으로 읽기(빌더용 · 수는 float) — 줄의 「열쇠」 = 원문번호 · 키·갈래 칸 뺌.
|
||||
준용·통합·연결 같은 참조는 키 그대로."""
|
||||
m = re.match(r"^(인력|기계)_(.+)\.json$", name)
|
||||
if m:
|
||||
file, slot, labels = MERGED[m.group(1)]
|
||||
data = _read(folder / file, exact=False)
|
||||
label = labels[m.group(2)]
|
||||
head = {"그룹": m.group(1), **data[slot][label]}
|
||||
rows = [r for r in data["줄"] if r.get(slot) == label]
|
||||
else:
|
||||
c = OLD_CHAPTER.match(name)
|
||||
path = folder / (chapter_file(c[1], c[2], c[3], int(c[4])) if c else name)
|
||||
data = _read(path, exact=False)
|
||||
head, rows, slot = (
|
||||
{k: v for k, v in data.items() if k != _items(data)},
|
||||
data[_items(data)],
|
||||
"",
|
||||
)
|
||||
drop = ("키", "원문번호", slot)
|
||||
out = [{"열쇠": r["원문번호"], **{k: v for k, v in r.items() if k not in drop}} for r in rows]
|
||||
return {**head, _items(head): out}
|
||||
|
||||
|
||||
def _old_ref(book: dict, text: str) -> str:
|
||||
"""옛 참조 `그룹:원문:열쇠` → 키(대장에 없으면 그대로 — check_master 가 잡음)."""
|
||||
group, src, number = (text.split(":", 2) + ["", ""])[:3]
|
||||
try:
|
||||
tid = table_id(group, src)
|
||||
except KeyError:
|
||||
return text
|
||||
return lookup(book).get((tid, number), text)
|
||||
|
||||
|
||||
def _convert(book: dict, row: dict, names: dict) -> dict:
|
||||
"""줄 속 옛 참조 — 준용·통합 · 후보 글(키 옆 이름) · 품셈재료 연결 · 시중물가 조달 나라장터 연결."""
|
||||
for col in ("준용", "통합"):
|
||||
value = row.get(col)
|
||||
if isinstance(value, str) and ":" in value:
|
||||
row[col] = _old_ref(book, value)
|
||||
if isinstance(row.get("후보"), str):
|
||||
|
||||
def one(m):
|
||||
key = _old_ref(book, m.group(0))
|
||||
return f"{key} {names.get(key, m.group(3))}" if KEY.fullmatch(key) else key
|
||||
|
||||
row["후보"] = _OLDREF.sub(one, row["후보"])
|
||||
link = row.get("연결")
|
||||
if isinstance(link, str) and not KEY.fullmatch(link):
|
||||
row["연결"] = lookup(book).get(("MT", link), link)
|
||||
value = row.get("값")
|
||||
supply = value.get("조달") if isinstance(value, dict) else None
|
||||
if isinstance(supply, dict) and str(supply.get("출처") or "").startswith("나라장터:"):
|
||||
code = supply["출처"].split(":", 1)[1]
|
||||
if not KEY.fullmatch(code): # 묶음은 새로 만듦 — 옛 줄과 나눠 쓰지 않게
|
||||
source = "나라장터:" + lookup(book).get(("MN", code), code)
|
||||
row["값"] = {**value, "조달": {**supply, "출처": source}}
|
||||
return row
|
||||
|
||||
|
||||
def _store():
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(MASTER.parents[1]))
|
||||
from M01_MasterData import M01_MasterData_Store as st
|
||||
|
||||
return st
|
||||
|
||||
|
||||
def dump_rows(head: dict, rows: list[dict]) -> str:
|
||||
"""요소 파일 — 머리는 들여쓰기 2칸 · 줄은 한 줄씩(길면 펼침)."""
|
||||
st = _store()
|
||||
out = ["{"] + [f" {st._atom(k)}: {st.dump(v, 2)}," for k, v in head.items()]
|
||||
out.append(' "줄": [')
|
||||
for i, row in enumerate(rows):
|
||||
line = st._flat(row)
|
||||
body = line if len(line) <= 450 else st.dump(row, 4, expand=True)
|
||||
out.append(" " + body + ("," if i < len(rows) - 1 else ""))
|
||||
return "\n".join(out + [" ]", "}"]) + "\n"
|
||||
|
||||
|
||||
def keep_text(text: str, old: dict, new: dict) -> str | None:
|
||||
"""글 모양 그대로 — 「열쇠」 칸 · 바뀐 글 · 머리 부문·차례만 갈음 · 읽어 맞대어 다르면 None."""
|
||||
rows_old, rows_new = old[_items(old)], new[_items(new)]
|
||||
spots = list(re.finditer(r'"열쇠": ("(?:[^"\\]|\\.)*"|[-\d.]+)', text))
|
||||
if len(spots) != len(rows_new):
|
||||
return None
|
||||
parts, at = [], 0
|
||||
for m, row in zip(spots, rows_new):
|
||||
number = json.dumps(row["원문번호"], ensure_ascii=False)
|
||||
parts += [text[at : m.start()], f'"키": "{row["키"]}", "원문번호": {number}']
|
||||
at = m.end()
|
||||
text = "".join(parts) + text[at:]
|
||||
swap = {}
|
||||
for a_row, b_row in zip(rows_old, rows_new):
|
||||
for k in b_row:
|
||||
if k in a_row and a_row[k] != b_row[k]:
|
||||
for a, b in zip(_strings(a_row[k]), _strings(b_row[k])):
|
||||
if a != b:
|
||||
swap[json.dumps(a, ensure_ascii=False)] = json.dumps(b, ensure_ascii=False)
|
||||
text = _STR.sub(lambda m: swap.get(m.group(0), m.group(0)), text)
|
||||
add = "".join(
|
||||
f" {json.dumps(k, ensure_ascii=False)}: {_store()._atom(new[k])},\n"
|
||||
for k in ("부문", "차례")
|
||||
if k in new and k not in old
|
||||
)
|
||||
if add:
|
||||
text = re.sub(r'(\n "원문": "[^"]*",\n)', lambda m: m.group(1) + add, text, count=1)
|
||||
same = json.loads(text, parse_float=Decimal, parse_int=Decimal) == new
|
||||
return text if same else None
|
||||
|
||||
|
||||
def _strings(v):
|
||||
if isinstance(v, str):
|
||||
yield v
|
||||
elif isinstance(v, dict):
|
||||
for x in v.values():
|
||||
yield from _strings(x)
|
||||
elif isinstance(v, list):
|
||||
for x in v:
|
||||
yield from _strings(x)
|
||||
|
||||
|
||||
def order_chapters(folder: Path = MASTER) -> None:
|
||||
"""장 파일 머리 「차례」 — 그룹마다 산림 1~14장 → 건설 공통 · 토목 · 건축 · 기계설비 · 유지관리."""
|
||||
found = {}
|
||||
for path in folder.glob("*.json"):
|
||||
m = CHAPTER.match(path.name)
|
||||
if m:
|
||||
data = _read(path)
|
||||
division = data.get("부문") or ""
|
||||
rank = (m[2] != "산림품셈", DIVISIONS.index(division) if division else -1, int(m[3]))
|
||||
found.setdefault(m[1], []).append((rank, path.name, data.get("차례")))
|
||||
for items in found.values():
|
||||
for i, (_, name, had) in enumerate(sorted(items), start=1):
|
||||
if had != i:
|
||||
path = folder / name
|
||||
text = re.sub(
|
||||
r'"차례": \d+', f'"차례": {i}', path.read_text(encoding="utf-8"), count=1
|
||||
)
|
||||
path.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def adopt(folder: Path = MASTER) -> list[str]:
|
||||
"""빌더가 옛 모양으로 쓴 파일 → 새 틀(키 · 원문번호 · 한 테이블 · 장 파일 이름) · 옛 파일은 지움.
|
||||
키는 대장의 (테이블ID, 원문번호) 키 · 없으면 다음 번호. 로직은 키로 적어 넣으므로 받지 않음."""
|
||||
book = load_book(folder / BOOK.name)
|
||||
names = {}
|
||||
if (folder / "인력.json").is_file():
|
||||
names = {r["키"]: r.get("이름") for r in _read(folder / "인력.json")["줄"]}
|
||||
done = []
|
||||
for path in sorted(folder.glob("*.json")):
|
||||
if path.name.startswith("_"):
|
||||
continue
|
||||
data = _read(path)
|
||||
rows = data.get(_items(data)) or []
|
||||
if not rows or "열쇠" not in rows[0]:
|
||||
continue
|
||||
group, src = data["그룹"], data["원문"]
|
||||
if group == "로직":
|
||||
raise SystemExit(f"{path.name} · 로직은 키로 적음 — 옛 모양은 받지 않음")
|
||||
c = OLD_CHAPTER.match(path.name)
|
||||
target = chapter_file(c[1], c[2], c[3], int(c[4])) if c else path.name
|
||||
target = MERGED[group][0] if group in MERGED else target
|
||||
if c and c[3]: # 건설품셈 원문번호 앞에 부문(「기계설비 7-1-1 …」)
|
||||
head = c[3] + " "
|
||||
rows = [
|
||||
r if str(r["열쇠"]).startswith(head) else {**r, "열쇠": head + r["열쇠"]}
|
||||
for r in rows
|
||||
]
|
||||
data[_items(data)] = rows
|
||||
stamped = [
|
||||
_convert(book, r, names) for r in stamp(book, table_id(group, src), rows, target)
|
||||
]
|
||||
if group in MERGED:
|
||||
file, slot, labels = MERGED[group]
|
||||
label = labels[src]
|
||||
whole = _read(folder / file) if (folder / file).is_file() else {"그룹": group}
|
||||
heads = whole.setdefault(slot, {})
|
||||
heads[label] = {k: v for k, v in data.items() if k not in ("그룹", "줄")}
|
||||
stamped = [
|
||||
{"키": r["키"], "원문번호": r["원문번호"], slot: label, **r} for r in stamped
|
||||
]
|
||||
keep = [r for r in whole.get("줄", []) if r.get(slot) != label]
|
||||
rows_all = sorted(keep + stamped, key=lambda r: r["키"])
|
||||
text = dump_rows({k: v for k, v in whole.items() if k != "줄"}, rows_all)
|
||||
else:
|
||||
head = {"그룹": group, "원문": src}
|
||||
if c and c[3]:
|
||||
head["부문"] = c[3]
|
||||
if c:
|
||||
head["차례"] = Decimal(0) # order_chapters 가 채움
|
||||
rest = {k: v for k, v in data.items() if k not in head}
|
||||
new = {**head, **rest, _items(data): stamped}
|
||||
old_text = path.read_text(encoding="utf-8")
|
||||
text = keep_text(old_text, data, new) or _store().dump(new)
|
||||
(folder / target).write_text(text, encoding="utf-8", newline="\n")
|
||||
if target != path.name:
|
||||
path.unlink()
|
||||
done.append(target)
|
||||
save_book(finish(book), folder / BOOK.name)
|
||||
order_chapters(folder)
|
||||
return done
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"비고": [
|
||||
"다산소프트 EST 2026-07 자재 · 개발용 · 배포 전 판권 구매",
|
||||
"값 = 시중(물가지 다섯 · 면수) · 조달(값 · 출처 · 기준) — 기본가·채택값 없음 · 쓰는 곳이 값을 고르고 출처를 밝힘",
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<열쇠>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)"
|
||||
"조달 출처 「가격정보」 = ESTX 가격정보 칸의 조달청 관급 단가 · 「나라장터:<키>」 = 재료_나라장터자재 줄로 이음(값 안 베낌)"
|
||||
],
|
||||
"줄": [
|
||||
{ "키": "MT000001", "원문번호": "M0003039885", "이름": "휘 발 유", "규격": "", "단위": "ℓ", "값": {"시중": {"가격정보": 1825, "물가자료": null, "유통물가": null, "물가정보": null, "거래가격": null, "면수": {"가격정보": "2026.7", "물가자료": null, "유통물가": null, "물가정보": null, "거래가격": null}}, "조달": {"값": null, "출처": null, "기준": null}}, "출처": "시중물가 M0003039885" },
|
||||
|
||||
Reference in New Issue
Block a user