"""M02 일위대가 계산 · 알약 길(PLAN 39-1a · 설계_정함 1장) — `/api/m02/layers/{층}/labor/{열}/…`. 문서 읽기 · 쓰기는 양식 길(`GET/PUT /api/m02/layers/{층}/templates/labor/{열}`) 그대로 · 여기는 저장하지 않는 셈만 + 서고(단가승격 · 사전조합 · 50-3) 새로 · 복사(키는 서버가 줌 · 고침 · 지우기는 양식 길 그대로) + 개인 자재 목록 · 추가(148-2 · 개인 층만 · 품번은 서버) + 서고 나무 자유 폴더(158-1 · 716-4 `{종류}-tree` · `M02_Precombo_Folders` · 읽기 = 층 읽기 · 고침 = 층 쓰기) + 일위대가 실무자료 길 = `M02_Hopyo_Router`(785-5). 층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌). ⚠ 등록은 `main.py`(로그인만). """ from __future__ import annotations from typing import Any, Literal from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from common_util.common_util_auth import verify_session from M02_MasterTemplete import M02_Labor_Copy as lc from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_Library_Ask as library_ask from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Material_Own as own from M02_MasterTemplete import M02_Precombo_Folders as pre_folders from M02_MasterTemplete.M02_MasterTemplete_Router_Items import ( Layer, Session, TreeBody, _call, _root, ) from M02_MasterTemplete.M02_MasterTemplete_Router_Items import ( PasteBody as TreePasteBody, ) router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Labor"]) class CalcBody(BaseModel): """`doc` = 저장 전 화면 문서(없으면 저장본) · `수량` = {알약 키: 값}(없으면 저장된 면적산출).""" doc: dict[str, Any] | None = None 수량: dict[str, Any] | None = None def _doc(column: str, sent: dict[str, Any] | None) -> dict[str, Any]: got = sent if sent is not None else store.read("labor", column)["문서"] return labor.clean({**got, "열": column}) def _calc(column: str, body: CalcBody) -> dict[str, Any]: doc = _doc(column, body.doc) got = lc.mark(labor.calc(doc, body.수량), lc.settle(doc)) # 사본 표지(795-2) return {**got, "알약": labor.pills(doc)} _JOBS = {"price": lc.PRICE, "regen": lc.REGEN} @router.post("/layers/{layer}/labor/{column}/copy/{how}") def labor_copy( layer: Layer, column: str, how: str, body: CalcBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """로직 사본(795-2) — price [단가 갱신] · regen [재생성] · 일위대가 열 · 사전조합 SC 키 · 답 = 할일 얹은 doc(화면 캐시 · [저장] 때 서버가 적용) + 전 · 후 합 + 바뀐자원 | 못얹은.""" if how not in _JOBS: raise HTTPException(status_code=404, detail="price · regen") return _call(_root(session, layer, owner), _copy_job, column, body, _JOBS[how]) def _copy_job(column: str, body: CalcBody, job: str) -> dict[str, Any]: """미리 셈은 정리한 문서로 · 답 doc = 보낸 화면 문서 + 할일(SC · 일위대가 꼴 그대로).""" got = lc.preview(_doc(column, body.doc), job, body.수량) sent = body.doc if body.doc is not None else store.read("labor", column)["문서"] return {**got, "doc": {**sent, lc.JOB: got["doc"][lc.JOB]}} def _pills(column: str) -> dict[str, Any]: return {"열": column, "알약": labor.pills(_doc(column, None))} @router.post("/layers/{layer}/labor/{column}/calc") def calc_labor( layer: Layer, column: str, body: CalcBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """줄마다 기준 · 반영 · 합 + 알약(저장 안 함 · 화면은 최신 응답만).""" return _call(_root(session, layer, owner), _calc, column, body) @router.get("/layers/{layer}/labor/{column}/pills") def labor_pills( layer: Layer, column: str, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """면적산출 알약 — 로직 물량 알약 「이름|단위」 + 자재 알약 「자재|MT키」(저장본).""" return _call(_root(session, layer, owner), _pills, column) class LibraryBody(BaseModel): """새로 = `문서` · 복사 = `본뜬` {층, 주인, 키}(원본 읽기 권한 · 못 읽는 참조는 같이 복사) · 새로 같은 내용 409 → `새로` = 그래도 새로 · `씀` = 그 키(733-2 · `M02_Library_Ask`).""" 문서: dict[str, Any] | None = None 본뜬: dict[str, Any] | None = None 새로: bool = False 씀: str | None = None def _copy(kind: str, doc: dict[str, Any], source: refs.Here, ref: dict) -> dict[str, Any]: return refs.create(kind, refs.carry(refs.origin_mark(doc, ref), source)) @router.post("/layers/{layer}/library/{kind}") def new_library( layer: Layer, kind: Literal["precombo", "sangeun"], body: LibraryBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """서고 새로 · 복사 — 키는 서버가 그 층 다음 번호(SP · SC) · 답 `이름` = 새 키.""" folder = _root(session, layer, owner, write=True) if body.본뜬 is None: return _call(folder, library_ask.create, kind, body.문서 or {}, body.새로, body.씀) src = body.본뜬 if src.get("층") not in ("system", "company", "personal"): raise HTTPException(status_code=400, detail="본뜬 층이 틀림") got = access.grant(session, src["층"], src.get("주인")) # 원본 읽기 권한 ref = {"층": got.layer, "주인": got.owner, "키": str(src.get("키") or "")} if refs.kind_of(ref["키"]) != kind: raise HTTPException(status_code=400, detail="본뜬 종류가 틀림") doc = refs.read_ref(ref) if refs._KEY.match(ref["키"]) else None if doc is None: raise HTTPException(status_code=404, detail=f"없는 {refs.TITLE[kind]} 「{ref['키']}」") return _call(folder, _copy, kind, doc, (got.layer, got.owner), ref) @router.get("/layers/{layer}/{kind}-tree") def library_tree( layer: Layer, kind: Literal["precombo", "sangeun"], owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """서고 나무 폴더(일위대가 · 단가산출) — `{판, 폴더: [경로 …], 자리: {키: 경로}}` · 자리 없는 문서 = 뿌리.""" return _call(_root(session, layer, owner), pre_folders.view, kind) @router.post("/layers/{layer}/{kind}-tree") def edit_library_tree( layer: Layer, kind: Literal["precombo", "sangeun"], body: TreeBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """새 폴더 · 이름 바꾸기 · 옮기기 · 지우기(폴더 = 안에 든 문서째 · 쓰는 곳 있으면 409) · 여럿 = `대상` → 새 나무 · 판 다르면 409.""" folder = _root(session, layer, owner, write=True) return _call(folder, pre_folders.edit, body.model_dump(), kind, session) @router.post("/layers/{layer}/{kind}-tree/paste") def paste_library_tree( layer: Layer, kind: Literal["precombo", "sangeun"], body: TreePasteBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """복사한 층(읽기 권한) 문서 · 폴더 → 이 층(쓰기 권한) `경로` 아래 새 키 → 새 나무 + 알림.""" target = _root(session, layer, owner, write=True) source = _root(session, body.from_layer, body.from_owner) return _call(target, pre_folders.paste, source, body.대상, body.경로, kind) @router.get("/layers/{layer}/materials") def own_materials( layer: Layer, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """개인 자재 목록(148-2) — 개인 층만 · 회사 · 마스터 = 빈 목록.""" if layer != "personal": return {"줄": []} return {"줄": own.rows(_root(session, layer, owner))} @router.post("/layers/{layer}/materials") def new_material( layer: Layer, body: dict[str, Any], owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """신규 자재(148-2) — `{이름, 규격, 단위, 값}` → 새 줄(품번 `PM` + 6자리 서버 일련) · 개인 층만.""" if layer != "personal": raise HTTPException(status_code=400, detail="신규 자재는 개인 층에서만") folder = _root(session, layer, owner, write=True) return _call(folder, own.add, folder, body)