TaskDashboard
surface작업 관리 대시보드. TaskList + ActivityFeed + TeamMemberList + QuickActions + FilterBar 조합.
컴포넌트 의존 관계
깊이
100%
전체 기능
프로젝트 대시보드
작업 현황 및 팀 활동
빠른 작업
통계
전체 작업24↑ +3
완료18↑ +5
진행 중4↓ -2
대기2→ 0
작업 목록
1/3 완료컴팩트 레이아웃
작업 목록
1/3 완료최근 활동
김
김철수 생성 새 작업 생성
이
이영희 완료 테스트 코드 작성 완료
팀원
작업 목록만
작업 목록
1/3 완료테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
TaskDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
TaskDashboard Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
tasks | TaskDef[] | — | 작업 목록 배열 |
activities | ActivityDef[] | — | 활동 피드 배열 |
teamMembers | TeamMemberDef[] | — | 팀원 목록 배열 |
quickActions | QuickActionDef[] | — | 빠른 작업 배열 |
filters | FilterGroupDef[] | — | 필터 정의 배열 |
stats | StatCard[] | — | 통계 카드 배열 |
onTaskClick | (task: TaskDef) => void | — | 작업 클릭 핸들러 |
onTaskStatusChange | (task: TaskDef, completed: boolean) => void | — | 완료 상태 변경 핸들러 |
onActivityClick | (activity: ActivityDef) => void | — | 활동 클릭 핸들러 |
onMemberClick | (member: TeamMemberDef) => void | — | 팀원 클릭 핸들러 |
onFilterChange | (filterId: string, value: string | string[]) => void | — | 필터 변경 핸들러 |
layout | "default" | "compact" | "default" | 레이아웃 모드 |
header | ReactNode | — | 상단 헤더 영역 |
actions | ReactNode | — | 헤더 우측 액션 버튼 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add task-dashboardConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { TaskDashboard } from "@/components/surfaces/task-dashboard";Registry metadata
- 설명
- 작업 관리 대시보드. TaskList + ActivityFeed + TeamMemberList + QuickActions + FilterBar 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- lucide-react
- 태그
- filterdashboard
- Install notes
- Install additional packages: lucide-react.
포함 파일
task-dashboard.tsxtask-dashboard.tsx
Surface 소스 보기
task-dashboard.tsx
"use client";
import { useId, type ReactNode } from "react";
import { ClipboardList } from "lucide-react";
import { TaskList, ActivityFeed, TeamMemberList, QuickActions, FilterBar, DashboardGrid, cn, OPT_TEXT_PRIMARY, OPT_TEXT_SECONDARY, SurfaceLayout } from "@reopt-ai/opt-ui";
import type { TaskDef, ActivityDef, TeamMemberDef, QuickActionDef, FilterGroupDef, StatCard as StatCardType } from "@reopt-ai/opt-ui";
/** Localized labels for the task dashboard component. */
export interface TaskDashboardLabels {
tasks?: string;
activities?: string;
team?: string;
actions?: string;
stats?: string;
emptyTitle?: string;
emptyDescription?: string;
}
const defaultLabels: Required<TaskDashboardLabels> = {
tasks: "작업 목록",
activities: "최근 활동",
team: "팀원",
actions: "빠른 작업",
stats: "통계",
emptyTitle: "대시보드가 비어 있습니다",
emptyDescription: "작업, 활동, 팀원, 또는 통계 데이터를 전달하세요.",
};
/** Props for the task dashboard component. */
export interface TaskDashboardProps {
// Data
tasks?: TaskDef[];
activities?: ActivityDef[];
teamMembers?: TeamMemberDef[];
quickActions?: QuickActionDef[];
filters?: FilterGroupDef[];
stats?: StatCardType[];
// Event handlers
onTaskClick?: (task: TaskDef) => void;
onTaskStatusChange?: (task: TaskDef, completed: boolean) => void;
onActivityClick?: (activity: ActivityDef) => void;
onMemberClick?: (member: TeamMemberDef) => void;
onFilterChange?: (
filterId: string,
value: string | string[] | boolean,
) => void;
onClearFilters?: () => void;
// Customization
tasksTitle?: string;
activitiesTitle?: string;
teamTitle?: string;
actionsTitle?: string;
statsTitle?: string;
maxActivities?: number;
maxTeamMembers?: number;
statsColumns?: number;
// Layout
header?: ReactNode;
actions?: ReactNode;
className?: string;
layout?: "default" | "compact";
// Section visibility
showTasks?: boolean;
showActivities?: boolean;
showTeam?: boolean;
showActions?: boolean;
showStats?: boolean;
/** Custom labels for i18n support */
labels?: TaskDashboardLabels;
loading?: boolean;
}
/** Renders the task dashboard component. */
export function TaskDashboard({
tasks,
activities,
teamMembers,
quickActions,
filters,
stats,
onTaskClick,
onTaskStatusChange,
onActivityClick,
onMemberClick,
onFilterChange,
onClearFilters,
tasksTitle,
activitiesTitle,
teamTitle,
actionsTitle,
statsTitle,
maxActivities = 5,
maxTeamMembers = 5,
statsColumns = 4,
header,
actions,
className,
layout = "default",
showTasks = true,
showActivities = true,
showTeam = true,
showActions = true,
showStats = true,
labels: customLabels,
loading = false,
}: TaskDashboardProps) {
const labels = { ...defaultLabels, ...customLabels };
const statsHeadingId = useId();
const hasTasks = showTasks && tasks && tasks.length > 0;
const hasActivities = showActivities && activities && activities.length > 0;
const hasTeamMembers = showTeam && teamMembers && teamMembers.length > 0;
const hasQuickActions =
showActions && quickActions && quickActions.length > 0;
const hasFilters = filters && filters.length > 0;
const hasStats = showStats && stats && stats.length > 0;
const hasSidebar = hasActivities || hasTeamMembers;
const isEmpty = !hasTasks && !hasSidebar && !hasQuickActions && !hasStats;
return (
<SurfaceLayout
loading={loading}
className={className}
data-opt-id="9YG6C"
data-opt-slug="task-dashboard.TaskDashboard"
>
{/* 헤더 영역 */}
{(header || actions) && (
<div className="flex items-center justify-between">
{header && <div>{header}</div>}
{actions && (
<div className="gap-element flex items-center">{actions}</div>
)}
</div>
)}
{/* 빠른 작업 */}
{hasQuickActions && (
<section aria-label={actionsTitle ?? labels.actions}>
<QuickActions
actions={quickActions}
title={actionsTitle ?? labels.actions}
layout={layout === "compact" ? "list" : "grid"}
columns={4}
/>
</section>
)}
{/* 통계 그리드 */}
{hasStats && (
<section aria-labelledby={statsHeadingId}>
<h3
id={statsHeadingId}
className="text-text-tertiary mb-3 text-sm font-medium"
>
{statsTitle ?? labels.stats}
</h3>
<DashboardGrid stats={stats} columns={statsColumns} />
</section>
)}
{/* 필터 바 */}
{hasFilters && (
<FilterBar
filters={filters}
onFilterChange={onFilterChange}
onClearAll={onClearFilters}
/>
)}
{/* 메인 콘텐츠 영역 */}
{layout === "compact" ? (
// 컴팩트 레이아웃: 작업 목록 전체 너비, 활동/팀원 2컬럼
<div className="gap-section flex flex-col">
{hasTasks && (
<section aria-label={tasksTitle ?? labels.tasks}>
<TaskList
tasks={tasks}
title={tasksTitle ?? labels.tasks}
onTaskClick={onTaskClick}
onTaskStatusChange={onTaskStatusChange}
showSubtasks
/>
</section>
)}
{hasSidebar && (
<div
className={cn(
"gap-section grid",
hasActivities && hasTeamMembers && "lg:grid-cols-2",
)}
>
{hasActivities && (
<section aria-label={activitiesTitle ?? labels.activities}>
<ActivityFeed
activities={activities}
title={activitiesTitle ?? labels.activities}
maxItems={maxActivities}
onActivityClick={onActivityClick}
layout="compact"
/>
</section>
)}
{hasTeamMembers && (
<section aria-label={teamTitle ?? labels.team}>
<TeamMemberList
members={teamMembers}
title={teamTitle ?? labels.team}
layout="compact"
maxDisplay={maxTeamMembers}
onMemberClick={onMemberClick}
/>
</section>
)}
</div>
)}
</div>
) : (
// 기본 레이아웃: 콘텐츠에 따라 그리드 조정
<div
className={cn(
"gap-section grid",
hasTasks && hasSidebar && "lg:grid-cols-3",
)}
>
{/* 작업 목록 */}
{hasTasks && (
<section
aria-label={tasksTitle ?? labels.tasks}
className={cn(hasSidebar && "lg:col-span-2")}
>
<TaskList
tasks={tasks}
title={tasksTitle ?? labels.tasks}
onTaskClick={onTaskClick}
onTaskStatusChange={onTaskStatusChange}
showSubtasks
/>
</section>
)}
{/* 사이드바 */}
{hasSidebar && (
<aside className="gap-section flex flex-col">
{/* 활동 피드 */}
{hasActivities && (
<section aria-label={activitiesTitle ?? labels.activities}>
<ActivityFeed
activities={activities}
title={activitiesTitle ?? labels.activities}
maxItems={maxActivities}
onActivityClick={onActivityClick}
layout="list"
/>
</section>
)}
{/* 팀원 목록 */}
{hasTeamMembers && (
<section aria-label={teamTitle ?? labels.team}>
<TeamMemberList
members={teamMembers}
title={teamTitle ?? labels.team}
layout="list"
maxDisplay={maxTeamMembers}
onMemberClick={onMemberClick}
/>
</section>
)}
</aside>
)}
</div>
)}
{/* 빈 상태 */}
{isEmpty && (
<div
role="status"
className="flex flex-col items-center justify-center py-16 text-center"
>
<ClipboardList className="mb-4 size-10" aria-hidden />
<h3 className={`text-lg font-medium ${OPT_TEXT_PRIMARY}`}>
{labels.emptyTitle}
</h3>
<p className={`mt-1 text-sm ${OPT_TEXT_SECONDARY}`}>
{labels.emptyDescription}
</p>
</div>
)}
</SurfaceLayout>
);
}