diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 6aa6667..c42e219 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -703,6 +703,27 @@ async def api_create_subproject( return JSONResponse({"project": project}, status_code=201) +@router.put("/api/projects/order") +async def api_reorder_projects( + request: Request, payload: dict[str, Any] = Body(...) +) -> JSONResponse: + """홈 화면 프로젝트 카드 드래그 순서 변경. 최상위 프로젝트만 대상이며, + 순서는 이 화면을 보는 모든 사용자에게 공유되므로(세션 재정렬과 동일 원칙) + 관리자 전용으로 제한한다. + + ⚠️ 반드시 `PUT /api/projects/{project_id}` 보다 먼저 등록해야 한다 — 아니면 + "order" 가 project_id 로 매칭 시도되어(정수 변환 실패로 422) 이 라우트가 + 영영 실행되지 않는다(경로 매칭은 등록 순서, path param 특이도 무관). + """ + _require_admin(request) + st = _db_or_503(request) + ids = payload.get("ordered_ids") or [] + if not isinstance(ids, list) or not ids: + raise HTTPException(status_code=400, detail="ordered_ids 가 필요합니다.") + st.reorder_projects(ordered_ids=[int(i) for i in ids]) + return JSONResponse({"ok": True}) + + @router.put("/api/projects/{project_id}") async def api_update_project( request: Request, project_id: int, payload: dict[str, Any] = Body(...) @@ -733,22 +754,6 @@ async def api_delete_project(request: Request, project_id: int) -> JSONResponse: return JSONResponse({"ok": True}) -@router.put("/api/projects/order") -async def api_reorder_projects( - request: Request, payload: dict[str, Any] = Body(...) -) -> JSONResponse: - """홈 화면 프로젝트 카드 드래그 순서 변경. 최상위 프로젝트만 대상이며, - 순서는 이 화면을 보는 모든 사용자에게 공유되므로(세션 재정렬과 동일 원칙) - 관리자 전용으로 제한한다.""" - _require_admin(request) - st = _db_or_503(request) - ids = payload.get("ordered_ids") or [] - if not isinstance(ids, list) or not ids: - raise HTTPException(status_code=400, detail="ordered_ids 가 필요합니다.") - st.reorder_projects(ordered_ids=[int(i) for i in ids]) - return JSONResponse({"ok": True}) - - # ──────────────────────────────────────────────────────────── # JSON API — 멤버 (관리자가 사용자 배정) # ──────────────────────────────────────────────────────────── diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index f85bfe9..1553a31 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -45,5 +45,5 @@ {% endif %} - + {% endblock %} diff --git a/app/modules/project/templates/project/index.html b/app/modules/project/templates/project/index.html index 0e75246..b12933f 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -393,5 +393,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 5beed52..3c23399 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 %} - + @@ -533,5 +533,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index c8f4157..e74a13e 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -229,6 +229,9 @@ .pj-tree-acts .pj-icon-btn { width: 22px; height: 22px; font-size: 14px; } .pj-tree-li[draggable="true"] { cursor: grab; } .pj-tree-li.is-dragging { opacity: .45; } +/* 드래그 가능 손잡이 — 평소엔 흐리게 숨겨두고 행에 마우스 올리면 보인다 */ +.pj-tree-handle { flex: 0 0 auto; width: 14px; text-align: center; color: #c2c6cc; font-size: 12px; cursor: grab; opacity: 0; transition: opacity .12s; } +.pj-tree-row:hover .pj-tree-handle { opacity: 1; } /* 구글 머티리얼 심볼 아이콘(담당자) */ .material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; line-height: 1; vertical-align: middle; } diff --git a/app/static/project.js b/app/static/project.js index 41427b2..b7f1c93 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -1865,7 +1865,7 @@ if (stageModal) wireModalClose(stageModal); function openStageModal(stage) { if (!stageModal) return; - el("pj-stage-modal-title").textContent = stage ? "세션 편집" : "새 세션"; + el("pj-stage-modal-title").textContent = stage ? "\"" + stage.name + "\" 세션 편집" : "새 세션"; el("pj-stg-id").value = stage ? stage.id : ""; el("pj-stg-name").value = stage ? stage.name : ""; getStageColor = renderColorPalette( @@ -3037,8 +3037,11 @@ ""; } const initial = escapeHtml((p.name || "?").trim().charAt(0).toUpperCase() || "?"); + // 드래그로 순서 변경 가능함을 알려주는 손잡이(⣿) — 관리자에게만 보인다. + const handle = isAdmin ? '⣿' : ""; return '