Files
Aislo/M02_MasterTemplete/M02_Labor_Router.py
T

626 lines
25 KiB
Python

"""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` · 읽기 = 층 읽기 ·
고침 = 층 쓰기) +
일위대가 실무자료(324 · `M02_Hopyo_*` · 올리기 · 지우기 = 층 쓰기 · 목록 · 읽기 = 층 읽기) ·
338 = 확인한 줄만 만들기(몸 없음 400 · 옛 즉시 확정 길 없앰) · 별칭 장부 보기 · 고치기 ·
올린 뒤 맞춤 뒤 셈(`M02_Hopyo_Fit.ensure`) · 시동 때 우리 지문 데움(`M02_Hopyo_Ours.warm`) ·
718-1 한시 불러오기 = 올림 지문(작업 중 같은 엑셀 = 그 파일 `이어서` · 끝낸 것 = 새로 · 743-1) ·
지우기 = [끝내기](`M02_Hopyo_Forget.finish` · 올림 때 하루 지난 HP 도 끝냄).
층 권한 = `Router_Items._root`(`M02_Layer_Access` 한 벌).
⚠ 등록은 `main.py`(로그인만).
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Literal
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from pydantic import BaseModel, ConfigDict, Field
from common_util.common_util_auth import verify_session
from M02_MasterTemplete import M02_Hopyo_Build as hopyo_build
from M02_MasterTemplete import M02_Hopyo_Excel as hopyo_excel
from M02_MasterTemplete import M02_Hopyo_Fit as hopyo_fit
from M02_MasterTemplete import M02_Hopyo_Forget as hopyo_forget
from M02_MasterTemplete import M02_Hopyo_Grid as hopyo_grid
from M02_MasterTemplete import M02_Hopyo_Item as hopyo_item
from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger
from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks
from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match
from M02_MasterTemplete import M02_Hopyo_Ours as hopyo_ours
from M02_MasterTemplete import M02_Hopyo_Paste as hopyo_paste
from M02_MasterTemplete import M02_Hopyo_Relink as hopyo_relink
from M02_MasterTemplete import M02_Hopyo_Sangeun as hopyo_sangeun
from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store
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)
return {**labor.calc(doc, body.수량), "알약": labor.pills(doc)}
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)
# ── 일위대가 실무자료(324 · 338) ───────────────────────────────────────
HOPYO_MAX_BYTES = 20 * 1024 * 1024
hopyo_ours.warm() # 시동 — 우리 지문 캐시(디스크) 읽어 둠
def _who(session: Session) -> dict[str, Any]:
return {"id": session.get("user_id"), "이름": session.get("name")}
def _upload(name: str, data: bytes, who: dict[str, Any]) -> dict[str, Any]:
mark = hopyo_store.fingerprint(data)
hopyo_forget.sweep(store.root()) # 하루 지난 작업 중 HP 끝냄(743-1)
if same := next((r for r in hopyo_store.rows(store.root()) if r.get("지문") == mark), None):
return {**same, "이어서": True}
try:
parsed = hopyo_excel.read_book(name, data)
except hopyo_excel.ExcelError as error:
raise store.StoreError(400, str(error)) from error
try:
grid = hopyo_grid.read(name, data, parsed)
except Exception: # 격자는 보기용 덤 — 못 만들어도 올리기는 됨(보기 창 404)
grid = None
row = hopyo_store.save(store.root(), name, parsed, who, grid, mark)
hopyo_fit.ensure(store.root(), row["키"]) # 맞춤 뒤 셈(일감 줄 · 화면 안 기다림)
return row
@router.get("/layers/{layer}/hopyo")
def hopyo_list(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`."""
folder = _root(session, layer, owner)
rows = hopyo_store.rows(folder)
for row in rows: # 지금 판 맞춤셈 없는 파일 = 목록 열 때 뒤 셈 예약(358-3 ③)
hopyo_fit.ensure(folder, row["키"])
return {"줄": rows}
@router.get("/layers/{layer}/hopyo-picks")
def hopyo_picks(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""[로직 추가] 「실무 호표」 줄 — 이 층 · 위 층 파일의 호표마다
`{층, 주인, 키, 파일, 번, 이름, 규격, 단위, 맞춤, 확인}`(확인 = 이 층에 확인된 사전조합)."""
folder = _root(session, layer, owner)
return {"줄": _call(folder, _picks, folder)}
def _picks(folder: Path) -> list[dict[str, Any]]:
rows, here, done = hopyo_match.picks(folder), refs.where(folder), {}
for row in rows:
src = (row["층"], row.get("주인"))
if src not in done:
done[src] = hopyo_build.confirmed(folder, src)
key = done[src].get(hopyo_build.mark(row["키"], row["번"]))
row["확인"] = {"층": here[0], "주인": here[1], "키": key} if key else None
return rows
@router.post("/layers/{layer}/hopyo")
def hopyo_upload(
layer: Layer,
file: UploadFile = File(...),
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""엑셀(xls · xlsx) 올리기 — 「일위대가」 시트를 읽어 결과 · 격자 저장 · 20 MB ·
엉터리 · 시트 없음 400."""
folder = _root(session, layer, owner, write=True)
data = file.file.read(HOPYO_MAX_BYTES + 1)
if len(data) > HOPYO_MAX_BYTES:
raise HTTPException(status_code=400, detail="파일이 너무 큼(20 MB 넘음)")
name = Path(file.filename or "실무자료").name[:120]
return _call(folder, _upload, name, data, _who(session))
@router.get("/layers/{layer}/hopyo/{key}")
def hopyo_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 하나 — 검산 · 호표마다 `맞춤` [들어감, 줄 수] · 파일 `맞춤` 합."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_match.view, folder, key)
@router.get("/layers/{layer}/hopyo-grid/{key}")
def hopyo_grid_read(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""엑셀꼴 격자(329-1) `{책: Workbook, 호표: [{번, 이름, 행}], 단산: [{번, 이름, 행}]}` ·
옛 파일 404."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_store.grid, folder, key)
@router.get("/layers/{layer}/hopyo/{key}/{no}")
def hopyo_one(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""호표 하나 — 줄마다 맞춤(상태 · 체크 제안 · 대상 · 후보 5) · `to` = 만들 층(없으면 파일 층 ·
호표 참조 줄 체크 · 만든)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
return _call(folder, lambda: hopyo_match.view(folder, key, no, to=target))
class GuessBody(BaseModel):
"""`차례` = 호표 안 줄(0부터) · `대상` = [{로직 | 자재, 입력값?, 몫?}](낱개도 받음)."""
차례: int
대상: list[dict[str, Any]] | dict[str, Any]
@router.post("/layers/{layer}/hopyo/{key}/{no}/guess")
def hopyo_guess(
layer: Layer,
key: str,
no: int,
body: GuessBody,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""미리 셈(저장 안 함 · 설계 3-4) — 고른 대상 · 옵션으로 엔진 한 번 → `{대상, 품, 글}` ·
`to` = 만들 층(없으면 파일 층)."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to is not None else None
args = (folder, key, no, body.차례, body.대상)
return _call(folder, lambda: hopyo_match.guess(*args, to=target))
class _Strict(BaseModel):
model_config = ConfigDict(extra="forbid") # 금액 · 품 칸 = 400(서버가 셈)
class BuildTarget(_Strict):
"""대상 하나 — 로직 · 자재 · 호표 중 하나 · 배 = 우리 수량 ÷ 엑셀 수량 · 몫 = 섞인 줄 몫."""
로직: str | None = None
자재: str | None = None
호표: int | None = None
전국: bool | None = None # 「전국」 자재(361-1 · 키 없음 · 서버가 줄에서 다시 셈)
노임: str | None = None # 729-2 사용자 이음 — LB 키(엑셀 숫자 + 이음)
기계: str | None = None # EQ 키
비율: Literal["노무비", "재료비", "경비"] | None = None # % 줄 밑수
입력값: dict[str, Any] = Field(default_factory=dict)
배: float = 1
몫: float | None = None
class BuildLine(_Strict):
"""확인한 줄 — `차례` = 호표 안 줄(0부터) · `대상` 목록 또는 `뺌` 까닭."""
차례: int
대상: list[BuildTarget] = Field(default_factory=list)
뺌: Literal["우리에 없음", "안 씀"] | None = None
class BuildWhole(_Strict):
대상: list[BuildTarget]
class BuildBody(_Strict):
"""실무 호표 → 이 층 사전조합 — 파일 층 `층` · `주인` · `키` + 호표 `번` + 확인한 `줄` ·
직접 줄 호표 = `통째` · 같은 본뜬은 늘 덮음."""
층: Layer
주인: str | None = None
키: str
번: int
줄: list[BuildLine] = Field(default_factory=list, max_length=500)
통째: BuildWhole | None = None
@router.post("/layers/{layer}/hopyo-build")
def hopyo_make(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[확인하고 사전조합 만들기] — 확인한 줄만(이 층 쓰기 · 파일 층 읽기) · 별칭 장부 씀 →
`{참조, 이름, 다시씀, 재사용, 빠진줄, 하위, 비교, 장부}` · 하위 호표 확인 전 = 함께 만듦(실패 409)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, _who(session))
return _call(folder, hopyo_build.build, *args)
@router.post("/layers/{layer}/hopyo-unlinked")
def hopyo_unlinked(
layer: Layer,
body: BuildBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""안 이은 줄 미리 보기(729-2) — 만들기 몸 그대로 → `{남은줄 [{차례, 이름, 규격, 단위, 갈래, 까닭,
후보}], 수}`(저장 없음)."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole)
return _call(folder, hopyo_build.unlinked, *args)
class BuildAllBody(_Strict):
"""시트 통째(705-3) — 파일 층 `층` · `주인` · `키` 만(숫자 · 줄 칸 없음 · 서버가 파일에서 읽음)."""
층: Layer
주인: str | None = None
키: str
@router.post("/layers/{layer}/hopyo-build-all")
def hopyo_make_all(
layer: Layer,
body: BuildAllBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[모두 고정으로 들여오기] — 시트 호표 모두 고정 줄로 · 하위 먼저 · 확인된 호표 그대로 →
`{호표, 줄, 품셈, 고정, 호표참조, 빠짐, 이음, 이음후보, 같음, 그대로, 다름, 실패}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
args = (folder, source, body.키, _who(session), True) # 729-2 — 남은 줄 있는 호표 = 실패
return _call(folder, hopyo_build.build_all, *args)
@router.get("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_read(
layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""별칭 장부 — 이 층 + 위 층 줄 `{열쇠, 원래, 갈래, 짝, 누가, 때, 처음파일, 층, 주인, 고침}`
(고침 = 이 층 · 쓰기 권한)."""
folder = _root(session, layer, owner)
writable = access.grant(session, layer, owner).write
return {"줄": _call(folder, hopyo_ledger.rows, folder, writable)}
class LedgerBody(_Strict):
"""`줄` = {열쇠: 장부 줄 | null(지움)} — 이 층만 · 서버가 짝을 다시 검사."""
줄: dict[str, Any]
@router.put("/layers/{layer}/hopyo-ledger")
def hopyo_ledger_write(
layer: Layer,
body: LedgerBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""별칭 장부 [저장] — 고친 줄 · 지운 줄(층 쓰기) → `{씀, 줄}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_ledger.put, folder, body.줄, _who(session))
@router.delete("/layers/{layer}/hopyo/{key}")
def hopyo_delete(
layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session)
) -> dict:
"""실무자료 지우기 = [끝내기](718-1 · 743-1 · 화면 떠남도) — HP · 격자 · 맞춤셈 캐시 지움 ·
만든 일위대가 · 단가산출은 엑셀 사본만 걷고 남음 · 별칭 장부 그대로 → `{지움, 만든}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_forget.finish, folder, key)
class SangeunBody(_Strict):
"""`바탕` = 고름 대기 블록에 사용자가 고른 기초 `{블록 번: {로직, 입력값?}}`(729-1)."""
바탕: dict[str, dict[str, Any]] = Field(default_factory=dict)
@router.post("/layers/{layer}/hopyo/{key}/sangeun")
def hopyo_make_sangeun(
layer: Layer,
key: str,
body: SangeunBody | None = None,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""불러오기 결과 「단가산출」(718-1 · 729-1) — 산근 블록마다 매칭 → 비교 → 기초 · SK · 고름 대기
(파일 층 = 이 층) → `{블록, 만듦, 그대로, 기초, SK, 고름대기, 별칭}` · 맞춤 셈 전 409."""
folder = _root(session, layer, owner, write=True)
picks = body.바탕 if body else {}
return _call(folder, hopyo_sangeun.build, folder, key, _who(session), picks)
class PasteBody(_Strict):
"""엑셀 붙여넣기(705-2) — `글` = 붙인 글 그대로 · `열` = 기억한 열 짝(없으면 머리 글 · 짐작 + 물음) ·
`줄만` = 열린 문서에 줄로 · `있음` = 문서의 고정 줄 가름(이름 겹침 피함)."""
글: str = Field(max_length=hopyo_paste.TEXT_MAX)
열: dict[str, int] | None = None
줄만: bool = False
있음: list[str] = Field(default_factory=list, max_length=500)
@router.post("/layers/{layer}/hopyo-paste")
def hopyo_paste_run(
layer: Layer,
body: PasteBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[Ctrl+V] — `갈래` 물음(열 짝) · 줄(고정 줄 · 저장은 문서 [저장]) · 만듦(실무자료 + 호표마다 사전조합 ·
층 쓰기 · `build_all` 답 + 키)."""
folder = _root(session, layer, owner, write=True)
args = (folder, body.글, body.열, _who(session), body.줄만, body.있음)
return _call(folder, hopyo_paste.paste, *args)
class RelinkRowsBody(BaseModel):
"""다시 잇기 표(705-2) — `키` = 이 층 사전조합들 · `문서` = 열린 문서(저장 전 · 줄 · 후보만)."""
키: list[str] = Field(default_factory=list, max_length=500)
문서: dict[str, Any] | None = None
@router.post("/layers/{layer}/hopyo-relink/rows")
def hopyo_relink_rows(
layer: Layer,
body: RelinkRowsBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""「이 공사 단가로 다시 잇기」 표 — 고정 줄마다 붙인 단가 · 이은 단가 · 후보(층 읽기)."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_relink.rows, body.키, body.문서)
class RelinkBody(_Strict):
"""바꿈 `[{키, 줄, 노임?, 자재?}]` — 이음 키만(숫자 칸 없음 · 값 null = 풂)."""
바꿈: list[dict[str, Any]] = Field(max_length=hopyo_relink.CHANGES_MAX)
@router.post("/layers/{layer}/hopyo-relink")
def hopyo_relink_apply(
layer: Layer,
body: RelinkBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""[바꿈] · [되돌리기] — 이 층 사전조합 이음 칸 고쳐 씀(층 쓰기) → `{바뀜, 문서, 되돌림}`."""
folder = _root(session, layer, owner, write=True)
return _call(folder, hopyo_relink.apply, body.바꿈)
@router.get("/layers/{layer}/hopyo/{key}/{no}/siblings")
def hopyo_siblings(
layer: Layer,
key: str,
no: int,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""규격 묶음 제안(704-4) — 이름 · 단위 · 줄 구성 같고 규격만 다른 호표 `{묶음 [{번, 규격, 열}], 이름}`."""
folder = _root(session, layer, owner)
return _call(folder, hopyo_item.siblings, folder, key, no)
class ItemBody(BuildBody):
"""[구조물 조합으로](704-4) — 확인 창 몸 그대로 + `묶음` = 규격 열로 묶을 호표 번 · `이름` = 항목 이름."""
묶음: list[int] = Field(default_factory=list, max_length=20)
이름: str | None = Field(None, max_length=120)
새로: bool = False # 같은 내용 항목 409 뒤 — 그래도 새로(733-2)
씀: str | None = Field(None, max_length=80) # 같은 내용 409 뒤 — 그 항목 씀
@router.post("/layers/{layer}/hopyo-item")
def hopyo_to_item(
layer: Layer,
body: ItemBody,
owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""실무 호표 → 이 층 구조물 조합 항목(사전조합 안 만듦 · 이 층 쓰기 · 파일 층 읽기) →
`{key, 열, 이름, 줄수, 열수, 빠진줄}`."""
folder = _root(session, layer, owner, write=True)
source = _root(session, body.층, body.주인)
lines = [one.model_dump(exclude_none=True) for one in body.줄]
whole = body.통째.model_dump(exclude_none=True) if body.통째 else None
args = (folder, source, body.키, body.번, lines, whole, body.묶음, body.이름, _who(session))
args += (body.새로, body.씀)
return _call(folder, hopyo_item.make_item, *args)
@router.get("/layers/{layer}/hopyo-marks/{key}")
def hopyo_marks_read(
layer: Layer,
key: str,
owner: str | None = None,
to: Layer | None = None,
to_owner: str | None = None,
session: Session = Depends(verify_session),
) -> dict:
"""하이라이트 재료(779-2 · 읽기만) — `블록 [{번, 판정, SK, 갈래, 못옮김}]` · `to` = SK 를 볼 층."""
folder = _root(session, layer, owner)
target = _root(session, to, to_owner) if to else folder
return _call(folder, lambda: {"블록": hopyo_marks.blocks(folder, key, target)})