- 회사 층 폴더 올리기요청.json 하나 · DB 안 바꿈 - 승인 = 「회사로 복사…」와 같은 길(구조물 = 붙여넣기 · 사전조합 = 서고 복사 + 폴더 자리) · 원본 개인 것 그대로 - 권한 = M02_Layer_Access 재사용 · 일반 승인 403 · 다른 회사 장부 안 봄 · 같은 것 다시 요청 409 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
244 lines
9.4 KiB
Python
244 lines
9.4 KiB
Python
"""회사에 올리기 요청 · 승인(PLAN 706-1 · 요구서 4장 6) — 일반 사용자가 개인 층 항목 ·
|
|
사전조합을 회사 층에 올려 달라고 요청 · 회사 관리자가 승인(= 「회사로 복사…」와 같은 길 ·
|
|
원본은 그대로) · 거절.
|
|
|
|
장부 = 회사 층 폴더 `올리기요청.json` 하나(DB 안 씀) · `{줄: [요청 …]}`.
|
|
요청 = `{id, 종류(item · precombo), 키, 이름, 경로(항목 폴더), 주인 C/U, 요청자, 요청자이름,
|
|
글, 날짜, 상태(요청 · 승인 · 거절), 처리자, 처리날짜, 사유, 사본, 봄}` · `봄` = 요청자가 결과
|
|
알림을 닫음. 권한 = `M02_Layer_Access` 한 벌 — 요청 = 자기 개인 층 · 목록 = 회사 관리자는
|
|
그 회사 전부 · 일반은 자기 것 ·
|
|
승인 · 거절 = 회사 층 쓰기(일반 403) · 다른 회사 장부는 안 봄(시스템 관리자만 `company`).
|
|
같은 항목(주인 · 종류 · 키)이 「요청」 상태면 다시 요청 = 409 「요청 중」.
|
|
⚠ 등록은 `main.py`(로그인만).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import threading
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any, Literal
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
|
|
from common_util.common_util_auth import verify_session
|
|
from common_util.common_util_json import atomic_write_json
|
|
from M02_MasterTemplete import M02_Item_Paste as paste_store
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
from M02_MasterTemplete import M02_Layer_Access as access
|
|
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete import M02_Precombo_Folders as pre_folders
|
|
from M02_MasterTemplete.M02_MasterTemplete_Router_Items import (
|
|
NotedRoute,
|
|
Session,
|
|
_call,
|
|
_root,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/m02", tags=["M02 Share Request"], route_class=NotedRoute)
|
|
|
|
FILE = "올리기요청.json"
|
|
# ponytail: 한 프로세스 잠금 — 서버를 여럿 띄우면 파일 잠금으로
|
|
_LOCK = threading.Lock()
|
|
|
|
|
|
class AskBody(BaseModel):
|
|
종류: Literal["item", "precombo"]
|
|
키: str
|
|
글: str = ""
|
|
|
|
|
|
class ApproveBody(BaseModel):
|
|
경로: list[str] = []
|
|
|
|
|
|
class RejectBody(BaseModel):
|
|
사유: str = ""
|
|
|
|
|
|
def _company(session: Session, company: str | None) -> str:
|
|
"""장부 회사 — 시스템 관리자만 `company` 로 남의 회사 · 그 밖은 자기 회사(없으면 409)."""
|
|
if company and access.is_system_admin(session):
|
|
return str(company)
|
|
own = session.get("company_id")
|
|
if own is None:
|
|
raise HTTPException(status_code=409, detail=access.NO_COMPANY)
|
|
return str(own)
|
|
|
|
|
|
def _path(company: str) -> Path:
|
|
return access.folder(access.Grant("company", company, False, True)) / FILE
|
|
|
|
|
|
def _read(company: str) -> list[dict[str, Any]]:
|
|
path = _path(company)
|
|
if not path.is_file():
|
|
return []
|
|
return list(json.loads(path.read_bytes()).get("줄") or [])
|
|
|
|
|
|
def _write(company: str, rows: list[dict[str, Any]]) -> None:
|
|
atomic_write_json(_path(company), {"줄": rows})
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now().isoformat(timespec="seconds")
|
|
|
|
|
|
def _source(session: Session, kind: str, key: str) -> dict[str, Any]:
|
|
"""자기 개인 층 원본 — `{이름, 경로}`(없으면 404)."""
|
|
folder = _root(session, "personal")
|
|
if kind == "item":
|
|
found = next((r for r in items_store.items(folder) if r.get("key") == key), None)
|
|
if found is None:
|
|
raise HTTPException(status_code=404, detail=f"없는 항목 「{key}」")
|
|
path = list(found.get("경로") or [])
|
|
return {"이름": path[-1] if path else key, "경로": path[:-1]}
|
|
owner = access.own_owner(session, "personal")
|
|
doc = (
|
|
refs.read_ref({"층": "personal", "주인": owner, "키": key})
|
|
if refs._KEY.match(key)
|
|
else None
|
|
)
|
|
if doc is None or refs.kind_of(key) != "precombo":
|
|
raise HTTPException(status_code=404, detail=f"없는 사전조합 「{key}」")
|
|
return {"이름": str(doc.get("이름") or key), "경로": []}
|
|
|
|
|
|
@router.post("/share-requests")
|
|
def ask(body: AskBody, session: Session = Depends(verify_session)) -> dict[str, Any]:
|
|
"""요청 — 자기 개인 층 것만 · 같은 것이 「요청」 이면 409 「요청 중」."""
|
|
owner = access.own_owner(session, "personal")
|
|
if owner is None:
|
|
raise HTTPException(status_code=409, detail=access.NO_COMPANY)
|
|
src = _source(session, body.종류, body.키)
|
|
company = owner.split("/")[0]
|
|
row = {
|
|
"id": uuid.uuid4().hex[:12],
|
|
"종류": body.종류,
|
|
"키": body.키,
|
|
**src,
|
|
"주인": owner,
|
|
"요청자": session.get("user_id"),
|
|
"요청자이름": session.get("name") or "",
|
|
"글": body.글.strip()[:200],
|
|
"날짜": _now(),
|
|
"상태": "요청",
|
|
}
|
|
with _LOCK:
|
|
rows = _read(company)
|
|
same = (owner, body.종류, body.키)
|
|
if any((r["주인"], r["종류"], r["키"]) == same and r["상태"] == "요청" for r in rows):
|
|
raise HTTPException(status_code=409, detail="요청 중 — 회사 관리자가 아직 안 봄")
|
|
_write(company, [*rows, row])
|
|
return row
|
|
|
|
|
|
@router.get("/share-requests")
|
|
def listing(
|
|
company: str | None = None, session: Session = Depends(verify_session)
|
|
) -> dict[str, Any]:
|
|
"""목록 — 회사를 맡은 사람 = 그 회사 전부 · 그 밖 = 자기 요청만 · `관리` = 승인할 수 있나."""
|
|
at = _company(session, company)
|
|
rows = _read(at)
|
|
boss = access.manages(session, at)
|
|
if not boss:
|
|
me = session.get("user_id")
|
|
rows = [r for r in rows if r.get("요청자") == me]
|
|
return {"회사": at, "관리": boss, "줄": rows}
|
|
|
|
|
|
def _settle(session: Session, company: str | None, rid: str, done: dict[str, Any]) -> dict:
|
|
"""요청 한 줄을 끝냄 — 회사 층 쓰기 권한(일반 403) · 「요청」 상태만(아니면 409)."""
|
|
at = _company(session, company)
|
|
access.require(session, "company", at, write=True)
|
|
with _LOCK:
|
|
rows = _read(at)
|
|
row = next((r for r in rows if r.get("id") == rid), None)
|
|
if row is None:
|
|
raise HTTPException(status_code=404, detail="없는 요청")
|
|
if row["상태"] != "요청":
|
|
raise HTTPException(status_code=409, detail=f"이미 {row['상태']}")
|
|
extra = done.pop("run", None)
|
|
if extra:
|
|
done["사본"] = extra(row, at)
|
|
row.update({**done, "처리자": session.get("name") or "", "처리날짜": _now(), "봄": False})
|
|
_write(at, rows)
|
|
return row
|
|
|
|
|
|
def _copy(session: Session, dest: list[str]):
|
|
"""승인 = 「회사로 복사…」와 같은 길 — 구조물 항목 = 붙여넣기 ·
|
|
사전조합 = 서고 복사(+ 폴더 자리)."""
|
|
|
|
def run(row: dict[str, Any], company: str) -> str:
|
|
target = _root(session, "company", company, write=True)
|
|
source = _root(session, "personal", row["주인"])
|
|
if row["종류"] == "item":
|
|
ref = [{"경로": row.get("경로") or [], "키": row["키"]}]
|
|
origin = {"layer": "personal", "owner": row["주인"]}
|
|
try:
|
|
note = paste_store.paste(source, target, ref, dest, origin)
|
|
except store.StoreError as e:
|
|
raise HTTPException(status_code=e.status, detail=e.detail) from e
|
|
return str(note["열"][0])
|
|
ref = {"층": "personal", "주인": row["주인"], "키": row["키"]}
|
|
doc = refs.read_ref(ref)
|
|
if doc is None:
|
|
raise HTTPException(status_code=404, detail=f"원본이 없음 「{row['키']}」")
|
|
|
|
def make() -> dict[
|
|
str, Any
|
|
]: # 못 읽는 참조 같이 복사 = 받는 층 안에서(`new_library` 와 같음)
|
|
return refs.create("precombo", refs.carry(refs.origin_mark(doc, ref), here))
|
|
|
|
here = ("personal", row["주인"])
|
|
made = _call(target, make)
|
|
if dest:
|
|
view = _call(target, pre_folders.view)
|
|
move = {"op": "move", "키": made["이름"], "새경로": dest, "판": view["판"]}
|
|
_call(target, pre_folders.edit, move)
|
|
return str(made["이름"])
|
|
|
|
return run
|
|
|
|
|
|
@router.post("/share-requests/{rid}/approve")
|
|
def approve(
|
|
rid: str,
|
|
body: ApproveBody,
|
|
company: str | None = None,
|
|
session: Session = Depends(verify_session),
|
|
) -> dict[str, Any]:
|
|
"""승인 — 회사 층 `경로` 폴더에 사본(새 키 · 같은 이름 「(1)」) · 원본 개인 것은 그대로."""
|
|
return _settle(session, company, rid, {"상태": "승인", "run": _copy(session, body.경로)})
|
|
|
|
|
|
@router.post("/share-requests/{rid}/reject")
|
|
def reject(
|
|
rid: str,
|
|
body: RejectBody,
|
|
company: str | None = None,
|
|
session: Session = Depends(verify_session),
|
|
) -> dict[str, Any]:
|
|
return _settle(session, company, rid, {"상태": "거절", "사유": body.사유.strip()[:200]})
|
|
|
|
|
|
@router.post("/share-requests/seen")
|
|
def seen(session: Session = Depends(verify_session)) -> dict[str, int]:
|
|
"""요청자가 결과 알림 줄을 닫음 — 자기 요청 가운데 끝난 것 `봄` = true."""
|
|
at = _company(session, None)
|
|
me = session.get("user_id")
|
|
with _LOCK:
|
|
rows = _read(at)
|
|
hit = [r for r in rows if r.get("요청자") == me and r.get("봄") is False]
|
|
for r in hit:
|
|
r["봄"] = True
|
|
if hit:
|
|
_write(at, rows)
|
|
return {"봄": len(hit)}
|