From 4dbb3e7856a8bc1fea638a99b56d1dd4978b03ec Mon Sep 17 00:00:00 2001 From: king Date: Wed, 16 Sep 2026 15:17:46 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=EB=82=B4=20?= =?UTF-8?q?=EC=97=85=EB=AC=B4=20=ED=8C=A8=EB=84=90=EC=97=90=EB=8F=84=20?= =?UTF-8?q?=EC=9A=B0=ED=81=B4=EB=A6=AD=20=EB=A9=94=EB=89=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지난번엔 홈 화면 내 업무 패널만 처리했는데, 프로젝트 상세 페이지 우측 내 업무 패널(.pj-project-mytasks)에도 동일하게 열기/업무 추가/수정/ 완료토글/삭제 메뉴를 추가. "업무 추가"는 openTaskModal(null) 이 항상 현재 열려 있는 프로젝트에만 새 업무를 만들 수 있는 제약이 있어, 업무가 다른 프로젝트 소속이면 그 프로젝트 페이지로 이동시켜 거기서 추가하도록 처리(실제 프로젝트 A/B 두 개로 헤드리스 크롬에서 검증). --- .../project/templates/project/inbox.html | 2 +- .../project/templates/project/index.html | 2 +- .../project/templates/project/project.html | 2 +- app/static/project.js | 93 ++++++++++++++++++- 4 files changed, 92 insertions(+), 7 deletions(-) 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()] : "";