From 03916a61b1f075a2a053ac9ba99f56cf1e8abfb7 Mon Sep 17 00:00:00 2001 From: king Date: Thu, 25 Jun 2026 13:20:28 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=EC=84=9C=EB=B8=8C=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A0=9C=EA=B1=B0,=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=ED=8A=B8=EB=A6=AC=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=E2=86=92=EC=97=85=EB=AC=B4=202?= =?UTF-8?q?=EB=8B=A8=EA=B3=84=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 요구에 따라 서브프로젝트 개념을 UI 에서 제거. 좌측 트리를 프로젝트(최상위) → 그 안의 업무 2단계로 재구성: - 프로젝트 노드(folder) 펼치면 소속 업무가 체크 아이콘으로 표시 - 업무 클릭 → /project/p/{id}?task={tid} 로 이동 후 해당 업무 모달 자동 오픈 - 완료 업무는 취소선. 트리에서 프로젝트 삭제(관리자) - 과거 parent_id 서브프로젝트는 트리/홈에서 숨김(최상위만 노출) - 홈 카드의 '서브 N' 칩 제거 Co-Authored-By: Claude Opus 4.8 --- app/modules/project/router.py | 28 ++++-- .../project/templates/project/inbox.html | 4 +- .../project/templates/project/index.html | 5 +- .../project/templates/project/project.html | 86 ++++++++++--------- app/static/project.css | 2 + app/static/project.js | 23 ++--- 6 files changed, 84 insertions(+), 64 deletions(-) diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 30d04d2..7712123 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -356,12 +356,31 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: include_archived=False, member_email=None if admin else user["email"], ) - tree = _build_tree(all_projects) stages = st.list_stages(project_id=project_id) - tasks = st.list_tasks(project_id=project_id, include_subprojects=True, limit=2000) + tasks = st.list_tasks(project_id=project_id, limit=2000) members = st.list_members(project_id=project_id) can_manage = _can_manage(request, st, user, project_id) - subprojects = [p for p in all_projects if p.get("parent_id") == project_id] + + # 좌측 트리: 프로젝트(최상위) → 그 안의 업무. (서브프로젝트 개념 제거) + nav_projects: list[dict[str, Any]] = [] + for p in all_projects: + if p.get("parent_id"): + continue # 과거 서브프로젝트는 트리에서 제외 + p_tasks = ( + tasks if p["id"] == project_id + else st.list_tasks(project_id=p["id"], limit=500) + ) + nav_projects.append({ + "id": p["id"], + "name": p["name"], + "color": p.get("color") or "#4573d2", + "tasks": [ + {"id": t["id"], "title": t["title"], + "color": t.get("project_color") or p.get("color") or "#4573d2", + "completed": bool(t.get("completed_at"))} + for t in p_tasks + ], + }) # 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달 today = today_kst() @@ -387,11 +406,10 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: "page_title": project["name"], "page_subtitle": project.get("description") or "프로젝트 보드", "project": project, - "tree": tree, + "nav_projects": nav_projects, "stages": stages, "tasks": tasks, "members": members, - "subprojects": subprojects, "priority_labels": store.PRIORITY_LABELS, "color_palette": list(store.COLOR_PALETTE), "today": today.isoformat(), diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index a01803c..ad0c92a 100644 --- a/app/modules/project/templates/project/inbox.html +++ b/app/modules/project/templates/project/inbox.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -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 264a05c..f278d0f 100644 --- a/app/modules/project/templates/project/index.html +++ b/app/modules/project/templates/project/index.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} @@ -43,7 +43,6 @@
{{ p.name }}
{% if p.description %}
{{ p.description }}
{% endif %}
- {% if p.children %}서브 {{ p.children|length }}{% endif %} {% if p.due_date %}마감 {{ p.due_date }}{% endif %}
@@ -101,5 +100,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 273bc59..040c358 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,49 +1,13 @@ {% extends "erp_base.html" %} {% block head_extra %} - + {% endblock %} -{# ── 아사나식 프로젝트/서브프로젝트 재귀 트리 ── #} -{% macro proj_tree(nodes, active_id, can_edit, depth) %} - -{% endmacro %} - {% block topbar_actions %} notifications @@ -71,8 +35,50 @@ {% endif %} - {% if tree %} - {{ proj_tree(tree, project.id, (is_admin or can_manage), 0) }} + {% if nav_projects %} + {% else %}
프로젝트 없음
{% endif %} @@ -284,5 +290,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index d1e61c3..eb6e0db 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -85,6 +85,8 @@ .pj-tree-acts { display: none; gap: 0; flex: 0 0 auto; } .pj-tree-row:hover .pj-tree-acts { display: inline-flex; } .pj-tree-acts .pj-icon-btn { width: 22px; height: 22px; font-size: 14px; } +.pj-tree-done { text-decoration: line-through; color: #9aa1a9; } +.pj-tree-task .pj-tree-name { font-size: 13px; color: #444; } /* 구글 머티리얼 심볼 아이콘(담당자) */ .material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; line-height: 1; vertical-align: middle; } diff --git a/app/static/project.js b/app/static/project.js index 889acac..ff12b57 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -637,23 +637,11 @@ } })(); - // 서브프로젝트 추가 (트리 각 노드) - 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; + 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/"; @@ -720,6 +708,13 @@ // 최초 렌더 — 달력은 서버 렌더, 클릭 핸들러만 연결 wireCalendar(); + + // 사이드 트리에서 업무 클릭(?task=ID)으로 들어오면 해당 업무 모달 자동 열기 + const taskParam = new URLSearchParams(location.search).get("task"); + if (taskParam) { + const t = tasks.find(function (x) { return String(x.id) === String(taskParam); }); + if (t) openTaskModal(t); + } } function idOf(email) {