From 3e39969c9ae5d180834290bfd75f613b0d9f4f1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A7=80=EC=9B=85?= Date: Fri, 10 Jul 2026 12:26:34 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EC=A7=84=ED=96=89=EB=A5=A0=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[workspaceId]/progress-chart/page.tsx | 15 ++ src/entities/progress-chart/index.ts | 5 + .../model/progress-chart.types.ts | 20 ++ src/features/manage-progress-chart/index.ts | 1 + .../model/progress-chart.ts | 80 +++++++ .../ui/ProgressChartView.tsx | 223 ++++++++++++++++++ src/views/progress-chart/index.ts | 1 + .../progress-chart/ui/ProgressChartPage.tsx | 14 ++ 8 files changed, 359 insertions(+) create mode 100644 src/app/workspaces/[workspaceId]/progress-chart/page.tsx create mode 100644 src/entities/progress-chart/index.ts create mode 100644 src/entities/progress-chart/model/progress-chart.types.ts create mode 100644 src/features/manage-progress-chart/index.ts create mode 100644 src/features/manage-progress-chart/model/progress-chart.ts create mode 100644 src/features/manage-progress-chart/ui/ProgressChartView.tsx create mode 100644 src/views/progress-chart/index.ts create mode 100644 src/views/progress-chart/ui/ProgressChartPage.tsx diff --git a/src/app/workspaces/[workspaceId]/progress-chart/page.tsx b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx new file mode 100644 index 0000000..046bc65 --- /dev/null +++ b/src/app/workspaces/[workspaceId]/progress-chart/page.tsx @@ -0,0 +1,15 @@ +import { ProgressChartPage } from '@/views/progress-chart'; + +interface WorkspaceProgressChartPageProps { + params: Promise<{ + workspaceId: string; + }>; +} + +export default async function WorkspaceProgressChartPage({ + params, +}: WorkspaceProgressChartPageProps) { + const { workspaceId } = await params; + + return ; +} diff --git a/src/entities/progress-chart/index.ts b/src/entities/progress-chart/index.ts new file mode 100644 index 0000000..5d441d3 --- /dev/null +++ b/src/entities/progress-chart/index.ts @@ -0,0 +1,5 @@ +export type { + ProgressChartSummary, + ProgressChartAssigneeItem, + ProgressChartStatusItem, +} from './model/progress-chart.types'; diff --git a/src/entities/progress-chart/model/progress-chart.types.ts b/src/entities/progress-chart/model/progress-chart.types.ts new file mode 100644 index 0000000..cd2946c --- /dev/null +++ b/src/entities/progress-chart/model/progress-chart.types.ts @@ -0,0 +1,20 @@ +export interface ProgressChartStatusItem { + id: 'done' | 'in-progress' | 'todo'; + label: string; + count: number; + color: string; +} + +export interface ProgressChartAssigneeItem { + name: string; + count: number; +} + +export interface ProgressChartSummary { + totalTaskCount: number; + doneTaskCount: number; + inProgressTaskCount: number; + overallProgressRate: number; + assigneeItems: ProgressChartAssigneeItem[]; + statusItems: ProgressChartStatusItem[]; +} diff --git a/src/features/manage-progress-chart/index.ts b/src/features/manage-progress-chart/index.ts new file mode 100644 index 0000000..4f4b7bc --- /dev/null +++ b/src/features/manage-progress-chart/index.ts @@ -0,0 +1 @@ +export { ProgressChartView } from './ui/ProgressChartView'; diff --git a/src/features/manage-progress-chart/model/progress-chart.ts b/src/features/manage-progress-chart/model/progress-chart.ts new file mode 100644 index 0000000..1c90bd6 --- /dev/null +++ b/src/features/manage-progress-chart/model/progress-chart.ts @@ -0,0 +1,80 @@ +import type { Task, TaskStatus } from '@/entities/task'; +import type { + ProgressChartAssigneeItem, + ProgressChartStatusItem, + ProgressChartSummary, +} from '@/entities/progress-chart'; + +// 진행률 차트는 별도 목업 숫자를 두지 않고 프로젝트 관리 task 목록에서 직접 파생한다. +const STATUS_META: Record> = { + done: { + label: '완료', + color: '#574BEA', + }, + 'in-progress': { + label: '진행 중', + color: '#7D6AF3', + }, + todo: { + label: '대기', + color: '#DED9FF', + }, +}; + +function toPercentage(value: number, total: number) { + if (total === 0) { + return 0; + } + + return Math.round((value / total) * 100); +} + +function countByStatus(tasks: Task[], status: TaskStatus) { + return tasks.filter((task) => task.status === status).length; +} + +export function createProgressChartSummary(tasks: Task[]): ProgressChartSummary { + const totalTaskCount = tasks.length; + const doneTaskCount = countByStatus(tasks, 'done'); + const inProgressTaskCount = countByStatus(tasks, 'in-progress'); + const overallProgressRate = toPercentage(doneTaskCount, totalTaskCount); + + // 담당자별 막대 차트는 "담당자가 가진 전체 업무 수"를 기준으로 집계한다. + const assigneeMap = new Map(); + tasks.forEach((task) => { + const current = assigneeMap.get(task.assignee) ?? { + name: task.assignee, + count: 0, + }; + + current.count += 1; + assigneeMap.set(task.assignee, current); + }); + + const assigneeItems = [...assigneeMap.values()].sort((left, right) => { + if (right.count !== left.count) { + return right.count - left.count; + } + + return left.name.localeCompare(right.name, 'ko'); + }); + + // 도넛 차트 범례도 task status 집계값을 그대로 사용한다. + const statusItems: ProgressChartStatusItem[] = ( + Object.entries(STATUS_META) as Array<[TaskStatus, (typeof STATUS_META)[TaskStatus]]> + ).map(([status, meta]) => ({ + id: status, + label: meta.label, + count: countByStatus(tasks, status), + color: meta.color, + })); + + return { + totalTaskCount, + doneTaskCount, + inProgressTaskCount, + overallProgressRate, + assigneeItems, + statusItems, + }; +} diff --git a/src/features/manage-progress-chart/ui/ProgressChartView.tsx b/src/features/manage-progress-chart/ui/ProgressChartView.tsx new file mode 100644 index 0000000..edd8048 --- /dev/null +++ b/src/features/manage-progress-chart/ui/ProgressChartView.tsx @@ -0,0 +1,223 @@ +'use client'; + +import type { ProgressChartAssigneeItem, ProgressChartStatusItem } from '@/entities/progress-chart'; +import { getMockTasksByWorkspaceId } from '@/entities/task'; +import { cn } from '@/shared/lib/utils'; +import { createProgressChartSummary } from '../model/progress-chart'; + +interface ProgressChartViewProps { + workspaceId: string; +} + +function SummaryNumberCard({ + value, + label, + valueClassName, +}: { + value: number; + label: string; + valueClassName?: string; +}) { + return ( +
+
+ + {value} + +

