- Z01_MasterData_Repository_BasePrices.py 신설 — master_base_price·master_revision Raw SQL(읽기·줄 수·판번호·저장·되돌리기) · 판번호 줄을 잠그고 다시 봄 · 틀리면 통째로 되돌림
- GET /base-prices/{kind} 가 DB 를 읽음 · 응답에 revision · 줄마다 changed_columns·is_added 추가
- PUT /base-prices/{kind} {base_revision, edits, added, deleted} — 판 다르면 409 · 하나라도 틀리면 아무것도 안 씀 · edits 500 상한 · 모르는 칸 거절
- POST /base-prices/{kind}/reset {row_keys} — 주입 줄은 data := seed(지운 줄도 되살림) · 추가 줄은 지움
- 기계 시간당 단가는 DB 노임·유가로 읽을 때마다 셈 · 제원은 줄 값에서(추가한 기종도 섬)
- 옛 덮개 층 삭제 — Z01_MasterData_Overrides.py · 칸 단위 PUT · /overrides·/overrides/clear
- 새 줄 열쇠는 서버가 줌(added/{난수}) · 주입 data 에 '@' 칸이 없어도 축 잠금은 파일 원본으로 섬
- 시험: DB 가짜(helper_z01_fake_repo.py) 로 API 손질 · 저장소 SQL 흐름은 가짜 커넥션으로 잼
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
169 lines
6.6 KiB
Python
169 lines
6.6 KiB
Python
"""Z01 기초단가 저장소 — `master_base_price` · `master_revision` (020 · PLAN 1-0 계약 · 2026-09-18).
|
|
|
|
정본 = DB. 줄 = (kind, row_key) 한 줄 · `data` 는 지금 값 한 줄 전체 · `seed` 는 주입 때 초기값(관리자 추가 줄은 NULL).
|
|
판번호 = `master_revision.revision`(scope `base:{kind}`) — 쓸 때마다 +1 · 읽어 간 판과 다르면 안 씀(409).
|
|
⚠ 여기는 SQL 만 — 무엇을 고칠 수 있나·값 검사는 `Z01_MasterData_BasePrices` 가 함.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
import aiomysql
|
|
|
|
from config.config_db import get_db_pool
|
|
|
|
|
|
class RevisionConflict(Exception):
|
|
"""읽어 간 판번호가 지금 판과 다름 — 그 사이 누가 저장함."""
|
|
|
|
|
|
class UnknownRows(LookupError):
|
|
"""없는 줄 열쇠 — 되돌릴 줄을 못 찾음."""
|
|
|
|
|
|
def _scope(kind: str) -> str:
|
|
return f"base:{kind}"
|
|
|
|
|
|
def _json(text: Any) -> Any:
|
|
return json.loads(text) if isinstance(text, (str, bytes)) else text
|
|
|
|
|
|
def _dump(data: dict[str, Any]) -> str:
|
|
return json.dumps(data, ensure_ascii=False, default=str)
|
|
|
|
|
|
async def load(kind: str) -> list[dict[str, Any]]:
|
|
"""지운 줄 뺀 전부 — 넣은 차례(id) 그대로. {row_key, data, seed}."""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor(aiomysql.DictCursor) as cursor:
|
|
await cursor.execute(
|
|
"""SELECT row_key, data, seed FROM master_base_price
|
|
WHERE kind = %s AND is_deleted = 0 ORDER BY id""",
|
|
(kind,),
|
|
)
|
|
return [
|
|
{"row_key": r["row_key"], "data": _json(r["data"]), "seed": _json(r["seed"])}
|
|
for r in await cursor.fetchall()
|
|
]
|
|
|
|
|
|
async def revision(kind: str) -> int:
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT revision FROM master_revision WHERE scope = %s", (_scope(kind),)
|
|
)
|
|
row = await cursor.fetchone()
|
|
return int(row[0]) if row else 0
|
|
|
|
|
|
async def counts() -> dict[str, int]:
|
|
"""kind 마다 줄 수(지운 줄 뺌) — 표 목록 한 번에."""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
await cursor.execute(
|
|
"SELECT kind, COUNT(*) FROM master_base_price WHERE is_deleted = 0 GROUP BY kind"
|
|
)
|
|
return {kind: int(n) for kind, n in await cursor.fetchall()}
|
|
|
|
|
|
async def _lock_revision(cursor: Any, kind: str) -> int:
|
|
"""판번호 줄을 잠그고 읽음 — 없으면 0 으로 세움(같은 kind 쓰기는 여기서 한 줄로 섬)."""
|
|
await cursor.execute(
|
|
"INSERT IGNORE INTO master_revision (scope, revision) VALUES (%s, 0)", (_scope(kind),)
|
|
)
|
|
await cursor.execute(
|
|
"SELECT revision FROM master_revision WHERE scope = %s FOR UPDATE", (_scope(kind),)
|
|
)
|
|
return int((await cursor.fetchone())[0])
|
|
|
|
|
|
async def _bump(cursor: Any, kind: str) -> None:
|
|
await cursor.execute(
|
|
"UPDATE master_revision SET revision = revision + 1 WHERE scope = %s", (_scope(kind),)
|
|
)
|
|
|
|
|
|
async def save(
|
|
kind: str,
|
|
base_revision: int,
|
|
changed: dict[str, dict[str, Any]],
|
|
added: dict[str, dict[str, Any]],
|
|
deleted: list[str],
|
|
by: int | None,
|
|
) -> int:
|
|
"""한 트랜잭션 — 판이 맞을 때만 고친 줄·새 줄·지운 줄을 한꺼번에(전부 아니면 전무). 새 판번호."""
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
try:
|
|
await connection.begin()
|
|
current = await _lock_revision(cursor, kind)
|
|
if current != base_revision:
|
|
raise RevisionConflict(current)
|
|
for row_key, data in changed.items():
|
|
await cursor.execute(
|
|
"""UPDATE master_base_price
|
|
SET data = %s, updated_by = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE kind = %s AND row_key = %s AND is_deleted = 0""",
|
|
(_dump(data), by, kind, row_key),
|
|
)
|
|
for row_key, data in added.items():
|
|
await cursor.execute(
|
|
"""INSERT INTO master_base_price
|
|
(kind, row_key, data, seed, edition, is_deleted, updated_by, updated_at)
|
|
VALUES (%s, %s, %s, NULL, %s, 0, %s, CURRENT_TIMESTAMP)""",
|
|
(kind, row_key, _dump(data), str(data.get("effective_date") or ""), by),
|
|
)
|
|
for row_key in deleted:
|
|
await cursor.execute(
|
|
"""UPDATE master_base_price
|
|
SET is_deleted = 1, updated_by = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE kind = %s AND row_key = %s""",
|
|
(by, kind, row_key),
|
|
)
|
|
await _bump(cursor, kind)
|
|
await connection.commit()
|
|
return current + 1
|
|
except Exception:
|
|
await connection.rollback()
|
|
raise
|
|
|
|
|
|
async def reset(kind: str, row_keys: list[str], by: int | None) -> int:
|
|
"""고른 줄을 초기값으로 — 주입 줄은 data := seed(지운 줄도 되살림) · 추가 줄(seed NULL)은 지움. 새 판번호."""
|
|
keys = list(dict.fromkeys(row_keys))
|
|
marks = ", ".join(["%s"] * len(keys))
|
|
pool = get_db_pool()
|
|
async with pool.acquire() as connection, connection.cursor() as cursor:
|
|
try:
|
|
await connection.begin()
|
|
current = await _lock_revision(cursor, kind)
|
|
await cursor.execute(
|
|
f"SELECT row_key FROM master_base_price WHERE kind = %s AND row_key IN ({marks})",
|
|
(kind, *keys),
|
|
)
|
|
missing = set(keys) - {r[0] for r in await cursor.fetchall()}
|
|
if missing:
|
|
raise UnknownRows(sorted(missing))
|
|
await cursor.execute(
|
|
f"""UPDATE master_base_price
|
|
SET data = seed, is_deleted = 0, updated_by = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE kind = %s AND seed IS NOT NULL AND row_key IN ({marks})""",
|
|
(by, kind, *keys),
|
|
)
|
|
await cursor.execute(
|
|
f"""UPDATE master_base_price
|
|
SET is_deleted = 1, updated_by = %s, updated_at = CURRENT_TIMESTAMP
|
|
WHERE kind = %s AND seed IS NULL AND row_key IN ({marks})""",
|
|
(by, kind, *keys),
|
|
)
|
|
await _bump(cursor, kind)
|
|
await connection.commit()
|
|
return current + 1
|
|
except Exception:
|
|
await connection.rollback()
|
|
raise
|