@@ -291,5 +291,5 @@
-
+
{% endblock %}
diff --git a/app/static/project.js b/app/static/project.js
index 7687ce3..6ae8ff3 100644
--- a/app/static/project.js
+++ b/app/static/project.js
@@ -763,8 +763,9 @@
}
function refreshActiveView() {
- // 모든 뷰 클라이언트 재렌더(달력 포함, 새로고침 없음).
+ // 모든 뷰 + 좌측 트리 클라이언트 재렌더(새로고침 없음).
renderView(currentView);
+ if (typeof renderTree === "function") renderTree();
}
// ── 업무 모달 ──
@@ -978,6 +979,8 @@
if (id) res = await api("PUT", "/project/api/tasks/" + id, payload);
else res = await api("POST", "/project/api/projects/" + projectId + "/tasks", payload);
mergeTask(res.task);
+ // 새 업무는 해당 프로젝트 트리를 펼쳐 바로 보이게.
+ if (!id) treeSaveOpen((res.task && res.task.project_id) || projectId, true);
closeModal(taskModal);
refreshActiveView();
} catch (e) { alert("저장 실패: " + e.message); }
@@ -997,7 +1000,7 @@
} catch (e) { alert("삭제 실패: " + e.message); }
});
- // ── 프로젝트 트리 (아사나식 접기/펼치기) ──
+ // ── 프로젝트 트리 (아사나식) — tasks 로부터 클라이언트 렌더(업무 추가 즉시 반영) ──
function setTreeOpen(li, open) {
const children = li.querySelector(":scope > .pj-tree-children");
const caret = li.querySelector(":scope > .pj-tree-row > .pj-tree-caret");
@@ -1015,19 +1018,59 @@
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 treeTaskLi(t, pid) {
+ return '
";
+ }
+ function treeProjLi(p) {
+ const ptasks = tasks.filter(function (t) { return t.project_id === p.id; });
+ const has = ptasks.length > 0;
+ const caret = has
+ ? '
'
+ : '
';
+ const del = isAdmin
+ ? '
'
+ : "";
+ let h = '
";
+ if (has) h += '' +
+ ptasks.map(function (t) { return treeTaskLi(t, p.id); }).join("") + "
";
+ return h + "";
+ }
+ function wireTreeHandlers(rootUl) {
+ rootUl.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() {
+ rootUl.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);
+ if (String(b.dataset.id) === String(projectId)) location.href = "/project/";
+ else location.reload();
+ } catch (err) { alert("삭제 실패: " + err.message); }
+ });
+ });
+ }
+ function restoreTreeState() {
const openSet = treeOpenSet();
document.querySelectorAll(".pj-tree-li").forEach(function (li) {
if (openSet.has(String(li.dataset.id))) setTreeOpen(li, true);
@@ -1040,21 +1083,16 @@
if (li) setTreeOpen(li, true);
box = li ? (li.parentElement ? li.parentElement.closest(".pj-tree-children") : null) : null;
}
- })();
+ }
+ function renderTree() {
+ const rootUl = el("pj-tree-root");
+ if (!rootUl) return;
+ rootUl.innerHTML = projectsMeta.map(treeProjLi).join("");
+ wireTreeHandlers(rootUl);
+ restoreTreeState();
+ }
+ renderTree();
- // 프로젝트 삭제 (트리)
- 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);
- 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 () {