From 4a2e20520e60d595aeef15f6801e5036e32ce58b Mon Sep 17 00:00:00 2001 From: king Date: Thu, 17 Sep 2026 14:48:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=ED=99=88=20=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=EB=8F=84=20=EC=99=84=EB=A3=8C=20=EC=B2=B4=ED=81=AC=20=EC=8B=9C?= =?UTF-8?q?=20=EB=B0=98=ED=88=AC=EB=AA=85=20+=20=EB=8C=80=EC=8B=9C?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=81=9D=EC=9C=BC=EB=A1=9C(=EC=84=B8?= =?UTF-8?q?=EC=85=98=EA=B3=BC=20=EB=8F=99=EC=9D=BC=20=EC=9B=90=EC=B9=99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존엔 완료 프로젝트가 "완료된 프로젝트" 별도 섹션(숨기기 토글 포함)으로 빠졌는데, 요청에 따라 세션 완료 처리와 같은 방식으로 바꿈 — 같은 그리드 안에서 완료 여부 → sort_order → id 순으로 안정 정렬해 완료면 맨 끝으로, 해제하면 sort_order 기준 원래 자리로 자동 복귀(resortProjectGrid, 서버 쪽도 index() 에서 같은 기준으로 미리 정렬해 내려줌: _group 아님, 별도 정렬 한 줄 sorted(tree, key=...)). 완료 섹션/숨기기 토글 관련 죽은 코드(JS·CSS·템플릿) 정리. Co-Authored-By: Claude Sonnet 5 --- app/modules/project/router.py | 11 +++-- .../project/templates/project/inbox.html | 4 +- .../project/templates/project/index.html | 29 ++++------- .../project/templates/project/project.html | 4 +- app/static/project.css | 2 - app/static/project.js | 48 ++++++++----------- 6 files changed, 41 insertions(+), 57 deletions(-) diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 3dbb671..9dda316 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -389,9 +389,11 @@ async def index(request: Request) -> HTMLResponse: member_email=None if admin else user["email"], ) tree = _build_tree(projects) - # 진행중/완료 프로젝트를 구분해 별도 섹션으로 표시. - active_projects = [p for p in tree if p.get("status") != "completed"] - done_projects = [p for p in tree if p.get("status") == "completed"] + # 완료 프로젝트는 별도 섹션 대신, 같은 대시보드 안에서 반투명 + 맨 + # 끝으로만 보낸다(세션 완료 처리와 같은 원칙). sorted() 는 안정 정렬이라 + # tree 가 이미 sort_order 순이면 완료 여부로만 다시 나눠도 각 그룹 + # 내부 순서는 그대로 유지된다 — 완료 해제 시 원래 자리로 자동 복귀. + home_projects = sorted(tree, key=lambda p: p.get("status") == "completed") # 아바타 맵: 이메일(소문자) → 구글 프로필 이미지 URL (홈 카드 업무 담당자 표시용) from app.main import user_store # noqa: WPS433 @@ -473,8 +475,7 @@ async def index(request: Request) -> HTMLResponse: "page_title": "프로젝트 관리", "page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.", "force_sidebar_collapsed": True, - "active_projects": active_projects, - "done_projects": done_projects, + "home_projects": home_projects, "my_projects": my_projects, "avatars": avatars, "home_tasks": home_tasks, diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 6a94e88..59839b8 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 fc5c19e..0255bcd 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 %} @@ -21,7 +21,8 @@ data-color="{{ p.color }}" data-status="{{ p.status or 'active' }}" data-creator="{{ p.created_by or '' }}" - data-has-stages="{{ 'true' if p.has_stages else 'false' }}"> + data-has-stages="{{ 'true' if p.has_stages else 'false' }}" + data-sort-order="{{ p.sort_order or 0 }}">
{{ p.name }} @@ -124,29 +125,19 @@
- {% if not active_projects and not done_projects %} + {% if not home_projects %}
아직 프로젝트가 없습니다. 상단의 새 프로젝트 버튼으로 만들어 보세요.
- {% elif not active_projects %} -
진행중인 프로젝트가 없습니다.
{% endif %} +
- {% for p in active_projects %}{{ project_card(p) }}{% endfor %} + {% for p in home_projects %}{{ project_card(p) }}{% endfor %}
- - {% if done_projects %} -
-

완료된 프로젝트

