feat(M01): 일위대가 조합 마스터 — 데이터 · 엔진 · 서버 길(화면은 아직)
PLAN 4-2. 조합 = 관리자가 이름을 붙이고 단가산출 로직을 골라 담은 묶음. 마스터에는 담은 로직 목록만 두고 수량·비율·계산은 두지 않음(4-0 확정 · 계산은 프로젝트 몫). - 새 파일 `일위대가조합.json`(빈 채 · 줄은 사용자가 화면에서 만듦) · 테이블ID `UA` · 원문이 없어 머리 `원문` 과 줄 `원문번호` 를 두지 않음(`mk.NO_NUMBER` 에 `UA`) - 엔진 `scripts/master_combo.py` — 검사 넷(없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합이 조합을 담음) · 「이 로직을 쓰는 조합」 거꾸로 찾기 · 미리 보기 계산 - `check_master.py` 에 「조합」 갈래 더함 · 저장 전 검사(`check_saved`)에도 얹어 422 로 막음 - 서버 `M01_MasterData_Store_Combo.py` — `GET /combos`·`/combo`·`/logic/combos` · `POST /combo/new`·`/edit`·`/delete`·`/preview`(저장 없음) - 조합은 요소 그룹이 아님 — `/groups` 는 여덟 그대로 두고 제 길로만 다룸 - 틀 `_틀.md` 10장 · 계약 `_화면_계약.md` 9장 · 시험 `test_m01_combo.py` 17 검증 — 견본 조합(13-4-1 · 13-4-2 · 13-2-1) 미리 보기 합계가 그 로직 셋의 시험 계산을 비목별로 더한 값과 같음 · 못 도는 줄은 그 줄만 멈추고 나머지 합계는 그대로 · 저장 전 조합 줄로도 미리 보기 되고 아무것도 안 생김 · 검사 넷 다 422 · 낡은 판본 409 · 정본 로직 안 바뀜 · `check_master 틀`·`조합` 0건 · pytest 44 통과(조합 17 + api 27) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WaS48Nvs26o3aFUpcfJxVv
This commit is contained in:
@@ -255,4 +255,39 @@
|
||||
|
||||
## 9. 검사
|
||||
|
||||
- 고친 뒤 `scripts/check_master.py` — 틀 · 본문 대조 · 로직 검사 · 키(모양 · 겹침 · 인력·재료의 원문번호 겹침 · 대장 · 끊긴 키). 로직은 `계산` 으로 손 계산과 맞댐.
|
||||
- 고친 뒤 `scripts/check_master.py` — 틀 · 본문 대조 · 로직 검사 · 조합 검사 · 키(모양 · 겹침 · 인력·재료의 원문번호 겹침 · 대장 · 끊긴 키). 로직은 `계산` 으로 손 계산과 맞댐.
|
||||
|
||||
## 10. 일위대가 조합 파일
|
||||
|
||||
`일위대가조합.json` 하나 — 관리자가 이름을 붙이고 **단가산출 로직을 골라 담은 묶음**. 원문 근거가 없어 원문·장으로 나누지 않음(머리에 `원문` 을 두지 않음).
|
||||
|
||||
```json
|
||||
{
|
||||
"그룹": "일위대가조합",
|
||||
"판": "2026",
|
||||
"줄": [
|
||||
{
|
||||
"키": "UA000001",
|
||||
"이름": "돌쌓기 한 벌",
|
||||
"구분": "자체",
|
||||
"상세구분": "13장 구조물",
|
||||
"단위": "㎡",
|
||||
"담은로직": [
|
||||
{ "로직": "GF000219", "메모": "인력 메쌓기" },
|
||||
{ "로직": "GF000220", "메모": null }
|
||||
],
|
||||
"출처": "자체",
|
||||
"소유": "현장",
|
||||
"비고": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 열 한 벌 — `키` · `이름` · `구분` · `상세구분` · `단위` · `담은로직` · `출처` · `소유` · `비고`(차례까지 이대로 · 안 채운 칸은 `null`).
|
||||
- **담은 로직 목록만 둠** — 수량 · 비율 · 계산 칸을 두지 않음. 로직마다 하는 일이 달라 수량은 로직마다 따로 들어가고, 그 대입과 합계는 프로젝트 몫(B08 · B09).
|
||||
- `담은로직` 줄 = `{로직, 메모}` 뿐 · 목록 차례가 곧 조합 안 차례.
|
||||
- `키` = `UA` + 6자리 — 대장 `_키대장.json` 의 `UA` 다음 번호(서버가 줌). 원문이 없어 **줄에 `원문번호` 를 두지 않음**(대장 열쇠는 조합 이름).
|
||||
- `소유` = `현장`(기본) · `공용` · `출처` = 「자체」.
|
||||
- 검사(`check_master.py 조합` · 엔진 `scripts/master_combo.py`) — 없는 로직 키 · 단가산출 로직이 아닌 키 · 같은 로직 두 번 · 빈 조합 · **조합이 조합을 담음**(1단만).
|
||||
- 「이 로직을 쓰는 조합」 은 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음(`master_combo.using`) — 조합은 관리자가 그때그때 만드는 것이라 박아 두면 낡음. 소요량 「용도」 와 결은 같되 자리가 다름.
|
||||
|
||||
@@ -157,3 +157,32 @@
|
||||
- `save` = true 면 `POST /logic/new` 와 같은 자리에 저장(키 `GX` 는 서버 · 소유 `owner`) → 답에 `{file, version, key, logic}` 이 더 옴. 문제가 하나라도 있으면 422 `{detail: {errors, 문제}}` 로 아무것도 안 씀.
|
||||
- 옮기는 법 — 돈 로직(`결과단위` 가 「원」 으로 시작)은 결과 식을 `+` 로 갈라 항마다: 마스터요소 변수 하나면 **호표 줄**(나머지 곱이 `수량`) · 비목만 정한 변수면 **덧줄** · 그 밖은 문제 `항`. 나머지 정의 줄은 `중간`(쓰는 차례대로). 돈 아닌 로직은 호표 없이 `중간` + `결과` 식.
|
||||
- 표 찾기는 글자로 치지 않음 — 변수 하나로 두고 `무엇` 을 `표찾기` 로 정할 때 모달이 `찾기` 묶음을 채움.
|
||||
|
||||
## 9. 일위대가 조합
|
||||
|
||||
관리자가 이름을 붙이고 **단가산출 로직을 골라 담은 묶음**. 파일 모양은 `_틀.md` 10장 · 엔진은 `scripts/master_combo.py`.
|
||||
|
||||
- 자리 = `일위대가조합.json` 한 파일 · 없으면 서버가 머리(`{"그룹": "일위대가조합", "판": <올해>, "줄": []}`)와 함께 만듦.
|
||||
- 키 = 서버가 `_키대장.json` 에서 `UA` 다음 번호로 줌 — 화면은 키를 만들지 않음.
|
||||
- 조합은 **담은 로직 목록만** 지님 — 수량 · 비율 · 계산은 두지 않음(프로젝트 몫 · B08 · B09).
|
||||
- 조합은 **요소 그룹이 아님** — `GET /groups` 목록에 끼지 않고 `/rows`·`/groups/{group}/files` 로 다루지 않음(줄 모양이 요소와 아주 다름). 아래 제 길로만.
|
||||
|
||||
| 길 | 받음 | 줌 |
|
||||
| --------------------- | -------------------------------- | -------------------------------------------------------- |
|
||||
| `GET /combos` | `sub?` `detail?` `q?` `owner?` | `{combos: […]}` — 줄마다 `count` · `blocked` · `reasons` |
|
||||
| `GET /combo` | `key` | `{file, version, combo, 줄: […], blocked, reasons}` |
|
||||
| `GET /logic/combos` | `key` | `{key, combos: […]}` — 이 로직을 담은 조합 |
|
||||
| `POST /combo/new` | `{combo, owner?}` | `{file, version, key, combo}` |
|
||||
| `POST /combo/edit` | `{key, version, combo, owner?}` | `{file, version, combo}` |
|
||||
| `POST /combo/delete` | `{key, version}` | `{file, version}` |
|
||||
| `POST /combo/preview` | `{combo, inputs}` | `{키, 이름, 단위, 줄: […], 노무비, 재료비, 경비, 계, 멈춤}` |
|
||||
|
||||
- `combo` = 조합 한 줄 통째(`_틀.md` 10장) · `키`·`소유` 는 보내도 서버 것이 이김 · 안 준 칸은 `null` 로 채워 열 한 벌을 맞춤.
|
||||
- `version` = `GET /combo` 가 준 그 파일 판본 — 다르면 409 `{detail: {stale: ["일위대가조합.json"]}}`.
|
||||
- `GET /combo` 의 `줄` = 담은 로직에 이름을 붙인 목록 `{차례, 로직, 메모, ok, 이름, 원문번호, 구분, 상세구분, 결과단위, 출처}` — 없는 키는 `ok` false 와 `까닭`.
|
||||
- `GET /logic/combos` 의 `combos` 줄 = `{키, 이름, 구분, 상세구분, 단위, 소유, 차례}` — `차례` 는 그 조합 안 몇 번째인지(여러 번이면 여럿). 로직 줄에 칸으로 박지 않고 물을 때 훑음.
|
||||
- `preview` 는 **보기만** — 마스터에 아무것도 쓰지 않음. `combo` 는 저장된 키(글자)여도 되고 저장 전 조합 줄(묶음)이어도 됨.
|
||||
- `inputs` = `{로직 키: {입력 이름: 값}}` — 담은 로직마다 그 자리에서 따로 받음(로직마다 하는 일이 달라 수량이 따로 들어감).
|
||||
- `줄` = `{차례, 로직, 메모, 이름, 원문번호, 결과단위, 구분, 상세구분, 출처, ok, 갈래, 노무비, 재료비, 경비, 계}` · 돈이 아닌 로직은 `갈래` 「결과」 와 `결과` 수 하나(비목이 없어 합계에 안 얹음).
|
||||
- 못 도는 줄은 통째로 멈추지 않고 그 줄만 `ok` false · `까닭` · `멈춤` 에 한 줄 — 나머지 줄의 합계는 그대로 냄.
|
||||
- 답 — 200 · 400(틀린 몸 · 이름 없음 · 모르는 소유) · 404(없는 키) · 409(낡은 판본) · 422 `{detail: {errors: […]}}`(검사 걸림 — 없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음).
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""마스터 데이터 검사 도구 (`resources/master_data/_틀.md`).
|
||||
|
||||
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/check_master.py [틀|본문|로직] [파일 …]
|
||||
돌리기: ./venv/Scripts/python.exe resources/master_data/scripts/check_master.py [틀|본문|로직|조합] [파일 …]
|
||||
./venv/Scripts/python.exe resources/master_data/scripts/check_master.py 계산 GF000123 뒷길이=35 돌=견치돌 …
|
||||
|
||||
(1) 틀 — 파일 이름 · 머리 칸 · 줄 칸 · 출처 모양 · 키 모양·겹침 · 대장
|
||||
(2) 본문 — 표형 요소의 출처 절을 본문 md 에서 찾아 수를 맞댐 · 결손(본문 표에 있고 요소에 없음)·허구(요소에 있고 본문 절에 없음)
|
||||
(3) 로직 — 변수가 가리키는 요소·표·칸 · 찾기 조건 이름 · 로직 입력 · 재료 고르기 조건 · 돌고 도는 참조 · 끊긴 키
|
||||
(4) 계산 — 로직 키와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
|
||||
(4) 조합 — 일위대가 조합이 담은 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음 (`master_combo.py`)
|
||||
(5) 계산 — 로직 키와 입력값으로 호표 줄별 수량·단가·금액 · 비목 합 (`master_formula.py`)
|
||||
수 뽑기는 `resources/knowledge/original/_pipeline/pum_md_tool.py` 의 것을 씀.
|
||||
"""
|
||||
|
||||
@@ -26,6 +27,7 @@ COST = ROOT / "resources/knowledge/original/원가계산"
|
||||
sys.path.insert(0, str(HERE))
|
||||
sys.path.insert(0, str(ROOT / "resources/knowledge/original/_pipeline"))
|
||||
|
||||
import master_combo as mcb # noqa: E402
|
||||
import master_formula as mf # noqa: E402
|
||||
import master_keys as mk # noqa: E402
|
||||
from pum_md_tool import _NUM, md_lines # noqa: E402
|
||||
@@ -173,9 +175,13 @@ def master(folder: Path = MASTER) -> mf.Master:
|
||||
|
||||
|
||||
def check_saved(files: dict[str, dict], changed: list[str], book: dict | None = None) -> list[str]:
|
||||
"""저장 전 검사(M01) — 고친 파일의 틀 + 모든 로직의 변수 · 키. `files` = 폴더 전부(고친 뒤) · `book` = 키 대장."""
|
||||
"""저장 전 검사(M01) — 고친 파일의 틀 + 모든 로직의 변수 · 키 + 조합.
|
||||
|
||||
`files` = 폴더 전부(고친 뒤) · `book` = 키 대장.
|
||||
"""
|
||||
whole = mf.Master(files)
|
||||
out = [x for name in changed for x in check_form(name, files[name])]
|
||||
return out + check_logics(files, mf.Master(files), book)
|
||||
return out + check_logics(files, whole, book) + mcb.check_all(files, whole)
|
||||
|
||||
|
||||
def calc(files: dict[str, dict], ref: str, given: dict) -> dict:
|
||||
@@ -215,7 +221,39 @@ def _head(name: str, data: dict, m) -> list[str]:
|
||||
return out
|
||||
|
||||
|
||||
def check_combo_form(name: str, data: dict) -> list[str]:
|
||||
"""조합 파일 틀 — 원문·장이 없는 한 파일(관리자가 만드는 것이라 원문 근거가 없음)."""
|
||||
out = []
|
||||
if data.get("그룹") != mcb.GROUP:
|
||||
out.append(f"{name} · 머리 그룹이 「{mcb.GROUP}」 아님")
|
||||
if "판" not in data:
|
||||
out.append(f"{name} · 머리 「판」 없음")
|
||||
if "원문" in data:
|
||||
out.append(f"{name} · 조합은 원문이 없음 — 머리 「원문」 을 두지 않음")
|
||||
rows_ = data.get("줄")
|
||||
if not isinstance(rows_, list):
|
||||
return out + [f"{name} · 머리 「줄」 목록 없음"]
|
||||
for row in rows_:
|
||||
key = str(row.get("키"))
|
||||
where = f"{name} · {key} {row.get('이름') or ''}".strip()
|
||||
if not mk.KEY.fullmatch(key) or key[:2] != mcb.TID:
|
||||
out.append(f"{where} · 키 모양이 「{mcb.TID}」 + 6자리 아님")
|
||||
if "원문번호" in row:
|
||||
out.append(f"{where} · 원문번호는 줄에 두지 않음 — 대장에만")
|
||||
if tuple(row) != mcb.COLS:
|
||||
out.append(f"{where} · 열이 한 벌이 아님 — 「{' · '.join(mcb.COLS)}」")
|
||||
if not str(row.get("이름") or "").strip():
|
||||
out.append(f"{where} · 「이름」 없음")
|
||||
if row.get("소유") not in mcb.OWNERS:
|
||||
out.append(f"{where} · 소유 「{row.get('소유')}」 — {' · '.join(mcb.OWNERS)} 아님")
|
||||
if not _source_ok(row.get("출처")):
|
||||
out.append(f"{where} · 출처 모양 「{row.get('출처')}」")
|
||||
return out
|
||||
|
||||
|
||||
def check_form(name: str, data: dict) -> list[str]:
|
||||
if name == mcb.FILE:
|
||||
return check_combo_form(name, data)
|
||||
m = _FILE.match(name)
|
||||
if not m:
|
||||
return [
|
||||
@@ -632,6 +670,8 @@ def main(argv: list[str]) -> int:
|
||||
report["본문"] = check_body(files)
|
||||
if mode in ("로직", "전부"):
|
||||
report["로직"] = check_logics(files, whole)
|
||||
if mode in ("조합", "전부"):
|
||||
report["조합"] = mcb.check_all(files, whole)
|
||||
if mode in ("단위", "전부"):
|
||||
report["단위"] = check_units(files, whole)
|
||||
for title, found in report.items():
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""일위대가 조합 — 단가산출 로직을 골라 담은 묶음 (`_틀.md` 10장 · 계약 `_화면_계약.md` 9장).
|
||||
|
||||
조합은 **담은 로직 목록만** 지님 — 수량 · 비율 · 계산은 두지 않음(프로젝트 몫).
|
||||
미리 보기(`run`)는 담은 로직마다 그 자리에서 받은 입력값으로 값을 뽑아
|
||||
비목별로 갈라 합침 — 저장하지 않음.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import master_formula as mf
|
||||
import master_keys as mk
|
||||
|
||||
GROUP = "일위대가조합"
|
||||
FILE = "일위대가조합.json"
|
||||
TID = "UA"
|
||||
OWNERS = ("현장", "공용")
|
||||
# 줄 열 한 벌 — 차례까지 이대로(`_틀.md` 10장)
|
||||
COLS = ("키", "이름", "구분", "상세구분", "단위", "담은로직", "출처", "소유", "비고")
|
||||
ITEM_COLS = ("로직", "메모")
|
||||
|
||||
|
||||
def rows(files: dict[str, dict]):
|
||||
"""(파일 이름, 조합 줄) — 조합 파일은 하나지만 그룹으로 찾음."""
|
||||
for name, data in files.items():
|
||||
if data.get("그룹") == GROUP:
|
||||
for row in data.get("줄") or []:
|
||||
yield name, row
|
||||
|
||||
|
||||
def items(row: dict) -> list[dict]:
|
||||
"""담은 로직 줄 목록 — 차례가 곧 조합 안 차례."""
|
||||
got = row.get("담은로직")
|
||||
return got if isinstance(got, list) else []
|
||||
|
||||
|
||||
def logic_keys(row: dict) -> list[str]:
|
||||
return [str(one.get("로직") or "") for one in items(row) if isinstance(one, dict)]
|
||||
|
||||
|
||||
# ── 검사 ──────────────────────────────────────────────────────────────
|
||||
def check_row(whole: mf.Master, row: dict) -> list[str]:
|
||||
"""조합 한 줄 — 빈 조합 · 없는 로직 키 · 같은 로직 두 번 · 조합을 담음(1단만)."""
|
||||
where = f"{row.get('키')} {row.get('이름') or ''}".strip()
|
||||
got = items(row)
|
||||
out = [] if got else [f"{where} · 빈 조합 — 담은 로직이 없음"]
|
||||
if row.get("담은로직") is not None and not isinstance(row.get("담은로직"), list):
|
||||
out.append(f"{where} · 「담은로직」 이 목록 아님")
|
||||
seen: set[str] = set()
|
||||
for at, one in enumerate(got, start=1):
|
||||
if not isinstance(one, dict):
|
||||
out.append(f"{where} 줄{at} · 담은 로직 줄이 묶음 아님 「{one}」")
|
||||
continue
|
||||
extra = set(one) - set(ITEM_COLS)
|
||||
if extra:
|
||||
out.append(f"{where} 줄{at} · 모르는 칸 {sorted(extra)}")
|
||||
key = str(one.get("로직") or "")
|
||||
if not mk.KEY.fullmatch(key):
|
||||
out.append(f"{where} 줄{at} · 로직 키 모양 아님 「{key}」")
|
||||
continue
|
||||
if mk.id_of(key) == TID:
|
||||
out.append(f"{where} 줄{at} · 조합은 조합을 담지 못함 「{key}」 — 단가산출 로직만")
|
||||
continue
|
||||
try:
|
||||
whole.logic(key)
|
||||
except mf.FormulaError as e:
|
||||
out.append(f"{where} 줄{at} · {e}")
|
||||
continue
|
||||
if key in seen:
|
||||
out.append(f"{where} 줄{at} · 같은 로직을 두 번 담음 「{key}」")
|
||||
seen.add(key)
|
||||
return out
|
||||
|
||||
|
||||
def check_all(files: dict[str, dict], whole: mf.Master) -> list[str]:
|
||||
"""조합 전부 — 파일 이름을 앞에 붙임."""
|
||||
return [f"{name} · {x}" for name, row in rows(files) for x in check_row(whole, row)]
|
||||
|
||||
|
||||
def using(files: dict[str, dict], key: str) -> list[dict]:
|
||||
"""「이 로직을 쓰는 조합」 — 소요량 「용도」 와 결은 같되 자리가 다름.
|
||||
|
||||
조합은 관리자가 그때그때 만드는 것이라 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음
|
||||
(박아 두면 조합을 고칠 때마다 낡음).
|
||||
"""
|
||||
out = []
|
||||
for _, row in rows(files):
|
||||
at = [i for i, k in enumerate(logic_keys(row), start=1) if k == key]
|
||||
if at:
|
||||
out.append(
|
||||
{
|
||||
**{k: row.get(k) for k in ("키", "이름", "구분", "상세구분", "단위", "소유")},
|
||||
"차례": at,
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# ── 미리 보기 ─────────────────────────────────────────────────────────
|
||||
def run(master: mf.Master, row: dict, given: dict[str, dict] | None = None) -> dict:
|
||||
"""담은 로직마다 그 자리의 입력값으로 값을 뽑아 비목별로 갈라 합침 — 저장하지 않음.
|
||||
|
||||
`given` = {로직 키: {입력 이름: 값}} · 못 도는 줄은 까닭을 적고 합계에서 뺌(통째로 멈추지 않음).
|
||||
돈이 아닌 로직(결과 수 하나)은 비목이 없어 합계에 얹지 않고 결과만 보임.
|
||||
"""
|
||||
given = given or {}
|
||||
sums = dict.fromkeys(mf.COST_ITEMS, Decimal(0))
|
||||
lines, stuck = [], []
|
||||
for at, one in enumerate(items(row), start=1):
|
||||
key = str(one.get("로직") or "") if isinstance(one, dict) else ""
|
||||
head = {"차례": at, "로직": key, "메모": one.get("메모") if isinstance(one, dict) else None}
|
||||
try:
|
||||
target = master.logic(key)
|
||||
except mf.FormulaError as e:
|
||||
lines.append({**head, "ok": False, "까닭": str(e)})
|
||||
stuck.append(f"{key} · {e}")
|
||||
continue
|
||||
head.update(
|
||||
{k: target.get(k) for k in ("이름", "원문번호", "결과단위", "구분", "상세구분", "출처")}
|
||||
)
|
||||
try:
|
||||
got = mf.run(master, key, dict(given.get(key) or {}))
|
||||
except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e:
|
||||
reason = str(e) or type(e).__name__
|
||||
lines.append({**head, "ok": False, "까닭": reason})
|
||||
stuck.append(f"{key} {target.get('이름')} · {reason}")
|
||||
continue
|
||||
if "결과" in got: # 돈 아닌 로직 — 비목이 없어 합계에 안 얹음
|
||||
lines.append({**head, "ok": True, "갈래": "결과", "결과": got["결과"]})
|
||||
continue
|
||||
part = {k: got[k] for k in mf.COST_ITEMS}
|
||||
for k, v in part.items():
|
||||
sums[k] += v
|
||||
lines.append({**head, "ok": True, "갈래": "돈", **part, "계": got["계"]})
|
||||
return {"줄": lines, **sums, "계": sum(sums.values()), "멈춤": stuck}
|
||||
@@ -13,6 +13,8 @@ from decimal import ROUND_DOWN, ROUND_HALF_UP, ROUND_UP, Decimal
|
||||
|
||||
import master_keys as mk
|
||||
|
||||
# 식이 부르는 요소 그룹 — 일위대가 조합(`master_combo`)은 식에 안 들어와 여기 두지 않음
|
||||
# (조합은 `/rows`·`/files` 같은 요소 길이 아니라 제 길 `/combos`·`/combo` 로만 다룸)
|
||||
GROUPS = ("인력", "재료", "기계", "소요량", "계수", "환율", "요율", "로직")
|
||||
TABLE_GROUPS = ("소요량", "계수")
|
||||
TABLE_IDS = tuple(t for t in mk.IDS if mk.GROUP_OF[t] in TABLE_GROUPS)
|
||||
|
||||
@@ -31,11 +31,13 @@ TABLES = {
|
||||
("로직", "산림품셈"): "GF",
|
||||
("로직", "건설품셈"): "GC",
|
||||
("로직", "자체"): "GX",
|
||||
("일위대가조합", None): "UA",
|
||||
}
|
||||
GROUP_OF = {tid: group for (group, _), tid in TABLES.items()}
|
||||
IDS = tuple(GROUP_OF)
|
||||
# 줄에 원문번호를 두지 않는 테이블 — 번호가 남의 품번이라 이름 몫을 못 함(대조는 대장에만)
|
||||
NO_NUMBER = ("MT",)
|
||||
# 줄에 원문번호를 두지 않는 테이블 — 자재품목은 번호가 남의 품번이라 이름 몫을 못 하고,
|
||||
# 조합은 원문이 없어 번호 자체가 없음(대조는 대장에만)
|
||||
NO_NUMBER = ("MT", "UA")
|
||||
|
||||
|
||||
def table_id(group: str, book: str | None) -> str:
|
||||
@@ -172,6 +174,17 @@ ROW_ORDER = {
|
||||
"결과",
|
||||
"끝수",
|
||||
),
|
||||
"일위대가조합": (
|
||||
"키",
|
||||
"이름",
|
||||
"구분",
|
||||
"상세구분",
|
||||
"단위",
|
||||
"담은로직",
|
||||
"출처",
|
||||
"소유",
|
||||
"비고",
|
||||
),
|
||||
}
|
||||
OLD_CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈)_(\D*)(\d+)장\.json$")
|
||||
CHAPTER = re.compile(r"^(소요량|계수|로직)_(산림품셈|건설품셈|자체)_(\d\d)장_.+\.json$")
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"그룹": "일위대가조합",
|
||||
"판": "2026",
|
||||
"비고": ["조합 = 단가산출 로직을 골라 담은 묶음 · 수량·비율·계산은 두지 않음(프로젝트 몫)"],
|
||||
"줄": []
|
||||
}
|
||||
Reference in New Issue
Block a user