+ {label} +

+
+
+ ); +} + +function OverallProgressCard({ + progress, + doneCount, + totalCount, +}: { + progress: number; + doneCount: number; + totalCount: number; +}) { + const safeProgress = Math.max(progress, 0); + + return ( +
+

전체 진행률

+

+ {doneCount} / {totalCount} 업무 완료 +

+ +
+
+ + {safeProgress}% + +
+
+
+ ); +} + +function AssigneeBarChartCard({ items }: { items: ProgressChartAssigneeItem[] }) { + const maxValue = Math.max(...items.map((item) => item.count), 0); + const gridValues = [0, 2, 4, 6, 8]; + + return ( +
+

+ 담당자별 업무 현황 +

+ +
+
+ {gridValues + .slice() + .reverse() + .map((value) => ( + {value} + ))} +
+ +
+
+ {gridValues + .slice(1) + .reverse() + .map((value) => ( +
+ ))} +
+
+ +
+ {items.map((item) => ( +
+
+ + {item.name} + +
+ ))} +
+
+
+
+ ); +} + +function StatusDistributionCard({ items }: { items: ProgressChartStatusItem[] }) { + const total = items.reduce((sum, item) => sum + item.count, 0); + const segments = items + .map((item, index) => { + const start = items + .slice(0, index) + .reduce((sum, current) => sum + (total === 0 ? 0 : (current.count / total) * 100), 0); + const end = start + (total === 0 ? 0 : (item.count / total) * 100); + + return `${item.color} ${start}% ${end}%`; + }) + .join(', '); + + return ( +
+

상태 분포

+ +
+
+
+
+ +
+ {items.map((item) => ( +
+
+ ))} +
+
+
+ ); +} + +export function ProgressChartView({ workspaceId }: ProgressChartViewProps) { + const tasks = getMockTasksByWorkspaceId(workspaceId); + const summary = createProgressChartSummary(tasks); + + return ( +
+
+

+ 진행률 차트 +

+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ ); +} diff --git a/src/views/progress-chart/index.ts b/src/views/progress-chart/index.ts new file mode 100644 index 0000000..370b669 --- /dev/null +++ b/src/views/progress-chart/index.ts @@ -0,0 +1 @@ +export { default as ProgressChartPage } from './ui/ProgressChartPage'; diff --git a/src/views/progress-chart/ui/ProgressChartPage.tsx b/src/views/progress-chart/ui/ProgressChartPage.tsx new file mode 100644 index 0000000..aeaab95 --- /dev/null +++ b/src/views/progress-chart/ui/ProgressChartPage.tsx @@ -0,0 +1,14 @@ +import { ProgressChartView } from '@/features/manage-progress-chart'; +import { plusJakartaSans } from '@/shared/lib/fonts'; + +interface ProgressChartPageProps { + workspaceId: string; +} + +export default function ProgressChartPage({ workspaceId }: ProgressChartPageProps) { + return ( +
+ +
+ ); +}