reopt designreopt design
DocsExploreToolsPricingBuilder
Start
Overview
Start
Next.js 설치
Private install
Core Concepts
아키텍처
Composition Patterns
Accessibility
Keyboard Patterns
Styling
Theme System
Advanced Patterns
Build & Operate
Skills
AI Integration
CLI (opt surface add)
Dependency Graph
Tools
Canvas Catalog
Theme Builder
Form Builder
Templates
Templates
Releases
Release Notes
Oopt-ui
reopt designreopt design

A design system for the AI era

  • Docs
  • Pricing
  • Releases
  • GitHub
  • Terms of Service
  • Privacy Policy

© 2026 reopt-ai. All rights reserved.

TaskDashboard

surface

작업 관리 대시보드. TaskList + ActivityFeed + TeamMemberList + QuickActions + FilterBar 조합.

컴포넌트 의존 관계

깊이
▼ USES (7)TaskDashboardtask-listactivity-feedteam-member-listquick-actionsfilter-bardashboard-gridloading-overlay
100%

전체 기능

프로젝트 대시보드

작업 현황 및 팀 활동

빠른 작업

통계

전체 작업24↑ +3
완료18↑ +5
진행 중4↓ -2
대기2→ 0

작업 목록

1/3 완료

최근 활동

김
김철수 생성 새 작업 생성
2026. 5. 24.
이
이영희 완료 테스트 코드 작성 완료
2026. 5. 24.

팀원

1/3 온라인

컴팩트 레이아웃

작업 목록

1/3 완료

최근 활동

김
김철수 생성 새 작업 생성
2026. 5. 24.
이
이영희 완료 테스트 코드 작성 완료
2026. 5. 24.

팀원

작업 목록만

작업 목록

1/3 완료

테스트 커버리지

2026년 2월 4일

생성된 테스트 결과를 찾지 못했습니다.

TaskDashboard 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.

테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.

TaskDashboard Props

Prop타입기본값설명
tasksTaskDef[]—작업 목록 배열
activitiesActivityDef[]—활동 피드 배열
teamMembersTeamMemberDef[]—팀원 목록 배열
quickActionsQuickActionDef[]—빠른 작업 배열
filtersFilterGroupDef[]—필터 정의 배열
statsStatCard[]—통계 카드 배열
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"레이아웃 모드
headerReactNode—상단 헤더 영역
actionsReactNode—헤더 우측 액션 버튼

Surface 설치

CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.

bash
npx @reopt-ai/opt-cli surface add task-dashboard

Consumer 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.tsx→task-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>
  );
}