From cc73f79b7c82096ac29c7cb9550a99a65ab35884 Mon Sep 17 00:00:00 2001 From: king Date: Thu, 17 Sep 2026 14:30:34 +0900 Subject: [PATCH] =?UTF-8?q?feat(project):=20=EB=82=B4=20=EC=97=85=EB=AC=B4?= =?UTF-8?q?=20=ED=8C=A8=EB=84=90=EC=97=90=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EB=B3=84=20=EC=A0=84=EC=B2=B4/=EC=A7=84=ED=96=89/?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20=EA=B0=9C=EC=88=98=20=EB=B0=B0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로젝트 이름 오른쪽에 "전체5 진행2 완료3" 형식으로 표시, 숫자는 색깔 있는 동그라미 안에(전체=회색, 진행=파랑, 완료=초록). 서버 렌더용 카운트는 _group_my_tasks() 헬퍼로 통합(홈/프로젝트 화면 중복 제거), 실시간 갱신은 DOM의 실제
  • 개수를 다시 세는 updateMyTreeCounts()를 기존 sync 함수 (syncMyTaskPanel/syncProjectMyTaskPanel) 안에서 호출해 처리. Co-Authored-By: Claude Sonnet 5 --- app/modules/project/router.py | 57 +++++++++---------- .../project/templates/project/inbox.html | 4 +- .../project/templates/project/index.html | 9 ++- .../project/templates/project/project.html | 9 ++- app/static/project.css | 11 ++++ app/static/project.js | 36 ++++++++++-- 6 files changed, 87 insertions(+), 39 deletions(-) diff --git a/app/modules/project/router.py b/app/modules/project/router.py index 0a5075e..3dbb671 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -205,6 +205,31 @@ def _fmt_date_kr(d: str | None) -> str: return dd.strftime("%m/%d") + f"({_WEEKDAYS_KR[dd.weekday()]})" +def _group_my_tasks(my_tasks: list[dict[str, Any]]) -> list[dict[str, Any]]: + """담당 업무를 프로젝트별로 묶고, "내 업무" 패널에 표시할 전체/진행/완료 + 개수를 함께 센다(홈 화면·프로젝트 화면 양쪽에서 재사용).""" + groups: dict[int, dict[str, Any]] = {} + for t in my_tasks: + pid = t.get("project_id") + g = groups.get(pid) + if g is None: + g = { + "id": pid, + "name": t.get("project_name") or "프로젝트", + "color": t.get("project_color") or "#4573d2", + "tasks": [], + } + groups[pid] = g + g["tasks"].append(t) + for g in groups.values(): + total = len(g["tasks"]) + done = sum(1 for t in g["tasks"] if t.get("completed_at")) + g["total"] = total + g["done"] = done + g["open"] = total - done + return list(groups.values()) + + def _build_tree(projects: list[dict[str, Any]]) -> list[dict[str, Any]]: """평면 프로젝트 목록 → parent_id 기준 트리(children 키).""" by_id: dict[int, dict[str, Any]] = {} @@ -426,21 +451,8 @@ async def index(request: Request) -> HTMLResponse: for t in my_tasks: t["start_label"] = _fmt_date_kr(t.get("start_date")) t["due_label"] = _fmt_date_kr(t.get("due_date")) - # 내 업무를 프로젝트별로 묶어 트리로 표시 - my_groups: dict[int, dict[str, Any]] = {} - for t in my_tasks: - pid = t.get("project_id") - g = my_groups.get(pid) - if g is None: - g = { - "id": pid, - "name": t.get("project_name") or "프로젝트", - "color": t.get("project_color") or "#4573d2", - "tasks": [], - } - my_groups[pid] = g - g["tasks"].append(t) - my_projects = list(my_groups.values()) + # 내 업무를 프로젝트별로 묶어 트리로 표시(전체/진행/완료 개수 포함) + my_projects = _group_my_tasks(my_tasks) # 홈 화면 카드/내업무에 노출된 업무 전체(id→업무) — 클릭 시 페이지 이동 없이 # 팝업으로 편집하기 위해 필요한 원본 데이터를 그대로 클라이언트에 넘긴다. @@ -567,20 +579,7 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse: for t in my_tasks: t["start_label"] = _fmt_date_kr(t.get("start_date")) t["due_label"] = _fmt_date_kr(t.get("due_date")) - my_groups: dict[int, dict[str, Any]] = {} - for t in my_tasks: - pid = t.get("project_id") - g = my_groups.get(pid) - if g is None: - g = { - "id": pid, - "name": t.get("project_name") or "프로젝트", - "color": t.get("project_color") or "#4573d2", - "tasks": [], - } - my_groups[pid] = g - g["tasks"].append(t) - my_projects = list(my_groups.values()) + my_projects = _group_my_tasks(my_tasks) # 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달 today = today_kst() diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 07f83b1..7715b85 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 eb4ea4d..2948e8a 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 %} @@ -165,6 +165,11 @@ {{ p.name }} + + 전체{{ p.total }} + 진행{{ p.open }} + 완료{{ p.done }} +
      {% for t in p.tasks %} @@ -424,5 +429,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 90affae..bd73555 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 %} - + @@ -207,6 +207,11 @@ {{ p.name }} + + 전체{{ p.total }} + 진행{{ p.open }} + 완료{{ p.done }} +
        {% for t in p.tasks %} @@ -538,5 +543,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index 34b72df..1b5b297 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -408,6 +408,17 @@ .pj-mytree-toggle:hover { background: #f1f2f4; } .pj-mytree-proj { display: flex; align-items: center; gap: 7px; text-decoration: none; color: #1e1f21; font-weight: 700; font-size: 13.5px; padding: 4px 2px; border-radius: 7px; flex: 1 1 auto; min-width: 0; } .pj-mytree-proj:hover { background: #f6f7f8; } +/* 프로젝트 이름 오른쪽 — 전체/진행/완료 개수(숫자는 색깔 있는 동그라미 안에) */ +.pj-mytree-counts { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; padding-right: 4px; } +.pj-mtc { display: inline-flex; align-items: center; gap: 3px; font-size: 10.5px; color: #9aa1a9; font-weight: 600; } +.pj-mtc-num { + display: inline-flex; align-items: center; justify-content: center; + min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; + font-size: 10px; font-weight: 800; color: #fff; +} +.pj-mtc-num.pj-mtc-total { background: #8b93a0; } /* 전체 = 중립 회색 */ +.pj-mtc-num.pj-mtc-open { background: #4573d2; } /* 진행 = 파랑 */ +.pj-mtc-num.pj-mtc-done { background: #37a36c; } /* 완료 = 초록 */ .pj-mytree-tasks { list-style: none; margin: 2px 0 0; padding: 0 0 0 8px; border-left: 2px solid #eef0f2; margin-left: 10px; } .pj-mytree-group.is-collapsed .pj-mytree-tasks { display: none; } .pj-mytree-task a { display: flex; align-items: flex-start; gap: 7px; padding: 6px 6px; border-radius: 7px; text-decoration: none; color: inherit; } diff --git a/app/static/project.js b/app/static/project.js index 35888bd..676cbaf 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -157,6 +157,22 @@ return "rgba(" + r + "," + g + "," + b + "," + alpha + ")"; } + // "내 업무" 그룹 헤더의 전체/진행/완료 개수 배지 — 그 그룹 안의 실제 + //
      • 개수를 다시 세어 채운다(서버 재조회 없이, DOM 이 곧 정답). + function updateMyTreeCounts(groupEl) { + if (!groupEl) return; + const items = groupEl.querySelectorAll(".pj-mytree-tasks > .pj-mytree-task"); + let done = 0; + items.forEach(function (li) { if (li.classList.contains("is-done")) done++; }); + const total = items.length; + const totalEl = groupEl.querySelector(".pj-mtc-total"); + const openEl = groupEl.querySelector(".pj-mtc-open"); + const doneEl = groupEl.querySelector(".pj-mtc-done"); + if (totalEl) totalEl.textContent = total; + if (openEl) openEl.textContent = total - done; + if (doneEl) doneEl.textContent = done; + } + // 업무 모달(#pj-modal-task) 크기 — 기존 업무 편집은 하위업무/첨부/연결/ // 댓글까지 보여야 해서 800x1100 고정, "새 업무" 작성은 그 칸들이 전부 // 숨는 홑겹 폼이라 480x700 로 작게(홈 카드·프로젝트 화면 양쪽에서 공용). @@ -816,12 +832,17 @@ // "내 업무" 패널(우측) 동기화 — 이 프로젝트에 내 업무 그룹이 이미 있을 // 때만 patch 한다(그룹 자체가 없던 경우는 드물어 다음 새로고침까지 생략). function syncMyTaskPanel(t, removed) { - const group = document.querySelector('.pj-mytree-group[data-group-id="' + t.project_id + '"] .pj-mytree-tasks'); + const groupEl = document.querySelector('.pj-mytree-group[data-group-id="' + t.project_id + '"]'); + const group = groupEl ? groupEl.querySelector(".pj-mytree-tasks") : null; if (!group) return; const isMine = !removed && t.assignee_email && String(t.assignee_email).toLowerCase() === me; const existingLink = group.querySelector('.pj-task-popup-link[data-task-id="' + t.id + '"]'); const existingLi = existingLink ? existingLink.closest("li") : null; - if (!isMine) { if (existingLi) existingLi.remove(); return; } + if (!isMine) { + if (existingLi) existingLi.remove(); + updateMyTreeCounts(groupEl); + return; + } const dates = t.start_date || t.due_date ? '' + (t.start_date && t.due_date ? fmtDateKr(t.start_date) + " ~ " + fmtDateKr(t.due_date) @@ -844,6 +865,7 @@ wireHomeTaskPopupLink(newLi.querySelector(".pj-task-popup-link"), t); if (cmt) cmt.wireBadge(newLi.querySelector(".pj-cmt-badge")); if (existingLi) existingLi.replaceWith(newLi); else group.appendChild(newLi); + updateMyTreeCounts(groupEl); } // ── 완료 프로젝트 섹션 접기/펼치기 (localStorage 기억) ── @@ -1431,17 +1453,23 @@ "
      • "; } function syncProjectMyTaskPanel(t, removed) { - const group = document.querySelector('.pj-project-mytasks .pj-mytree-group[data-group-id="' + t.project_id + '"] .pj-mytree-tasks'); + const groupEl = document.querySelector('.pj-project-mytasks .pj-mytree-group[data-group-id="' + t.project_id + '"]'); + const group = groupEl ? groupEl.querySelector(".pj-mytree-tasks") : null; if (!group) return; // 이 프로젝트에 내 업무가 하나도 없었으면 다음 새로고침까지 생략. const existingLink = group.querySelector('.pj-task-popup-link[data-task-id="' + t.id + '"]'); const existingLi = existingLink ? existingLink.closest("li") : null; - if (removed) { if (existingLi) existingLi.remove(); return; } + if (removed) { + if (existingLi) existingLi.remove(); + updateMyTreeCounts(groupEl); + return; + } const wrap = document.createElement("div"); wrap.innerHTML = myTaskRowHtml(t); const newLi = wrap.firstElementChild; wireMyTaskPopupLink(newLi.querySelector(".pj-task-popup-link")); if (cmt) cmt.wireBadge(newLi.querySelector(".pj-cmt-badge")); if (existingLi) existingLi.replaceWith(newLi); else group.appendChild(newLi); + updateMyTreeCounts(groupEl); } // 내 업무 항목 우클릭 메뉴 — 열기(프로젝트 페이지로 이동)/업무 추가/수정 // (팝업 편집)/완료토글/삭제. "업무 추가"는 현재 열려 있는 프로젝트로만