From 5af9834b71a4c188fb9e02d340523d12be27d414 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Tue, 14 Jul 2026 16:52:13 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(dashboard):=20=EC=82=AC=EC=9D=B4?= =?UTF-8?q?=EB=93=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EB=8C=80?= =?UTF-8?q?=EC=8B=9C=EB=B3=B4=EB=93=9C=20=EC=9C=84=EC=A0=AF(=EB=82=B4?= =?UTF-8?q?=EC=97=85=EB=AC=B4,=20=EC=8A=A4=ED=94=84=EB=A6=B0=ED=8A=B8?= =?UTF-8?q?=EC=9A=94=EC=95=BD,=EB=B2=A8=EB=A1=9C=EC=8B=9C=ED=8B=B0,?= =?UTF-8?q?=EB=B0=B1=EB=A1=9C=EA=B7=B8)=20=EC=8B=A4=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[workspaceId]/dashboard/page.tsx | 7 +- src/entities/side-project/sprint/index.ts | 2 +- .../sprint/model/sprint.selectors.ts | 11 ++ src/shared/dashboard/model/widget.types.ts | 6 + src/views/dashboard/config/widget-catalog.tsx | 15 +- src/views/dashboard/ui/DashboardGrid.tsx | 9 +- src/views/dashboard/ui/DashboardView.tsx | 5 + .../dashboard-backlog/ui/Backlog.tsx | 31 +++- .../dashboard-my-tasks/ui/MyTasks.tsx | 170 ++++++++++++++++-- .../ui/SprintSummary.tsx | 27 ++- .../dashboard-velocity/ui/Velocity.tsx | 44 ++++- 11 files changed, 288 insertions(+), 39 deletions(-) diff --git a/src/app/workspaces/[workspaceId]/dashboard/page.tsx b/src/app/workspaces/[workspaceId]/dashboard/page.tsx index 3ec3d1f..4cfaa90 100644 --- a/src/app/workspaces/[workspaceId]/dashboard/page.tsx +++ b/src/app/workspaces/[workspaceId]/dashboard/page.tsx @@ -5,6 +5,7 @@ import { getDashboardLayout } from '@/entities/dashboard-layout/api/get-dashboar import { DashboardView } from '@/views/dashboard'; import { notFound } from 'next/navigation'; import { getWorkspaceById } from '@/entities/workspace/api/get-workspace-by-id'; +import { getCurrentUserId } from '@/shared/api/supabase/current-user'; interface DashboardPageProps { params: Promise<{ workspaceId: string }>; @@ -19,12 +20,16 @@ export default async function DashboardPage({ params }: DashboardPageProps) { notFound(); } - const initialLayout = await getDashboardLayout(workspaceId, 'dashboard'); + const [initialLayout, currentUserId] = await Promise.all([ + getDashboardLayout(workspaceId, 'dashboard'), + getCurrentUserId(), + ]); return ( ); diff --git a/src/entities/side-project/sprint/index.ts b/src/entities/side-project/sprint/index.ts index 2a13f47..f26429a 100644 --- a/src/entities/side-project/sprint/index.ts +++ b/src/entities/side-project/sprint/index.ts @@ -7,7 +7,7 @@ export { sprintsQueryKey, useSprints } from './api/use-sprints'; export { useCreateSprint } from './api/use-create-sprint'; export { useUpdateSprint } from './api/use-update-sprint'; export { useDeleteSprint } from './api/use-delete-sprint'; -export { resolveCurrentSprint, selectVelocity } from './model/sprint.selectors'; +export { resolveCurrentSprint, selectVelocity, selectVelocityMax } from './model/sprint.selectors'; export { toSprint } from './model/sprint.mapper'; export type { SprintRow, SprintRpcRow } from './model/sprint.db.types'; export { sprintInputSchema, type SprintInput } from './model/sprint.schema'; diff --git a/src/entities/side-project/sprint/model/sprint.selectors.ts b/src/entities/side-project/sprint/model/sprint.selectors.ts index 714e3c4..2e1d8aa 100644 --- a/src/entities/side-project/sprint/model/sprint.selectors.ts +++ b/src/entities/side-project/sprint/model/sprint.selectors.ts @@ -15,6 +15,17 @@ export function selectVelocity(sprints: Sprint[]): VelocityPoint[] { })); } +/** + * 벨로시티 차트 Y축 최댓값을 데이터에서 파생한다 — 실 포인트에 맞춰 축을 스케일한다. + * 가장 큰 계획/완료 포인트를 10 단위로 올림하고, 데이터가 없으면 기본 눈금(10)을 쓴다. + * 이렇게 하면 포인트가 상수보다 크면 막대가 잘리고, 작으면 차트가 납작해지는 문제를 막는다. + */ +export function selectVelocityMax(points: VelocityPoint[]): number { + const peak = points.reduce((max, point) => Math.max(max, point.planned, point.completed), 0); + if (peak <= 0) return 10; + return Math.ceil(peak / 10) * 10; +} + export function resolveCurrentSprint(sprints: Sprint[]): Sprint | undefined { const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String( diff --git a/src/shared/dashboard/model/widget.types.ts b/src/shared/dashboard/model/widget.types.ts index 41871a3..6666ae2 100644 --- a/src/shared/dashboard/model/widget.types.ts +++ b/src/shared/dashboard/model/widget.types.ts @@ -4,9 +4,15 @@ import type { ReactNode } from 'react'; import type { LayoutItem } from 'react-grid-layout'; import type { WidgetSize } from '../lib/widget-size'; +import type { WorkspacePurpose } from './template.types'; export interface WidgetRenderContext { + /** 위젯 데이터 조회 스코프 */ workspaceId: string; + /** 워크스페이스 용도 — 같은 위젯이라도 템플릿별로 데이터 소스가 다를 때 분기용 */ + purpose: WorkspacePurpose; + /** 현재 로그인 사용자 id — "내 업무"처럼 본인 기준 필터가 필요한 위젯용 */ + currentUserId: string; } export interface WidgetDefinition { diff --git a/src/views/dashboard/config/widget-catalog.tsx b/src/views/dashboard/config/widget-catalog.tsx index fb46c76..0165bae 100644 --- a/src/views/dashboard/config/widget-catalog.tsx +++ b/src/views/dashboard/config/widget-catalog.tsx @@ -23,22 +23,29 @@ export const WIDGET_CATALOG = { 'sprint-summary': { layout: { i: 'sprint-summary', x: 0, y: 0, w: 12, h: 4, minW: 6, minH: 4 }, title: '스프린트 요약', - render: () => , + render: (_size, { workspaceId }) => , }, 'my-tasks': { layout: { i: 'my-tasks', x: 0, y: 4, w: 6, h: 5, minW: 2, minH: 3 }, title: '내 업무', - render: (size) => , + render: (size, { workspaceId, purpose, currentUserId }) => ( + + ), }, velocity: { layout: { i: 'velocity', x: 6, y: 4, w: 6, h: 5, minW: 4, minH: 4 }, title: '벨로시티', - render: () => , + render: (_size, { workspaceId }) => , }, backlog: { layout: { i: 'backlog', x: 0, y: 9, w: 6, h: 5, minW: 2, minH: 3 }, title: '백로그', - render: (size) => , + render: (size, { workspaceId }) => , }, 'recent-notes': { layout: { i: 'recent-notes', x: 0, y: 14, w: 6, h: 5, minW: 2, minH: 3 }, diff --git a/src/views/dashboard/ui/DashboardGrid.tsx b/src/views/dashboard/ui/DashboardGrid.tsx index 270fa43..004299c 100644 --- a/src/views/dashboard/ui/DashboardGrid.tsx +++ b/src/views/dashboard/ui/DashboardGrid.tsx @@ -14,6 +14,7 @@ import { Maximize2, GripVertical, Trash2 } from 'lucide-react'; import { getWidgetSize } from '@/shared/dashboard/lib/widget-size'; import type { WidgetDefinition } from '@/shared/dashboard/model/widget.types'; +import type { WorkspacePurpose } from '@/shared/dashboard/model/template.types'; // 우하단 리사이즈 핸들 커스텀(원형). react-resizable 기본 클래스로 위치를 잡고 배경 삼각형은 제거한다. const renderResizeHandle = (axis: ResizeHandleAxis, ref: Ref) => ( @@ -28,6 +29,10 @@ const renderResizeHandle = (axis: ResizeHandleAxis, ref: Ref) => ( interface DashboardGridProps { /** 위젯 데이터 조회 스코프 */ workspaceId: string; + /** 워크스페이스 용도 — 위젯이 템플릿별로 데이터 소스를 분기할 때 사용 */ + purpose: WorkspacePurpose; + /** 현재 로그인 사용자 id — 본인 기준 필터 위젯에 전달 */ + currentUserId: string; /** 위젯 카탈로그 (id → 렌더러) */ widgets: WidgetDefinition[]; layout: Layout; @@ -38,6 +43,8 @@ interface DashboardGridProps { export default function DashboardGrid({ workspaceId, + purpose, + currentUserId, widgets, layout, editMode, @@ -107,7 +114,7 @@ export default function DashboardGrid({ )} - {widget.render(size, { workspaceId })} + {widget.render(size, { workspaceId, purpose, currentUserId })} ); })} diff --git a/src/views/dashboard/ui/DashboardView.tsx b/src/views/dashboard/ui/DashboardView.tsx index e527984..20acecf 100644 --- a/src/views/dashboard/ui/DashboardView.tsx +++ b/src/views/dashboard/ui/DashboardView.tsx @@ -26,6 +26,8 @@ interface DashboardViewProps { workspaceId: string; /** 워크스페이스 용도 — 추가 가능한 위젯을 템플릿별로 거른다(레이아웃 조회와는 무관) */ purpose: WorkspacePurpose; + /** 현재 로그인 사용자 id — "내 업무"처럼 본인 기준 필터가 필요한 위젯에 전달 */ + currentUserId: string; /** 서버(RSC)에서 조회한 초기 레이아웃 */ initialLayout: DashboardLayoutState; /** 향후 페이지별 레이아웃 확장을 위한 구분값 — 현재 DB에는 저장하지 않는다 */ @@ -35,6 +37,7 @@ interface DashboardViewProps { export default function DashboardView({ workspaceId, purpose, + currentUserId, initialLayout, pageType = 'dashboard', }: DashboardViewProps) { @@ -62,6 +65,8 @@ export default function DashboardView({ {editMode && } 보드} /> ); -const backlogItems: Task[] = getMockBacklogTasks(currentSprint.workspaceId); +function StateMessage({ message }: { message: string }) { + return ( + + {header} +
+ {message} +
+
+ ); +} + +interface BacklogProps { + workspaceId: string; + size?: WidgetSize; +} + +export default function Backlog({ workspaceId, size = 'md' }: BacklogProps) { + const { data: backlogItems, isError, isPending } = useBacklogTasks(workspaceId); + + if (isError) return ; + if (isPending) return ; + if (backlogItems.length === 0) return ; -export default function Backlog({ size = 'md' }: { size?: WidgetSize }) { if (size === 'sm') { const [top, ...rest] = backlogItems; return ( diff --git a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx index b736b3f..221daff 100644 --- a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx @@ -1,27 +1,75 @@ -// 내 업무 위젯 — 타일 크기에 따라 밀도가 다른 변형을 렌더 -// · sm: 진행 중 개수 헤드라인 + 대기 건수 요약 -// · md: task 리스트(상태 뱃지) -// · lg: 상태별 카운트 요약 + task 리스트 -// 현재 스프린트에 편입된 업무를 셀렉터로 가져온다(백로그는 애초에 포함되지 않음). -import { currentSprint } from '@/entities/side-project/sprint'; +'use client'; + +// 내 업무 위젯 — 같은 tasks 테이블을 템플릿(purpose)별로 다르게 투영한다. +// · side-project: 현재 스프린트에 편입된 "나에게 배정된" 업무 (포인트/상태) +// · team-project: 워크스페이스의 "나에게 배정된" 보드 업무 (마감일/상태) +// 두 도메인의 Task 모양·status 표기가 달라, 훅 규칙을 지키려 서브컴포넌트로 분기한다 +// (조건부 훅 호출 불가 → purpose로 컴포넌트를 갈라 각자 자기 훅만 호출). +import { resolveCurrentSprint, useSprints } from '@/entities/side-project/sprint'; import { - getMockSprintTasks, - type Task, TASK_STATUS, - type TaskStatus, + useSprintTasks, + type TaskStatus as SprintTaskStatus, } from '@/entities/side-project/task'; +import { useTasksByWorkspaceId, type TaskStatus as TeamTaskStatus } from '@/entities/task'; import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; +import type { WorkspacePurpose } from '@/shared/dashboard/model/template.types'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; const header = ( 전체 보기} /> ); -// 현재 스프린트 편입 업무 -const sprintTasks: Task[] = getMockSprintTasks(currentSprint.id); -const countBy = (status: TaskStatus) => sprintTasks.filter((task) => task.status === status).length; +function StateMessage({ message }: { message: string }) { + return ( + + {header} +
+ {message} +
+
+ ); +} + +interface MyTasksProps { + workspaceId: string; + purpose: WorkspacePurpose; + currentUserId: string; + size?: WidgetSize; +} + +export default function MyTasks({ workspaceId, purpose, currentUserId, size = 'md' }: MyTasksProps) { + if (purpose === 'side-project') { + return ; + } + return ; +} + +interface BranchProps { + workspaceId: string; + currentUserId: string; + size: WidgetSize; +} + +// ── side-project: 현재 스프린트 × 나에게 배정된 업무 (포인트/상태) ────────────── +function SideMyTasks({ workspaceId, currentUserId, size }: BranchProps) { + const sprintsQuery = useSprints(workspaceId); + const currentSprint = sprintsQuery.data ? resolveCurrentSprint(sprintsQuery.data) : undefined; + const tasksQuery = useSprintTasks(currentSprint?.id); + + if (sprintsQuery.isError || tasksQuery.isError) { + return ; + } + if (sprintsQuery.isPending) return ; + if (!currentSprint) return ; + if (tasksQuery.isPending) return ; + + const myTasks = tasksQuery.data.filter((task) => task.assigneeId === currentUserId); + if (myTasks.length === 0) return ; + + const countBy = (status: SprintTaskStatus) => + myTasks.filter((task) => task.status === status).length; -export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) { if (size === 'sm') { return ( @@ -36,7 +84,7 @@ export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) { const list = (
    - {sprintTasks.map((task) => { + {myTasks.map((task) => { const status = TASK_STATUS[task.status]; return (
  • @@ -81,7 +129,99 @@ export default function MyTasks({ size = 'md' }: { size?: WidgetSize }) { ); } - // md + return ( + + {header} + {list} + + ); +} + +// ── team-project: 워크스페이스 × 나에게 배정된 보드 업무 (마감일/상태) ────────── +interface TeamStatusStyle { + label: string; + dot: string; + bg: string; + text: string; +} + +const TEAM_STATUS: Record = { + todo: { label: '대기', dot: '#d1d5dc', bg: '#f3f4f6', text: '#6a7282' }, + 'in-progress': { label: '진행 중', dot: '#2b7fff', bg: '#e0e7ff', text: '#432dd7' }, + done: { label: '완료', dot: '#22c55e', bg: '#dcfce7', text: '#16a34a' }, +}; + +function TeamMyTasks({ workspaceId, currentUserId, size }: BranchProps) { + const { data, isError, isPending } = useTasksByWorkspaceId(workspaceId); + + if (isError) return ; + if (isPending) return ; + + const myTasks = data.filter((task) => task.assigneeId === currentUserId); + if (myTasks.length === 0) return ; + + const countBy = (status: TeamTaskStatus) => + myTasks.filter((task) => task.status === status).length; + + if (size === 'sm') { + return ( + + {header} +
    +

    {countBy('in-progress')}

    +

    진행 중 · 대기 {countBy('todo')}건

    +
    +
    + ); + } + + const list = ( +
      + {myTasks.map((task) => { + const status = TEAM_STATUS[task.status]; + return ( +
    • + + {task.title} + {task.dueDate} + + {status.label} + +
    • + ); + })} +
    + ); + + if (size === 'lg') { + return ( + + {header} +
    + + 진행 중{' '} + + {countBy('in-progress')} + + + + 대기 {countBy('todo')} + + + 완료 {countBy('done')} + +
    + {list} +
    + ); + } + return ( {header} diff --git a/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx b/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx index ec55bd6..bfd7872 100644 --- a/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx +++ b/src/widgets/side-project/dashboard-sprint-summary/ui/SprintSummary.tsx @@ -1,6 +1,9 @@ +'use client'; + // 스프린트 요약 위젯 — 스프린트 배너 + 포인트 통계 3종을 하나의 카드로 조립 -// 통계(계획/완료/남은)와 기간 표시는 currentSprint 메타에서 파생한다. -import { currentSprint } from '@/entities/side-project/sprint'; +// 통계(계획/완료/남은)와 기간 표시는 현재 스프린트 메타에서 파생한다. +// 현재 스프린트는 실데이터 스프린트 목록에서 resolveCurrentSprint로 판정한다. +import { resolveCurrentSprint, useSprints } from '@/entities/side-project/sprint'; import { StatCard, type Stat } from '@/shared/dashboard/ui/stat-card'; // 'YYYY-MM-DD' → 'M/D' @@ -9,8 +12,24 @@ const monthDay = (iso: string) => { return `${Number(month)}/${Number(day)}`; }; -export default function SprintSummary() { - const { name, startDate, endDate, daysLeft, totalPoints, completedPoints } = currentSprint; +function StateMessage({ message }: { message: string }) { + return ( +
    + {message} +
    + ); +} + +export default function SprintSummary({ workspaceId }: { workspaceId: string }) { + const { data: sprints, isError, isPending } = useSprints(workspaceId); + + if (isError) return ; + if (isPending) return ; + + const current = resolveCurrentSprint(sprints); + if (!current) return ; + + const { name, startDate, endDate, daysLeft, totalPoints, completedPoints } = current; const period = `${monthDay(startDate)} – ${monthDay(endDate)}`; const planned: Stat = { diff --git a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx index 62bed42..4735d7c 100644 --- a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx +++ b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx @@ -1,19 +1,47 @@ +'use client'; + // 벨로시티 위젯 — 스프린트별 계획/완료 포인트를 막대로 비교 // 막대가 2그룹뿐이라 별도 차트 라이브러리 없이 순수 CSS(div height %)로 구현한다. -import { mockSprints, selectVelocity, VELOCITY_MAX } from '@/entities/side-project/sprint'; +// Y축 최댓값은 실데이터 포인트에서 파생(selectVelocityMax)해 막대 잘림·납작함을 막는다. +import { + selectVelocity, + selectVelocityMax, + useSprints, +} from '@/entities/side-project/sprint'; import { WidgetCard, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; -export default function Velocity() { - const sprintVelocity = selectVelocity(mockSprints); +const header = ; + +function StateMessage({ message }: { message: string }) { + return ( + + {header} +
    + {message} +
    +
    + ); +} + +export default function Velocity({ workspaceId }: { workspaceId: string }) { + const { data: sprints, isError, isPending } = useSprints(workspaceId); + + if (isError) return ; + if (isPending) return ; + + const sprintVelocity = selectVelocity(sprints); + if (sprintVelocity.length === 0) return ; + + const velocityMax = selectVelocityMax(sprintVelocity); return ( - + {header}
    {/* Y축 눈금 */}
    - {VELOCITY_MAX} - {VELOCITY_MAX / 2} + {velocityMax} + {velocityMax / 2} 0
    @@ -23,12 +51,12 @@ export default function Velocity() {
    From 0dd2a48f55b9a5c245d6bfd49b8713d362c66d24 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Wed, 15 Jul 2026 09:10:31 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=9C=84=EC=A0=AF=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EA=B3=B5=EC=9A=A9=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/ui/widget-state-message.tsx | 21 ++++++++++++++ .../dashboard-backlog/ui/Backlog.tsx | 19 ++++--------- .../dashboard-my-tasks/ui/MyTasks.tsx | 28 ++++++------------- .../dashboard-velocity/ui/Velocity.tsx | 19 ++++--------- 4 files changed, 40 insertions(+), 47 deletions(-) create mode 100644 src/shared/dashboard/ui/widget-state-message.tsx diff --git a/src/shared/dashboard/ui/widget-state-message.tsx b/src/shared/dashboard/ui/widget-state-message.tsx new file mode 100644 index 0000000..97a21f1 --- /dev/null +++ b/src/shared/dashboard/ui/widget-state-message.tsx @@ -0,0 +1,21 @@ +// 위젯 상태 메시지 — 로딩/에러/빈 상태를 카드 안에 중앙 정렬로 보여준다. +// 여러 위젯이 동일한 상태 표시를 쓰므로 공용화한다(헤더는 위젯별로 다르니 prop으로 받는다). +import type { ReactNode } from 'react'; +import { WidgetCard } from './widget-card'; + +interface WidgetStateMessageProps { + /** 위젯별 헤더(제목/액션). 헤더가 필요 없는 위젯은 생략 가능 */ + header?: ReactNode; + message: string; +} + +export function WidgetStateMessage({ header, message }: WidgetStateMessageProps) { + return ( + + {header} +
    + {message} +
    +
    + ); +} diff --git a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx index 88fba7a..e99f881 100644 --- a/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx +++ b/src/widgets/side-project/dashboard-backlog/ui/Backlog.tsx @@ -7,22 +7,12 @@ import { TASK_PRIORITY, useBacklogTasks } from '@/entities/side-project/task'; import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; +import { WidgetStateMessage } from '@/shared/dashboard/ui/widget-state-message'; const header = ( 보드} /> ); -function StateMessage({ message }: { message: string }) { - return ( - - {header} -
    - {message} -
    -
    - ); -} - interface BacklogProps { workspaceId: string; size?: WidgetSize; @@ -31,9 +21,10 @@ interface BacklogProps { export default function Backlog({ workspaceId, size = 'md' }: BacklogProps) { const { data: backlogItems, isError, isPending } = useBacklogTasks(workspaceId); - if (isError) return ; - if (isPending) return ; - if (backlogItems.length === 0) return ; + if (isError) return ; + if (isPending) return ; + if (backlogItems.length === 0) + return ; if (size === 'sm') { const [top, ...rest] = backlogItems; diff --git a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx index 221daff..d0a7cad 100644 --- a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx @@ -15,22 +15,12 @@ import { useTasksByWorkspaceId, type TaskStatus as TeamTaskStatus } from '@/enti import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; import type { WorkspacePurpose } from '@/shared/dashboard/model/template.types'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; +import { WidgetStateMessage } from '@/shared/dashboard/ui/widget-state-message'; const header = ( 전체 보기} /> ); -function StateMessage({ message }: { message: string }) { - return ( - - {header} -
    - {message} -
    -
    - ); -} - interface MyTasksProps { workspaceId: string; purpose: WorkspacePurpose; @@ -58,14 +48,14 @@ function SideMyTasks({ workspaceId, currentUserId, size }: BranchProps) { const tasksQuery = useSprintTasks(currentSprint?.id); if (sprintsQuery.isError || tasksQuery.isError) { - return ; + return ; } - if (sprintsQuery.isPending) return ; - if (!currentSprint) return ; - if (tasksQuery.isPending) return ; + if (sprintsQuery.isPending) return ; + if (!currentSprint) return ; + if (tasksQuery.isPending) return ; const myTasks = tasksQuery.data.filter((task) => task.assigneeId === currentUserId); - if (myTasks.length === 0) return ; + if (myTasks.length === 0) return ; const countBy = (status: SprintTaskStatus) => myTasks.filter((task) => task.status === status).length; @@ -154,11 +144,11 @@ const TEAM_STATUS: Record = { function TeamMyTasks({ workspaceId, currentUserId, size }: BranchProps) { const { data, isError, isPending } = useTasksByWorkspaceId(workspaceId); - if (isError) return ; - if (isPending) return ; + if (isError) return ; + if (isPending) return ; const myTasks = data.filter((task) => task.assigneeId === currentUserId); - if (myTasks.length === 0) return ; + if (myTasks.length === 0) return ; const countBy = (status: TeamTaskStatus) => myTasks.filter((task) => task.status === status).length; diff --git a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx index 4735d7c..e572aec 100644 --- a/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx +++ b/src/widgets/side-project/dashboard-velocity/ui/Velocity.tsx @@ -9,28 +9,19 @@ import { useSprints, } from '@/entities/side-project/sprint'; import { WidgetCard, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; +import { WidgetStateMessage } from '@/shared/dashboard/ui/widget-state-message'; const header = ; -function StateMessage({ message }: { message: string }) { - return ( - - {header} -
    - {message} -
    -
    - ); -} - export default function Velocity({ workspaceId }: { workspaceId: string }) { const { data: sprints, isError, isPending } = useSprints(workspaceId); - if (isError) return ; - if (isPending) return ; + if (isError) return ; + if (isPending) return ; const sprintVelocity = selectVelocity(sprints); - if (sprintVelocity.length === 0) return ; + if (sprintVelocity.length === 0) + return ; const velocityMax = selectVelocityMax(sprintVelocity); From 166be3ff1d08167af428b1fe5dfbd8ed2fe87955 Mon Sep 17 00:00:00 2001 From: Kwon812 Date: Wed, 15 Jul 2026 09:16:36 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor:=20=ED=8C=80=ED=94=8C=20task=20?= =?UTF-8?q?=EC=9C=84=EC=A0=AF=20=EC=A7=84=ED=96=89=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=83=81=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/entities/task/index.ts | 1 + src/entities/task/model/task.types.ts | 14 ++++++++++ .../dashboard-my-tasks/ui/MyTasks.tsx | 27 +++++++------------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/entities/task/index.ts b/src/entities/task/index.ts index 04a481a..f54f5d3 100644 --- a/src/entities/task/index.ts +++ b/src/entities/task/index.ts @@ -1,4 +1,5 @@ export type { Task, TaskStatus } from './model/task.types'; +export { TASK_STATUS } from './model/task.types'; export type { TaskRow, TaskStatusDb } from './model/task.db.types'; export { toTask, toDbTaskStatus, toUiTaskStatus } from './model/task.mapper'; export { taskTitleSchema, taskBoardItemSchema, updateTaskBoardSchema } from './model/task.schema'; diff --git a/src/entities/task/model/task.types.ts b/src/entities/task/model/task.types.ts index 383a812..40544af 100644 --- a/src/entities/task/model/task.types.ts +++ b/src/entities/task/model/task.types.ts @@ -1,5 +1,19 @@ export type TaskStatus = 'todo' | 'in-progress' | 'done'; +interface StatusStyle { + label: string; + dot: string; + bg: string; + text: string; +} + +// 상태 뱃지 스타일 — 대시보드 등 상태 표시가 필요한 곳에서 공용으로 쓴다. +export const TASK_STATUS: Record = { + todo: { label: '대기', dot: '#d1d5dc', bg: '#f3f4f6', text: '#6a7282' }, + 'in-progress': { label: '진행 중', dot: '#2b7fff', bg: '#e0e7ff', text: '#432dd7' }, + done: { label: '완료', dot: '#22c55e', bg: '#dcfce7', text: '#16a34a' }, +}; + export type Task = { id: string; workspaceId: string; diff --git a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx index d0a7cad..7a0e37f 100644 --- a/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx +++ b/src/widgets/side-project/dashboard-my-tasks/ui/MyTasks.tsx @@ -11,7 +11,11 @@ import { useSprintTasks, type TaskStatus as SprintTaskStatus, } from '@/entities/side-project/task'; -import { useTasksByWorkspaceId, type TaskStatus as TeamTaskStatus } from '@/entities/task'; +import { + TASK_STATUS as TEAM_TASK_STATUS, + useTasksByWorkspaceId, + type TaskStatus as TeamTaskStatus, +} from '@/entities/task'; import type { WidgetSize } from '@/shared/dashboard/lib/widget-size'; import type { WorkspacePurpose } from '@/shared/dashboard/model/template.types'; import { WidgetCard, WidgetCardAction, WidgetCardHeader } from '@/shared/dashboard/ui/widget-card'; @@ -128,19 +132,6 @@ function SideMyTasks({ workspaceId, currentUserId, size }: BranchProps) { } // ── team-project: 워크스페이스 × 나에게 배정된 보드 업무 (마감일/상태) ────────── -interface TeamStatusStyle { - label: string; - dot: string; - bg: string; - text: string; -} - -const TEAM_STATUS: Record = { - todo: { label: '대기', dot: '#d1d5dc', bg: '#f3f4f6', text: '#6a7282' }, - 'in-progress': { label: '진행 중', dot: '#2b7fff', bg: '#e0e7ff', text: '#432dd7' }, - done: { label: '완료', dot: '#22c55e', bg: '#dcfce7', text: '#16a34a' }, -}; - function TeamMyTasks({ workspaceId, currentUserId, size }: BranchProps) { const { data, isError, isPending } = useTasksByWorkspaceId(workspaceId); @@ -168,7 +159,7 @@ function TeamMyTasks({ workspaceId, currentUserId, size }: BranchProps) { const list = (
      {myTasks.map((task) => { - const status = TEAM_STATUS[task.status]; + const status = TEAM_TASK_STATUS[task.status]; return (
    • 진행 중{' '} - + {countBy('in-progress')} - 대기 {countBy('todo')} + 대기 {countBy('todo')} - 완료 {countBy('done')} + 완료 {countBy('done')}
    {list}