feat(M02): 층 인식 집계표 저장소 — 회사 · 개인 층 · 항목 목록 · 항목 복사 (PLAN 21-2)
- Store · Matset · Branch 가 지금 층 폴더(store.at · 기본 시스템 층)를 봄 — 시스템 층 동작 · 옛 길 그대로
- 회사 · 개인 층 첫 진입 때 빈 뼈대(집계표 열 0 · 조합 장부 다음 1 · 자재 조합 빈 표)
- /api/m02/layers/{layer}/… 집계표 길(번호 · 조합 장부 · 구조물 새로 · 복사 · 조합 갈라 내기 · 자재 조합) · 권한 = 시스템 SYSTEM_ADMIN · 회사 관리자 · 개인 본인
- 층 양식 저장(시스템 · 회사 · 개인)은 집계표 규칙(새 열 파일 · 번호 · SS) 거침 · 시스템 층 쓰기는 SYSTEM_ADMIN · 쓰는 양식 지우기 409
- GET /api/m02/items(한 층 · ?all=1 세 층) · POST /api/m02/layers/personal/items/copy · 회사 없음 409 「회사부터 등록」
- 시험 test_m02_layers_store
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"""M02 양식 층 API — 계약 `tmp/M02_분석/6_계약.md` 「층 (sub4)」.
|
||||
|
||||
⚠ 등록은 `main.py`(로그인만) · 층마다 권한은 여기서:
|
||||
system 읽기만(고치기는 `/api/m02/templates` 시스템 관리자 길)
|
||||
system 읽기 · 쓰기는 시스템 관리자(옛 `/api/m02/templates` 길과 같은 저장소)
|
||||
company 같은 회사 읽기 · 쓰기는 회사 관리자(ADMIN · 마스터 · 시스템 관리자)
|
||||
personal 본인 읽기·쓰기 · 같은 회사 사람 것은 읽기만(가져오기)
|
||||
project 같은 회사 프로젝트 · 작업본 쓰기 · `_initial/` 은 안 씀
|
||||
@@ -26,6 +26,8 @@ from common_util import common_util_spreadsheet as spreadsheet
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from config.config_db import get_db_pool, run_with_connection
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Table_Fill as fill
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
@@ -152,8 +154,8 @@ async def _project(session: dict[str, Any], project_id: str | None) -> dict[str,
|
||||
|
||||
def _company_of(session: dict[str, Any], project: dict[str, Any] | None) -> int:
|
||||
company_id = project["company_id"] if project else session.get("company_id")
|
||||
if company_id is None:
|
||||
raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.")
|
||||
if company_id is None: # 회사 없는 사용자는 없음(21-0) — 회사 · 개인 층은 회사부터
|
||||
raise HTTPException(status_code=409, detail="회사부터 등록 — 회사 연결이 필요합니다.")
|
||||
return int(company_id)
|
||||
|
||||
|
||||
@@ -174,8 +176,8 @@ async def _layer_dir(
|
||||
) -> Path:
|
||||
"""층 폴더 — 읽기·쓰기 권한까지 여기서 가름."""
|
||||
if layer == "system":
|
||||
if write:
|
||||
raise HTTPException(status_code=403, detail="시스템 양식은 마스터 템플릿 화면에서만.")
|
||||
if write and not _is_system_admin(session):
|
||||
raise HTTPException(status_code=403, detail="시스템 양식은 시스템 관리자만.")
|
||||
return layers.system_dir()
|
||||
if layer == "project":
|
||||
return layers.project_dir((await _project(session, project_id))["root"])
|
||||
@@ -184,16 +186,22 @@ async def _layer_dir(
|
||||
if layer == "company":
|
||||
if write and not _is_company_admin(session):
|
||||
raise HTTPException(status_code=403, detail="회사 공식 양식은 회사 관리자만.")
|
||||
return layers.company_dir(company_id)
|
||||
return _ensured(layers.company_dir(company_id))
|
||||
if layer == "personal":
|
||||
owner = int(user_id) if user_id is not None else int(session["user_id"])
|
||||
if write and owner != session.get("user_id"):
|
||||
raise HTTPException(status_code=403, detail="개인 양식은 본인만 고칩니다.")
|
||||
await _same_company_user(session, owner, company_id)
|
||||
return layers.personal_dir(company_id, owner)
|
||||
return _ensured(layers.personal_dir(company_id, owner))
|
||||
raise HTTPException(status_code=404, detail="없는 층입니다.")
|
||||
|
||||
|
||||
def _ensured(folder: Path) -> Path:
|
||||
"""회사 · 개인 층 첫 진입 — 빈 구조물집계표 뼈대(PLAN 21-2 · `Items.ensure`)."""
|
||||
items_store.ensure(folder)
|
||||
return folder
|
||||
|
||||
|
||||
def _bad(error: ValueError) -> HTTPException:
|
||||
return HTTPException(status_code=400, detail=str(error))
|
||||
|
||||
@@ -261,6 +269,13 @@ async def save_layer_template(
|
||||
if kind == "table" and fill.is_fillable(document):
|
||||
# 채운 표가 와도 설계값은 안 받아 적음 — 양식 + 손 값만(5장 · 브라우저 값을 믿지 않음)
|
||||
document = fill.strip_design(document)
|
||||
if layer != "project":
|
||||
# 시스템 · 회사 · 개인 층 = 마스터 저장소 규칙 그대로(집계표 저장 때 새 열 파일 ·
|
||||
# 번호 · SS 장부 ·
|
||||
# 계산값 서버 풀이 · 번호 칸 보존 — `M02_MasterTemplete_Store.write`)
|
||||
return await asyncio.to_thread(
|
||||
_store_save, folder, kind, name, body.판 or "", document, layer
|
||||
)
|
||||
if kind in ("area", "material"):
|
||||
# 면적산출 · 재료산출 계산값은 서버가 같은 TS 엔진을 Node 로 돌려 새로 적음
|
||||
# (브라우저 값 버림 · 5장 ②)
|
||||
@@ -287,6 +302,15 @@ async def save_layer_template(
|
||||
return {"종류": kind, "이름": name, "판": version, "층": layer}
|
||||
|
||||
|
||||
def _store_save(folder: Path, kind: str, name: str, version: str, doc: Any, layer: str) -> dict:
|
||||
with store.at(folder):
|
||||
try:
|
||||
info = store.write(kind, name, version, doc)
|
||||
except store.StoreError as error:
|
||||
raise HTTPException(status_code=error.status, detail=error.detail) from error
|
||||
return {"종류": kind, "이름": name, "판": info["판"], "층": layer}
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/templates/{kind}/{name}")
|
||||
async def delete_layer_template(
|
||||
layer: Layer,
|
||||
@@ -296,10 +320,15 @@ async def delete_layer_template(
|
||||
project_id: str | None = Query(None),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
"""개인 = 본인만 · 회사 = 회사 관리자만 · 시스템 · 프로젝트 층은 거절 · 판이 다르면 409."""
|
||||
if layer in ("system", "project"):
|
||||
raise HTTPException(status_code=403, detail="개인 · 회사 층 양식만 지울 수 있습니다.")
|
||||
"""개인 = 본인만 · 회사 = 회사 관리자만 · 시스템 = 시스템 관리자 · 프로젝트 층은 거절 ·
|
||||
판이 다르면 409 · 프로그램이 이름으로 부르는 양식(`store.IN_USE` — 구조물집계표 등)은 409."""
|
||||
if layer == "project":
|
||||
raise HTTPException(
|
||||
status_code=403, detail="개인 · 회사 · 시스템 층 양식만 지울 수 있습니다."
|
||||
)
|
||||
folder = await _layer_dir(session, layer, project_id=project_id, write=True)
|
||||
if (kind, name) in store.IN_USE:
|
||||
raise HTTPException(status_code=409, detail=f"쓰는 양식이라 못 지움 「{name}」")
|
||||
try:
|
||||
layers.check_kind(kind)
|
||||
layers.check_name(name)
|
||||
|
||||
Reference in New Issue
Block a user