"""M02 단가산출(SK) 셈 길(PLAN 715-1b) — `POST /api/m02/layers/{층}/sangeun/calc` · [단가산출로 저장](715-3) `POST /api/m02/layers/{층}/sangeun/from-line`(층 쓰기 권한) · 식 칸 검사(792-1) `POST /api/m02/layers/{층}/sangeun/formula-check`(셈 없음 · 층 읽기 권한) · 가져온 식(793-2 · `M02_Sangeun_Imported`) `…/sangeun/imported`(만들기 · 층 쓰기) · `/price` · `/link` · `/to-base`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기) · 불러오기 단산 블록 → 가져온 식(793-3b · `M02_Sangeun_FromHopyo`) `…/sangeun/from-hopyo` (만들 층 쓰기). 로직 사본(795-1 · `M02_Sangeun_Copy`) `…/sangeun/copy/price` [단가 갱신] · `/regen` [재생성] (화면 캐시 문서에 할일 얹음 · 저장 없음 · 층 읽기). 수식 탭(796-2b · `M02_Sangeun_Equation`) `…/sangeun/equation`(층 읽기) · `/preview`(고침 미리 셈 · 층 쓰기) — 저장 없음 · 고침 = 화면 캐시 `수식고침` → [저장]. 더한 기초 [연결 끊기](796-2e · `M02_Sangeun_Block`) `…/sangeun/block/unlink` · `/relink`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기) · 끊은 기초 수식 = 같은 equation 길 몸 `블록`(더한줄 차례). 저장 안 함 · 로그인 사용자 · 층 읽기 권한(`Router_Items._root`) · 화면은 최신 응답만. 몸 = `문서`(저장 전 화면 문서) 또는 `키`(그 층 저장본) + `입력값`(열린입력 · 시험 값) · `보기` false = 보기 덧칸(`글` · `중간` · 750-1) 안 지음(바탕 원값 셈처럼 금액만 쓸 때). 문서 읽기 · 쓰기 · 새로 · 복사 · 지우기는 양식 길 · 서고 길 그대로(종류 `sangeun`). ⚠ 등록은 `main.py`(로그인만). """ 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 M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_Block as blk from M02_MasterTemplete import M02_Sangeun_Calc as calc from M02_MasterTemplete import M02_Sangeun_Copy as skc from M02_MasterTemplete import M02_Sangeun_Doc as sd from M02_MasterTemplete import M02_Sangeun_Equation as eqm from M02_MasterTemplete import M02_Sangeun_Formula as fx from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo from M02_MasterTemplete import M02_Sangeun_Imported as imp from M02_MasterTemplete import M02_Sangeun_QtyLinks as qty from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root router = APIRouter(prefix="/api/m02", tags=["M02 MasterTemplete Sangeun"]) class SangeunCalcBody(BaseModel): 문서: dict[str, Any] | None = None 키: str | None = None 입력값: dict[str, Any] | None = None 보기: bool = True def _calc(body: SangeunCalcBody) -> dict[str, Any]: doc = body.문서 if doc is None: if not sd.KEY.match(str(body.키 or "")): raise store.StoreError(400, "문서 또는 키(SK-######)") doc = store.read(sd.KIND, str(body.키))["문서"] return calc.calc_doc(doc, body.입력값, body.보기) class FromLineBody(BaseModel): 줄: dict[str, Any] 이름: str 단위: str 새로: bool = False # 같은 내용 409 뒤 — 그래도 새로(733-2) 씀: str | None = None # 같은 내용 409 뒤 — 그 SK 씀 @router.post("/layers/{layer}/sangeun/from-line") def sangeun_from_line( layer: Layer, body: FromLineBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """[단가산출로 저장](715-3) — 일위대가 줄 → 그 층 새 단가산출 · 답 = 새 키 + 바꿀 참조 줄.""" folder = _root(session, layer, owner, write=True) return _call(folder, calc.from_line, body.줄, body.이름, body.단위, body.새로, body.씀) @router.post("/layers/{layer}/sangeun/calc") def calc_sangeun( layer: Layer, body: SangeunCalcBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """단가산출 셈 — 줄(cid · 덮개 · 원값) · 기준 · 1단위 · 짝잃음(저장 안 함).""" if body.문서 is None and body.키 is None: raise HTTPException(status_code=400, detail="문서 또는 키") return _call(_root(session, layer, owner), _calc, body) class FormulaCheckBody(BaseModel): 문서: dict[str, Any] 자리: str 식: str @router.post("/layers/{layer}/sangeun/formula-check") def check_formula( layer: Layer, body: FormulaCheckBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """식 칸 검사(792-1) — {되나, 까닭, 알림, 식(엔진), 글(이름 꼴)} · 셈 · 저장 없음.""" return _call(_root(session, layer, owner), fx.check_doc, body.문서, body.자리, body.식) class ImportedBody(BaseModel): 로직: dict[str, Any] 원본: dict[str, Any] | None = None 이름: str = "" 단위: str = "" 구분: str = "" 상세구분: str = "" 새로: bool = False 씀: str | None = None @router.post("/layers/{layer}/sangeun/imported") def sangeun_imported( layer: Layer, body: ImportedBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """가져온 식 단가산출 만들기(793-2) — 답 = {이름, 검산, 단가기준} · 같은 내용 409.""" folder = _root(session, layer, owner, write=True) head = {"구분": body.구분, "상세구분": body.상세구분} args = (body.로직, body.원본, body.이름, body.단위, head, body.새로, body.씀) return _call(folder, _sd(imp.create), *args) class FromHopyoBody(BaseModel): 층: Layer 주인: str | None = None 키: str 번: int 이름: str = "" @router.post("/layers/{layer}/sangeun/from-hopyo") def sangeun_from_hopyo( layer: Layer, body: FromHopyoBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """불러오기 단산 블록 → 가져온 식 SK(793-3b) — 경로 층 = 만들 층 · 몸 층 = 파일 층 · 답 = {이름, 검산, 단가기준, 다시씀}(같은 내용 = 그 SK).""" folder = _root(session, layer, owner, write=True) source = _root(session, body.층, body.주인) return _call(folder, _sd(from_hopyo.make), source, body.키, body.번, body.이름) class ImportedEditBody(BaseModel): 문서: dict[str, Any] 기준: str | None = None # price 자리: str | None = None # link 이음: str | None = None # link 로직: str | None = None # to-base 입력값: dict[str, Any] | None = None # to-base 환산: dict[str, Any] | None = None # to-base(793-3b · SK 단위 환산 계수) 칸: dict[str, Any] | None = None # qty-pick(797-2 · 고른 M01 표 칸 {표, 줄, 칸}) 되돌리기: bool = False # qty-drop(797-2 · 버림 풀기) def _sd(fn: Any) -> Any: """`sd.DocError` → 400 · 422(`store.StoreError`).""" def run(*args: Any) -> Any: try: return calc.labor._json(fn(*args)) except sd.DocError as e: raise calc._fail(e) from e return run _EDIT = { "price": lambda b: (imp.to_price, b.문서, b.기준), "link": lambda b: (imp.link, b.문서, b.자리, b.이음), "to-base": lambda b: (imp.to_base, b.문서, b.로직, b.입력값, b.환산), "qty": lambda b: (qty.view, b.문서), "qty-pick": lambda b: (qty.pick, b.문서, b.자리, b.칸), "qty-drop": lambda b: (qty.drop, b.문서, b.자리, b.되돌리기), } @router.post("/layers/{layer}/sangeun/imported/{how}") def sangeun_imported_edit( layer: Layer, how: str, body: ImportedEditBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """가져온 식 고치기(793-2) — price [우리 · 엑셀 단가로] · link 이음 고름 · to-base [우리 기초로 바꾸기] · 답 = 고친 문서(화면 캐시 · 저장 없음) + 전 · 후 금액.""" if how not in _EDIT: raise HTTPException( status_code=404, detail="price · link · to-base · qty · qty-pick · qty-drop" ) fn, *args = _EDIT[how](body) return _call(_root(session, layer, owner), _sd(fn), *args) class CopyBody(BaseModel): 문서: dict[str, Any] 입력값: dict[str, Any] | None = None _JOBS = {"price": skc.PRICE, "regen": skc.REGEN} @router.post("/layers/{layer}/sangeun/copy/{how}") def sangeun_copy( layer: Layer, how: str, body: CopyBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """로직 사본(795-1) — price [단가 갱신] · regen [재생성] · 답 = 할일 얹은 문서(화면 캐시 · [저장] 때 서버가 적용) + 전 · 후 금액 + 바뀐자원 | 못얹은.""" if how not in _JOBS: raise HTTPException(status_code=404, detail="price · regen") args = (body.문서, _JOBS[how], body.입력값) return _call(_root(session, layer, owner), _sd(skc.preview), *args) class EquationBody(BaseModel): 문서: dict[str, Any] inputs: dict[str, Any] | None = None 고침: list[Any] | None = None 블록: int | None = None # 끊은 더한 기초 차례(796-2e) — 없으면 바탕 줄 @router.post("/layers/{layer}/sangeun/equation") def sangeun_equation( layer: Layer, body: EquationBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """수식 탭 읽기(796-2b) — 사본(가져온 식) 줄 + 문서 `수식고침` → {ok, 수식 | reason}.""" if body.블록 is not None: return _call(_root(session, layer, owner), _sd(blk.read), body.문서, body.블록, body.inputs) return _call(_root(session, layer, owner), _sd(eqm.read), body.문서, body.inputs) class LinksBody(BaseModel): 문서: dict[str, Any] | None = None 갈래: str 검색: str = "" 분류: str = "인력" 블록: int | None = None @router.post("/layers/{layer}/sangeun/equation/links") def sangeun_equation_links( layer: Layer, body: LinksBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """새 기호 연결 후보(800-3) — 표 · 요소 · 로직 이름 찾기(읽기만).""" ask = {k: getattr(body, k) for k in ("갈래", "검색", "분류")} return _call(_root(session, layer, owner), _sd(eqm.links), body.문서 or {}, ask) @router.post("/layers/{layer}/sangeun/equation/preview") def sangeun_equation_preview( layer: Layer, body: EquationBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """수식 고침 미리 셈(796-2b · 층 쓰기 · 저장 없음) — 고침 = `수식고침` 통째 → {되나, 까닭, 줄, 수식, 전, 후}.""" folder = _root(session, layer, owner, write=True) if body.블록 is not None: args = (body.문서, body.블록, body.고침 or [], body.inputs) return _call(folder, _sd(blk.preview), *args) return _call(folder, _sd(eqm.preview), body.문서, body.고침 or [], body.inputs) class BlockBody(BaseModel): 문서: dict[str, Any] 차례: int @router.post("/layers/{layer}/sangeun/block/{how}") def sangeun_block( layer: Layer, how: str, body: BlockBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """더한 기초 연결(796-2e) — unlink [연결 끊기] · relink [다시 잇기] · 답 = 고친 문서(화면 캐시 · [저장] 때 서버가 다시 셈) + 전 · 후 SK 금액.""" if how not in ("unlink", "relink"): raise HTTPException(status_code=404, detail="unlink · relink") return _call(_root(session, layer, owner), _sd(blk.change), body.문서, body.차례, how)