Files
Aislo/resources/tester/test_b09_edits.py
T
eomsangdonandClaude Opus 5 c3618fe872 feat(b09): 2차 편집 ② 일위대가·단산 구성행 수정 · ③ Q 식 편집 — 프로젝트 단위 · 서버가 다시 계산 · ↺
- 구성행(sheet_rows): 수량 고치기 · 줄 빼기(수량 0, 흐리게) · 줄 더하기(단가표 고르개) — 비율 줄·돌림 참조는 받지 않음
- Q 식(price_basis_q): 식은 명세 13장 식 언어 — B08 구조물도 풀이기(evaluate_sheets, Node 한 벌)를 그대로 부름 ·
  소수 2자리 사사오입 확정 → 새 수량 = 수량 × 옛 Q ÷ 새 Q · 비고에 「Q(사용자) = 식 = 값」
  · PriceDetail.output(시공능력 Q)을 Q 로 선 장비 줄 넷 자리(굴착기·도자·직접 작업량·암 잎)에서 실음 · 저장 모양에도 실음
- 따로 짰던 파이썬 식 셈(B09_Estimation_Expression)은 걷어냄 — 식 풀이 두 벌 금지(브레인 판정)
- 새 문: GET /estimation/edits/sheet/{code}(본표 + 줄마다 고친 값 표시) · GET /estimation/edits/search(줄 더하기 고르개)
- 화면: 본표 [편집] — 수량 칸 · ✕ · Q 식 칸 · 줄 더하기 · 고친 줄 「사용자」 + ↺
- 검증: 시험 1664 통과 · 골든셋 초록 · ORCA — 제 3 호표 보통인부 0.023→0.046 이면 5,652→9,610 · 내역 본체 122,848,989→122,857,198,
  ↺ 뒤 5,652 · 산근 2호표 Q 58.21→116.42 이면 1,695→847, ↺ 뒤 1,695 · 틀린 식 422 「모르는 이름: abc」 · 고친 값 {} 로 복구

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 03:37:18 +09:00

147 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 _book_with_basis() -> UnitPriceBuild:
build = _build()
book = build.book
book.add_title(
PriceTitle("L-1", PriceKind.LABOR, "보통인부", slots=[None] * 5 + [Decimal(100000)])
)
book.add_title(
PriceTitle("X-1", PriceKind.MACHINE_BASE, "굴착기", slots=[None] * 5 + [Decimal(55700)])
)
book.add_detail(PriceDetail("B-1", "L-1", Decimal("0.1")))
# D — Q 15.71 로 선 장비 줄(몫 1): 수량 1/15.71
book.add_output_detail("B-1", "X-1", Decimal(1) / Decimal("15.71"), "Q 식", Decimal("15.71"))
return build
def test_구성행_수량을_고치고_빼고_더하면_금액이_다시_서고_지우면_돌아온다() -> None:
base = _book_with_basis()
before = base.book.resolve("B-1")
changes = [
{
"section": "sheet_rows",
"key": "B-1|1",
"value": {"quantity": "0.2"},
}, # 보통인부 0.1 → 0.2
{"section": "sheet_rows", "key": "B-1|0", "value": {"removed": True}}, # 시멘트 뺌
{"section": "sheet_rows", "key": "B-1|+1", "value": {"ref": "L-1", "quantity": "0.05"}},
]
edits = merge_changes({}, base, changes)
edited = apply_edits(base, edits)
after = edited.book.resolve("B-1")
assert after.material == 0 and after.labor == Decimal(25000) # 0.2×10만 + 0.05×10만
marks = edited.edit_rows["B-1"]
assert marks[0]["removed"] and marks[1]["was_quantity"] == "0.1" and marks[3]["added"]
assert base.book.resolve("B-1") == before # 기본 벌 그대로
cleared = merge_changes(edits, base, [{**c, "value": None} for c in changes])
assert cleared == {} and apply_edits(base, cleared).book.resolve("B-1") == before
def test_Q_식을_고치면_수량이_옛_Q_를_새_Q_로_바꾼_만큼_변한다() -> None:
base = _book_with_basis()
key = "D-1|0"
edits = merge_changes(
{},
base,
[
{
"section": "price_basis_q",
"key": key,
"value": {"q_formula": "3600*0.2*0.7*0.85*0.55/15*2"},
}
],
)
edited = apply_edits(base, edits)
detail = edited.book.details["D-1"][0]
assert detail.output == Decimal("31.42")
assert detail.quantity == Decimal(1) / Decimal("15.71") * Decimal("15.71") / Decimal("31.42")
assert edited.edit_rows["D-1"][0]["q_formula"].startswith("3600")
# 줄 0.1원 · 머리 원 미만 절사: 55,700 ÷ 31.42 = 1,772.7 → 1,772
assert edited.book.resolve("D-1").expense == Decimal(1772)
def test_Q_로_안_선_줄·비율_줄·돌림_참조는_받지_않는다() -> None:
base = _book_with_basis()
with pytest.raises(EditError):
merge_changes(
{}, base, [{"section": "price_basis_q", "key": "B-1|1", "value": {"q_formula": "10"}}]
)
with pytest.raises(EditError):
merge_changes(
{},
base,
[{"section": "sheet_rows", "key": "B-1|+1", "value": {"ref": "B-1", "quantity": "1"}}],
)
with pytest.raises(EditError):
merge_changes(
{},
base,
[{"section": "sheet_rows", "key": "D-1|+1", "value": {"ref": "B-1", "quantity": "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