fix(822-1): 사본 빈틈 막기 — 셈이 읽은 M01 줄을 저장 때 값 사본에 박음 · 가져온 식 우리 = 사본 · 빈틈 표지

- 새 M02_Doc_Gap: 문서 셈이 읽은 요소 · 표 줄 가운데 값 사본에 없는 것(gaps) → 지금 M01 닫힌 묶음으로 더함(fill)
- 수식 길(M02_Doc_Sk_Path.edited): 줄 쌓기 품셈 기계 · 바꿔 끼운 기계 · 자원 줄 · 단가산출 참조 · 내 기계 모두 저장 때 박음(옛 _mine 대신)
- 저장 첫머리(M02_Doc_Sk.settle): 따로 = [저장] · [단가 갱신] 때 빈틈 메움 · 파생 = 옛 로직 사본에 더함
- 가져온 식 단가기준 우리(M02_Doc_Import): 셈 · 연결 기록 = 로직 사본 위(지금 M01 직접 읽던 빈틈) · 엑셀 기준 그대로
- 읽기 답 사본 표지 「사본없음」 n · 화면 배지 「사본 없는 줄 n」(지금 값으로 셈 · 저장하면 굳음) · 일괄 고쳐 쓰지 않음
- 시험 test_822_1_copy_gap 14 · storage SK 122 고치기 전후 금액 같음 · 빈틈 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 17:21:13 +09:00
co-authored by Claude Opus 5.5
parent c5c45dc92a
commit cbe9a3d2ee
7 changed files with 382 additions and 17 deletions
+9
View File
@@ -7,6 +7,7 @@
* 층 쓰기 권한 없으면 「읽기만」 배지
* (821-3) 값 줄만 바뀜(M01 단가 · 내 기계 고침) = 「새 값 있음」(같은 자리 · 같은 꼴) · 지운 키(M01 · 내 기계) = 「지워짐 n」 ·
* [단가 갱신] 확인 창 = 바뀐 자원 줄(이름 · 바뀐 칸 전 → 후 · 지워짐 = 마지막 값 유지)
* (822-1) 값 사본에 없는 셈 줄(옛 저장본 빈틈) = 「사본 없는 줄 n」(지금 값으로 셈 · 저장하면 굳음)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
@@ -23,6 +24,8 @@ export interface CopyMark {
M01새판?: boolean;
지운키?: string[];
단가새값?: boolean;
/** 값 사본에 없는 셈 줄 수(822-1) — 지금 값으로 셈 · 저장하면 굳음 */
사본없음?: number;
문서새판?: string[];
따로?: boolean;
/** 가져온 식(엑셀 불러오기 · 807-1) 단가기준 — 엑셀 = 엑셀 값 기억 · 우리 = 연결 값 */
@@ -64,6 +67,12 @@ export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[
b.dataset.copyGone = m.지운키.join(",");
out.push(b);
}
if (m?.사본없음) {
const b = createBadge(`${ft("Gap")} ${m.사본없음}`, "warning");
b.title = ft("Gap_Note");
b.dataset.copyGap = String(m.사본없음);
out.push(b);
}
if (!edit) out.push(createBadge(ft("Read_Only"), "neutral"));
return out;
}
+2
View File
@@ -66,6 +66,8 @@ const TEXT = {
New_Value: ["새 값 있음", "New values"],
Gone: ["지워짐", "Deleted"],
Gone_Note: ["마지막 값 유지", "last value kept"],
Gap: ["사본 없는 줄", "Uncopied rows"],
Gap_Note: ["저장하면 지금 값으로 굳음", "saving freezes current values"],
Price: ["단가 갱신", "Refresh prices"],
Regen: ["재생성", "Regenerate"],
Price_Ask: ["자원 단가만 지금 M01 값으로 — 식 · 연결 그대로", "Prices only — equation unchanged"],
+66
View File
@@ -0,0 +1,66 @@
"""M02 단가산출(SK) 수식 사본 빈틈(PLAN 822-1 · 요구 797-1) — 서버 단독.
빈틈 = 문서 셈이 읽은 M01 요소 · 표 줄 가운데 값 사본에 없는 것(줄 쌓기 기계 · 바꿔 끼운 기계 ·
더한 자원 줄 · 단가산출 참조가 읽는 노임 · 유가 · 내 기계 …) — 사본 색인이 지금 M01 로 흘러
M01 이 바뀌면 금액이 바로 따라 바뀜.
`gaps` = 빈틈 키(셈 한 번 · 읽은 줄 적기) · `fill` = 빈틈을 지금 M01 닫힌 묶음으로 값 사본에 더함
(있는 줄 그대로 — `skc.grow`) — [저장] · [단가 갱신] 때만(열기 · 미리 셈은 지금 값으로 셈 + 표지).
"""
from __future__ import annotations
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_Sangeun_Copy as skc
class _Seen:
"""셈 색인 겉 — `get` · `value` · `logic` 이 내준 줄 키를 적음(그 밖 = 속 색인 그대로)."""
def __init__(self, inner: Any) -> None:
self._inner, self.keys = inner, set()
def get(self, ref: str) -> dict:
return self._note(self._inner.get(ref))
def value(self, ref: str):
self.get(ref)
return self._inner.value(ref)
def logic(self, key: str) -> dict:
return self._note(self._inner.logic(key))
def _note(self, row: Any) -> Any:
if isinstance(row, dict) and row.get("키"):
self.keys.add(str(row["키"]))
return row
def __getattr__(self, name: str) -> Any:
return getattr(self._inner, name)
def _have(copy: dict) -> set[str]:
return {str(r.get("키")) for r in [*(copy.get("로직") or []), *(copy.get("단가") or [])]}
def gaps(copy: dict) -> list[str]:
"""값 사본에 없는데 문서 셈이 읽은 M01 줄 키 + 문서의 내 기계 키(셈 틀림 = 읽은 데까지)."""
if "로직" not in copy or not isinstance(copy.get("문서"), dict):
return []
seen = _Seen(dc.over(copy))
try:
dc.mc.calc(copy["문서"], dc.ml.Links(seen, dc.docs_of(copy)))
except dc._ERR:
pass
have = _have(copy)
return sorted({*seen.keys, *mym.keys_in(copy["문서"])} - have)
def fill(copy: dict) -> dict:
"""빈틈을 지금 M01(+ 지금 층 내 기계) 닫힌 묶음으로 값 사본에 더함 · 빈틈 없음 = 그대로."""
keys = gaps(copy)
if not keys:
return copy
return {**copy, **skc.grow(dc.values_of(copy), skc.snap([], keys))}
+12 -2
View File
@@ -51,6 +51,16 @@ def basis_of(sk: dict) -> str:
return "우리" if (sk.get(imp.KEY) or {}).get("단가기준") == "우리" else "엑셀"
def _index(sk: dict, master: Any = None) -> Any:
"""셈 색인 — 우리 = 로직 사본 위(822-1 · 이음 값이 M01 따라 안 바뀜 · [단가 갱신] 으로만) ·
엑셀 = 받은 색인 · 없으면 지금 M01."""
from M02_MasterTemplete import M02_Sangeun_Copy as skc
if basis_of(sk) == "우리":
return skc.over(sk, imp._whole())
return master or imp._whole()
# ── 엑셀 단가 그대로 엔진 줄(옛 셈 길 · SK 고침 얹은 것) ───────────────────
def _ctx(sk: dict, whole: Any) -> tuple:
"""엑셀 단가 기준 고친 줄 — 옛 셈 길(`OLD`)로 돌려 엔진에 넣은 (색인, 키, 입력, 답, 재료)."""
@@ -514,7 +524,7 @@ def record(doc: dict, sk: dict) -> dict[str, Any]:
import master_doc_record as mr # `M02_Doc_Copy` 가 scripts 를 길에 둔 뒤
basis = basis_of(sk)
links = Links(imp._whole(), basis)
links = Links(_index(sk), basis)
mine = view(doc, basis)
rec = mr.Rec(links)
try:
@@ -529,7 +539,7 @@ def record(doc: dict, sk: dict) -> dict[str, Any]:
def run(doc: dict, sk: dict, master: Any = None) -> dict:
"""가져온 식 문서 셈(SK 단가기준) — 계산기 = M01 과 같은 `master_doc_calc`."""
basis = basis_of(sk)
links = Links(master or imp._whole(), basis)
links = Links(_index(sk, master), basis)
mine = view(doc, basis)
try:
links.learn(mine)
+8 -2
View File
@@ -22,6 +22,7 @@ from decimal import Decimal
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Gap as gap
from M02_MasterTemplete import M02_Doc_Import as dim
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
@@ -196,8 +197,10 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
"수식 문서로 고친 단가산출 — 옛 칸(덮개 · 줄) 고침은 수식 탭에서 · 또는 [재생성]",
409,
)
copy = dc.refresh(have)[0] if job == skc.PRICE and not dim.is_imported(out) else have
return {**out, KEY: norm(copy)}
if dim.is_imported(out):
return {**out, KEY: norm(have)}
copy = dc.refresh(have)[0] if job == skc.PRICE else have
return {**out, KEY: norm(gap.fill(copy))} # 사본 빈틈 = 지금 값으로 박음(822-1)
# 빈 SK(바탕 · 줄 없음) = 빈 문서(요구서_664 0 장) · 수식 길로 고친 빈 SK = 위 따로(804-4)
if not skc.has(out) and not dim.is_imported(out):
empty = seed(out)
@@ -207,4 +210,7 @@ def settle(out: dict, whole: Any, job: Any, raw: Any) -> dict:
calls = {} if job == skc.REGEN else (have or {}).get("부름") or {}
first = {**(dc.values_of(out[skc.KEY]) if skc.has(out) else {}), "부름": calls}
copy = copy_of({**out, KEY: first}, box[-1] if box else None)
if copy and skc.has(out) and (keys := gap.gaps(copy)): # 파생 — 옛 로직 사본에 박음(822-1)
out = {**out, skc.KEY: skc.grow(out[skc.KEY], skc.snap([], keys))}
copy = {**copy, **dc.values_of(out[skc.KEY])}
return {**out, KEY: norm({k: v for k, v in (copy or first).items() if k != "따로"})}
+7 -13
View File
@@ -17,11 +17,11 @@ import json
from typing import Any
from M02_MasterTemplete import M02_Doc_Copy as dc
from M02_MasterTemplete import M02_Doc_Gap as gap
from M02_MasterTemplete import M02_Doc_Import as dim
from M02_MasterTemplete import M02_Doc_Screen as screen
from M02_MasterTemplete import M02_Doc_Sk as dsk
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MyMachine as mym
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
@@ -77,7 +77,11 @@ def answer(copy: dict, sk: dict) -> dict[str, Any]:
got = dsk.run(sk, copy)
head = {k: copy.get(k) for k in ("시점", "단가시점")}
mark = {**head, "따로": bool(copy.get("따로"))}
mark |= {"단가기준": dim.basis_of(sk)} if dim.is_imported(sk) else dc.state(copy)
mark |= (
{"단가기준": dim.basis_of(sk)}
if dim.is_imported(sk)
else {**dc.state(copy), "사본없음": len(gap.gaps(copy))} # 저장하면 지금 값으로 굳음
)
body = {
"문서": copy["문서"],
"부름": copy.get("부름") or {},
@@ -129,22 +133,12 @@ def edited(copy: dict, body: dict, key: str, sk: dict | None = None) -> dict:
*(c for d in out.get("끊음", {}).values() for c in dc.calls(d)),
]
if not dim.is_imported(sk):
out |= _mine(out)
out["부름"] = dc.closure(dc.over(out), calls, copy.get("부름"))
out = gap.fill(out) # 셈이 읽은 줄 가운데 사본 없는 것 = 지금 값으로 박음(822-1)
out["따로"] = True
return out
def _mine(copy: dict) -> dict:
"""줄 쌓기에서 고른 내 기계(기계='개인 EQP…') 가운데 값 사본에 없는 것만 지금 값으로 더함
(821-3) — 그 뒤 내 기계를 고쳐도 금액 그대로 + 「단가새값」 · [단가 갱신] 으로 반영 ·
지워도 마지막 값."""
keys = mym.keys_in(copy.get("문서"))
if not keys or "로직" not in copy:
return {}
return skc.grow(dc.values_of(copy), skc.snap([], keys))
def preview(key: str, body: dict) -> dict[str, Any]:
"""미리 셈(저장 없음) — {셈, 전, 후, 바뀐자원?, 버림?}."""
doc = _file(key)["문서"]
+278
View File
@@ -0,0 +1,278 @@
# ruff: noqa: F811 — 임시 층 fixture(world)를 test_m02_labor_ref 에서 받아 씀
"""822-1 사본 빈틈 막기(요구 797-1) — 길마다: 만들기 → [저장] → M01 값(임시 · 메모리 덮개)을
기계 가격 · 노임 · 유가 · 자재 값으로 바꿈 → 금액 그대로 + 「단가새값」 → [단가 갱신] → 반영.
길 = 수식 길 줄 쌓기(품셈 기계 · 바꿔 끼운 기계 · 자원 줄 · 단가산출 참조) ·
옛 로직 길(더한줄 로직 · 재료 · 인력) · 가져온 식(단가기준 우리) · 층 복사 · [재생성] ·
빈틈 SK 표시(`사본없음`).
M01 바꿈 = `skc.Copy` 아닌 진짜 색인 사본(`dc.base` 가 못 벗김) · 정본 파일 안 고침.
"""
from __future__ import annotations
import copy
import json
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Doc_Sk_Path as path
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Sangeun_Copy as skc
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 같은 임시 층
DIG, DIG_NO, DIG_LOGIC = "EQ000022", "0201-0100", "GC000267" # 굴착기(무한궤도) 1.0㎥
DIG2_NO = "0201-0070" # 바꿔 끼울 굴착기
MAN = "LB000002" # 보통인부
PCT = "GC001109" # 금속기와 해체(인력 둘 + 노무비 3 %)
def _live():
return m01.loaded()[1]
def _move(monkeypatch, *keys: str) -> None:
"""M01 값 두 배 — 키마다 값 칸(기계 = 가격 · 자재 = 물가 칸) · 진짜 색인 사본(Copy 아님) ·
수식 문서 폴더 그대로."""
files, live = m01.loaded()
fake = copy.copy(live)
fake.index, fake.numbers = dict(live.index), dict(live.numbers)
for key in keys:
tid = key[:2]
row = copy.deepcopy(live.index[tid][key])
if tid == "EQ":
row["값"]["가격"] *= 2
elif tid == "MT":
for k in ("물가자료", "유통물가", "물가정보", "거래가격"):
row[k] = row[k] * 2 if row.get(k) else row.get(k)
else:
row["값"] *= 2
fake.index[tid] = {**fake.index[tid], key: row}
if row.get("원문번호"):
fake.numbers[tid] = {**fake.numbers[tid], str(row["원문번호"]): row}
monkeypatch.setattr(m01, "loaded", lambda: (files, fake))
def _at(folder, fn, *args):
with store.at(folder):
return fn(*args)
def _sk(folder, **body) -> str:
body = {"이름": "sub1임시 빈틈", "단위": "시간", "더한줄": [], **body}
return _at(folder, lambda: refs.create("sangeun", body)["이름"])
def _var(sym: str, link: dict, name: str = "") -> dict:
return {"기호": "P", "아래": sym, "위": "", "이름": name or sym, "단위": "", "연결": link}
def _line(sym: str, part: str = "경비") -> dict:
return {"이름": sym, "단위": "hr", "비목": part, "수량": "1", "단가": f"P_{sym}"}
def _stack(folder, key, rows: list[tuple[str, dict, str]]) -> None:
"""줄 쌓기 — 지금 문서에 (기호, 연결, 비목) 줄을 더해 수식 길로 저장."""
got = _at(folder, path.read, key)
doc = copy.deepcopy(got["문서"])
doc.setdefault("변수", []).extend(_var(s, link) for s, link, _ in rows)
doc.setdefault("금액", []).extend(_line(s, part) for s, _, part in rows)
_at(folder, path.save, key, {"문서": doc, "판": got["판"]})
def _sum(folder, key):
return _at(folder, path.read, key)["셈"]["계"]
def _price(folder, key) -> dict:
"""[단가 갱신] 저장 — 바뀐 자원 키 · 뒤 금액."""
look = _at(folder, path.preview, key, {"할일": skc.PRICE})
판 = _at(folder, path.read, key)["판"]
_at(folder, path.save, key, {"할일": skc.PRICE, "판": 판})
return {"바뀐": sorted(x["키"] for x in look["바뀐자원"]), "후": _sum(folder, key)}
MACHINE = {"갈래": "단가산출", "키": DIG_LOGIC, "입력": {"기계": f"'{DIG_NO}'"}}
def _fuel_and_crew(no: str = DIG_NO) -> tuple[str, str]:
"""기계 시간당 식이 읽는 유가(MO) · 조종원 노임(LB) 키."""
w = _live()
row = w.numbers["EQ"][no]
fuel = w.numbers["MO"][f"{row['값']['주연료종류']}:전국평균"]["키"]
crew = w.numbers["LB"][str(row["값"]["조종원직종"])]["키"]
return fuel, crew
# ── 수식 길 줄 쌓기 ───────────────────────────────────────────────────
@pytest.mark.parametrize("what", ["가격", "유가", "노임"])
def test_stack_m01_machine(world, monkeypatch, what):
"""품셈 기계(굴착기 1.0㎥) 줄 쌓기 → 저장 → 기계 가격 · 유가 · 조종원 노임 바뀜 →
금액 그대로 + 단가새값 → [단가 갱신] → 반영."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
was = _sum(me, key)
assert was > 0
fuel, crew = _fuel_and_crew()
moved = {"가격": DIG, "유가": fuel, "노임": crew}[what]
_move(monkeypatch, moved)
now = _at(me, path.read, key)
assert now["셈"]["계"] == was and now["사본"]["단가새값"] is True
got = _price(me, key)
assert got["바뀐"] == [moved] and got["후"] > was
def test_stack_swapped_machine(world, monkeypatch):
"""바꿔 끼운 기계 — 기계 입력을 다른 굴착기로 바꿔 저장 → 그 기계 값이 사본에."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
got = _at(me, path.read, key)
doc = copy.deepcopy(got["문서"])
for v in doc["변수"]:
if (v.get("연결") or {}).get("키") == DIG_LOGIC:
v["연결"]["입력"] = {"기계": f"'{DIG2_NO}'"}
_at(me, path.save, key, {"문서": doc, "판": got["판"]})
was = _sum(me, key)
other = _live().numbers["EQ"][DIG2_NO]["키"]
_move(monkeypatch, other)
assert _sum(me, key) == was
assert _price(me, key)["바뀐"] == [other]
@pytest.mark.parametrize("kind", ["인력", "재료"])
def test_stack_resource(world, monkeypatch, kind):
"""자원 줄 더하기(노임 · 자재 요소) — 저장 뒤 값 바뀌어도 그대로 · 갱신 뒤 반영."""
me = world["personal"]
res = MAN if kind == "인력" else next(iter(_live().index["MT"]))
key = _sk(me)
part = "노무비" if kind == "인력" else "재료비"
_stack(me, key, [("자원", {"갈래": "요소", "키": res}, part)])
was = _sum(me, key)
assert was > 0
_move(monkeypatch, res)
assert _sum(me, key) == was
assert _price(me, key)["바뀐"] == [res]
def test_stack_logic_ref(world, monkeypatch):
"""단가산출 참조(M01 단가산출을 부름) — 그 식이 읽는 노임도 사본에."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("참조", {"갈래": "단가산출", "키": PCT, "입력": {}}, "묶음")])
was = _sum(me, key)
assert was > 0
_move(monkeypatch, MAN)
assert _sum(me, key) == was
assert MAN in _price(me, key)["바뀐"]
# ── 옛 로직 길(더한줄 · 파생) ────────────────────────────────────────
OLD_ADDS = {
"로직 기계": ([{"종류": "로직", "로직": DIG_LOGIC, "입력값": {"기계": DIG_NO}, "수량": 1}],
{"기계": DIG_NO}, DIG),
"재료 요소": ([{"종류": "재료", "요소": "MT000008", "수량": 1}], {}, "MT000008"),
"인력 요소": ([{"종류": "인력", "요소": MAN, "수량": 1}], {}, MAN),
} # fmt: skip
@pytest.mark.parametrize("name", list(OLD_ADDS))
def test_old_added_lines(world, monkeypatch, name):
"""옛 로직 길 더한줄(로직 기계 · 재료 요소 · 인력 요소) — 저장 뒤 그대로 · 갱신 뒤 반영."""
me = world["personal"]
adds, given, moved = OLD_ADDS[name]
key = _sk(me, 더한줄=adds, 입력값=given)
was = _sum(me, key)
assert was > 0
assert _at(me, path.read, key)["사본"]["사본없음"] == 0
_move(monkeypatch, moved)
assert _sum(me, key) == was
got = _price(me, key)
assert moved in got["바뀐"] and got["후"] > was
# ── 가져온 식 · 층 복사 · 재생성 ──────────────────────────────────────
def test_imported_ours(world, monkeypatch):
"""가져온 식 단가기준 우리 — 이음 노임 바뀌어도 그대로 · 갱신 뒤 반영 · 엑셀 기준 = 엑셀 값."""
from M02_MasterTemplete import M02_Sangeun_Imported as imp
me = world["personal"]
man = {"종류": "인력", "이름": "보통인부", "단위": "인", "수량": "0.5", "비목": "노무비"}
row = {
"이름": "sub1임시 가져온",
"결과단위": "원/㎡",
"호표": [{**man, "단가": 165545, "이음": MAN}],
}
key = _at(me, lambda: imp.create(row, None, "sub1임시 가져온")["이름"])
assert _sum(me, key) == 82772 # 엑셀 단가 그대로
doc = _at(me, lambda: store.read("sangeun", key)["문서"])
ours = _at(me, lambda: imp.to_price(doc, "우리")["문서"])
_at(me, lambda: store.write("sangeun", key, store.read("sangeun", key)["판"], ours))
was = _sum(me, key)
_move(monkeypatch, MAN)
assert _sum(me, key) == was
doc = _at(me, lambda: store.read("sangeun", key)["문서"])
got = _at(me, skc.preview, doc, skc.PRICE)
assert [x["키"] for x in got["바뀐자원"]] == [MAN] and got["후"]["계"] > was
def test_layer_copy_keeps_copy(world, monkeypatch):
"""층 복사(개인 → 회사) — 줄 쌓기 기계 사본째 · 원본 M01 값 바뀐 뒤 복사해도 그 값."""
me, co = world["personal"], world["company"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
was = _sum(me, key)
_move(monkeypatch, DIG)
ref = {"층": "personal", "주인": "7/42", "키": key}
with store.at(co):
doc = refs.read_ref(ref)
new = refs.create("sangeun", refs.carry(refs.origin_mark(doc, ref), ("personal", "7/42")))
assert _sum(co, new["이름"]) == was
assert _at(co, path.read, new["이름"])["사본"]["단가새값"] is True
def test_regen_follows_m01(world, monkeypatch):
"""[재생성] — 지금 M01 로 다시 찍음(따로 문서 버림) · 그 뒤 M01 바뀌어도 그대로."""
me = world["personal"]
key = _sk(me, 바탕={"로직": PCT}, 단위="㎡")
_stack(me, key, [("기계", MACHINE, "묶음")])
_move(monkeypatch, MAN)
판 = _at(me, path.read, key)["판"]
_at(me, path.save, key, {"할일": skc.REGEN, "판": 판})
after = _at(me, path.read, key)
assert after["사본"]["따로"] is False and after["사본"]["사본없음"] == 0
assert not after["사본"]["단가새값"]
was = after["셈"]["계"]
_move(monkeypatch, MAN) # 한 번 더(네 배)
assert _sum(me, key) == was
def test_gap_mark_then_save_fills(world, monkeypatch):
"""빈틈 있는 옛 SK — 열면 지금 값으로 셈 + 「사본없음 n」 · 일괄 고쳐 쓰지 않음 ·
[저장] 하면 지금 값으로 굳음(n = 0) · 그 뒤 M01 바뀌어도 그대로."""
me = world["personal"]
key = _sk(me)
_stack(me, key, [("기계", MACHINE, "묶음")])
file = me / "sangeun" / f"{key}.json"
raw = json.loads(file.read_text(encoding="utf-8"))
vals = raw["문서"]["수식"] if "문서" in raw else raw["수식"]
for part in ("로직", "단가"):
vals[part] = [] # 옛 저장본 흉내 — 줄 쌓기 기계를 사본에 안 박은 때
file.write_text(json.dumps(raw, ensure_ascii=False), encoding="utf-8")
before = file.read_bytes()
got = _at(me, path.read, key)
assert got["사본"]["사본없음"] > 0
was = got["셈"]["계"]
assert file.read_bytes() == before # 열기 = 안 고쳐 씀
files, live = m01.loaded()
_move(monkeypatch, DIG)
moved = _sum(me, key)
assert moved > was # 빈틈 = 지금 값으로 셈
got = _at(me, path.read, key)
_at(me, path.save, key, {"문서": got["문서"], "판": got["판"]})
now = _at(me, path.read, key)
assert now["사본"]["사본없음"] == 0 and now["셈"]["계"] == moved
monkeypatch.setattr(m01, "loaded", lambda: (files, live)) # 앞 값(절반)
assert _sum(me, key) == moved