From 6e0180944e3db4c6a92b0a19e533a7771a3337ea Mon Sep 17 00:00:00 2001 From: king Date: Thu, 25 Jun 2026 17:26:55 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=EB=8B=B4=EB=8B=B9=EC=9E=90?= =?UTF-8?q?=C2=B7=EB=8C=93=EA=B8=80=20=EC=9E=91=EC=84=B1=EC=9E=90=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=EC=9D=84=20=EA=B5=AC=EA=B8=80=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80(?= =?UTF-8?q?=EC=9B=90=ED=98=95)=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 라우터: 이메일→구글 picture URL 아바타 맵을 pj-data 로 전달 - 보드 카드/리스트 담당자, 댓글 작성자에 원형 프로필 이미지 표시 - 이미지 없으면 기존 기본 아이콘 폴백. referrerpolicy=no-referrer Co-Authored-By: Claude Opus 4.8 --- app/modules/project/router.py | 9 +++++++++ app/modules/project/templates/project/index.html | 4 ++-- .../project/templates/project/project.html | 7 ++++--- app/static/project.css | 5 ++++- app/static/project.js | 15 +++++++++++++-- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 8a1b589..091c1c2 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -441,6 +441,14 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: board_stage_names.append(s["name"]) board_stage_names.sort(key=lambda n: (_order.get(n, 999), n)) + # 아바타 맵: 이메일(소문자) → 구글 프로필 이미지 URL (담당자/댓글 작성자 표시용) + from app.main import user_store # noqa: WPS433 + avatars = { + store.norm_str(u["email"], lower=True): u.get("picture") or "" + for u in user_store.list_all() + if u.get("picture") + } + # 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달 today = today_kst() try: @@ -472,6 +480,7 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: "projects_meta": projects_meta, "stages_by_project": stages_by_project, "board_stage_names": board_stage_names, + "avatars": avatars, "priority_labels": store.PRIORITY_LABELS, "color_palette": list(store.COLOR_PALETTE), "today": today.isoformat(), diff --git a/app/modules/project/templates/project/index.html b/app/modules/project/templates/project/index.html index b564646..b46e6ad 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 %} @@ -167,5 +167,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 0a6f638..e517a00 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 %} - + @@ -285,10 +285,11 @@ "priorityLabels": {{ priority_labels | tojson }}, "projects": {{ projects_meta | tojson }}, "stagesByProject": {{ stages_by_project | tojson }}, - "boardStages": {{ board_stage_names | tojson }} + "boardStages": {{ board_stage_names | tojson }}, + "avatars": {{ avatars | tojson }} } - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index 9c16078..25a4940 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -108,7 +108,10 @@ .material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; line-height: 1; vertical-align: middle; } .pj-gicon { font-size: 24px; color: #5f6368; flex: 0 0 auto; } .pj-gicon-sm { font-size: 20px; } -.pj-assignee { display: inline-flex; align-items: center; gap: 3px; font-size: 12px; color: #5f6368; } +.pj-assignee { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: #5f6368; } +/* 구글 프로필 이미지 — 원형 */ +.pj-avatar { width: 22px; height: 22px; border-radius: 50%; object-fit: cover; flex: 0 0 auto; vertical-align: middle; background: #e7e9ec; } +.pj-avatar.pj-gicon-sm { width: 18px; height: 18px; } /* ── 툴바 + 탭 ── */ .pj-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } diff --git a/app/static/project.js b/app/static/project.js index f9594bf..e5ed5d1 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -193,6 +193,14 @@ const projectsMeta = data.projects || []; const stagesByProject = data.stagesByProject || {}; const boardStages = data.boardStages || []; + const avatars = data.avatars || {}; + // 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘. + function avatarHtml(email, cls) { + const url = email ? avatars[String(email).toLowerCase()] : ""; + const c = cls || ""; + if (url) return ''; + return 'account_circle'; + } let timeline = null; let currentView = "calendar"; @@ -620,7 +628,7 @@ ? '' + (priorityLabels[t.priority] || t.priority) + "" : "") + (t.assignee_email - ? 'account_circle' + + ? '' + avatarHtml(t.assignee_email, "pj-gicon-sm") + escapeHtml(idOf(t.assignee_email)) + "" : "") + (t.due_date ? '' + t.due_date + "" : "") + @@ -738,6 +746,9 @@ tr.innerHTML = LIST_COLS.map(function (c) { let inner = escapeHtml(String(c.text(t))); if (c.key === "title") inner += cmtBadge(t); + if (c.key === "assignee" && t.assignee_email) { + inner = '' + avatarHtml(t.assignee_email, "pj-gicon-sm") + escapeHtml(idOf(t.assignee_email)) + ""; + } return "" + inner + ""; }).join(""); if (canManage) { tr.style.cursor = "pointer"; tr.addEventListener("click", function () { openTaskModal(t); }); } @@ -856,7 +867,7 @@ const li = document.createElement("li"); li.className = "pj-comment-item"; li.innerHTML = - 'account_circle' + + avatarHtml(c.author_email, "pj-gicon-sm") + '
' + escapeHtml(idOf(c.author_email)) + ' ' + (c.created_at || "").slice(0, 16).replace("T", " ") + "
" +