feat(796-2e-1): 더한 기초 블록 [연결 끊기] · [다시 잇기] 서버 — 끊음 = 이 SK 안 자기 사본 줄 · 수식 고침 · 재생성 때 줄 안 바꿈

- M02_Sangeun_Block 신설: 끊기(닫힌 묶음 굳힘 · 금액 그대로) · 다시 잇기(고침 버림 + 원본 갈음) · 끊은 기초 수식 읽기 · 미리 셈 · 저장 검사
- Calc.sub_run 이 끊은 기초 자기 줄 + 수식 고침을 얹어 셈 · Copy.settle 이 다시 잇기 표지 처리 · [단가 갱신] · [재생성] 응답 끊은기초 한 줄
- Router: block/unlink · relink · equation 몸 블록 · 새 시험 test_796_2e_unlink_basis 6

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 15:38:36 +09:00
co-authored by Claude Sonnet 5.5
parent e171dda4e3
commit be43dad64e
7 changed files with 468 additions and 9 deletions
@@ -0,0 +1,186 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""796-2e M02 단가산출 더한 기초 블록 [연결 끊기] · [다시 잇기] (임시 storage · M01 은 메모리 덮개).
① 연결 블록 = 원본(M01) 바뀜을 [재생성] 에서 따라감 ② [연결 끊기] = 금액 그대로 · 이 SK 안 자기 사본 ·
원본 바뀜 안 따라감([재생성] 도) ③ 끊은 기초 줄 고침 = 금액 바뀜(미리 셈 = 저장 뒤 서버 셈 · 틀린 고침 400 ·
M01 안 바뀜) ④ [다시 잇기] = 고친 것 버림 · 원본 따라감 ⑤ [단가 갱신] = 연결 · 끊은 둘 다 자원 단가 따라감
⑥ 꼴 검사(연결 기초에 고침 · 이미 끊음 · 차례 밖).
"""
from __future__ import annotations
import copy
import pytest
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Block as blk
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete import M02_Sangeun_Equation_Full as full
from resources.tester.test_795_1_sk_copy import _calc, _changed, _doc, _fake, _live, _save, _sk
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
FORM = "GC000211" # 합판거푸집
BASIC = "GC001148" # 굴착기 시간당 — 호표 0 = 시간당 기계 × (1 / 작업량)
ADD = {"종류": "로직", "로직": BASIC, "수량": 2, "입력값": {}, "이름": "sub3임시 굴착기"}
ONLY = "LB000048" # 굴착기 기초에만 닿는 노임(거푸집엔 안 닿음)
DOUBLE = [{"할일": "식", "자리": "호표:0", "식": "3 / 작업량", "까닭": "sub3임시 시험"}]
def _sum(folder, doc):
return _calc(folder, doc)["기준"]["계"]
@pytest.fixture
def made(world):
me = world["personal"]
key = _sk(me, 바탕={"로직": FORM}, 더한줄=[copy.deepcopy(ADD)])
return me, key
def _cut(me, key):
"""끊어서 저장한 SK — (끊기 전 금액, 끊은 문서)."""
doc = _doc(me, key)
with store.at(me):
got = blk.change(doc, 0, "unlink")
assert got["전"]["계"] == got["후"]["계"] # 끊기 전후 같음
_save(me, key, got["문서"])
return got["전"]["계"], _doc(me, key)
def test_unlink_keeps_amount_and_freezes_rows(made):
"""② 끊기 = 금액 그대로 · 닫힌 묶음(하위 로직 포함) 이 SK 안에 · 저장 뒤도 같음 · 수식 탭 표지."""
me, key = made
was = _sum(me, _doc(me, key))
plain = _calc(me, _doc(me, key))
assert plain["기준"]["계"] == was
row_before = copy.deepcopy(_live().logic(BASIC))
price, doc = _cut(me, key)
assert price == was and _sum(me, doc) == was
cut = doc["더한줄"][0]["끊음"]
keys = {r["키"] for r in cut["로직"]}
assert BASIC in keys and "GC000267" in keys and "GC001147" in keys # 하위 로직까지
assert not any(k[:2] in skc.RES for k in keys) # 단가는 SK 사본 한 곳
assert _live().logic(BASIC) == row_before # M01 안 바뀜
with store.at(me):
eq = full.attach({"줄": []}, doc, skc.over(doc, skc.whole_()), {}, None, _calc(me, doc))
block = next(r for r in eq["소계"]["줄"] if r.get("블록") == 0)
assert block["끊음"] is True and block["나무"] and block["상태"] == "더함"
assert not block.get("덮개") # 끊기만으로 「세부」 고침 표지 없음
def test_connected_follows_original_on_regen_cut_does_not(made, monkeypatch):
"""①·② 원본 바뀜 — 연결 = [재생성] 에서 따라감 · 끊음 = [재생성] 도 안 따라감."""
me, key = made
was = _sum(me, _doc(me, key))
_fake(monkeypatch, [_changed(_live().logic(BASIC))]) # 호표 0 수량 × 2 (원본 바뀜)
assert _sum(me, _doc(me, key)) == was # 누르기 전 = 그대로(자동 갱신 없음)
with store.at(me):
got = skc.preview(_doc(me, key), skc.REGEN)
assert got["후"]["계"] > was and "끊은기초" not in got # 연결 = 원본 따라감
monkeypatch.undo()
price, doc = _cut(me, key)
assert price == was
_fake(monkeypatch, [_changed(_live().logic(BASIC))])
with store.at(me):
got = skc.preview(doc, skc.REGEN)
assert got["전"]["계"] == got["후"]["계"] == was # 끊음 = 안 따라감
assert got["끊은기초"] == ["sub3임시 굴착기"] # 보고 한 줄
_save(me, key, got["문서"])
assert _sum(me, _doc(me, key)) == was
def test_price_refresh_follows_in_both(made, monkeypatch):
"""⑤ [단가 갱신] — 연결 · 끊은 기초 모두 자원 단가는 따라감(요구 656-1) · 끊은 기초 줄은 그대로."""
me, key = made
was = _sum(me, _doc(me, key))
row = copy.deepcopy(_live().get(ONLY))
row["값"] = row["값"] * 2
_fake(monkeypatch, [row])
with store.at(me):
fresh = skc.preview(_doc(me, key), skc.PRICE)
assert fresh["후"]["계"] > was and [x["키"] for x in fresh["바뀐자원"]] == [ONLY]
monkeypatch.undo()
_, doc = _cut(me, key)
_fake(monkeypatch, [row])
with store.at(me):
cut = skc.preview(doc, skc.PRICE)
assert cut["후"]["계"] == fresh["후"]["계"] and cut["끊은기초"] == ["sub3임시 굴착기"]
_save(me, key, cut["문서"])
assert _sum(me, _doc(me, key)) == fresh["후"]["계"]
def test_edit_cut_block_changes_amount_and_saves(made):
"""③ 끊은 기초 줄 고침 — 미리 셈 = 금액 바뀜 · 저장 뒤 서버 셈 같음 · 틀린 고침 400 · M01 안 바뀜."""
me, key = made
price, doc = _cut(me, key)
row_before = copy.deepcopy(_live().logic(BASIC))
with store.at(me):
got = blk.preview(doc, 0, DOUBLE)
read = blk.read(doc, 0)
assert read["ok"] and got["되나"], got
assert got["금액전"]["계"] == price and got["금액후"]["계"] > price
assert got["수식"]["합계"]["계"] > got["전"]["계"] # 기초 1 단위 시간당 값
item = {**doc["더한줄"][0], blk.EDIT: DOUBLE}
_save(me, key, {**doc, "더한줄": [item]})
saved = _doc(me, key)
assert saved["더한줄"][0][blk.EDIT][0]["까닭"] == "sub3임시 시험"
assert _sum(me, saved) == got["금액후"]["계"] # 서버 셈 = 미리 셈
assert _live().logic(BASIC) == row_before
with store.at(me):
eq = full.attach(
{"줄": []}, saved, skc.over(saved, skc.whole_()), {}, None, _calc(me, saved)
)
block = next(r for r in eq["소계"]["줄"] if r.get("블록") == 0)
assert block["나무"]["합계"]["계"] == got["수식"]["합계"]["계"] # 접힘 블록 = 고친 수식
bad = [{"할일": "식", "자리": "호표:0", "식": "없는값 / 작업량"}]
with store.at(me):
assert not blk.preview(saved, 0, bad)["되나"]
with pytest.raises(store.StoreError):
_save(me, key, {**saved, "더한줄": [{**saved["더한줄"][0], blk.EDIT: bad}]})
assert _sum(me, _doc(me, key)) == got["금액후"]["계"]
def test_relink_drops_edits_and_follows_original(made, monkeypatch):
"""④ [다시 잇기] — 고친 것 버림 · 원본 따라감(저장 뒤 표지 없음) · 금액 = 새로 찍은 SK."""
me, key = made
was = _sum(me, _doc(me, key))
_, doc = _cut(me, key)
item = {**doc["더한줄"][0], blk.EDIT: DOUBLE}
_save(me, key, {**doc, "더한줄": [item]})
edited = _doc(me, key)
assert _sum(me, edited) > was
_fake(monkeypatch, [_changed(_live().logic(BASIC))]) # 원본 바뀜
fresh = _sum(me, _doc(me, _sk(me, 바탕={"로직": FORM}, 더한줄=[copy.deepcopy(ADD)])))
assert fresh > was
with store.at(me):
got = blk.change(edited, 0, "relink")
one = got["문서"]["더한줄"][0]
assert "끊음" not in one and blk.EDIT not in one and one[blk.REJOIN] is True
assert got["전"]["계"] > was and got["후"]["계"] == fresh # 고침 버림 + 원본 따라감
_save(me, key, got["문서"])
saved = _doc(me, key)
assert blk.REJOIN not in saved["더한줄"][0] and "끊음" not in saved["더한줄"][0]
assert _sum(me, saved) == fresh
def test_shape_checks(made):
"""⑥ 연결 기초에 고침 · 이미 끊음 · 차례 밖 · 연결 기초 다시 잇기 · 끊은 기초 줄 모양."""
me, key = made
doc = _doc(me, key)
with store.at(me):
with pytest.raises(sd.DocError, match="연결을 끊은 기초만"):
sd.clean({**doc, "더한줄": [{**ADD, blk.EDIT: DOUBLE}]})
with pytest.raises(sd.DocError, match="없음"):
blk.unlink(doc, 3)
with pytest.raises(sd.DocError, match="연결된 기초"):
blk.relink(doc, 0)
with pytest.raises(sd.DocError, match="연결된 기초"):
blk.read(doc, 0)
cut = blk.unlink(doc, 0)
with pytest.raises(sd.DocError, match="이미 연결을 끊은"):
blk.unlink(cut, 0)
broken = {**cut["더한줄"][0], "끊음": {"로직": [{"키": "GC999999"}]}}
with pytest.raises(sd.DocError, match="줄 없음"):
sd.clean({**cut, "더한줄": [broken]}, None, skc.whole_())