Files
Aislo/M01_MasterData/M01_MasterData_Store_Revise.py
T
eomsangdonandClaude Opus 5.5 7cf504a7c7 feat(713-1): 로직 정본 개정 확인 서버 — /save · 흐름 저장 · 지우기 · 더하기가 로직 정본 줄을 바꾸면 개정확인 없이는 409(개정 · 까닭) · 같은 줄 다시 보냄 · 요소 · 자체는 대상 밖 · 줄 바탕 지문 row_print(계산 칸) · GET /logic 지문 · 새 파일 Store_Revise
낡은 시험 고침: test_m01_api · test_m01_cache · test_m01_logic_flow_save · test_208_1_logic_alias · test_m01_logic_flow_project 가 임시 사본의 정본 로직 줄을 고쳐 저장 — 이제 개정 확인이 필요하므로 몸에 개정확인 true(뜻 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 15:15:30 +09:00

82 lines
3.3 KiB
Python

"""M01 로직 정본 개정 확인 (PLAN 713-1 · 요구서 내역12 4장 10 · 17).
- 로직 정본 파일(`로직_*.json` · 자체 `로직_자체.json` 뺌) 줄을 실제로 바꾸는 `/save`(흐름 저장 ·
지우기 포함)는 「원문 개정 반영」 확인 값(`개정확인`)이 있어야 함 — 없으면 409 + 까닭.
- 요소 파일(인력 · 재료 · 표 …) · 자체 로직 · 같은 줄 그대로 다시 보냄은 대상 밖.
- 줄 바탕 지문(`row_print`) = 계산 칸만 정렬 JSON(수는 Decimal 정규 글) sha256 앞 16 자 —
M02 덮개가 담아 두고 「M01 개정 — 확인」 배지를 띄울 값.
- 판 칸 · 옛 줄 사본은 안 둠 — 정본 이력은 git.
"""
from __future__ import annotations
import hashlib
import json
from decimal import Decimal
from M01_MasterData import M01_MasterData_Store as store
OWN = "로직_자체.json"
PRINT_FIELDS = ("결과단위", "입력", "중간", "호표", "덧줄", "결과", "끝수", "흐름")
WHY = "로직 정본(품셈 원문) 줄을 바꿈 — 「원문 개정 반영」 확인 뒤 저장(개정확인)"
def is_canon(file: str) -> bool:
return file.startswith("로직_") and file.endswith(".json") and file != OWN
def _norm(v):
"""수는 int · float · Decimal 어느 꼴로 와도 같은 글(「10」 · 「10.0」 → 「10」)."""
if isinstance(v, bool) or v is None or isinstance(v, str):
return v
if isinstance(v, (int, float, Decimal)):
return format(Decimal(str(v)).normalize(), "f")
if isinstance(v, list):
return [_norm(x) for x in v]
if isinstance(v, dict):
return {k: _norm(x) for k, x in v.items()}
return str(v)
def row_print(row: dict) -> str:
"""줄 바탕 지문 — 계산 칸(`PRINT_FIELDS`)만 · 이름 · 별칭 · 비고 고침은 지문을 안 바꿈."""
calc = {k: _norm(row[k]) for k in PRINT_FIELDS if row.get(k) is not None}
raw = json.dumps(calc, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(raw.encode("utf-8")).hexdigest()[:16]
def changed(batch: list[dict]) -> list[dict]:
"""정본 줄을 바꾸는 고침 — `[{file, key, op, 이름}]` · 같은 줄을 그대로 다시 보낸 edit 은 뺌."""
out = []
for part in batch:
file = str(part.get("file") or "")
if not is_canon(file):
continue
try:
rows = {str(r.get("키")): r for r in store.read(file)[0].get("줄") or []}
except store.StoreError:
continue # 없는 파일 — `store.save` 가 404
for ch in part.get("changes") or []:
op, key, row = ch.get("op"), str(ch.get("key") or ""), ch.get("row")
old = rows.get(key)
if op == "edit" and old is not None and store._dec(row) == old:
continue
name = (row or {}).get("이름") if isinstance(row, dict) else None
out.append(
{
"file": file,
"key": key or None,
"op": op,
"이름": name or (old or {}).get("이름"),
}
)
return out
def guard(batch: list[dict], confirmed: bool) -> None:
if confirmed:
return
hits = changed(batch)
if hits:
raise store.StoreError(409, {"개정": hits, "까닭": WHY})