feat(B09): 칸 단위 근거 호버·등급색 배선 + 줄 사유 덮어쓰기 버그 수정
① 버그 — BillRow.note 한 칸에 덮어쓰던 13곳·이어 붙이던 7곳을 조각 배열 notes 로 옮김. 한 줄에 사유가 둘이면 하나가 조용히 사라지던 것이 멈춤. 조각마다 닿는 열 키를 함께 실어 호버가 그 칸에만 띄우게 함. 화면 비고 칸은 종전과 같은 꼴 유지. ② 사전 — B09_Estimation_Provenance.py 신설(기존 B09 파일이 700줄 초과). 8장 57열. 이미 있던 formula_text·source_label·base_label 은 그대로 쓰고 없는 것만 새로 적음. ③ 배선 — 라우터 5개 응답에 _with_provenance(), 표 여섯에 markProvenanceCell + attachProvenance. 개발환경이 아니면 응답에 칸 자체가 안 생김(로직 보안). ④ 등급색 토글을 탭 줄 끝에. 사전이 안 오면 단추도 안 세움. ⚠ 원가계산서 금액은 같은 열에서 중간줄과 마지막줄의 성격이 갈리는데 등급이 열에 하나뿐이라 calc 로 두고 rule 에 어느 줄이 final 인지 적음(PLAN 8-36 ① 에 남김). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp
This commit is contained in:
@@ -28,6 +28,7 @@ from B09_Estimation.B09_Estimation_Engine_Cost import (
|
||||
proposed_profit_adjustment,
|
||||
)
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBookError
|
||||
from B09_Estimation.B09_Estimation_Provenance import estimation_provenance
|
||||
from B09_Estimation.B09_Estimation_BillOfQuantities import bill_summary, build_bill
|
||||
from B09_Estimation.B09_Estimation_Guards import DoubleCountError
|
||||
from B09_Estimation.B09_Estimation_Rates import RateLookupError
|
||||
@@ -47,6 +48,18 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/projects", tags=["B09 Estimation"])
|
||||
|
||||
|
||||
def _with_provenance(body: dict[str, Any]) -> dict[str, Any]:
|
||||
"""근거 사전을 응답에 얹는다 — **개발환경이 아니면 칸 자체를 안 만든다.**
|
||||
|
||||
⚠ 빈 dict 를 실으면 화면이 「사전이 있는데 비었다」로 읽어 빈 카드를 띄운다.
|
||||
그래서 `None` 이면 **키를 넣지 않는다**(로직 보안의 문은 서버 쪽 하나뿐이다).
|
||||
"""
|
||||
provenance = estimation_provenance()
|
||||
if provenance is not None:
|
||||
body["provenance"] = provenance
|
||||
return body
|
||||
|
||||
|
||||
class CostRequest(BaseModel):
|
||||
"""원가계산 입력 — 금액은 원 단위."""
|
||||
|
||||
@@ -176,7 +189,7 @@ async def compute_cost(project_id: UUID, payload: CostRequest) -> JSONResponse:
|
||||
body["suggested_profit_adjustment_krw"] = str(
|
||||
proposed_profit_adjustment(result, payload.target_contract_amount_krw)
|
||||
)
|
||||
return JSONResponse(content={"status": "success", **body})
|
||||
return JSONResponse(content=_with_provenance({"status": "success", **body}))
|
||||
|
||||
|
||||
@router.get("/{project_id}/estimation/items")
|
||||
@@ -203,11 +216,13 @@ async def list_unit_price_titles(project_id: UUID) -> JSONResponse:
|
||||
try:
|
||||
build = await _build_for(project_id)
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"summary": build_summary(build),
|
||||
"rows": list_unit_prices(build),
|
||||
}
|
||||
content=_with_provenance(
|
||||
{
|
||||
"status": "success",
|
||||
"summary": build_summary(build),
|
||||
"rows": list_unit_prices(build),
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 일위대가 목록 실패: project_id=%s", project_id)
|
||||
@@ -274,7 +289,9 @@ async def get_base_data_lists(project_id: UUID) -> JSONResponse:
|
||||
|
||||
try:
|
||||
return JSONResponse(
|
||||
content={"status": "success", **all_lists(await _build_for(project_id))}
|
||||
content=_with_provenance(
|
||||
{"status": "success", **all_lists(await _build_for(project_id))}
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("B09 기초자료 목록 실패: project_id=%s", project_id)
|
||||
@@ -689,7 +706,9 @@ async def get_unit_price_detail(project_id: UUID, code: str) -> JSONResponse:
|
||||
"""일위대가 **본표** — 「무엇으로 이루어졌나」. 줄마다 원천·파고들기 표시가 붙는다."""
|
||||
try:
|
||||
return JSONResponse(
|
||||
content={"status": "success", **detail_of(await _build_for(project_id), code)}
|
||||
content=_with_provenance(
|
||||
{"status": "success", **detail_of(await _build_for(project_id), code)}
|
||||
)
|
||||
)
|
||||
except PriceBookError as error:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(error)})
|
||||
@@ -759,14 +778,18 @@ async def get_bill(project_id: UUID) -> JSONResponse:
|
||||
)
|
||||
|
||||
return JSONResponse(
|
||||
content={
|
||||
"status": "success",
|
||||
"rows": [row.as_dict() for row in result.rows],
|
||||
"excluded": [row.as_dict() for row in result.excluded],
|
||||
"materials": [row.as_dict() for row in result.material_rows],
|
||||
"summary": bill_summary(result),
|
||||
"price_basis": result.price_basis.as_dict() if result.price_basis else {"entries": []},
|
||||
}
|
||||
content=_with_provenance(
|
||||
{
|
||||
"status": "success",
|
||||
"rows": [row.as_dict() for row in result.rows],
|
||||
"excluded": [row.as_dict() for row in result.excluded],
|
||||
"materials": [row.as_dict() for row in result.material_rows],
|
||||
"summary": bill_summary(result),
|
||||
"price_basis": (
|
||||
result.price_basis.as_dict() if result.price_basis else {"entries": []}
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user