feat(project): 아사나식 프로젝트/서브프로젝트 트리 사이드바
좌측 사이드바를 직속 서브 평면 목록 → 전체 프로젝트 재귀 트리로 교체. - 접기/펼치기 캐럿(localStorage 로 펼침 상태 저장, 활성 프로젝트 상위 자동 펼침) - 프로젝트 색상 아이콘, 들여쓰기(depth), 활성 프로젝트 강조 - hover 시 서브추가(+)/삭제(×) 액션(관리자·멤버, 서버 권한 검증) - 상단 '새 프로젝트'(관리자). Material Symbols 아이콘(self-host) 사용. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+74
-12
@@ -605,23 +605,85 @@
|
||||
} catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
|
||||
// ── 서브프로젝트 추가/삭제 ──
|
||||
const newSubBtn = el("pj-new-sub");
|
||||
if (newSubBtn) newSubBtn.addEventListener("click", async function () {
|
||||
const name = prompt("서브프로젝트 이름");
|
||||
// ── 프로젝트 트리 (아사나식 접기/펼치기) ──
|
||||
function setTreeOpen(li, open) {
|
||||
const children = li.querySelector(":scope > .pj-tree-children");
|
||||
const caret = li.querySelector(":scope > .pj-tree-row > .pj-tree-caret");
|
||||
if (!children) return;
|
||||
children.hidden = !open;
|
||||
if (caret) { caret.setAttribute("aria-expanded", open ? "true" : "false"); caret.classList.toggle("is-open", open); }
|
||||
}
|
||||
function treeOpenSet() {
|
||||
try { return new Set(JSON.parse(localStorage.getItem("pj_tree_open") || "[]")); }
|
||||
catch (_) { return new Set(); }
|
||||
}
|
||||
function treeSaveOpen(id, open) {
|
||||
const s = treeOpenSet();
|
||||
if (open) s.add(String(id)); else s.delete(String(id));
|
||||
try { localStorage.setItem("pj_tree_open", JSON.stringify(Array.from(s))); } catch (_) {}
|
||||
}
|
||||
|
||||
document.querySelectorAll(".pj-tree-caret").forEach(function (caret) {
|
||||
caret.addEventListener("click", function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const li = caret.closest(".pj-tree-li");
|
||||
const children = li.querySelector(":scope > .pj-tree-children");
|
||||
const open = children.hidden; // 현재 닫혀있으면 → 연다
|
||||
setTreeOpen(li, open);
|
||||
treeSaveOpen(li.dataset.id, open);
|
||||
});
|
||||
});
|
||||
|
||||
// 저장된 펼침 상태 복원 + 활성 프로젝트의 상위 자동 펼침
|
||||
(function restoreTree() {
|
||||
const openSet = treeOpenSet();
|
||||
document.querySelectorAll(".pj-tree-li").forEach(function (li) {
|
||||
if (openSet.has(String(li.dataset.id))) setTreeOpen(li, true);
|
||||
});
|
||||
const active = document.querySelector(".pj-tree-row.is-active");
|
||||
let box = active ? active.closest(".pj-tree-children") : null;
|
||||
while (box) {
|
||||
box.hidden = false;
|
||||
const li = box.closest(".pj-tree-li");
|
||||
if (li) setTreeOpen(li, true);
|
||||
box = li ? (li.parentElement ? li.parentElement.closest(".pj-tree-children") : null) : null;
|
||||
}
|
||||
})();
|
||||
|
||||
// 서브프로젝트 추가 (트리 각 노드)
|
||||
document.querySelectorAll(".pj-tree-addsub").forEach(function (b) {
|
||||
b.addEventListener("click", async function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
const name = prompt("서브프로젝트 이름");
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await api("POST", "/project/api/projects/" + b.dataset.id + "/subprojects", { name: name.trim() });
|
||||
location.reload();
|
||||
} catch (err) { alert("생성 실패: " + err.message); }
|
||||
});
|
||||
});
|
||||
// 프로젝트/서브 삭제 (트리)
|
||||
document.querySelectorAll(".pj-tree-del").forEach(function (b) {
|
||||
b.addEventListener("click", async function (e) {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
if (!confirm("'" + (b.dataset.name || "") + "' 삭제할까요? 하위 서브프로젝트·업무도 모두 삭제됩니다.")) return;
|
||||
try {
|
||||
await api("DELETE", "/project/api/projects/" + b.dataset.id);
|
||||
if (String(b.dataset.id) === String(projectId)) location.href = "/project/";
|
||||
else location.reload();
|
||||
} catch (err) { alert("삭제 실패: " + err.message); }
|
||||
});
|
||||
});
|
||||
// 새 최상위 프로젝트 (관리자)
|
||||
const newProjSide = el("pj-new-project-side");
|
||||
if (newProjSide) newProjSide.addEventListener("click", async function () {
|
||||
const name = prompt("새 프로젝트 이름");
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
await api("POST", "/project/api/projects/" + projectId + "/subprojects", { name: name.trim() });
|
||||
await api("POST", "/project/api/projects", { name: name.trim() });
|
||||
location.reload();
|
||||
} catch (e) { alert("생성 실패: " + e.message); }
|
||||
});
|
||||
document.querySelectorAll(".pj-del-sub").forEach(function (b) {
|
||||
b.addEventListener("click", async function () {
|
||||
if (!confirm("이 서브프로젝트를 삭제할까요? (하위 업무도 모두 삭제)")) return;
|
||||
try { await api("DELETE", "/project/api/projects/" + b.dataset.id); location.reload(); }
|
||||
catch (e) { alert("삭제 실패: " + e.message); }
|
||||
});
|
||||
});
|
||||
|
||||
// ── 멤버 배정/제외 (관리자) ──
|
||||
const memberModal = el("pj-modal-member");
|
||||
|
||||
Reference in New Issue
Block a user