diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 06c2697..87d25db 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -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 cf115ec..9cba93a 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -393,5 +393,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 7213f61..3e60b2a 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -461,5 +461,5 @@ - + {% endblock %} diff --git a/app/static/project.js b/app/static/project.js index 3d8dcad..0f84132 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -1081,15 +1081,100 @@ const avatars = data.avatars || {}; const myTasksById = {}; (data.myTasks || []).forEach(function (t) { myTasksById[t.id] = t; }); - // 우측 "내 업무" 패널 — openTaskModal 은 이 함수 뒤쪽에서 선언되지만 - // function 선언은 호이스팅되어 여기서 참조해도 문제없다. - document.querySelectorAll(".pj-project-mytasks .pj-task-popup-link").forEach(function (a) { + // 우측 "내 업무" 패널 — openTaskModal/mergeTask/refreshActiveView 는 이 + // 함수 뒤쪽에서 선언되지만 function 선언은 호이스팅되어 여기서 참조해도 + // 문제없다. 이 패널의 업무는 전부 "내가 담당자"인 게 보장돼 있어(서버가 + // assignee_email 로 조회) 홈 페이지 syncMyTaskPanel 과 달리 재배정 시 + // 사라지는 경우를 신경 쓸 필요 없다. + function myTaskRowHtml(t) { + const dates = t.start_date || t.due_date + ? '' + + (t.start_date && t.due_date ? fmtDateKr(t.start_date) + " ~ " + fmtDateKr(t.due_date) + : t.due_date ? "~ " + fmtDateKr(t.due_date) : fmtDateKr(t.start_date) + " ~") + + "" + : ""; + return '
  • ' + + '' + + '' + + (t.comment_count + ? '' + + '💬' + t.comment_count + "" + : "") + + "" + + '' + escapeHtml(t.title || "") + "" + dates + "" + + "
  • "; + } + function syncProjectMyTaskPanel(t, removed) { + const group = document.querySelector('.pj-project-mytasks .pj-mytree-group[data-group-id="' + t.project_id + '"] .pj-mytree-tasks'); + if (!group) return; // 이 프로젝트에 내 업무가 하나도 없었으면 다음 새로고침까지 생략. + const existingLink = group.querySelector('.pj-task-popup-link[data-task-id="' + t.id + '"]'); + const existingLi = existingLink ? existingLink.closest("li") : null; + if (removed) { if (existingLi) existingLi.remove(); return; } + const wrap = document.createElement("div"); + wrap.innerHTML = myTaskRowHtml(t); + const newLi = wrap.firstElementChild; + wireMyTaskPopupLink(newLi.querySelector(".pj-task-popup-link")); + if (existingLi) existingLi.replaceWith(newLi); else group.appendChild(newLi); + } + // 내 업무 항목 우클릭 메뉴 — 열기(프로젝트 페이지로 이동)/업무 추가/수정 + // (팝업 편집)/완료토글/삭제. "업무 추가"는 현재 열려 있는 프로젝트로만 + // 새 업무를 만들 수 있는 openTaskModal(null) 의 제약 때문에, 이 업무가 + // 다른 프로젝트 소속이면 그 프로젝트 페이지로 이동시켜 거기서 추가하게 한다. + function myTaskContextItems(t) { + return [ + { label: "열기", onClick: function () { location.href = "/project/p/" + t.project_id + "?task=" + t.id; } }, + { + label: "업무 추가", + onClick: function () { + if (t.project_id === projectId) openTaskModal(null); + else location.href = "/project/p/" + t.project_id; + }, + }, + { label: "수정", onClick: function () { openTaskModal(t); } }, + { + label: t.completed_at ? "완료 취소" : "완료로 표시", + onClick: async function () { + try { + const res = await api("PUT", "/project/api/tasks/" + t.id, { completed: !t.completed_at }); + mergeTask(res.task); + myTasksById[t.id] = Object.assign({}, t, res.task); + syncProjectMyTaskPanel(myTasksById[t.id]); + refreshActiveView(); + } catch (e) { showToast("처리 실패: " + e.message, "error"); } + }, + }, + null, + { + label: "삭제", danger: true, + onClick: async function () { + if (!canDelete(t.created_by)) { alert(DELETE_DENIED); return; } + if (!confirm("이 업무를 삭제할까요?")) return; + try { + await api("DELETE", "/project/api/tasks/" + t.id); + tasks = tasks.filter(function (x) { return x.id !== t.id; }); + delete myTasksById[t.id]; + syncProjectMyTaskPanel(t, true); + refreshActiveView(); + } catch (e) { showToast("삭제 실패: " + e.message, "error"); } + }, + }, + ]; + } + function wireMyTaskPopupLink(a) { a.addEventListener("click", function (e) { e.preventDefault(); const t = myTasksById[a.dataset.taskId]; if (t) openTaskModal(t); }); - }); + a.addEventListener("contextmenu", function (e) { + const t = myTasksById[a.dataset.taskId]; + if (!t) return; + e.preventDefault(); + e.stopPropagation(); + showContextMenu(e.clientX, e.clientY, myTaskContextItems(t)); + }); + } + document.querySelectorAll(".pj-project-mytasks .pj-task-popup-link").forEach(function (a) { wireMyTaskPopupLink(a); }); // 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘. function avatarHtml(email, cls) { const url = email ? avatars[String(email).toLowerCase()] : "";