"""B09 내역 줄 ↔ 우리 공종 잇기 길(PLAN 707-4) — 저장 안 함(화면 캐시로 → [저장] 때 서버 정본). POST /api/projects/{id}/estimation/link/suggest {user?} → {줄: 제안(`B09_Estimation_Bill_Link.suggest` · 구조물 묶음에 딸린 줄 뺌) · 저절로: 지난번 이음 · 하나 수 · 남은 · 자리 · 이은(`board`)} POST /api/projects/{id}/estimation/link/apply {pairs [{line, target, mode?}], auto, mode, counts?, user?} · 알림 남은 = 안 이은 항목 수 잇기(`link`) → {user(새 캐시), 알림, payload(그 캐시로 셈)} POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기 공사 내역 맞춤 기억(`실무자료/내역맞춤.json` · 지난번 이음 · 잊기)은 걷음(815-2 · 요구 765-2) — 제안은 짝 표만 · 옛 기억 파일은 읽지도 쓰지도 않음(지우지 않음). 끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) · 화면 = `B09_Estimation_UI_Link`. """ from __future__ import annotations import asyncio from pathlib import Path from typing import Any from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from B08_Quantity.B08_Quantity_Router_Hydro import _project_root from B09_Estimation import B09_Estimation_Bill_Link as linker from B09_Estimation import B09_Estimation_Store as store from common_util.common_util_auth import verify_session router = APIRouter() PATH = "/{project_id}/estimation/link" def _draft(raw: Any) -> dict[str, Any] | None: return store.clean_user(raw) if isinstance(raw, dict) else None class SuggestBody(BaseModel): user: dict[str, Any] | None = None class Pair(BaseModel): line: str = Field(min_length=1, max_length=80) target: str = Field(min_length=1, max_length=200) mode: str | None = None class UndoBody(BaseModel): target: str = Field(min_length=1, max_length=200) user: dict[str, Any] | None = None class ApplyBody(BaseModel): pairs: list[Pair] = Field(default_factory=list, max_length=2000) auto: bool = False mode: str = "take" counts: dict[str, float] | None = None user: dict[str, Any] | None = None async def _rows(project_id: UUID, root: Path, user: dict[str, Any]) -> list[dict[str, Any]]: from B09_Estimation.B09_Estimation_Router import payload # 서로 부름 — 부를 때만 return (await payload(project_id, root, user, True))["sheets"]["bill"]["rows"] @router.post(PATH + "/suggest") async def suggest( project_id: UUID, body: SuggestBody, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: root = await _project_root(project_id) draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root) rows = await _rows(project_id, root, draft) board = linker.board(draft, rows) left = {r["key"] for r in board["남은"]} lines = [s for s in linker.suggest(rows, {}) if s["key"] in left] return {"줄": lines, "저절로": len(linker.auto_pairs(lines)), **board} @router.post(PATH + "/apply") async def apply( project_id: UUID, body: ApplyBody, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: from B09_Estimation.B09_Estimation_Router import payload # 서로 부름 — 부를 때만 modes = {p.line: p.mode for p in body.pairs if p.mode} if any(m not in linker.MODES for m in (body.mode, *modes.values())): raise HTTPException(status_code=400, detail="수량 갈래는 take · price 중 하나") root = await _project_root(project_id) draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root) rows = await _rows(project_id, root, draft) pairs = [(p.line, p.target) for p in body.pairs] if body.auto: pairs += [p for p in linker.auto_pairs(linker.suggest(rows, {})) if p not in pairs] made, report = linker.link(draft, rows, pairs, body.mode, body.counts, modes) made = store.clean_user(made) report.pop("기억") out = await payload(project_id, root, made, True) left = linker.board(made, out["sheets"]["bill"]["rows"])["남은"] report["남은"] = sum(1 for r in left if r["kind"] == "item") return {"user": made, "알림": report, "payload": out} @router.post(PATH + "/undo") async def undo( project_id: UUID, body: UndoBody, session: dict[str, Any] = Depends(verify_session) ) -> dict[str, Any]: from B09_Estimation.B09_Estimation_Router import payload # 서로 부름 — 부를 때만 root = await _project_root(project_id) draft = _draft(body.user) or await asyncio.to_thread(store.read_user, root) made, why = linker.unlink(draft, await _rows(project_id, root, draft), body.target) if why: raise HTTPException(status_code=400, detail=why) made = store.clean_user(made) return { "user": made, "알림": {"풀음": 1}, "payload": await payload(project_id, root, made, True), }