diff --git a/CLAUDE.md b/CLAUDE.md index 3a14565..9d433a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ Claude Code는 이 저장소에서 작업을 시작하기 전에 **반드시 아 - 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver` - 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia` - 말레이시아 배송 (`app/modules/dispatch/`, `dispatch_db`) — TikTok·Shopee 출고관리. 플랫폼별 데이터 엑셀 업로드(TikTok=03_TikTok_Order_Export.xlsx, Shopee=Packing List.Doorstep Delivery.xlsx) → 1박스=1카드 출고 작업 리스트·SKU 피킹 요약·Kagayaku 전달표 자동 생성. 1박스 묶음 기준 Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산. 작업 상태 토글(`dispatch_logs` 기록). 받는 사람 이름/전화/주소는 박스 단위로 저장(작업 카드 표시 + 출고 엑셀 생성용 — 개인정보). 배치 다운로드 zip 에 업로드 원본 + 취합 출고 엑셀(`YYYY.MM.DD(Ddd)_tictoc|shopee.xlsx`) 포함. 엑셀은 openpyxl 파싱/생성. 권한키 `dispatch`. 상세는 `docs/DISPATCH_MODULE.md` -- 프로젝트 관리 (`app/modules/project/`, `project_db`) — 아사나식. 프로젝트/서브프로젝트(self-FK `parent_id`, CASCADE)·업무(`tasks`: 담당자·우선순위·시작/마감)·진행단계(`project_stages` 칸반, 생성시 기본 4단계 seed)·멤버 배정(`project_members`)·활동이력(`project_activity`). 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 버튼 토글 + 보드(드래그로 단계 이동)/리스트. **진입은 로그인한 회사 직원 전원**(권한키 없음 — MODULE_KEYS 미포함, 메뉴 `allowed` 강제 True). 프로젝트 생성/삭제·사용자 배정은 `is_admin` 만, 배정 멤버(또는 owner)는 서브프로젝트/업무/단계 CRUD. 업무 배정·완료 시 관리자에게 메일(`app/mail.py` stdlib smtplib, `SMTP_*`+`PROJECT_NOTIFY_EMAIL` env, 미설정 시 조용히 skip, `BackgroundTasks` 비동기). 상세는 `docs/PROJECT_MODULE.md` +- 프로젝트 관리 (`app/modules/project/`, `project_db`) — 아사나식. 프로젝트/서브프로젝트(self-FK `parent_id`, CASCADE)·업무(`tasks`: 담당자·우선순위·시작/마감)·진행단계(`project_stages` 칸반, 생성시 기본 4단계 seed)·멤버 배정(`project_members`)·활동이력(`project_activity`). 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 버튼 토글 + 보드(드래그로 단계 이동)/리스트. 진입 권한키 `project`(관리자 페이지 토글로 직원별 부여, admin 자동). 프로젝트 생성/삭제·사용자 배정은 `is_admin` 만, 배정 멤버(또는 owner)는 서브프로젝트/업무/단계 CRUD. 멤버 배정 후보는 `project` 권한 보유 등록 사용자에서 자동 목록(`GET /project/api/assignable-users`). 업무 배정·완료 시 관리자에게 메일(`app/mail.py` stdlib smtplib, `SMTP_*`+`PROJECT_NOTIFY_EMAIL` env, 미설정 시 조용히 skip, `BackgroundTasks` 비동기). 상세는 `docs/PROJECT_MODULE.md` 상세는 `docs/PROJECT_OVERVIEW.md`. diff --git a/app/main.py b/app/main.py index 0f0d441..b26499d 100644 --- a/app/main.py +++ b/app/main.py @@ -353,8 +353,7 @@ def _menu_items_for(user_rec: dict[str, Any]) -> list[dict[str, Any]]: ] allowed = allowed_modules(user_rec) for item in items: - # project 는 권한키 없이 로그인한 회사 직원 전원 진입(전사 협업). - item["allowed"] = item["key"] == "project" or item["key"] in allowed + item["allowed"] = item["key"] in allowed return items diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 79672c5..d910890 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -40,10 +40,13 @@ def _store(request: Request) -> Any: def _require_user(request: Request) -> dict[str, Any]: from app.main import get_current_user_record # noqa: WPS433 + from app.store import has_module # noqa: WPS433 user = get_current_user_record(request) if user is None: raise HTTPException(status_code=401, detail="로그인이 필요합니다.") + if not has_module(user, "project"): + raise HTTPException(status_code=403, detail="프로젝트 관리 모듈 권한이 없습니다.") return user @@ -455,6 +458,29 @@ async def api_delete_project(request: Request, project_id: int) -> JSONResponse: # ──────────────────────────────────────────────────────────── # JSON API — 멤버 (관리자가 사용자 배정) # ──────────────────────────────────────────────────────────── +@router.get("/api/assignable-users") +async def api_assignable_users(request: Request) -> JSONResponse: + """배정 후보 = 등록된 사용자 중 프로젝트 권한(project 모듈) 보유자. 관리자 전용. + + 관리자 페이지의 '프로젝트 관리' 토글을 켠 직원이 자동으로 후보가 된다. + """ + _require_admin(request) + from app.main import user_store # noqa: WPS433 + from app.store import has_module # noqa: WPS433 + + out = [ + { + "email": u["email"], + "name": u.get("name") or u["email"].split("@")[0], + "id": u["email"].split("@")[0], + } + for u in user_store.list_all() + if has_module(u, "project") + ] + out.sort(key=lambda x: x["id"]) + return JSONResponse({"users": out}) + + @router.get("/api/projects/{project_id}/members") async def api_list_members(request: Request, project_id: int) -> JSONResponse: _require_user(request) diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 07b8169..440a7ff 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + @@ -190,6 +190,30 @@ +{% if is_admin %} + +
관리자 페이지에서 프로젝트 관리 권한을 켠 직원이 후보로 나옵니다.
+ + +