feat(project): 카드 드래그 순서변경/전체멤버/탭 슬라이드/내업무 실시간반영
- 홈 프로젝트 카드 드래그 재정렬(관리자, projects.sort_order 공유) - 프로젝트 제목 색상 pill 스타일로 강조 - 보드 세션 우클릭에 "업무 추가" 메뉴 추가 - 좌측 "멤버"→"프로젝트 멤버" + "전체 멤버" 그리드(클릭 시 담당 업무 팝업) - 뷰 탭/스코프 토글에 슬라이드 하이라이트 애니메이션 - "내 업무" 패널이 8초 실시간 폴링에서 빠져 있던 문제 수정(홈/프로젝트 화면 모두 /api/my-tasks 로 patch) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -801,6 +801,18 @@ class ProjectStore:
|
||||
raise KeyError(stage_id)
|
||||
return self._serialize(row)
|
||||
|
||||
def reorder_projects(self, *, ordered_ids: list[int]) -> None:
|
||||
"""홈 화면 프로젝트 카드 드래그 순서 변경. 최상위 프로젝트(parent_id
|
||||
IS NULL)만 대상 — 서브프로젝트 정렬(부모 안에서의 순서)은 건드리지 않는다."""
|
||||
with self._pool.connection() as conn:
|
||||
with conn.transaction():
|
||||
for i, pid in enumerate(ordered_ids):
|
||||
conn.execute(
|
||||
"UPDATE projects SET sort_order = %s "
|
||||
"WHERE id = %s AND parent_id IS NULL",
|
||||
(i, int(pid)),
|
||||
)
|
||||
|
||||
def reorder_stages(self, *, project_id: int, ordered_ids: list[int]) -> None:
|
||||
"""주어진 순서대로 sort_order 재배정. 해당 프로젝트 단계만 갱신."""
|
||||
with self._pool.connection() as conn:
|
||||
|
||||
@@ -732,6 +732,22 @@ 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 — 멤버 (관리자가 사용자 배정)
|
||||
# ────────────────────────────────────────────────────────────
|
||||
@@ -759,6 +775,38 @@ async def api_assignable_users(request: Request) -> JSONResponse:
|
||||
return JSONResponse({"users": out})
|
||||
|
||||
|
||||
@router.get("/api/all-users")
|
||||
async def api_all_users(request: Request) -> JSONResponse:
|
||||
"""프로젝트 화면 좌측 "전체 멤버" 표시용 — project 모듈 권한 보유 등록
|
||||
사용자 전원. assignable-users 와 후보군은 같지만, 이건 멤버 배정(쓰기)이
|
||||
아니라 단순 열람용이라 관리자가 아니어도 볼 수 있다."""
|
||||
_require_user(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],
|
||||
"avatar": u.get("picture") or "",
|
||||
}
|
||||
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/users/tasks")
|
||||
async def api_user_tasks(request: Request, email: str) -> JSONResponse:
|
||||
"""'전체 멤버' 아이콘 클릭 팝업 — 지정한 사용자가 담당자인 업무 목록
|
||||
(프로젝트/세션/마감일). /api/my-tasks 와 같은 조회를 이메일만 바꿔 재사용."""
|
||||
_require_user(request)
|
||||
st = _db_or_503(request)
|
||||
return JSONResponse({"tasks": st.list_tasks(assignee_email=email, limit=500)})
|
||||
|
||||
|
||||
@router.get("/api/projects/{project_id}/members")
|
||||
async def api_list_members(request: Request, project_id: int) -> JSONResponse:
|
||||
_require_user(request)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917i" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script src="/static/project.js?v=20260917n" defer></script>
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917i" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
{% endblock %}
|
||||
|
||||
@@ -393,5 +393,5 @@
|
||||
<script>
|
||||
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
|
||||
</script>
|
||||
<script src="/static/project.js?v=20260917n" defer></script>
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917i" />
|
||||
<link rel="stylesheet" href="/static/project.css?v=20260917j" />
|
||||
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
|
||||
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
|
||||
<!-- 타임라인 vis-timeline — self-host -->
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
<div class="pj-side-block">
|
||||
<div class="pj-side-head">
|
||||
<span>멤버</span>
|
||||
<span>프로젝트 멤버</span>
|
||||
{% if is_admin %}
|
||||
<button type="button" class="pj-icon-btn" id="pj-add-member" title="멤버 배정">+</button>
|
||||
{% endif %}
|
||||
@@ -90,6 +90,13 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 전체 멤버 — 등록된 전체 직원(프로젝트 권한 보유자). 아이콘 클릭 시
|
||||
그 사람이 담당자인 업무를 팝업으로 보여준다(프로젝트/세션/마감일). -->
|
||||
<div class="pj-side-block">
|
||||
<div class="pj-side-head"><span>전체 멤버</span></div>
|
||||
<div class="pj-allmembers-grid" id="pj-allmembers-grid">불러오는 중…</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ── 우측: 뷰 ── -->
|
||||
@@ -102,7 +109,9 @@
|
||||
<button type="button" class="pj-tab" data-view="timeline">타임라인</button>
|
||||
<button type="button" class="pj-tab" data-view="dashboard">대시보드</button>
|
||||
</div>
|
||||
<h1 class="pj-project-title">{{ project.name }}</h1>
|
||||
<h1 class="pj-project-title">
|
||||
<span class="pj-project-title-pill" style="--pj-color: {{ project.color or '#4573d2' }}">{{ project.name }}</span>
|
||||
</h1>
|
||||
<!-- 아사나처럼 기본은 "이 프로젝트"만 보여준다. 전체 프로젝트 보기는
|
||||
예전처럼 접근 가능한 모든 프로젝트를 합쳐서 보여주는 모드. -->
|
||||
<div class="pj-view-tabs" id="pj-scope-toggle">
|
||||
@@ -451,6 +460,18 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 전체 멤버 아이콘 클릭 팝업 — 그 사람이 담당자인 업무를 프로젝트/세션/마감일과 함께 나열 -->
|
||||
<div class="pj-modal" id="pj-modal-member-tasks" hidden>
|
||||
<div class="pj-modal-card pj-mt-card">
|
||||
<h3 id="pj-mt-title">업무</h3>
|
||||
<ul class="pj-mt-list" id="pj-mt-list"></ul>
|
||||
<div class="pj-modal-actions">
|
||||
<span style="flex:1"></span>
|
||||
<button type="button" class="pj-btn" data-close>닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>window.PJ_COLORS = {{ color_palette | tojson }};</script>
|
||||
<!-- 데이터 -->
|
||||
<script id="pj-data" type="application/json">
|
||||
@@ -470,5 +491,5 @@
|
||||
</script>
|
||||
|
||||
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
|
||||
<script src="/static/project.js?v=20260917n" defer></script>
|
||||
<script src="/static/project.js?v=20260917o" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user