style(project): 새 프로젝트 모달 멤버 선택도 멤버 배정 창과 동일하게 통일

/api/assignable-users 응답에 avatar 필드 추가하고, 아이콘/이름/체크박스
세로 카드(memberPickHtml)를 공용 함수로 뽑아 새 프로젝트 모달(홈·사이드바
둘 다)과 멤버 배정 모달 3곳이 전부 같은 모양을 쓰도록 통일. 홈 페이지는
avatars 맵이 따로 없어서 이 API 응답의 avatar 필드에만 의존하도록 함.
This commit is contained in:
2026-09-16 13:23:40 +09:00
parent 81c8a77f89
commit eb1bae9fc6
5 changed files with 28 additions and 18 deletions
+1
View File
@@ -750,6 +750,7 @@ async def api_assignable_users(request: Request) -> JSONResponse:
"email": u["email"], "email": u["email"],
"name": u.get("name") or u["email"].split("@")[0], "name": u.get("name") or u["email"].split("@")[0],
"id": u["email"].split("@")[0], "id": u["email"].split("@")[0],
"avatar": u.get("picture") or "",
} }
for u in user_store.list_all() for u in user_store.list_all()
if has_module(u, "project") if has_module(u, "project")
@@ -45,5 +45,5 @@
{% endif %} {% endif %}
</div> </div>
<script src="/static/project.js?v=20260917h" defer></script> <script src="/static/project.js?v=20260917i" defer></script>
{% 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=20260917h" defer></script> <script src="/static/project.js?v=20260917i" defer></script>
{% endblock %} {% endblock %}
@@ -461,5 +461,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=20260917h" defer></script> <script src="/static/project.js?v=20260917i" defer></script>
{% endblock %} {% endblock %}
+24 -15
View File
@@ -114,6 +114,21 @@
return mm + "/" + dd + "(" + wd + ")"; return mm + "/" + dd + "(" + wd + ")";
} }
// 멤버 선택 후보 카드(아이콘 위 / 이름 가운데 / 체크박스 아래) — 새 프로젝트
// 모달(홈·프로젝트 페이지)과 멤버 배정 모달 3곳이 전부 이 함수 하나를 쓴다.
// /api/assignable-users 응답의 avatar 필드를 그대로 쓰므로 페이지별
// avatars 맵(홈 페이지엔 없음)에 기대지 않는다.
function memberPickHtml(u) {
const icon = u.avatar
? '<img class="pj-avatar pj-member-pick-icon" src="' + escapeHtml(u.avatar) + '" alt="" referrerpolicy="no-referrer" />'
: '<span class="material-symbols-outlined pj-gicon pj-member-pick-icon">account_circle</span>';
return '<label class="pj-member-pick pj-member-pick-stacked">' +
'<span class="pj-member-pick-avatar">' + icon + "</span>" +
'<span class="pj-member-pick-name">' + escapeHtml(u.id) + "</span>" +
'<input type="checkbox" value="' + u.email + '" data-name="' + escapeHtml(u.name) + '" />' +
"</label>";
}
// ── 색상 팔레트 렌더 ─────────────────────────────────────── // ── 색상 팔레트 렌더 ───────────────────────────────────────
function renderColorPalette(container, onPick, initial) { function renderColorPalette(container, onPick, initial) {
const colors = window.PJ_COLORS || ["#4573d2"]; const colors = window.PJ_COLORS || ["#4573d2"];
@@ -477,10 +492,9 @@
try { try {
const res = await api("GET", "/project/api/assignable-users"); const res = await api("GET", "/project/api/assignable-users");
const users = res.users || []; const users = res.users || [];
membersBox.innerHTML = users.length ? users.map(function (u) { membersBox.innerHTML = users.length
return '<label class="pj-member-pick"><input type="checkbox" value="' + u.email + '" /> ' + ? users.map(memberPickHtml).join("")
escapeHtml(u.id) + "</label>"; : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다.</span>";
}).join("") : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다.</span>";
} catch (e) { } catch (e) {
membersBox.innerHTML = "<span class='pj-empty-sm'>불러오기 실패</span>"; membersBox.innerHTML = "<span class='pj-empty-sm'>불러오기 실패</span>";
} }
@@ -2503,10 +2517,9 @@
try { try {
const res = await api("GET", "/project/api/assignable-users"); const res = await api("GET", "/project/api/assignable-users");
const users = res.users || []; const users = res.users || [];
newProjMembersBox.innerHTML = users.length ? users.map(function (u) { newProjMembersBox.innerHTML = users.length
return '<label class="pj-member-pick"><input type="checkbox" value="' + u.email + '" /> ' + ? users.map(memberPickHtml).join("")
escapeHtml(u.id) + "</label>"; : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다.</span>";
}).join("") : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다.</span>";
} catch (e) { } catch (e) {
newProjMembersBox.innerHTML = "<span class='pj-empty-sm'>불러오기 실패</span>"; newProjMembersBox.innerHTML = "<span class='pj-empty-sm'>불러오기 실패</span>";
} }
@@ -2632,13 +2645,9 @@
const res = await api("GET", "/project/api/assignable-users"); const res = await api("GET", "/project/api/assignable-users");
const existing = new Set(members.map(function (m) { return m.user_email; })); const existing = new Set(members.map(function (m) { return m.user_email; }));
const candidates = res.users.filter(function (u) { return !existing.has(u.email); }); const candidates = res.users.filter(function (u) { return !existing.has(u.email); });
box.innerHTML = candidates.length ? candidates.map(function (u) { box.innerHTML = candidates.length
return '<label class="pj-member-pick pj-member-pick-stacked">' + ? candidates.map(memberPickHtml).join("")
'<span class="pj-member-pick-avatar">' + avatarHtml(u.email, "pj-member-pick-icon") + "</span>" + : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다(권한 미부여 또는 전원 배정됨)</span>";
'<span class="pj-member-pick-name">' + escapeHtml(u.id) + "</span>" +
'<input type="checkbox" value="' + u.email + '" data-name="' + escapeHtml(u.name) + '" />' +
"</label>";
}).join("") : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다(권한 미부여 또는 전원 배정됨)</span>";
} catch (e) { } catch (e) {
box.innerHTML = "<span class='pj-empty-sm'>불러오기 실패: " + escapeHtml(e.message) + "</span>"; box.innerHTML = "<span class='pj-empty-sm'>불러오기 실패: " + escapeHtml(e.message) + "</span>";
} }