feat(project): 프로젝트 화면 좌측메뉴 강제접힘 + 우측 내업무 패널 + 보드 가로스크롤 격리

- 프로젝트 관리(index/project/inbox) 진입 시 메인 좌측 사이드바를 전역 설정과
  무관하게 항상 접어서 렌더(force_sidebar_collapsed) — 서버에서 바로
  data-collapsed=true 로 그려 깜빡임도 없다.
- 프로젝트 상세 화면(보드/리스트/달력/타임라인) 오른쪽에 홈 화면과 동일한
  "내 업무" 패널 추가(프로젝트별 그룹, 클릭 시 팝업 편집).
- 보드에 세션이 늘어나면 .pj-board 안에서만 가로 스크롤돼야 하는데
  .pj-main 에 min-width:0 이 없어 grid 트랙 자체가 넓어져 페이지 전체가
  가로 스크롤되던 문제 수정.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:13:49 +09:00
parent 5c72e66305
commit fc2ae9d1a3
7 changed files with 105 additions and 10 deletions
+25
View File
@@ -457,6 +457,7 @@ async def index(request: Request) -> HTMLResponse:
"nav_items": build_erp_nav(user, active="project"), "nav_items": build_erp_nav(user, active="project"),
"page_title": "프로젝트 관리", "page_title": "프로젝트 관리",
"page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.", "page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.",
"force_sidebar_collapsed": True,
"active_projects": active_projects, "active_projects": active_projects,
"done_projects": done_projects, "done_projects": done_projects,
"my_projects": my_projects, "my_projects": my_projects,
@@ -552,6 +553,26 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse:
if u.get("picture") if u.get("picture")
} }
# 우측 "내 업무" 패널 — 홈 화면과 동일한 쿼리/그룹핑(프로젝트별)을 재사용.
my_tasks = st.list_tasks(assignee_email=user["email"], limit=500)
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())
# 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달 # 달력(휴가식 월간 그리드) — ?y=&m= 로 월 이동, 기본 이번 달
today = today_kst() today = today_kst()
try: try:
@@ -575,8 +596,11 @@ async def project_page(request: Request, project_id: int) -> HTMLResponse:
"nav_items": build_erp_nav(user, active="project"), "nav_items": build_erp_nav(user, active="project"),
"page_title": project["name"], "page_title": project["name"],
"page_subtitle": project.get("description") or "프로젝트 보드", "page_subtitle": project.get("description") or "프로젝트 보드",
"force_sidebar_collapsed": True,
"project": project, "project": project,
"current_project_id": project_id, "current_project_id": project_id,
"my_projects": my_projects,
"my_tasks": my_tasks,
"nav_projects": nav_projects, "nav_projects": nav_projects,
"stages": stages, "stages": stages,
"tasks": all_tasks, "tasks": all_tasks,
@@ -1331,6 +1355,7 @@ async def inbox_page(request: Request) -> HTMLResponse:
"nav_items": build_erp_nav(user, active="project"), "nav_items": build_erp_nav(user, active="project"),
"page_title": "알림센터", "page_title": "알림센터",
"page_subtitle": "내게 온 프로젝트 알림", "page_subtitle": "내게 온 프로젝트 알림",
"force_sidebar_collapsed": True,
"notifications": items, "notifications": items,
}, },
) )
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916f" /> <link rel="stylesheet" href="/static/project.css?v=20260916g" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %} {% endblock %}
@@ -45,5 +45,5 @@
{% endif %} {% endif %}
</div> </div>
<script src="/static/project.js?v=20260916g" defer></script> <script src="/static/project.js?v=20260916h" defer></script>
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916f" /> <link rel="stylesheet" href="/static/project.css?v=20260916g" />
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
{% endblock %} {% endblock %}
@@ -376,5 +376,5 @@
<script> <script>
window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }}; window.PJ_COLORS = {{ ["#4573d2","#37a3a3","#62a420","#e8a33d","#e8384f","#aa62e3","#f06a6a","#5a6772"] | tojson }};
</script> </script>
<script src="/static/project.js?v=20260916g" defer></script> <script src="/static/project.js?v=20260916h" defer></script>
{% endblock %} {% endblock %}
@@ -1,7 +1,7 @@
{% extends "erp_base.html" %} {% extends "erp_base.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/project.css?v=20260916f" /> <link rel="stylesheet" href="/static/project.css?v=20260916g" />
<!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host --> <!-- 구글 머티리얼 심볼(담당자 아이콘 등) — self-host -->
<link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" /> <link rel="stylesheet" href="/static/vendor/material-symbols/material-symbols.css" />
<!-- 타임라인 vis-timeline — self-host --> <!-- 타임라인 vis-timeline — self-host -->
@@ -152,6 +152,50 @@
</table> </table>
</div> </div>
</section> </section>
<!-- ── 우측: 내 업무 (홈 화면과 동일) ── -->
<aside class="pj-home-side pj-project-mytasks">
<div class="pj-section-head"><h2>내 업무</h2></div>
{% if not my_projects %}
<div class="pj-empty pj-empty-sm">배정된 업무가 없습니다.</div>
{% else %}
<div class="pj-mytree">
{% for p in my_projects %}
<div class="pj-mytree-group">
<a class="pj-mytree-proj" href="/project/p/{{ p.id }}">
<span class="material-symbols-outlined" style="color: {{ p.color }}; font-size:18px;">folder</span>
<span class="pj-mytree-proj-name">{{ p.name }}</span>
</a>
<ul class="pj-mytree-tasks">
{% for t in p.tasks %}
<li class="pj-mytree-task {% if t.completed_at %}is-done{% endif %}">
<a href="/project/p/{{ t.project_id }}?task={{ t.id }}" data-task-id="{{ t.id }}" class="pj-task-popup-link">
<span class="pj-mytree-lead">
{% if t.comment_count %}
<span class="pj-cmt-badge" data-task-id="{{ t.id }}" data-count="{{ t.comment_count }}" title="댓글 {{ t.comment_count }}개">
<span class="pj-cmt-ic">💬</span><span class="pj-cmt-n">{{ t.comment_count }}</span>
</span>
{% endif %}
</span>
<span class="pj-mytree-body">
<span class="pj-mytree-title">{{ t.title }}</span>
{% if t.start_date or t.due_date %}
<span class="pj-mytree-dates">
{% if t.start_date and t.due_date %}{{ t.start_label }} ~ {{ t.due_label }}
{% elif t.due_date %}~ {{ t.due_label }}
{% else %}{{ t.start_label }} ~{% endif %}
</span>
{% endif %}
</span>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
{% endif %}
</aside>
</div> </div>
</div> </div>
@@ -324,10 +368,11 @@
"projects": {{ projects_meta | tojson }}, "projects": {{ projects_meta | tojson }},
"stagesByProject": {{ stages_by_project | tojson }}, "stagesByProject": {{ stages_by_project | tojson }},
"boardStages": {{ board_stage_names | tojson }}, "boardStages": {{ board_stage_names | tojson }},
"avatars": {{ avatars | tojson }} "avatars": {{ avatars | tojson }},
"myTasks": {{ my_tasks | tojson }}
} }
</script> </script>
<script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script> <script src="/static/vendor/vis-timeline/vis-timeline-graph2d.min.js"></script>
<script src="/static/project.js?v=20260916g" defer></script> <script src="/static/project.js?v=20260916h" defer></script>
{% endblock %} {% endblock %}
+11 -2
View File
@@ -126,8 +126,17 @@
.pj-mytask-title { font-size: 13.5px; font-weight: 600; } .pj-mytask-title { font-size: 13.5px; font-weight: 600; }
.pj-mytask-meta { margin-left: auto; font-size: 11px; color: #9aa1a9; } .pj-mytask-meta { margin-left: auto; font-size: 11px; color: #9aa1a9; }
/* ── 프로젝트 페이지 레이아웃 ── */ /* ── 프로젝트 페이지 레이아웃 (좌: 프로젝트 트리 / 가운데: 뷰 / 우: 내 업무) ──
.pj-layout { display: grid; grid-template-columns: 230px 1fr; gap: 22px; align-items: start; } 가운데 칸에 min-width:0 이 없으면 보드에 세션이 많이 늘어날 때 grid 트랙
자체가 넓어져 페이지 전체가 가로 스크롤된다(overflow-x:auto 는 .pj-board
안에서만 작동해야 한다) — 반드시 필요한 규칙. */
.pj-layout { display: grid; grid-template-columns: 230px 1fr 260px; gap: 22px; align-items: start; }
.pj-main { min-width: 0; }
.pj-project-mytasks { min-width: 0; }
@media (max-width: 1280px) {
.pj-layout { grid-template-columns: 230px 1fr; }
.pj-project-mytasks { display: none; }
}
@media (max-width: 900px) { .pj-layout { grid-template-columns: 1fr; } } @media (max-width: 900px) { .pj-layout { grid-template-columns: 1fr; } }
.pj-side-block { margin-bottom: 22px; } .pj-side-block { margin-bottom: 22px; }
+11
View File
@@ -614,6 +614,17 @@
const stagesByProject = data.stagesByProject || {}; const stagesByProject = data.stagesByProject || {};
const boardStages = data.boardStages || []; const boardStages = data.boardStages || [];
const avatars = data.avatars || {}; const avatars = data.avatars || {};
const myTasksById = {};
(data.myTasks || []).forEach(function (t) { myTasksById[t.id] = t; });
// 우측 "내 업무" 패널 — openTaskModal 은 이 함수 뒤쪽에서 선언되지만
// function 선언은 호이스팅되어 여기서 참조해도 문제없다.
document.querySelectorAll(".pj-project-mytasks .pj-task-popup-link").forEach(function (a) {
a.addEventListener("click", function (e) {
e.preventDefault();
const t = myTasksById[a.dataset.taskId];
if (t) openTaskModal(t);
});
});
// 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘. // 담당자/작성자 아이콘 = 구글 프로필 이미지(원형). 없으면 기본 아이콘.
function avatarHtml(email, cls) { function avatarHtml(email, cls) {
const url = email ? avatars[String(email).toLowerCase()] : ""; const url = email ? avatars[String(email).toLowerCase()] : "";
+6 -1
View File
@@ -15,7 +15,11 @@
<div class="erp-app"> <div class="erp-app">
<!-- ── 좌측 사이드바 ── --> <!-- ── 좌측 사이드바 ── -->
<aside class="erp-sidebar" data-collapsed="false"> <!-- force_sidebar_collapsed: 프로젝트 관리처럼 자체 좌측 패널(프로젝트
트리)을 가진 화면은 전역 접힘 설정과 무관하게 항상 접어서 시작한다
(서버에서 바로 true 로 렌더 — 깜빡임 자체가 없다). -->
<aside class="erp-sidebar" data-collapsed="{{ 'true' if force_sidebar_collapsed else 'false' }}">
{% if not force_sidebar_collapsed %}
<script> <script>
// 접힘 상태를 렌더 직후(첫 페인트 전) 바로 반영 — 아래쪽 DOMContentLoaded // 접힘 상태를 렌더 직후(첫 페인트 전) 바로 반영 — 아래쪽 DOMContentLoaded
// 스크립트에서 뒤늦게 바꾸면 "펼쳐졌다가 접히는" 깜빡임이 보인다. // 스크립트에서 뒤늦게 바꾸면 "펼쳐졌다가 접히는" 깜빡임이 보인다.
@@ -25,6 +29,7 @@
} }
} catch (_) {} } catch (_) {}
</script> </script>
{% endif %}
<div class="erp-sidebar-head"> <div class="erp-sidebar-head">
<a href="/" class="erp-sidebar-brand"> <a href="/" class="erp-sidebar-brand">