diff --git a/A00_Common/formula_doc_copy.ts b/A00_Common/formula_doc_copy.ts index 495b40aa0..fd5107cb1 100644 --- a/A00_Common/formula_doc_copy.ts +++ b/A00_Common/formula_doc_copy.ts @@ -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; } diff --git a/A00_Common/formula_doc_text.ts b/A00_Common/formula_doc_text.ts index 91c3073be..010f39b62 100644 --- a/A00_Common/formula_doc_text.ts +++ b/A00_Common/formula_doc_text.ts @@ -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"], diff --git a/M02_MasterTemplete/M02_Doc_Gap.py b/M02_MasterTemplete/M02_Doc_Gap.py new file mode 100644 index 000000000..b8e22a8e5 --- /dev/null +++ b/M02_MasterTemplete/M02_Doc_Gap.py @@ -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))} diff --git a/M02_MasterTemplete/M02_Doc_Import.py b/M02_MasterTemplete/M02_Doc_Import.py index 0e4d53ca5..96d2084b8 100644 --- a/M02_MasterTemplete/M02_Doc_Import.py +++ b/M02_MasterTemplete/M02_Doc_Import.py @@ -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) diff --git a/M02_MasterTemplete/M02_Doc_Sk.py b/M02_MasterTemplete/M02_Doc_Sk.py index 292245c77..6e4d6acf7 100644 --- a/M02_MasterTemplete/M02_Doc_Sk.py +++ b/M02_MasterTemplete/M02_Doc_Sk.py @@ -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 != "따로"})} diff --git a/M02_MasterTemplete/M02_Doc_Sk_Path.py b/M02_MasterTemplete/M02_Doc_Sk_Path.py index 0b0da4de6..6d4f3303d 100644 --- a/M02_MasterTemplete/M02_Doc_Sk_Path.py +++ b/M02_MasterTemplete/M02_Doc_Sk_Path.py @@ -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)["문서"] diff --git a/resources/tester/test_822_1_copy_gap.py b/resources/tester/test_822_1_copy_gap.py new file mode 100644 index 000000000..04d56e83f --- /dev/null +++ b/resources/tester/test_822_1_copy_gap.py @@ -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