"""B09 내역 줄 ↔ 우리 공종 잇기 길(PLAN 707-4) — 저장 안 함(화면 캐시로 → [저장] 때 서버 정본). POST /api/projects/{id}/estimation/link/suggest {user?} → {줄: 제안(`B09_Estimation_Bill_Link.suggest`) · 저절로: 지난번 이음 · 하나 수} POST /api/projects/{id}/estimation/link/apply {pairs [{line, target}], auto, mode, counts?, user?} 잇기(`link`) — `auto` = 지난번 이음 · 하나도 더함 → {user(새 캐시), 알림, payload(그 캐시로 셈)} · 이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층) 맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김). 끼워 넣기 = `B09_Estimation_Router`(같은 머리 · 같은 권한) — 화면 고리 일감 몫. """ 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 from M02_MasterTemplete import M02_Hopyo_Store as hopyo_store from M02_MasterTemplete import M02_Layer_Access as access router = APIRouter() PATH = "/{project_id}/estimation/link" LAYERS = ("company", "personal") def _folders(session: dict[str, Any]) -> dict[str, tuple[Path, bool]]: """{층: (폴더, 쓰기)} — 회사 없음 등 = 그 층 건너뜀.""" out = {} for layer in LAYERS: try: got = access.grant(session, layer) out[layer] = (access.folder(got, create=True), got.write) except HTTPException: continue return out def recalled(session: dict[str, Any]) -> dict[str, dict[str, Any]]: """맞춤 기억 — 회사 위에 개인을 덮음.""" out: dict[str, dict[str, Any]] = {} for folder, _write in _folders(session).values(): out.update(hopyo_store.memory(folder, linker.MEMORY)) return out def keep(session: dict[str, Any], entries: dict[str, dict[str, Any]]) -> str | None: """이은 짝 → 회사 층(쓸 수 있으면) · 아니면 개인 층 → 쓴 층(없음 = None).""" folders = _folders(session) layer = next((n for n in LAYERS if n in folders and folders[n][1]), None) if entries and layer: hopyo_store.remember(folders[layer][0], entries, linker.MEMORY) return layer if entries else None 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) 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) lines = linker.suggest(rows, await asyncio.to_thread(recalled, session)) return {"줄": lines, "저절로": len(linker.auto_pairs(lines))} @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 # 서로 부름 — 부를 때만 if body.mode not in linker.MODES: 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: memory = await asyncio.to_thread(recalled, session) pairs += [p for p in linker.auto_pairs(linker.suggest(rows, memory)) if p not in pairs] made, report = linker.link(draft, rows, pairs, body.mode, body.counts) made = store.clean_user(made) layer = await asyncio.to_thread(keep, session, report.pop("기억")) return { "user": made, "알림": {**report, "기억층": layer}, "payload": await payload(project_id, root, made, True), }