Merge origin/sub_laptop_5 into sub_laptop_8 — PLAN 702-3 · B09 Router 머리 설명 겹침 양쪽 살림([업데이트] 703-3 · calc 702-3)

남은 것: test_298_2 test_b08_b09_next[B09] 가 701-3 빈 줄 409 와 부딪힘(root=Path(".") 로 실제 셈) — sub5 다음 일 첫 걸음에서 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-31 14:16:31 +09:00
co-authored by Claude Opus 5.5
17 changed files with 451 additions and 49 deletions
+34 -3
View File
@@ -18,6 +18,8 @@
POST /api/projects/{id}/estimation/prices/preview {user?}
[업데이트] 미리 보기(703-3) — 바뀌는 단가 줄 · 다시 가져올 사본 · 지킬 고친 칸 ·
노임 판(쓰지 않음)
POST /api/projects/{id}/estimation/calc {user}
조작 중 서버 셈(702-3 · 단가연결 즉시 금액) — 화면 캐시로 GET 꼴 셈 · 저장 안 함
POST /api/projects/{id}/estimation/logic-preview {code, given, rate, picks}
「운반 단가」 모달 계산 절차(253-3) — 그 로직 한 산근을 프로젝트 단가로 풀어 글 줄 · 단가 ·
저장 안 함(`render_sangeun_text` 를 `Sangeun.Solver` 로) · `blocks` = 칸 나눔(277-2)
@@ -47,6 +49,7 @@ import asyncio
import copy
import importlib
import logging
import time
from pathlib import Path
from typing import Any
from uuid import UUID
@@ -92,6 +95,25 @@ except ImportError: # pragma: no cover
router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"])
# 702-3 조작 중 셈 = 마지막 GET · [저장] 이 읽은 B08 원천 다시 씀(B08 은 B09 조작으로 안 바뀜)
# ponytail: 시간 창만 봄 — 창 안에 남이 B08 을 고치면 미리보기만 옛 수량 · [저장] 은 늘 새로 읽음
B08_REUSE_S = 120.0
_b08_seen: dict[UUID, tuple[float, dict[str, Any], list[str]]] = {}
async def _b08_for(project_id: UUID, notes: list[str], reuse: bool) -> dict[str, Any]:
"""B08 원천 — reuse = 시간 창 안 마지막 값(사본) · 아니면 새로 읽고 남김."""
seen = _b08_seen.get(project_id)
if reuse and seen and time.monotonic() - seen[0] < B08_REUSE_S:
notes.extend(seen[2])
return copy.deepcopy(seen[1])
mine: list[str] = []
out = await _b08(project_id, mine)
_b08_seen[project_id] = (time.monotonic(), copy.deepcopy(out), mine)
notes.extend(mine)
return out
async def _b08(project_id: UUID, notes: list[str]) -> dict[str, Any]:
"""B08 수량 원천 — 못 읽으면 빈칸 + 알림(B09 는 B08 를 고치지 않음)."""
out: dict[str, Any] = {}
@@ -182,10 +204,10 @@ async def _demand(root: Path, draft: dict[str, Any] | None = None) -> list[str]:
async def payload(
project_id: UUID, root: Path, draft: dict[str, Any] | None = None
project_id: UUID, root: Path, draft: dict[str, Any] | None = None, reuse_b08: bool = False
) -> dict[str, Any]:
notes: list[str] = []
b08 = await _b08(project_id, notes)
b08 = await _b08_for(project_id, notes, reuse_b08)
keys = await _demand(root, draft)
if not keys:
notes.append(
@@ -194,7 +216,8 @@ async def payload(
book = await asyncio.to_thread(store.read_book, root)
inputs = await asyncio.to_thread(prices.build_inputs, book, keys, b08["materials"])
hauls = await asyncio.to_thread(haul_routes, root)
user = await asyncio.to_thread(store.read_user, root)
# 702-3 draft = 화면 캐시로 셈(저장 안 함) · 없으면 저장 값
user = await asyncio.to_thread(store.read_user, root) if draft is None else draft
calc = await asyncio.to_thread(library.resolve_hauls, root, {**user, **haul_user(hauls)})
inputs["bill"] = bill_items({**b08, **calc, "haul_routes": hauls})
inputs["hopyo"] = hopyo_prices(root)
@@ -316,6 +339,14 @@ async def refresh_preview(
return await asyncio.to_thread(refresh.preview, root, book, keys, user.get("daega_cover"))
@router.post("/{project_id}/estimation/calc")
async def calc_estimation(project_id: UUID, body: dict[str, Any] = Body(...)) -> dict[str, Any]:
"""702-3 조작 중 서버 셈 — 화면 캐시(user)로 GET 꼴 셈 · 저장 안 함 · 단가 = 굳힌 책
(없는 줄 = 지금 M01 · [저장] 때 책에 굳힘 → 같은 값)."""
root = await _project_root(project_id)
return await payload(project_id, root, store.clean_user(body.get("user")), reuse_b08=True)
class PreviewBody(BaseModel):
code: str = Field(min_length=1, max_length=40)
given: dict[str, Any] = Field(default_factory=dict)