- -
-
- {% for p in done_projects %}{{ project_card(p) }}{% endfor %} -
- {% endif %} @@ -429,5 +420,5 @@ window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }}; window.PJ_STAGE_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772","#e8398a","#a15c43"] | tojson }}; - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index 86c16ab..bb6dcf7 100644 --- a/app/modules/project/templates/project/project.html +++ b/app/modules/project/templates/project/project.html @@ -1,7 +1,7 @@ {% extends "erp_base.html" %} {% block head_extra %} - + @@ -543,5 +543,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index 1b5b297..cc82cdf 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -140,8 +140,6 @@ .pj-project-tasks-empty { padding: 4px 16px 14px; font-size: 12px; color: #b7bdc5; border-top: 1px solid #f0f1f3; margin-top: 2px; } /* 완료 프로젝트 섹션 헤더 */ -.pj-section-head-done { margin-top: 22px; } -.pj-section-head-done h2 { font-size: 14px; color: #6b7280; } /* 홈/프로젝트 상세 공통 "내 업무" 패널 — 부모 grid 행(1fr)에 맞춰 높이를 확정하고 자기 안에서만 스크롤. 300px 는 padding 포함(border-box). */ diff --git a/app/static/project.js b/app/static/project.js index bacbb74..8a23eb5 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -473,7 +473,7 @@ // ════════════════════════════════════════════════════════════ function initHome() { const grid = el("pj-project-grid"); - if (!grid && !el("pj-toggle-done")) return; // 홈 아님 + if (!grid) return; // 홈 아님 const wrap = document.querySelector(".pj-wrap"); const me = ((wrap && wrap.dataset.me) || "").toLowerCase(); const isSuper = !!(wrap && wrap.dataset.isSuper === "true"); @@ -570,10 +570,24 @@ } } else if (dueChip) dueChip.remove(); } - // 진행중 ↔ 완료 그리드 이동. 완료 그리드가 화면에 아직 없으면(첫 완료 - // 프로젝트) 다음 새로고침까지 원래 위치에 남는다 — 드문 경우라 허용. - const targetGrid = el(p.status === "completed" ? "pj-project-grid-done" : "pj-project-grid"); - if (targetGrid && card.parentElement !== targetGrid) targetGrid.appendChild(card); + // 완료 체크 시 반투명 + 대시보드 맨 끝으로, 해제하면 sort_order 기준 + // 원래 자리로(세션 완료 처리와 같은 원칙 — resortProjectGrid 가 매번 + // "완료 여부 → sort_order → id" 순으로 안정 재배치한다). + resortProjectGrid(); + } + function resortProjectGrid() { + const grid = el("pj-project-grid"); + if (!grid) return; + const cards = Array.prototype.slice.call(grid.querySelectorAll(".pj-project-card")); + cards.sort(function (a, b) { + const da = a.dataset.status === "completed" ? 1 : 0; + const db = b.dataset.status === "completed" ? 1 : 0; + if (da !== db) return da - db; + const oa = Number(a.dataset.sortOrder) || 0, ob = Number(b.dataset.sortOrder) || 0; + if (oa !== ob) return oa - ob; + return Number(a.dataset.id) - Number(b.dataset.id); + }); + cards.forEach(function (c) { grid.appendChild(c); }); } // 새 프로젝트 카드 DOM — project_card(p) 매크로(index.html)를 그대로 미러링. // 방금 만든 프로젝트라 멤버/업무는 아직 없다(멤버는 생성 직후 별도 배정, @@ -586,7 +600,7 @@ ' data-desc="' + escapeHtml(p.description || "") + '"' + ' data-start="' + (p.start_date || "") + '" data-due="' + (p.due_date || "") + '"' + ' data-color="' + color + '" data-status="active" data-creator="' + escapeHtml(p.created_by || "") + '"' + - ' data-has-stages="false">' + + ' data-has-stages="false" data-sort-order="' + (p.sort_order || 0) + '">' + '
' + '
' + escapeHtml(p.name || "") + "
" + '
' + @@ -872,26 +886,6 @@ updateMyTreeCounts(groupEl); } - // ── 완료 프로젝트 섹션 접기/펼치기 (localStorage 기억) ── - // display 를 인라인 스타일로 직접 제어 — 외부 CSS 규칙과의 특이도/우선순위 - // 문제를 원천 차단(어떤 스타일시트보다 인라인 style 이 항상 우선한다). - const toggleBtn = el("pj-toggle-done"); - const doneGrid = el("pj-project-grid-done"); - if (toggleBtn && doneGrid) { - const KEY = "pj_hide_done"; - let hideDone = localStorage.getItem(KEY) === "1"; - function apply() { - doneGrid.style.display = hideDone ? "none" : ""; - const n = toggleBtn.dataset.count; - toggleBtn.textContent = hideDone ? "완료 " + n + "개 보기" : "완료 " + n + "개 숨기기"; - } - apply(); - toggleBtn.addEventListener("click", function () { - hideDone = !hideDone; - try { localStorage.setItem(KEY, hideDone ? "1" : "0"); } catch (_) {} - apply(); - }); - } // ── 새 프로젝트 모달 ── const newBtn = el("pj-new-project"); @@ -1349,7 +1343,7 @@ // 프로젝트 카드 드래그 순서 변경 — 진행중/완료 그리드 각각 안에서만 // 재정렬(그리드 사이 드래그로 옮기면 상태가 바뀐 것처럼 보이니 막는다). function wireProjectGridDrag() { - const grids = [el("pj-project-grid"), el("pj-project-grid-done")].filter(Boolean); + const grids = [el("pj-project-grid")].filter(Boolean); if (!grids.length) return; let dragCard = null, sourceGrid = null; grids.forEach(function (grid) {