Files
Aislo/M02_MasterTemplete/M02_Sangeun_Router.py
T
eomsangdonandClaude Opus 5.5 520bb33909 feat(827-3): 불러오기 재료 걸음 3 — 고를 재료 판 · 그 파일 안 고름(별칭 아님) · SK 상세 같은 파일 물음
- 새 M02_San_MatPick: 종 열쇠(이름 · 규격 · 단위 · 단서) · 고름 자리 = 개인 층 실무자료/재료고름/<파일 지문>.json([끝내기] 뒤 남음 · 같은 파일 다시 올리면 그대로) · 판(블록마다 재료찾기 판정 → 종 · 첫 판 한 번 셈 → 산근 저장본 칸) · 만들 때 얹기 · 이미 만든 SK 얹기(가져온 그대로인 줄만 · 바꿈 · 따로 고른 줄 그대로 · 풀면 가져온 그대로)
- 만들기: from-hopyo 가 그 파일 고름째 만듦(저장 안 한 판 고름 = 만들 때 저장) · 원본 지문 · 고른 줄 = 이음 + 재료찾기.고름 · 문서 셈 우리 값(엑셀 값 기억 없음) · 별도계상 = 품목만(줄 이음 · 단가 0 그대로)
- 문서 머리 가져온.재료(판정 · 후보 · 단서 · 처음 연결) · 가져온.지문
- 길: GET/POST hopyo-materials/{HP}(판 · [저장] · 만든 것 적용은 물음 예일 때만) · GET/POST material-picks/{지문}(SK 상세 물음)
- 화면: 단가산출 목록 위 「고를 재료」 판(후보 고름 · 찾기 · 고름 풀기 · n 블록에 적용 · 이어진 재료 접힘 · [저장]) · 저장 안 한 고름 = 떠날 때 물음 · 줄 쌓기 재료 줄 판정 배지 · 풍선말(판정 · 단서 · 후보) · 이름 단추 · [이음 고르기] 뒤 「같은 파일 n 줄」 물음
- 별도계상 설명 = 「별도계상 · 품목: 이름 규격」(엔진 이은 품목은 자재표 이름) — test_827_2 기대 글 고침(품목 이음 → 품목 이름)
- 시험 test_827_3 셋(파일 고름 블록 둘 · 다른 파일 0 · 별칭 · 마스터 그대로 · 풀기 · 다시 올림 유지 · 물음 예일 때만 · 따로 바꾼 줄 보존 · 별도계상 품목 + 단가 0)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:51:04 +09:00

336 lines
13 KiB
Python

"""M02 단가산출(SK) 셈 길(PLAN 715-1b) — `POST /api/m02/layers/{층}/sangeun/calc` ·
[단가산출로 저장](715-3) `POST /api/m02/layers/{층}/sangeun/from-line`(층 쓰기 권한) ·
일위대가 [+ 줄] M01 기초 = 그 층 SK 사본(811-1 · `M02_Labor_SkRow`) `…/sangeun/from-logic`(층 쓰기) ·
식 칸 검사(792-1) `POST /api/m02/layers/{층}/sangeun/formula-check`(셈 없음 · 층 읽기 권한) ·
가져온 식(793-2 · `M02_Sangeun_Imported`) `…/sangeun/imported`(만들기 · 층 쓰기) · `/price` ·
`/link`(화면 캐시 문서 고침 · 저장 없음 · 층 읽기 · 기초로 바꾸기 없음 · 809-1) ·
불러오기 단산 블록 → 가져온 식(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_San_MatPick as mat_pick
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.씀)
class FromLogicBody(BaseModel):
로직: str
새로: bool = False
씀: str | None = None
@router.post("/layers/{layer}/sangeun/from-logic")
def sangeun_from_logic(
layer: Layer,
body: FromLogicBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""일위대가 [+ 줄] M01 기초(811-1) — 그 층 SK 사본 · 답 = 새 키 + 참조(일위대가 문서는 화면 [저장] 때)."""
from M02_MasterTemplete import M02_Labor_SkRow as skrow
folder = _root(session, layer, owner, write=True)
return _call(folder, skrow.from_logic, 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 = ""
고름: dict[str, dict[str, Any] | None] | None = (
None # 827-3 판에서 저장 안 한 재료 고름(만들 때 저장)
)
@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.주인)
mine = _root(session, "personal", None, write=body.고름 is not None)
fp, name = _call(source, mat_pick.fingerprint, source, body.키) # 827-3 그 파일 재료 고름
if body.고름 is not None and fp:
_call(mine, mat_pick.write, mine, fp, name, body.고름)
picks = mat_pick.read(mine, fp)["고름"] if fp else {}
def run(*args: Any) -> Any:
with mat_pick.using(picks):
return _sd(from_hopyo.make)(*args)
return _call(folder, run, source, body.키, body.번, body.이름)
class ImportedEditBody(BaseModel):
문서: dict[str, Any]
기준: str | None = None # price
자리: str | None = None # link
이음: str | None = None # link
칸: 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.이음),
"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 이음 고름 · qty 소요량 이음 · 답 = 고친 문서(화면 캐시 · 저장 없음) + 전 · 후 금액."""
if how not in _EDIT:
raise HTTPException(status_code=404, detail="price · link · 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)