Files
Aislo/M02_MasterTemplete/M02_Share_Request.py
T
eomsangdonandClaude Opus 5.5 74352ec6b8 feat(회사에 올리기): 요청 장부 · 요청 · 목록 · 승인 · 거절 · 알림 봄 길 + 임시 storage 시험 (PLAN 706-1)
- 회사 층 폴더 올리기요청.json 하나 · DB 안 바꿈
- 승인 = 「회사로 복사…」와 같은 길(구조물 = 붙여넣기 · 사전조합 = 서고 복사 + 폴더 자리) · 원본 개인 것 그대로
- 권한 = M02_Layer_Access 재사용 · 일반 승인 403 · 다른 회사 장부 안 봄 · 같은 것 다시 요청 409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:52:30 +09:00

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)}