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:
2026-09-21 23:50:42 +09:00
co-authored by Claude Sonnet 5
parent 9d6e60f08c
commit a4b3e7d77a
11 changed files with 800 additions and 7 deletions
+65
View File
@@ -12,6 +12,7 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
from M01_MasterData import M01_MasterData_Store_Make as make
router = APIRouter(prefix="/api/m01", tags=["M01 MasterData"])
@@ -80,6 +81,32 @@ class FormulaPreview(BaseModel):
별칭: list[dict[str, Any]] = []
class NewCombo(BaseModel):
"""일위대가 조합 — 담은 단가산출 로직 목록만(수량·비율·계산은 안 둠)."""
combo: dict[str, Any]
owner: str = combo_store.OWNERS[0]
class EditCombo(BaseModel):
key: str
version: str
combo: dict[str, Any]
owner: str = ""
class DeleteCombo(BaseModel):
key: str
version: str
class PreviewCombo(BaseModel):
"""저장 전 미리 보기 — `combo` 는 조합 줄 통째 또는 저장된 키 · `inputs` 는 로직마다."""
combo: dict[str, Any] | str
inputs: dict[str, dict[str, Any]] = {}
class DraftLogic(BaseModel):
text: str # 줄로 적은 식 — 줄마다 「이름 = 식」
decided: dict[str, Any] = {} # 변수마다 정한 것 {이름: {무엇, 단위, 비목, 요소·찾기·값}}
@@ -263,3 +290,41 @@ def post_logic_formula_preview(body: FormulaPreview) -> dict:
def post_logic_draft(body: DraftLogic) -> dict:
"""식으로 새 로직 만들기 — 변수 뽑기 · 검사 · 로직 줄로 옮기기(`save` 면 저장까지)."""
return _call(make.logic_draft, body.text, body.decided, body.head, body.save, body.owner)
# ── 일위대가 조합(계약 9장) ────────────────────────────────────────────
@router.get("/combos")
def get_combos(sub: str = "", detail: str = "", q: str = "", owner: str = "") -> dict:
return {"combos": _call(combo_store.combos, sub, detail, q, owner)}
@router.get("/combo")
def get_combo(key: str) -> dict:
return _call(combo_store.combo, key)
@router.get("/logic/combos")
def get_logic_combos(key: str) -> dict:
"""이 단가산출 로직을 담은 조합 — 로직 쪽에서 거꾸로 봄."""
return _call(combo_store.combos_of_logic, key)
@router.post("/combo/new")
def post_combo_new(body: NewCombo) -> dict:
return _call(combo_store.combo_new, body.combo, body.owner)
@router.post("/combo/edit")
def post_combo_edit(body: EditCombo) -> dict:
return _call(combo_store.combo_edit, body.key, body.version, body.combo, body.owner)
@router.post("/combo/delete")
def post_combo_delete(body: DeleteCombo) -> dict:
return _call(combo_store.combo_delete, body.key, body.version)
@router.post("/combo/preview")
def post_combo_preview(body: PreviewCombo) -> dict:
"""담은 로직마다 값을 뽑아 비목별로 갈라 합계 — 저장하지 않음."""
return _call(combo_store.combo_preview, body.combo, body.inputs)