Files
Aislo/M02_MasterTemplete/M02_Share_Request.py
T
eomsangdonandClaude Opus 5.5 40980910fa feat(733-2): 사람 길 만들 때 같은 내용 409 물음 · 복사 딸림 재사용
- 새 M02_Library_Ask — 같은 층 같은 내용(733-1 지문) = 409 {같은것: [{키, 이름, 층, 주인}], 까닭} · 몸 새로 = 그래도 새로 · 씀 = 그 키(새 문서 0 · 같은 내용 아닌 키 = 다시 409)
- 길 = 새로 만들기(빈 문서 · 같은 층 복사 = 안 물음) · [단가산출로 저장] from-line · B09 내 자료집(같은이름 409 와 한 몸 · 하위 호표 지문 = 굳힌 문서) · 공유 요청 승인(일위대가 · 씀 = 사본 키) · 실무 호표 [구조물 조합으로](구조물 조합 지문 = 줄 수량 넣음)
- 복사 딸림 carry — 받는 층에 같은 원본 + 같은 내용 사본이 있으면 그 키 재사용(refs.copied_from) · B09 하위 호표 재사용 목록 = 실무자료 한정 → 모든 사본 원본
- 낡은 시험 고침: test_703 자료집 같은 이름 물음 — 새 이름이라도 같은 내용이면 409(정함 3장 3) → new=True 로 새로 · 씀 확인 더함
- 새 시험 test_733_2_library_ask(새로 · from-line · 승인 · carry) · test_705_2 구조물 조합으로 409

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:54:28 +09:00

251 lines
10 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_Library_Ask as library_ask
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] = []
새로: bool = False # 회사 층 같은 내용 409 뒤 — 그래도 새로(733-2 · 일위대가)
씀: str | None = None # 같은 내용 409 뒤 — 그 회사 문서를 사본으로(새로 안 만듦)
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], new: bool = False, use: str | None = None):
"""승인 = 「회사로 복사…」와 같은 길 — 구조물 항목 = 붙여넣기 ·
사전조합 = 서고 복사(+ 폴더 자리) · 회사 층 같은 내용 = 409 물음(733-2 · `M02_Library_Ask`)."""
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` 와 같음)
got = library_ask.ask("precombo", doc, new, use) # 참조 지문 = 내용 → 옮기기 전 견줌
if got:
return {"이름": got, "씀": True}
return refs.create("precombo", refs.carry(refs.origin_mark(doc, ref), here))
here = ("personal", row["주인"])
made = _call(target, make)
if dest and not made.get("씀"):
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)」) · 원본 개인 것은 그대로."""
run = _copy(session, body.경로, body.새로, body.씀)
return _settle(session, company, rid, {"상태": "승인", "run": run})
@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)}