feat(B09 설계내역서): 내역 줄 ↔ 우리 공종 잇기 서버 몫 — 토공 이름 짝 표 · 맞춤 기억 · 구조물 묶음 통째 잇기 (PLAN 707-4)
- B09_Estimation_Bill_Link: 짝 표(우리 토공 17 줄 · EST · STmate 이름 24 짝 · 암 갈래 = 후보 둘) · 제안(지난번 이음 · 짝 표 · 하나 아님 = 고름) · 잇기(707-1 붙임 꼴 · take = 엑셀 수량 덮개 / price = B08 · 불러온 줄 지움 = 두 번 셈 0) · 묶음 ↔ 구조물 줄(take = 안 줄이 그 아래 / price = 1 단위 고정 단가) - B09_Estimation_Router_Link: suggest · apply 길(끼워 넣기 = 화면 고리 일감) · 맞춤 기억 = 층 실무자료/내역맞춤.json(회사 관리자 = 회사 · 아니면 개인 · 읽기 = 회사 + 개인) - M02_Hopyo_Store: memory · remember 에 기억 파일 이름 칸(기본 = 맞춤) - 시험 test_707_4 8(거창 엑셀 기억 → 장수 · 진안 .estx 저절로 11 · 두 번 셈 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
"""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),
|
||||
}
|
||||
Reference in New Issue
Block a user