fix(project): 프로젝트 순서변경 라우트 충돌 수정 + 세션편집 제목/드래그 손잡이 표시
- PUT /api/projects/order 가 /api/projects/{project_id} 뒤에 등록돼 있어
"order" 가 project_id(int) 파싱 실패로 422 나던 버그 수정(라우트 등록 순서
변경 — 경로 매칭은 등록 순서라 path param 뒤에 두면 리터럴 경로가 항상
가려진다). 홈 카드/좌측 프로젝트 트리 드래그 재정렬 둘 다 이 버그였음.
- 세션 편집 팝업 제목에 세션 이름 표시
- 좌측 프로젝트 트리에 드래그 가능 손잡이(⣿) 표시(관리자, hover 시 노출)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -703,6 +703,27 @@ async def api_create_subproject(
|
|||||||
return JSONResponse({"project": project}, status_code=201)
|
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}")
|
@router.put("/api/projects/{project_id}")
|
||||||
async def api_update_project(
|
async def api_update_project(
|
||||||
request: Request, project_id: int, payload: dict[str, Any] = Body(...)
|
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})
|
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 — 멤버 (관리자가 사용자 배정)
|
# JSON API — 멤버 (관리자가 사용자 배정)
|
||||||
# ────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
<link rel="stylesheet" href="/static/project.css?v=20260917l" />
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -45,5 +45,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/project.js?v=20260917p" defer></script>
|
<script src="/static/project.js?v=20260917q" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
<link rel="stylesheet" href="/static/project.css?v=20260917l" />
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -393,5 +393,5 @@
|
|||||||
<script>
|
<script>
|
||||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||||
</script>
|
</script>
|
||||||
<script src="/static/project.js?v=20260917p" defer></script>
|
<script src="/static/project.js?v=20260917q" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}
|
{% block head_extra %}
|
||||||
<link rel="stylesheet" href="/static/project.css?v=20260917k" />
|
<link rel="stylesheet" href="/static/project.css?v=20260917l" />
|
||||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||||
<!-- 타임라인 vis-timeline — self-host -->
|
<!-- 타임라인 vis-timeline — self-host -->
|
||||||
@@ -533,5 +533,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||||
<script src="/static/project.js?v=20260917p" defer></script>
|
<script src="/static/project.js?v=20260917q" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -229,6 +229,9 @@
|
|||||||
.pj-tree-acts .pj-icon-btn { width: 22px; height: 22px; font-size: 14px; }
|
.pj-tree-acts .pj-icon-btn { width: 22px; height: 22px; font-size: 14px; }
|
||||||
.pj-tree-li[draggable="true"] { cursor: grab; }
|
.pj-tree-li[draggable="true"] { cursor: grab; }
|
||||||
.pj-tree-li.is-dragging { opacity: .45; }
|
.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; }
|
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; line-height: 1; vertical-align: middle; }
|
||||||
|
|||||||
@@ -1865,7 +1865,7 @@
|
|||||||
if (stageModal) wireModalClose(stageModal);
|
if (stageModal) wireModalClose(stageModal);
|
||||||
function openStageModal(stage) {
|
function openStageModal(stage) {
|
||||||
if (!stageModal) return;
|
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-id").value = stage ? stage.id : "";
|
||||||
el("pj-stg-name").value = stage ? stage.name : "";
|
el("pj-stg-name").value = stage ? stage.name : "";
|
||||||
getStageColor = renderColorPalette(
|
getStageColor = renderColorPalette(
|
||||||
@@ -3037,8 +3037,11 @@
|
|||||||
"</span>";
|
"</span>";
|
||||||
}
|
}
|
||||||
const initial = escapeHtml((p.name || "?").trim().charAt(0).toUpperCase() || "?");
|
const initial = escapeHtml((p.name || "?").trim().charAt(0).toUpperCase() || "?");
|
||||||
|
// 드래그로 순서 변경 가능함을 알려주는 손잡이(⣿) — 관리자에게만 보인다.
|
||||||
|
const handle = isAdmin ? '<span class="pj-tree-handle" title="드래그로 순서 변경">⣿</span>' : "";
|
||||||
return '<li class="pj-tree-li"' + (isAdmin ? ' draggable="true"' : "") + ' data-id="' + p.id + '"><div class="pj-tree-row ' +
|
return '<li class="pj-tree-li"' + (isAdmin ? ' draggable="true"' : "") + ' data-id="' + p.id + '"><div class="pj-tree-row ' +
|
||||||
(p.id === projectId ? "is-active" : "") + '">' +
|
(p.id === projectId ? "is-active" : "") + '">' +
|
||||||
|
handle +
|
||||||
'<a class="pj-tree-link" href="/project/p/' + p.id + '">' +
|
'<a class="pj-tree-link" href="/project/p/' + p.id + '">' +
|
||||||
'<span class="pj-tree-avatar" style="background:' + (p.color || "#4573d2") + '">' + initial + "</span>" +
|
'<span class="pj-tree-avatar" style="background:' + (p.color || "#4573d2") + '">' + initial + "</span>" +
|
||||||
'<span class="pj-tree-name">' + escapeHtml(p.name) + "</span></a>" + acts + "</div></li>";
|
'<span class="pj-tree-name">' + escapeHtml(p.name) + "</span></a>" + acts + "</div></li>";
|
||||||
|
|||||||
Reference in New Issue
Block a user