feat(project): 멤버 배정을 체크박스 다중선택 + 아이콘 표시로 변경
<select> 하나씩 골라 순서대로 배정하던 걸 새 프로젝트 모달과 같은 체크박스 목록으로 바꿔 후보 전원을 한 화면에서 보고 여러 명을 한 번에 선택해 배정. 각 후보는 항상 아바타(구글 프로필, 없으면 기본 아이콘)와 이름을 같이 보여준다. 저장 시 선택된 인원마다 순차 POST 후 결과를 members 배열에 patch(기존 optimistic 흐름 그대로, reload 없음).
This commit is contained in:
@@ -45,5 +45,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/project.js?v=20260917f" defer></script>
|
<script src="/static/project.js?v=20260917g" 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=20260917f" defer></script>
|
<script src="/static/project.js?v=20260917g" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -423,10 +423,10 @@
|
|||||||
<div class="pj-modal" id="pj-modal-member" hidden>
|
<div class="pj-modal" id="pj-modal-member" hidden>
|
||||||
<div class="pj-modal-card">
|
<div class="pj-modal-card">
|
||||||
<h3>멤버 배정</h3>
|
<h3>멤버 배정</h3>
|
||||||
<p class="pj-modal-hint">관리자 페이지에서 <b>프로젝트 관리</b> 권한을 켠 직원이 후보로 나옵니다.</p>
|
<p class="pj-modal-hint">관리자 페이지에서 <b>프로젝트 관리</b> 권한을 켠 직원이 후보로 나옵니다. 여러 명을 한 번에 선택할 수 있습니다.</p>
|
||||||
<label>직원
|
<div class="pj-field-label">직원
|
||||||
<select id="pj-m-user"><option value="">불러오는 중…</option></select>
|
<div class="pj-member-picker" id="pj-m-users">불러오는 중…</div>
|
||||||
</label>
|
</div>
|
||||||
<label>역할
|
<label>역할
|
||||||
<select id="pj-m-role">
|
<select id="pj-m-role">
|
||||||
<option value="member" selected>멤버</option>
|
<option value="member" selected>멤버</option>
|
||||||
@@ -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=20260917f" defer></script>
|
<script src="/static/project.js?v=20260917g" defer></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+20
-17
@@ -2625,38 +2625,41 @@
|
|||||||
renderMemberList();
|
renderMemberList();
|
||||||
const addMemberBtn = el("pj-add-member");
|
const addMemberBtn = el("pj-add-member");
|
||||||
if (addMemberBtn) addMemberBtn.addEventListener("click", async function () {
|
if (addMemberBtn) addMemberBtn.addEventListener("click", async function () {
|
||||||
const sel = el("pj-m-user");
|
const box = el("pj-m-users");
|
||||||
openModal(memberModal);
|
openModal(memberModal);
|
||||||
sel.innerHTML = '<option value="">불러오는 중…</option>';
|
box.innerHTML = "불러오는 중…";
|
||||||
try {
|
try {
|
||||||
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 opts = res.users
|
const candidates = res.users.filter(function (u) { return !existing.has(u.email); });
|
||||||
.filter(function (u) { return !existing.has(u.email); })
|
box.innerHTML = candidates.length ? candidates.map(function (u) {
|
||||||
.map(function (u) {
|
return '<label class="pj-member-pick">' +
|
||||||
return '<option value="' + u.email + '" data-name="' + escapeHtml(u.name) + '">' +
|
'<input type="checkbox" value="' + u.email + '" data-name="' + escapeHtml(u.name) + '" /> ' +
|
||||||
escapeHtml(u.id) + " (" + escapeHtml(u.email) + ")</option>";
|
avatarHtml(u.email, "pj-gicon-sm") + " " + escapeHtml(u.id) +
|
||||||
});
|
"</label>";
|
||||||
sel.innerHTML = opts.length
|
}).join("") : "<span class='pj-empty-sm'>배정 가능한 직원이 없습니다(권한 미부여 또는 전원 배정됨)</span>";
|
||||||
? opts.join("")
|
|
||||||
: '<option value="">배정 가능한 직원이 없습니다(권한 미부여 또는 전원 배정됨)</option>';
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sel.innerHTML = '<option value="">불러오기 실패: ' + escapeHtml(e.message) + "</option>";
|
box.innerHTML = "<span class='pj-empty-sm'>불러오기 실패: " + escapeHtml(e.message) + "</span>";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const saveMemberBtn = el("pj-save-member");
|
const saveMemberBtn = el("pj-save-member");
|
||||||
if (saveMemberBtn) saveMemberBtn.addEventListener("click", async function () {
|
if (saveMemberBtn) saveMemberBtn.addEventListener("click", async function () {
|
||||||
const sel = el("pj-m-user");
|
const box = el("pj-m-users");
|
||||||
if (!sel.value) { alert("직원을 선택하세요."); return; }
|
const checked = box ? Array.from(box.querySelectorAll("input[type=checkbox]:checked")) : [];
|
||||||
const name = sel.selectedOptions[0] ? (sel.selectedOptions[0].dataset.name || "") : "";
|
if (!checked.length) { alert("직원을 선택하세요."); return; }
|
||||||
|
const role = el("pj-m-role").value;
|
||||||
|
let failCount = 0;
|
||||||
|
for (const cb of checked) {
|
||||||
try {
|
try {
|
||||||
const res = await api("POST", "/project/api/projects/" + projectId + "/members", {
|
const res = await api("POST", "/project/api/projects/" + projectId + "/members", {
|
||||||
user_email: sel.value, user_name: name, role: el("pj-m-role").value,
|
user_email: cb.value, user_name: cb.dataset.name || "", role: role,
|
||||||
});
|
});
|
||||||
members.push(res.member);
|
members.push(res.member);
|
||||||
|
} catch (e) { failCount++; }
|
||||||
|
}
|
||||||
renderMemberList();
|
renderMemberList();
|
||||||
closeModal(memberModal);
|
closeModal(memberModal);
|
||||||
} catch (e) { showToast("배정 실패: " + e.message, "error"); }
|
if (failCount) showToast(failCount + "명 배정 실패", "error");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 최초 렌더 — 아사나처럼 프로젝트에 들어오면 보드(세션)가 기본 화면이다
|
// 최초 렌더 — 아사나처럼 프로젝트에 들어오면 보드(세션)가 기본 화면이다
|
||||||
|
|||||||
Reference in New Issue
Block a user