"""M02 일위대가 조합 층 API(PLAN 27-2a) — `/api/m02/layers/{layer}/combos`(옛 `/api/m01/combo*`). ⚠ 등록은 `main.py`(로그인만) · 층 권한은 `M02_Layer_Access`(`Router_Items._root`) 한 규칙: system 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자(마스터 조합 · 키 UA) company 읽기 = 같은 회사 · 쓰기 = 회사 관리자(키 UC{회사}-######) personal 읽기 · 쓰기 = 본인(키 UP{사용자}-######) 저장소 = `M02_MasterTemplete_Combo_Store`. 담을 로직 고르기 · 미리 보기가 읽는 단가산출 로직(M01 길은 시스템 관리자 전용)은 `/api/m02/logic-lib/…` 읽기만(로그인) — 모양은 M01 길과 같음. """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from common_util.common_util_auth import verify_session from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_MasterTemplete_Combo_Copy as combo_copy from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Combos"]) class ComboBody(BaseModel): combo: dict[str, Any] 판: str | None = None # 새로 만들 때 빈 글 = 지금 판 · 고칠 때 어긋나면 409 class ComboTreeBody(BaseModel): """`{op: add-folder|rename|move|delete, 경로?, 이름?, 새경로?, 키?, 판}` — 폴더 깊이 2.""" op: str 경로: list[str] | None = None 이름: str | None = None 새경로: list[str] | None = None 키: str | None = None 판: str class TreeManyBody(BaseModel): """여러 개 옮기기 · 지우기(PLAN 30-3) — `ops` = 나무 고침(`ComboTreeBody` 모양) · 저장 한 번.""" 판: str ops: list[dict[str, Any]] class PasteBody(BaseModel): """층 사이 복사(PLAN 30-3) — 원본 층(`from_layer` · `from_owner`)의 조합 `키` · 폴더 `폴더` → 이 층 `경로` 폴더에 새 키로. `판` = 이 층 조합 표 판.""" from_layer: Layer from_owner: str | None = None 키: list[str] = [] 폴더: list[list[str]] = [] 경로: list[str] = [] 판: str = "" class PreviewBody(BaseModel): """저장 전 미리 보기 — `combo` 는 조합 줄 통째 또는 그 층 키 · `inputs` 는 로직마다.""" combo: dict[str, Any] | str inputs: dict[str, dict[str, Any]] = {} @router.get("/combos/all") def get_all(session: Session = Depends(verify_session)) -> list[dict]: """고르기(use-combo · 갈라 내기) — 세 층 `[{layer, owner, 판, 폴더, 줄}]` · 회사 없음 빈 층.""" out = [] for layer in ("system", "company", "personal"): who = None if layer == "system" else access.own_owner(session, layer) if layer != "system" and who is None: out.append({"layer": layer, "owner": None, "판": "", "폴더": [], "줄": []}) continue got = _call(_root(session, layer, who), combos.listing) out.append({"layer": layer, "owner": who, **got}) return out def _where(key: str) -> tuple[str, str | None]: got = combos.where(key) if got is None: raise HTTPException(status_code=404, detail=f"없는 조합 「{key}」") return got @router.get("/combos/key/{key}") def get_by_key(key: str, session: Session = Depends(verify_session)) -> dict: """키 하나(어느 층이든 · 그 층 읽기 권한) — `{layer, owner, 판, combo, 줄, …}`.""" layer, owner = _where(key) return {"layer": layer, "owner": owner, **_call(_root(session, layer, owner), combos.one, key)} @router.get("/combos/key/{key}/inputs") def get_inputs_by_key(key: str, session: Session = Depends(verify_session)) -> dict: """조합 알약(어느 층이든) — 구조물 조합 면적산출 연결이 부름.""" layer, owner = _where(key) return _call(_root(session, layer, owner), combos.pills, key) @router.get("/layers/{layer}/combos") def get_combos(layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)): return _call(_root(session, layer, owner), combos.listing) @router.get("/layers/{layer}/combos/{key}") def get_combo( layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) ): return _call(_root(session, layer, owner), combos.one, key) @router.get("/layers/{layer}/combos/{key}/inputs") def get_combo_inputs( layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) ): """조합 알약 — 담은 로직 결과단위 분모마다 물량 하나(M02 면적산출이 받음).""" return _call(_root(session, layer, owner), combos.pills, key) @router.post("/layers/{layer}/combos") def post_combo( layer: Layer, body: ComboBody, owner: str | None = None, session: Session = Depends(verify_session), ): folder = _root(session, layer, owner, write=True) return _call(folder, combos.create, body.combo, body.판 or None) @router.put("/layers/{layer}/combos/{key}") def put_combo( layer: Layer, key: str, body: ComboBody, owner: str | None = None, session: Session = Depends(verify_session), ): folder = _root(session, layer, owner, write=True) return _call(folder, combos.edit, key, body.combo, body.판 or "") @router.delete("/layers/{layer}/combos/{key}") def delete_combo( layer: Layer, key: str, 판: str = "", owner: str | None = None, session: Session = Depends(verify_session), ): return _call(_root(session, layer, owner, write=True), combos.delete, key, 판) @router.post("/layers/{layer}/combos/tree") def post_tree( layer: Layer, body: ComboTreeBody, owner: str | None = None, session: Session = Depends(verify_session), ): folder = _root(session, layer, owner, write=True) return _call(folder, combos.edit_tree, body.model_dump()) @router.post("/layers/{layer}/combos/tree-many") def post_tree_many( layer: Layer, body: TreeManyBody, owner: str | None = None, session: Session = Depends(verify_session), ): folder = _root(session, layer, owner, write=True) return _call(folder, combos.edit_tree_many, body.model_dump()) @router.post("/layers/{layer}/combos/paste") def post_paste( layer: Layer, body: PasteBody, owner: str | None = None, session: Session = Depends(verify_session), ): """원본 층 읽기 권한 · 이 층 쓰기 권한(M02_Layer_Access) — 마스터 붙이기 = 시스템 관리자.""" source = _call(_root(session, body.from_layer, body.from_owner), lambda: combos._load()[0]) folder = _root(session, layer, owner, write=True) return _call(folder, combo_copy.paste, source, body.model_dump()) @router.post("/layers/{layer}/combos/preview") def post_preview( layer: Layer, body: PreviewBody, owner: str | None = None, session: Session = Depends(verify_session), ): """보기만 — 수량(inputs)은 이 부름에만 실림 · 저장 안 함 · 읽기 권한.""" return _call(_root(session, layer, owner), combos.preview, body.combo, body.inputs) # ── 담을 로직(읽기만) — M01 `/logics` · `/subs?group=로직` · `/logic` · `/logic/auto` 모양 ── def _read(fn, *args): try: return fn(*args) except m01.StoreError as e: raise HTTPException(status_code=e.status, detail=e.detail) from e @router.get("/logic-lib/logics", dependencies=[Depends(verify_session)]) def get_logics() -> dict: return {"logics": _read(m01.logics, "", "", "", None)} @router.get("/logic-lib/subs", dependencies=[Depends(verify_session)]) def get_logic_subs() -> dict: return _read(m01.subs, "", "로직") @router.get("/logic-lib/logic", dependencies=[Depends(verify_session)]) def get_logic(key: str) -> dict: return _read(m01.logic, key) @router.get("/logic-lib/auto", dependencies=[Depends(verify_session)]) def get_logic_auto(key: str) -> dict: """견본 입력 — 미리 보기용 자동값.""" return _read(m01.auto, key)