diff --git a/app/modules/project/router.py b/app/modules/project/router.py index bff4c55..35b1462 100644 --- a/app/modules/project/router.py +++ b/app/modules/project/router.py @@ -321,6 +321,21 @@ async def index(request: Request) -> HTMLResponse: ) tree = _build_tree(projects) my_tasks = st.list_tasks(assignee_email=user["email"], limit=500) + # 내 업무를 프로젝트별로 묶어 트리로 표시 + 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()) return render_template( request, "project/index.html", @@ -331,7 +346,7 @@ async def index(request: Request) -> HTMLResponse: "page_title": "프로젝트 관리", "page_subtitle": "달력·타임라인·보드로 프로젝트를 관리하세요.", "tree": tree, - "my_tasks": my_tasks, + "my_projects": my_projects, "today": today_kst().isoformat(), }, ) diff --git a/app/modules/project/templates/project/inbox.html b/app/modules/project/templates/project/inbox.html index 9d99532..2b9efa2 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 33a74d8..569b92d 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 %} @@ -54,22 +54,38 @@ @@ -100,5 +116,5 @@ - + {% endblock %} diff --git a/app/modules/project/templates/project/project.html b/app/modules/project/templates/project/project.html index e208cd9..0f4e58e 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 %} - + @@ -297,5 +297,5 @@ - + {% endblock %} diff --git a/app/static/project.css b/app/static/project.css index af3056a..96fb3fb 100644 --- a/app/static/project.css +++ b/app/static/project.css @@ -98,7 +98,18 @@ .pj-toolbar { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; } .pj-view-tabs { display: inline-flex; background: #eef0f2; border-radius: 10px; padding: 3px; gap: 2px; } .pj-tab { border: none; background: transparent; padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; color: #525860; cursor: pointer; } -.pj-tab.is-active { background: #fff; color: #1e1f21; box-shadow: 0 1px 3px rgba(0,0,0,.08); } +.pj-tab.is-active { background: #1e1f21; color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.18); } + +/* 알림 벨 흔들림(미읽음 있을 때) */ +@keyframes pj-bell-ring { + 0%, 50%, 100% { transform: rotate(0); } + 10%, 30% { transform: rotate(14deg); } + 20%, 40% { transform: rotate(-14deg); } +} +.pj-bell.has-unread .material-symbols-outlined { + animation: pj-bell-ring 1.4s ease-in-out infinite; + transform-origin: top center; +} /* ── 보드(칸반) ── */ .pj-board { display: flex; gap: 14px; overflow-x: auto; padding-bottom: 8px; align-items: flex-start; } @@ -120,9 +131,29 @@ /* ── 리스트 ── */ .pj-table { width: 100%; border-collapse: collapse; font-size: 13.5px; } -.pj-table th { text-align: left; padding: 9px 10px; border-bottom: 2px solid #eef0f2; color: #6b7280; font-size: 12px; } +.pj-table th { text-align: left; padding: 9px 10px; border-bottom: 2px solid #eef0f2; color: #1e1f21; font-size: 14px; font-weight: 700; position: relative; } .pj-table td { padding: 10px; border-bottom: 1px solid #f1f2f4; } .pj-table tr.is-done td { color: #9aa1a9; text-decoration: line-through; } +.pj-table-fixed { table-layout: fixed; } +.pj-table-fixed th, .pj-table-fixed td { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.pj-th-label { cursor: pointer; user-select: none; display: inline-flex; align-items: center; gap: 4px; } +.pj-th-label:hover { color: #4573d2; } +.pj-th-sort { font-size: 11px; color: #9aa1a9; } +.pj-th-resize { position: absolute; top: 0; right: -3px; width: 8px; height: 100%; cursor: col-resize; z-index: 2; } +.pj-th-resize:hover { background: linear-gradient(90deg, transparent 40%, #c9d4ea 40%, #c9d4ea 60%, transparent 60%); } + +/* ── 내 업무 트리(홈) ── */ +.pj-mytree-group { margin-bottom: 14px; } +.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; } +.pj-mytree-proj:hover { background: #f6f7f8; } +.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-task a { display: flex; align-items: flex-start; gap: 7px; padding: 6px 6px; border-radius: 7px; text-decoration: none; color: inherit; } +.pj-mytree-task a:hover { background: #f6f7f8; } +.pj-mytree-ic { font-size: 16px; flex: 0 0 auto; margin-top: 1px; } +.pj-mytree-body { display: flex; flex-direction: column; min-width: 0; } +.pj-mytree-title { font-size: 13px; font-weight: 600; } +.pj-mytree-dates { font-size: 11px; color: #9aa1a9; margin-top: 1px; } +.pj-mytree-task.is-done .pj-mytree-title { text-decoration: line-through; color: #9aa1a9; } /* ── 달력 (휴가 모듈과 동일한 월간 그리드 룩) ── */ .pj-cal-head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } diff --git a/app/static/project.js b/app/static/project.js index fa58d9a..5770796 100644 --- a/app/static/project.js +++ b/app/static/project.js @@ -227,6 +227,9 @@ // ── 타임라인 (vis-timeline) ── function renderTimeline() { const node = el("pj-timeline"); + const proj = data.project || {}; + const grp = "P" + projectId; // 프로젝트 행(그룹) id + const projColor = proj.color || "#4573d2"; const items = tasks.filter(function (t) { return t.start_date || t.due_date; }) .map(function (t) { const s = t.start_date || t.due_date; @@ -237,18 +240,36 @@ const isRange = (e !== s) || (!!t.start_time && !!t.due_time && t.start_time !== t.due_time); return { id: t.id, - content: t.title, + group: grp, + content: t.title, // 프로젝트명 없이 업무명만 start: startISO, end: isRange ? endISO : undefined, type: isRange ? "range" : "point", - style: "background-color:" + (t.completed_at ? "#9aa5b1" : (t.project_color || "#4573d2")) + ";color:#fff;border:none;", + style: "background-color:" + (t.completed_at ? "#9aa5b1" : projColor) + ";color:#fff;border:none;", }; }); if (timeline) { timeline.destroy(); timeline = null; } if (!items.length) { node.innerHTML = '