"""M02 일위대가 실무자료 길(PLAN 785-5 · 요구 642 · 설계 642-A 2-9) — `/api/m02/layers/{층}/hopyo…`. 새 길(글 먼저 · 결정적 매칭) — 판정 `hopyo-judge`(1차 `M02_Hopyo_Row` + 2차 `M02_Hopyo_Solve` · `M02_Hopyo_Judge`) · 검산 `hopyo/{키}/check`(저장 없음) · 옮기기 `hopyo/{키}/move`(확인한 것 → SC · SK · 장부 · `M02_Hopyo_Make`) · 경로 층 = 파일 층(판정 · 검산 · `to` = 만들 층) / 만들 층(옮기기). 올림 = Book + 격자 + 엑셀 사전(`Row.lexicon`) + 블록 풀이 뒤 셈(`Judge.ensure` · 파생 저장본) · 옛 맞춤 뒤 셈 · 시동 데움 없음 · [끝내기] = 저장본도 지움. 근거 제안(793-3a) = `hopyo-basis/{키}`(목록) · `hopyo-basis/{키}/{번}`(블록) · `M02_San_Basis_Store` (올림 때 HP `숨은식` + 뒤 셈 · 파생 저장본 · 판정 길 `hopyo-judge` 칸은 그대로). 그대로 쓰는 길(목록 · 올림 · 엑셀 보기 머리 · 격자 · 지우기 · 장부 · 모두 고정 · 붙여넣기 · 다시 잇기 · 구조물 조합 · 칠 = B09 · M01 이 씀) · 옛 맞춤 길(호표 하나 맞춤 · 미리 셈 · 실무 호표 · 확인 만들기 · 안 이은 줄 · 단가산출 만들기)은 785-7 에서 지움. 층 권한 = `Router_Items._root` · ⚠ 등록은 `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, model_validator 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_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_Judge as hopyo_judge from M02_MasterTemplete import M02_Hopyo_Ledger as hopyo_ledger from M02_MasterTemplete import M02_Hopyo_Make as hopyo_maker from M02_MasterTemplete import M02_Hopyo_Marks as hopyo_marks 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_Row as hopyo_row from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Labor_Doc as labor from M02_MasterTemplete import M02_Layer_Access as access from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_Sangeun_FromHopyo as from_hopyo from M02_MasterTemplete import M02_San_Basis_Store as san_basis from M02_MasterTemplete.M02_MasterTemplete_Router_Items import Layer, Session, _call, _root router = APIRouter(prefix="/api/m02", tags=["M02 Hopyo"]) # ── 새 길(785-5) — 판정 · 검산 · 옮기기 ───────────────────────────────────── def _to(session: Session, folder: Path, to: Layer | None, to_owner: str | None) -> Path: return _root(session, to, to_owner) if to is not None else folder @router.get("/layers/{layer}/hopyo-judge/{key}") def hopyo_judge_list( layer: Layer, key: str, owner: str | None = None, to: Layer | None = None, to_owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """호표 목록 + 1차 판정 수 · 블록 목록 `{키, 파일, 호표 [...], 블록 [...]}`(읽기만).""" folder = _root(session, layer, owner) target = _to(session, folder, to, to_owner) return _call(folder, lambda: hopyo_judge.listing(folder, key, target)) @router.get("/layers/{layer}/hopyo-judge/{key}/{no}") def hopyo_judge_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: """호표 하나 — 줄 판(1차) · 단산 줄 `풀이`(2차 블록 풀이).""" folder = _root(session, layer, owner) target = _to(session, folder, to, to_owner) return _call(folder, lambda: hopyo_judge.hopyo(folder, key, no, target)) @router.get("/layers/{layer}/hopyo-judge/{key}/block/{no}") def hopyo_judge_block( 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: """단산 블록 하나 — 블록 풀이(`Solve.block`) + 이름 · 단위 · 부름 · 근거 · 만든.""" folder = _root(session, layer, owner) target = _to(session, folder, to, to_owner) return _call(folder, lambda: hopyo_judge.block(folder, key, no, target)) @router.get("/layers/{layer}/hopyo-basis/{key}") def hopyo_basis_list( layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) ) -> dict: """근거 제안(793-3a) — `{셈중, 블록 {번: 제안}, 보강}` · 셈 중 = 뒤 셈 시작(읽기만).""" folder = _root(session, layer, owner) return _call(folder, lambda: san_basis.listing(folder, key)) @router.get("/layers/{layer}/hopyo-basis/{key}/{no}") def hopyo_basis_block( layer: Layer, key: str, no: int, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """근거 제안 블록 하나 — 저장본 › 그 자리 셈(`M02_San_Basis.suggest` 꼴).""" folder = _root(session, layer, owner) return _call(folder, lambda: san_basis.block(folder, key, no)) class Pick(BaseModel): """고른 것(785-3 꼴) — 금액 · 품 칸 없음(서버가 셈) · 고정 단가 = 엑셀 줄 것.""" model_config = ConfigDict(extra="ignore") 갈래: Literal["기초", "단가산출", "일위대가", "인력", "재료", "유가", "기계", "율", "고정"] 키: str | None = Field(None, max_length=40) 이름: str | None = Field(None, max_length=200) 단위: str | None = Field(None, max_length=40) 입력값: dict[str, Any] = Field(default_factory=dict) 몫: float | None = None 배: float | None = None 비목: str | None = None 기준: str | list[str] | None = None 별도계상: bool | None = None 층: Layer | None = None 주인: str | None = None 항목: int | None = None # 블록 고른 것 — 블록 단위 줄 차례(수량 = 그 줄 품) @model_validator(mode="before") @classmethod def _no_money(cls, raw: Any) -> Any: """금액 · 품 · 수량 칸 = 400(서버가 셈 · 브라우저 값 안 받아 적음).""" if isinstance(raw, dict) and {"금액", "품", "수량"} & set(raw): raise ValueError("금액 · 품 · 수량은 서버가 셈") return raw class CheckBody(BaseModel): """`호표` + `차례` = 일위대가 줄 · `블록` = 단산 블록(항목 줄과 대조) · `고른` = 고른 것.""" 호표: int | None = None 차례: int | None = None 블록: int | None = None 고른: list[Pick] = Field(default_factory=list, max_length=20) @router.post("/layers/{layer}/hopyo/{key}/check") def hopyo_check( layer: Layer, key: str, body: CheckBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """검산(저장 없음) — 고른 것마다 입력값(엔진이 쓴 칸 모두) · 입력필요 · 줄 대조 · 금액(서버 셈).""" folder = _root(session, layer, owner) picks = [p.model_dump(exclude_none=True) for p in body.고른] args = (folder, key, body.호표, body.차례, body.블록, picks) return _call(folder, hopyo_judge.check, *args) class MoveBody(BaseModel): """확인한 것 — 파일 층 `층` · `주인` · `호표 {번: {차례: [고른 것]}}` · `블록 {번: [고른 것]}`.""" model_config = ConfigDict(extra="forbid") 층: Layer 주인: str | None = None 호표: dict[int, dict[int, list[Pick]]] = Field(default_factory=dict, max_length=500) 블록: dict[int, list[Pick]] = Field(default_factory=dict, max_length=500) 덮음: bool = False # 이미 옮긴 호표 · 블록(같은 HP 출처) 덮기 — 아니면 409 {까닭, 호표, 블록} @router.post("/layers/{layer}/hopyo/{key}/move") def hopyo_move( layer: Layer, key: str, body: MoveBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """[옮기기] — 확인한 것 → 이 층 SC · SK · 장부(하위 먼저 · 서버가 다시 셈) → `{만듦, 단가산출, 막힘, 하위, 장부}`.""" folder = _root(session, layer, owner, write=True) source = _root(session, body.층, body.주인) dump = lambda ps: [p.model_dump(exclude_none=True) for p in ps] # noqa: E731 units = {no: {at: dump(ps) for at, ps in rows.items()} for no, rows in body.호표.items()} blocks = {no: dump(ps) for no, ps in body.블록.items()} args = (folder, source, key, units, blocks, _who(session), body.덮음) return _call(folder, hopyo_maker.move, *args) class PriceBody(BaseModel): """우리 금액 미리 셈(786-2) — 파일 층 `층` · `주인` · `호표` 번 · `줄 {차례: [고른 것]}`.""" model_config = ConfigDict(extra="forbid") 층: Layer 주인: str | None = None 호표: int 줄: dict[int, list[Pick]] = Field(default_factory=dict, max_length=300) def _price(folder: Path, source: Path, key: str, no: int, rows: dict[int, list[dict]]) -> dict: """줄마다 옮기기와 같은 길(`Make.Move.pick` 재검사 · `Make.Lines.picks` 줄 · 자재)로 한 줄 문서 → `Labor_Doc.calc`(정본 셈 · 저장 없음 · 같은 기초 둘째 = 미리 참조) · 막힘 = 그 줄 까닭만.""" mv = hopyo_maker.Move(folder, source, key, None) if no not in mv.units: raise store.StoreError(404, f"없는 제{no}호표") lines = mv.units[no].get("줄") or [] b = hopyo_build.Builder(folder, source, key, no, None) b.dry, b.strict = True, False where = {"파일": mv.hp.get("파일") or "", "호표": no} out: dict[int, dict[str, Any]] = {} for at, picks in rows.items(): if not 0 <= at < len(lines) or not picks: continue try: got = [mv.pick(p, lines[at], f"제{no}호표 {at + 1}째 줄") for p in picks] except hopyo_maker.Stuck as e: out[at] = {"금액": None, "까닭": [e.why], "칸": e.cells} continue body = hopyo_maker.Lines(b, lines, where, "", hopyo_maker.hfix.Linker(False)) body.picks(at, lines[at], got) doc = {"종류": "일위대가", "열": "hopyo-price", "단위": None, "담은로직": body.lines} try: calc = labor.calc(labor.clean({**doc, "자재": list(body.mats.values())}), {}) except (ValueError, store.StoreError) as e: out[at] = {"금액": None, "까닭": [str(getattr(e, "detail", e))], "칸": []} continue why = [x["까닭"] for x in [*calc.get("줄", []), *calc.get("자재", [])] if x.get("까닭")] out[at] = {"금액": calc.get("반영합"), "자재": calc.get("자재", []), "까닭": why, "칸": []} return {"줄": out} @router.post("/layers/{layer}/hopyo/{key}/price") def hopyo_price( layer: Layer, key: str, body: PriceBody, owner: str | None = None, session: Session = Depends(verify_session), ) -> dict: """우리 금액 미리 셈(786-2 [수정] 모달 · 저장 없음) — 경로 층 = 만들 층(참조 읽기) · 몸 층 = 파일 층 → `{줄 {차례: {금액 {노무비 · 재료비 · 경비 · 계}, 자재, 까닭, 칸}}}`.""" folder = _root(session, layer, owner) source = _root(session, body.층, body.주인) rows = {at: [p.model_dump(exclude_none=True) for p in ps] for at, ps in body.줄.items()} return _call(folder, _price, folder, source, key, body.호표, rows) # ── 옛 길(785-7 까지 · 옛 화면) · 그대로 쓰는 길 ───────────────────────────── HOPYO_MAX_BYTES = 20 * 1024 * 1024 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): cannot = None if not from_hopyo.has(store.root(), same["키"]): # 793-3b 전에 올린 HP cannot = from_hopyo.keep(store.root(), same["키"], name, data) hopyo_judge.ensure(store.root(), same["키"]) san_basis.ensure(store.root(), same["키"]) return {**same, "이어서": True, **({"읽기불가": cannot} if cannot else {})} 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 sheets, _ = hopyo_excel._open(name, data, hopyo_row.SHEETS) parsed["사전"] = hopyo_row.lexicon(sheets) # 엑셀 사전(785-3 · 1차 줄 판정) parsed["숨은식"] = san_basis.hidden(parsed, sheets) # STmate 숨은 G 식(793-3a 근거 제안) row = hopyo_store.save(store.root(), name, parsed, who, grid, mark) cannot = from_hopyo.keep( store.root(), row["키"], name, data ) # 산근 시트(793-3b 가져온 식 열기) hopyo_judge.ensure(store.root(), row["키"]) # 블록 풀이 뒤 셈(파생 저장본 · 화면 안 기다림) san_basis.ensure(store.root(), row["키"]) # 근거 제안 뒤 셈(793-3a) return ( {**row, "읽기불가": cannot} if cannot else row ) # 797-1 가져온 식 못 씀 사유 → 화면 토스트 @router.get("/layers/{layer}/hopyo") def hopyo_list( layer: Layer, owner: str | None = None, session: Session = Depends(verify_session) ) -> dict: """실무자료 목록 `{줄: [{키, 파일, 올린때, 올린이, 시트, 호표수, 어긋남}]}`.""" return {"줄": hopyo_store.rows(_root(session, layer, owner))} @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: """실무자료 하나(엑셀 보기 머리 · 목록) — 검산 · 호표마다 1차 판정 `맞춤` [제안 있음, 줄 수] · `줄상태` · `만든` · 블록 풀이 `셈`(`M02_Hopyo_Judge.view`).""" folder = _root(session, layer, owner) return _call(folder, hopyo_judge.view, folder, key, folder) @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) 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 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) got = _call(folder, hopyo_forget.finish, folder, key) hopyo_judge.forget(folder, key) # 블록 풀이 저장본(785-5) san_basis.forget(folder, key) # 근거 제안 저장본(793-3a) return got 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)})