diff --git a/app/modules/project/db.py b/app/modules/project/db.py index da3d058..5f09203 100644 --- a/app/modules/project/db.py +++ b/app/modules/project/db.py @@ -716,6 +716,23 @@ class ProjectStore: ) return cur.rowcount or 0 + # ════════════════════════════════════════════════════════════ + # 실시간 새로고침 — 프로젝트/업무/댓글/멤버/단계 중 가장 최근 변경 시각 + # ════════════════════════════════════════════════════════════ + def latest_version(self) -> str: + with self._pool.connection() as conn: + row = conn.execute( + "SELECT GREATEST(" + " COALESCE((SELECT MAX(updated_at) FROM projects), 'epoch'::timestamptz)," + " COALESCE((SELECT MAX(updated_at) FROM tasks), 'epoch'::timestamptz)," + " COALESCE((SELECT MAX(created_at) FROM task_comments), 'epoch'::timestamptz)," + " COALESCE((SELECT MAX(created_at) FROM project_members), 'epoch'::timestamptz)," + " COALESCE((SELECT MAX(created_at) FROM project_stages), 'epoch'::timestamptz)" + ") AS ts" + ).fetchone() + ts = row["ts"] if row else None + return ts.isoformat() if ts else "" + # ════════════════════════════════════════════════════════════ # 직렬화 # ════════════════════════════════════════════════════════════ diff --git a/app/modules/project/router.py b/app/modules/project/router.py index a9c7c8f..de0d47b 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -195,14 +195,14 @@ _WEEKDAYS_KR = ["월", "화", "수", "목", "금", "토", "일"] def _fmt_date_kr(d: str | None) -> str: - """'YYYY-MM-DD' → 'yy/mm/dd(요일)'. 형식이 아니면 원본 그대로.""" + """'YYYY-MM-DD' → 'mm/dd(요일)'. 형식이 아니면 원본 그대로.""" if not d: return "" try: dd = date.fromisoformat(d[:10]) except (ValueError, TypeError): return d - return dd.strftime("%y/%m/%d") + f"({_WEEKDAYS_KR[dd.weekday()]})" + return dd.strftime("%m/%d") + f"({_WEEKDAYS_KR[dd.weekday()]})" def _build_tree(projects: list[dict[str, Any]]) -> list[dict[str, Any]]: @@ -335,6 +335,18 @@ async def health(request: Request) -> JSONResponse: return JSONResponse({"status": "error", "detail": str(exc)}, status_code=500) +@router.get("/api/live-version") +async def api_live_version(request: Request) -> JSONResponse: + """프로젝트/업무/댓글/멤버/단계 중 가장 최근 변경 시각. + + 페이지를 열어둔 클라이언트가 주기적으로 조회해 값이 바뀌면 새로고침한다 + (다른 사용자가 프로젝트·업무·댓글을 추가/수정했을 때 실시간 반영용). + """ + _require_user(request) + st = _db_or_503(request) + return JSONResponse({"ts": st.latest_version()}) + + @router.get("/", response_class=HTMLResponse) async def index(request: Request) -> HTMLResponse: from app.main import build_erp_nav, render_template # noqa: WPS433 @@ -389,6 +401,24 @@ async def index(request: Request) -> HTMLResponse: p["task_more"] = max(0, len(ordered_tasks) - 8) p["due_label"] = _fmt_date_kr(p.get("due_date")) + # 프로젝트 이름 아래 표시할 멤버(아이콘+이름) + 업무추가 버튼 노출 권한 + # (관리자 또는 해당 프로젝트 멤버/owner). + p_members = st.list_members(project_id=p["id"]) + p["members"] = [ + { + "email": m["user_email"], + "name": m.get("user_name") or m["user_email"].split("@")[0], + "avatar": avatars.get(store.norm_str(m["user_email"], lower=True), ""), + } + for m in p_members + ] + member_emails = {store.norm_str(m["user_email"], lower=True) for m in p_members} + p["can_add_task"] = ( + admin + or store.norm_str(user["email"], lower=True) in member_emails + or store.norm_str(p.get("owner_email"), lower=True) == store.norm_str(user["email"], lower=True) + ) + my_tasks = st.list_tasks(assignee_email=user["email"], limit=500) for t in my_tasks: t["start_label"] = _fmt_date_kr(t.get("start_date")) diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 00bb54f..130a8fc 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 5e1cbaf..4712ccc 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 %} @@ -28,6 +28,17 @@ {% if p.status == 'completed' %}완료{% endif %} {% if p.description %}
{{ p.description }}
{% endif %} + {% if p.members %} +
+ {% for m in p.members %} + + {% if m.avatar %} + {% else %}account_circle{% endif %} + {{ m.name }} + + {% endfor %} +
+ {% endif %}
{% if p.due_date %}마감 {{ p.due_label }}{% endif %} {% if p.task_total %}업무 {{ p.task_done }}/{{ p.task_total }}{% endif %} @@ -39,11 +50,18 @@ {% endif %}
- {% if is_admin %} - - {% endif %} +
+ {% if p.can_add_task %} + + {% endif %} + {% if is_admin %} + + {% endif %} +
{% if p.task_preview %}