feat(project): 업무·프로젝트 삭제를 '만든 사람'만 허용 + 경고

- 서버: api_delete_task/api_delete_project 가 created_by 일치자만 허용
  (슈퍼관리자 예외, created_by 없는 과거 데이터는 관리자 허용). 403 반환.
- 클라이언트: 삭제 버튼 클릭 시 만든 사람이 아니면 경고창 후 중단
  (created_by 를 트리 삭제버튼·홈 카드·업무 객체로 전달)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 17:10:24 +09:00
parent 523b9b3150
commit f8f4cded66
4 changed files with 58 additions and 21 deletions
+24 -1
View File
@@ -62,6 +62,14 @@
function initHome() {
const grid = el("pj-project-grid");
if (!grid && !el("pj-toggle-done")) return; // 홈 아님
const wrap = document.querySelector(".pj-wrap");
const me = ((wrap && wrap.dataset.me) || "").toLowerCase();
const isSuper = !!(wrap && wrap.dataset.isSuper === "true");
function canDelete(createdBy) {
if (isSuper) return true;
const cb = (createdBy || "").toLowerCase();
return !!cb && cb === me;
}
// ── 완료 프로젝트 숨기기 토글 (localStorage 기억) ──
const toggleBtn = el("pj-toggle-done");
@@ -140,6 +148,8 @@
});
el("pj-delete-project").addEventListener("click", async function () {
const id = el("pj-e-id").value;
const card = grid.querySelector('.pj-project-card[data-id="' + id + '"]');
if (card && !canDelete(card.dataset.creator)) { alert("삭제는 만든 사람만 할 수 있습니다."); return; }
if (!confirm("이 프로젝트를 삭제할까요? 안의 업무도 모두 삭제됩니다.")) return;
try { await api("DELETE", "/project/api/projects/" + id); location.reload(); }
catch (e) { alert("삭제 실패: " + e.message); }
@@ -156,6 +166,15 @@
const projectId = parseInt(root.dataset.projectId, 10);
const canManage = root.dataset.canManage === "true";
const isAdmin = root.dataset.isAdmin === "true";
const me = (root.dataset.me || "").toLowerCase();
const isSuper = root.dataset.isSuper === "true";
// 삭제는 만든 사람만(슈퍼관리자 예외). created_by 없으면 서버가 최종 판정.
function canDelete(createdBy) {
if (isSuper) return true;
const cb = (createdBy || "").toLowerCase();
return !!cb && cb === me;
}
const DELETE_DENIED = "삭제는 만든 사람만 할 수 있습니다.";
const dataEl = el("pj-data");
const data = JSON.parse(dataEl.textContent);
@@ -918,7 +937,10 @@
el("pj-delete-task").addEventListener("click", async function () {
const id = el("pj-t-id").value;
if (!id || !confirm("이 업무를 삭제할까요?")) return;
if (!id) return;
const t = tasks.find(function (x) { return String(x.id) === String(id); });
if (t && !canDelete(t.created_by)) { alert(DELETE_DENIED); return; }
if (!confirm("이 업무를 삭제할까요?")) return;
try {
await api("DELETE", "/project/api/tasks/" + id);
tasks = tasks.filter(function (x) { return String(x.id) !== String(id); });
@@ -976,6 +998,7 @@
document.querySelectorAll(".pj-tree-del").forEach(function (b) {
b.addEventListener("click", async function (e) {
e.preventDefault(); e.stopPropagation();
if (!canDelete(b.dataset.creator)) { alert(DELETE_DENIED); return; }
if (!confirm("'" + (b.dataset.name || "") + "' 프로젝트를 삭제할까요? 안의 업무도 모두 삭제됩니다.")) return;
try {
await api("DELETE", "/project/api/projects/" + b.dataset.id);