Files
Aislo/B09_Estimation/B09_Estimation_Router_Link.py
T
eomsangdonandClaude Opus 5.5 1a4c1b5178 feat(B09 설계내역서): 내역 잇기 서버 길 끼움 — 잇기 풀기 · 이 짝 잊기 · 줄마다 수량 갈래 · 잇기 판 (PLAN 707-4)
- Router 에 Router_Link 끼움
- 이은 자동 줄 was = 불러온 줄 · price 안 줄 · 자동 줄 덮개 · 같은 부모 앞 뒤 줄 키 → 풀기 = 제자리
- link/undo · link/forget 길 · suggest 답에 남은 줄 · 빈 자리 · 이은 줄(구조물 묶음에 딸린 줄 뺌)
- 시험 test_707_4 4 더함(풀기 차례 바꿔도 제자리 · 줄마다 갈래 · 판 · 일반 계정 회사 기억 못 지움)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:58:12 +09:00

182 lines
7.5 KiB
Python

"""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`) — `auto` = 지난번 이음 · 하나도 더함 →
{user(새 캐시), 알림, payload(그 캐시로 셈)} ·
이은 짝 = 맞춤 기억에 씀(회사 층 = 회사 관리자 · 아니면 개인 층)
POST …/link/undo {target, user?} → {user, 알림, payload} — 잇기 풀기(맞춤 기억 그대로)
POST …/link/forget {key, user?} → {잊음, 기억층, 남음} — 이 짝 잊기(불러온 줄 · 이은 줄 키 →
맞춤 열쇠 · 쓸 수 있는 층에서 지움 · 남음 = 못 지운 위 층 기억)
맞춤 기억 = 층 `실무자료/내역맞춤.json`(실무자료 맞춤 꼴) · 읽기 = 회사 + 개인(개인이 이김).
끼워 넣기 = `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
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)
mode: str | None = None
class UndoBody(BaseModel):
target: str = Field(min_length=1, max_length=200)
user: dict[str, Any] | None = None
class ForgetBody(BaseModel):
key: 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["남은"]}
memory = await asyncio.to_thread(recalled, session)
lines = [s for s in linker.suggest(rows, memory) 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:
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, modes)
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),
}
@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),
}
@router.post(PATH + "/forget")
async def forget(
project_id: UUID, body: ForgetBody, 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)
hit = next((r for r in board["남은"] if r["key"] == body.key), None)
memo = linker.key_of(hit) if hit else None
memo = memo or next((r["memo"] for r in board["이은"] if r["key"] == body.key), None)
if not memo:
raise HTTPException(status_code=400, detail="불러온 줄 · 이은 줄이 아님")
layer = await asyncio.to_thread(keep, session, {memo: None})
left = memo in await asyncio.to_thread(recalled, session)
return {"잊음": memo, "기억층": layer, "남음": left}