- B09_Estimation_Edits: 고친 값 한 벌 — 기본 조립(cached_build) 복사본에 얹음(캐시 키에 고친 값) · 기본 벌은 그대로(골든셋 무관)
· 얹지 못한 값은 edit_skipped 로 남김 · 값 없는 슬롯은 받지 않음(422 + 까닭)
- 새 라우터 B09_Estimation_Router_Edits(GET/PUT /estimation/edits) — Router.py 는 _build_for 가 고친 값을 얹게만 바꿈
- 화면: 줄마다 채택 슬롯 고르개 · 일괄(변동없음/1~5 단가/최소단가) · 고친 줄 「사용자」 + ↺(지우면 계산값으로)
- 곁: 사용자 식 셈(B09_Estimation_Expression — ROUND·ROUNDDOWN·INT·SQRT, eval 없음)과 본표 편집 칸(UI_DetailEdit)을 먼저 둠 — 서버 편집 문이 서기 전까지 잠자 있음
- 검증: 시험 1656 통과 · ORCA 채택 저장 → 「사용자」·↺ → ↺ 뒤 고친 값 {} 로 복구 · 값 없는 슬롯 422 「경유: 1번 원천에 값이 없어」
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
66 lines
2.6 KiB
Python
66 lines
2.6 KiB
Python
"""B09 사용자가 고친 값 — 프로젝트 단위 저장·얹기·되돌리기 (PLAN 12장 2차 · 2026-09-14 브레인 판정).
|
||
|
||
겨누는 것
|
||
① 고친 값은 **복사본에** 얹힘 — 기본 벌(캐시)은 그대로(골든셋·다른 프로젝트가 안 흔들림)
|
||
② 자재단가대비표 채택 슬롯 — 값 있는 슬롯만 받음 · 채택하면 금액이 그 슬롯 값으로 섬
|
||
③ 지우면(↺) 계산값으로 돌아감 · 고친 값이 없으면 캐시 키가 빈 글
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from decimal import Decimal
|
||
|
||
import pytest
|
||
|
||
from B09_Estimation.B09_Estimation_Edits import (
|
||
EditError,
|
||
apply_edits,
|
||
edits_key,
|
||
merge_changes,
|
||
)
|
||
from B09_Estimation.B09_Estimation_PriceBook import PriceBook, PriceDetail, PriceKind, PriceTitle
|
||
from B09_Estimation.B09_Estimation_UnitPrice import UnitPriceBuild
|
||
|
||
|
||
def _build() -> UnitPriceBuild:
|
||
book = PriceBook()
|
||
slots = [Decimal(100), None, Decimal(90), None, None, Decimal(120)]
|
||
book.add_title(PriceTitle("M-1", PriceKind.MATERIAL, "시멘트", slots=slots))
|
||
book.add_title(PriceTitle("B-1", PriceKind.UNIT_PRICE, "타설"))
|
||
book.add_detail(PriceDetail("B-1", "M-1", Decimal(2)))
|
||
return UnitPriceBuild(book=book)
|
||
|
||
|
||
def test_채택_슬롯을_바꾸면_복사본에서만_금액이_바뀐다() -> None:
|
||
base = _build()
|
||
edits = merge_changes({}, base, [{"section": "adopted_slots", "key": "M-1", "value": "3"}])
|
||
assert edits == {"adopted_slots": {"M-1": 3}}
|
||
edited = apply_edits(base, edits)
|
||
assert edited.book.resolve("B-1").material == Decimal(180) # 90 × 2
|
||
assert base.book.resolve("B-1").material == Decimal(240) # 기본 벌은 6번 120 그대로
|
||
assert edited.edit_skipped == []
|
||
|
||
|
||
def test_값_없는_슬롯은_받지_않는다() -> None:
|
||
with pytest.raises(EditError):
|
||
merge_changes({}, _build(), [{"section": "adopted_slots", "key": "M-1", "value": 2}])
|
||
with pytest.raises(EditError):
|
||
merge_changes({}, _build(), [{"section": "adopted_slots", "key": "B-1", "value": 1}])
|
||
|
||
|
||
def test_지우면_계산값으로_돌아가고_캐시_키가_빈다() -> None:
|
||
base = _build()
|
||
edits = merge_changes(
|
||
{"adopted_slots": {"M-1": 1}},
|
||
base,
|
||
[{"section": "adopted_slots", "key": "M-1", "value": None}],
|
||
)
|
||
assert edits == {} and edits_key(edits) == ""
|
||
assert edits_key({"adopted_slots": {"M-1": 1}}) != ""
|
||
|
||
|
||
def test_단가표에서_사라진_코드는_버리지_않고_남긴다() -> None:
|
||
edited = apply_edits(_build(), {"adopted_slots": {"M-없음": 1, "M-1": 2}})
|
||
assert len(edited.edit_skipped) == 2
|
||
assert edited.book.titles["M-1"].adopted_slot == 6
|