revert(Z01): 롤백된 마스터 DB·화면 작업이 dev 로 되들어온 것 걷어냄
- 사용자 「완전히 되돌려」 뒤 dev 를 거쳐 다시 들어온 1-2·1-3·1-4 코드 26 경로를 9629430a 모습으로(고친 17 되돌림 · 새로 생긴 9 지움)
- 대상: Z01_MasterData 17 · main.py · ui_template_locale_b3.ts · resources/tester 의 Z01 시험 7
- 품셈 md·도구는 안 건드림 · 이력 재작성 없음(되돌림 커밋)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0145cr1DMC5Ft8fouQSXtp69
This commit is contained in:
@@ -1,69 +1,32 @@
|
||||
"""Z01 마스터 데이터 라우터 — 읽기 + 기초단가 DB 정본 일괄 저장(2026-09-18 PLAN 1-0 계약).
|
||||
"""Z01 마스터 데이터 라우터 — 읽기 + 기초단가 고치기(2026-09-15 브레인).
|
||||
|
||||
GET /api/master-data/tree 갈래 → 파일 → 표
|
||||
GET /api/master-data/rows?file=&table=&page=&size=&q= 표 줄(쪽 나누기 · 검색)
|
||||
GET /api/master-data/base-prices 표 목록(갈래·kind·이름·줄 수 · 공종 축 산림·건설 포함)
|
||||
GET /api/master-data/base-prices/{kind}?page=&size=&q=&sort=&desc=[&거르기=값] 한 표 + revision · 줄마다 changed_columns · is_added
|
||||
PUT /api/master-data/base-prices/{kind} {base_revision, edits, added, deleted} → 409 · 전부 아니면 전무
|
||||
POST /api/master-data/base-prices/{kind}/reset {row_keys} → 초기값으로(추가 줄은 지움)
|
||||
GET /api/master-data/tree 갈래 → 파일 → 표
|
||||
GET /api/master-data/rows?file=&table=&page=&size=&q= 표 줄(쪽 나누기 · 검색)
|
||||
GET /api/master-data/base-prices 표 목록(갈래·kind·이름·줄 수 · 공종 축 산림·건설 포함)
|
||||
GET /api/master-data/base-prices/{kind}?page=&size=&q=&sort=&desc=[&거르기=값] 한 표(기초단가 다섯 · 품셈 기준 열 · 공종 축 둘)
|
||||
PUT /api/master-data/base-prices/{kind}/{row_id} {values:{열:값}} → 덮개에만 씀 · null = 되돌리기
|
||||
GET /api/master-data/overrides 고친 것·원본 바뀐 것·주인 없는 것(서버 정렬)
|
||||
⚠ 권한은 등록하는 쪽(`main.py` · 랩탑 서브)이 `dependencies=[verify_session, require_system_admin]` 로 붙임.
|
||||
⚠ 자동저장 없음 — 화면은 [저장] 에서만 PUT(지침 5장).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from Z01_MasterData import Z01_MasterData_BasePrices as base_prices
|
||||
from Z01_MasterData import Z01_MasterData_Repository_BasePrices as repo
|
||||
from Z01_MasterData import Z01_MasterData_Tables as tables
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
router = APIRouter(prefix="/api/master-data", tags=["Z01 MasterData"])
|
||||
|
||||
_STALE = "그 사이 누가 이 표를 저장했습니다 — 표를 다시 불러와 고칠 것."
|
||||
|
||||
|
||||
class CellEdit(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
row_key: str = Field(min_length=1, max_length=190)
|
||||
column: str = Field(min_length=1, max_length=100)
|
||||
value: Any = None
|
||||
|
||||
|
||||
class AddedRow(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
data: dict[str, Any] = Field(min_length=1)
|
||||
|
||||
|
||||
class BasePriceSave(BaseModel):
|
||||
"""[저장] 한 번 — 읽어 간 판번호와 함께 고친 칸·새 줄·지운 줄."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
base_revision: int = Field(ge=0)
|
||||
edits: list[CellEdit] = Field(default_factory=list, max_length=500)
|
||||
added: list[AddedRow] = Field(default_factory=list, max_length=500)
|
||||
deleted: list[str] = Field(default_factory=list, max_length=500)
|
||||
|
||||
|
||||
class BasePriceReset(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
row_keys: list[str] = Field(min_length=1, max_length=10000)
|
||||
|
||||
|
||||
def _error(status: int, message: str) -> JSONResponse:
|
||||
return JSONResponse(status_code=status, content={"status": "error", "message": message})
|
||||
class BasePriceEdit(BaseModel):
|
||||
values: dict[str, Any]
|
||||
|
||||
|
||||
@router.get("/tree")
|
||||
@@ -87,19 +50,14 @@ def _kind(kind: str) -> str:
|
||||
return kind
|
||||
|
||||
|
||||
def _db_kind(kind: str) -> str | None:
|
||||
"""고칠 수 있는 표(DB 정본) — 공종 축은 여기서 안 고침(`/work-items` 몫)."""
|
||||
return kind if kind in base_prices.KINDS and kind not in work_items.KINDS else None
|
||||
|
||||
|
||||
@router.get("/base-prices")
|
||||
async def get_base_price_kinds() -> dict:
|
||||
def get_base_price_kinds() -> dict:
|
||||
"""어떤 표가 있나 — 갈래·kind·한글 이름·줄 수. 화면이 목록을 제 코드에 들지 않게 서버가 냄."""
|
||||
return {"status": "success", "kinds": await base_prices.kinds()}
|
||||
return {"kinds": base_prices.kinds()}
|
||||
|
||||
|
||||
@router.get("/base-prices/{kind}")
|
||||
async def get_base_prices(
|
||||
def get_base_prices(
|
||||
kind: str,
|
||||
request: Request,
|
||||
page: int = 1,
|
||||
@@ -108,71 +66,39 @@ async def get_base_prices(
|
||||
sort: str = "",
|
||||
desc: int = 0,
|
||||
) -> dict:
|
||||
kind = _kind(kind)
|
||||
# 거르기 — 표가 준 `filters` 의 key 만 받음(공종 축 부문·줄 구실)
|
||||
picked = {k: v for k, v in request.query_params.items() if k in work_items.FILTER_KEYS}
|
||||
if _db_kind(kind):
|
||||
all_rows, revision = await asyncio.gather(base_prices.rows(kind), repo.revision(kind))
|
||||
else:
|
||||
all_rows, revision = await base_prices.rows(kind), 0
|
||||
try:
|
||||
table = await run_in_threadpool(
|
||||
base_prices.table,
|
||||
kind,
|
||||
all_rows,
|
||||
page=page,
|
||||
size=size,
|
||||
q=q,
|
||||
sort=sort,
|
||||
desc=bool(desc),
|
||||
filters=picked,
|
||||
return base_prices.table(
|
||||
_kind(kind), page=page, size=size, q=q, sort=sort, desc=bool(desc), filters=picked
|
||||
)
|
||||
except base_prices.SortError as error:
|
||||
raise HTTPException(status_code=400, detail=str(error)) from error
|
||||
return {"status": "success", "revision": revision, **table}
|
||||
|
||||
|
||||
@router.put("/base-prices/{kind}")
|
||||
async def put_base_prices(
|
||||
kind: str, body: BasePriceSave, session: dict = Depends(verify_session)
|
||||
@router.put("/base-prices/{kind}/{row_id:path}")
|
||||
def put_base_price(
|
||||
kind: str, row_id: str, body: BasePriceEdit, session: dict = Depends(verify_session)
|
||||
) -> JSONResponse:
|
||||
"""고친 칸·새 줄·지운 줄을 한 번에 — 다 맞춰 본 뒤 한 트랜잭션(하나라도 틀리면 아무것도 안 씀)."""
|
||||
if not _db_kind(kind):
|
||||
return _error(404 if kind not in base_prices.KINDS else 400, f"고칠 수 없는 표: {kind}")
|
||||
if await repo.revision(kind) != body.base_revision:
|
||||
return _error(409, _STALE)
|
||||
try:
|
||||
changed, added, deleted = base_prices.plan_save(
|
||||
kind,
|
||||
await base_prices.rows(kind),
|
||||
[edit.model_dump() for edit in body.edits],
|
||||
[row.data for row in body.added],
|
||||
body.deleted,
|
||||
)
|
||||
except base_prices.SaveError as error:
|
||||
return _error(error.status, str(error))
|
||||
if not (changed or added or deleted):
|
||||
return JSONResponse(
|
||||
content={"status": "success", "revision": body.base_revision, "added": []}
|
||||
)
|
||||
try:
|
||||
revision = await repo.save(
|
||||
kind, body.base_revision, changed, added, deleted, session.get("user_id")
|
||||
)
|
||||
except repo.RevisionConflict:
|
||||
return _error(409, _STALE)
|
||||
return JSONResponse(content={"status": "success", "revision": revision, "added": list(added)})
|
||||
status, payload = base_prices.edit(_kind(kind), row_id, body.values, session.get("user_id"))
|
||||
if status != 200:
|
||||
raise HTTPException(status_code=status, detail=payload["message"])
|
||||
return JSONResponse(content=payload)
|
||||
|
||||
|
||||
@router.post("/base-prices/{kind}/reset")
|
||||
async def reset_base_prices(
|
||||
kind: str, body: BasePriceReset, session: dict = Depends(verify_session)
|
||||
) -> JSONResponse:
|
||||
"""고른 줄을 초기값으로 — 주입 줄은 data := seed · 추가 줄은 지움. 판번호 +1."""
|
||||
if not _db_kind(kind):
|
||||
return _error(404 if kind not in base_prices.KINDS else 400, f"고칠 수 없는 표: {kind}")
|
||||
try:
|
||||
revision = await repo.reset(kind, body.row_keys, session.get("user_id"))
|
||||
except repo.UnknownRows as error:
|
||||
return _error(404, f"없는 줄: {', '.join(error.args[0])}")
|
||||
return JSONResponse(content={"status": "success", "revision": revision})
|
||||
@router.get("/overrides")
|
||||
def get_overrides(
|
||||
kind: str = "", state: str = "", page: int = 1, size: int = tables.DEFAULT_PAGE_SIZE
|
||||
) -> dict:
|
||||
return base_prices.override_items(kind and _kind(kind), state, page=page, size=size)
|
||||
|
||||
|
||||
class OverrideClear(BaseModel):
|
||||
kind: str
|
||||
state: Literal["edited", "source_changed", "orphan"]
|
||||
|
||||
|
||||
@router.post("/overrides/clear")
|
||||
def clear_overrides(body: OverrideClear, _session: dict = Depends(verify_session)) -> dict:
|
||||
"""한 kind 의 그 state 덮개를 한꺼번에 뺌 — kind·state 둘 다 있어야 함(통째로 비우기 막음)."""
|
||||
return {"removed": base_prices.clear_overrides(_kind(body.kind), body.state)}
|
||||
|
||||
Reference in New Issue
Block a user