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 '
' +
+ '";
+ }
+ 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()] : "";