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.

ProjectOverview

surface

프로젝트 개요 Surface. SummaryRow + NavCards + LineChart + BarChart + DataTable + CodeSnippetViewer + Dashboard CRUD 조합.

컴포넌트 의존 관계

깊이
▼ USES (9)ProjectOverviewline-chartbar-chartdata-tablecode-snippet-viewersummary-rowtime-range-selectorconfirm-dialogloading-overlaybutton
100%

기본 사용

Project Overview

Total Events1.2M↑ +12%
Active Users8,420↑ +5.3%
Sessions24.5K↓ -2.1%
Error Rate0.12%↓ -8%

Trends

Breakdown

Top Events

Data Table

EventCountChange
page_view45,200+8.2%
button_click32,100-3.1%
form_submit18,400+12.5%
api_request12,300+1.8%
error_thrown1,520-15.2%

Recent Events

Data Table

EventTimeUser
page_view2026-02-23T10:30:00Zuser_abc
button_click2026-02-23T10:29:45Zuser_def
form_submit2026-02-23T10:29:30Zuser_abc
api_request2026-02-23T10:29:15Zuser_ghi
error_thrown2026-02-23T10:29:00Zuser_jkl

Dashboards

Data Table

NameLast Updated
Marketing Overview2026-02-22T08:00:00Z
Engineering Metrics2026-02-21T16:30:00Z
Growth Dashboard2026-02-20T12:00:00Z

SDK Integration

Installation
1import posthog from 'posthog-js'
2posthog.init('phc_xxx', { api_host: 'https://app.posthog.com' })

테스트 커버리지

2026년 2월 4일

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

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

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

ProjectOverview Props

Prop타입기본값설명
statsStatCardType[]—통계 카드 배열 (2-4)
navCardsProjectOverviewNavCard[]—분석 도구 내비게이션 카드
trendDataChartDataPoint[]—시계열 차트 데이터
trendSeriesChartSeriesDef[]—시계열 시리즈 정의
barDataChartDataPoint[]—바 차트 데이터
barSeriesChartSeriesDef[]—바 차트 시리즈 정의
topEventsProjectOverviewTopEvent[]—상위 이벤트 테이블
recentEventsProjectOverviewRecentEvent[]—최근 이벤트 리스트
dashboardsProjectOverviewDashboard[]—대시보드 목록
onDashboardCreate() => void—대시보드 생성 핸들러
onDashboardClick(id: string) => void—대시보드 클릭 핸들러
onDashboardDelete(id: string) => void—대시보드 삭제 핸들러
sdkSnippetstring—SDK 코드 스니펫
sdkLanguagestring"typescript"스니펫 언어
timeRangeDateRange—시간 범위 필터
onTimeRangeChange(range: DateRange) => void—시간 범위 변경 핸들러
loadingboolean—로딩 상태
headerReactNode—커스텀 헤더 슬롯
actionsReactNode—액션 버튼 슬롯
labelsProjectOverviewLabels—커스텀 레이블
classNamestring—최외곽 CSS 클래스

Surface 설치

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

bash
npx @reopt-ai/opt-cli surface add project-overview

Consumer target

복사된 파일은 components/surfaces 아래에 저장됩니다.

tsx
import { ProjectOverview } from "@/components/surfaces/project-overview";

Registry metadata

설명
프로젝트 개요 Surface. SummaryRow + NavCards + LineChart + BarChart + DataTable + CodeSnippetViewer + Dashboard CRUD 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
charttabledashboard
Install notes
없음

포함 파일

  • project-overview.tsx→project-overview.tsx
Surface 소스 보기
project-overview.tsx
"use client";

import * as React from "react";
import {
  Button,
  DataTable,
  CodeSnippetViewer,
  SurfaceLayout,
  SummaryRow,
  TimeRangeSelector,
  ConfirmDialog,
  cn,
  type ColumnDef,
  type DateRange,
  type StatCardType,
} from "@reopt-ai/opt-ui";
import {
  LineChart,
  BarChart,
  type ChartDataPoint,
  type ChartSeriesDef,
} from "@reopt-ai/opt-charts";

/* ------------------------------------------------------------------ */
/*  Types                                                              */
/* ------------------------------------------------------------------ */

/** Card data for `ProjectOverviewNav`. */
export interface ProjectOverviewNavCard {
  id: string;
  label: string;
  description?: string;
  icon?: React.ReactNode;
  href?: string;
  onClick?: () => void;
}

/** Event data for `ProjectOverviewTop`. */
export interface ProjectOverviewTopEvent {
  name: string;
  count: number;
  change?: number;
}

/** Event data for `ProjectOverviewRecent`. */
export interface ProjectOverviewRecentEvent {
  name: string;
  timestamp: string;
  userId?: string;
}

/** Dashboard data for `ProjectOverviewDashboard`. */
export interface ProjectOverviewDashboard {
  id: string;
  name: string;
  updatedAt?: string;
}

/** Labels for `ProjectOverview`. */
export interface ProjectOverviewLabels {
  overview?: string;
  analytics?: string;
  trends?: string;
  barChart?: string;
  topEvents?: string;
  recentEvents?: string;
  dashboards?: string;
  sdk?: string;
  installation?: string;
  createDashboard?: string;
  deleteDashboard?: string;
  deleteConfirmTitle?: string;
  deleteConfirmDescription?: string;
  deleteConfirmButton?: string;
  cancelButton?: string;
  emptyTitle?: string;
  emptyDescription?: string;
  noDashboards?: string;
  eventName?: string;
  eventCount?: string;
  eventChange?: string;
  recentEventName?: string;
  recentTimestamp?: string;
  recentUserId?: string;
  dashboardName?: string;
  dashboardUpdated?: string;
  dashboardActions?: string;
}

const defaultLabels: Required<ProjectOverviewLabels> = {
  overview: "Project Overview",
  analytics: "Analysis Tools",
  trends: "Trends",
  barChart: "Breakdown",
  topEvents: "Top Events",
  recentEvents: "Recent Events",
  dashboards: "Dashboards",
  sdk: "SDK Integration",
  installation: "Installation",
  createDashboard: "Create Dashboard",
  deleteDashboard: "Delete",
  deleteConfirmTitle: "Delete Dashboard",
  deleteConfirmDescription:
    "Are you sure you want to delete this dashboard? This action cannot be undone.",
  deleteConfirmButton: "Delete",
  cancelButton: "Cancel",
  emptyTitle: "No data yet",
  emptyDescription: "Project overview data will appear here once available.",
  noDashboards: "No dashboards created yet.",
  eventName: "Event",
  eventCount: "Count",
  eventChange: "Change",
  recentEventName: "Event",
  recentTimestamp: "Time",
  recentUserId: "User",
  dashboardName: "Name",
  dashboardUpdated: "Last Updated",
  dashboardActions: "Actions",
};

/* ------------------------------------------------------------------ */
/*  Props                                                              */
/* ------------------------------------------------------------------ */

/** Props for `ProjectOverview`. */
export interface ProjectOverviewProps {
  /** Overview stat cards (2-4) */
  stats?: StatCardType[];
  /** Analysis tool navigation cards */
  navCards?: ProjectOverviewNavCard[];
  /** Time series chart data */
  trendData?: ChartDataPoint[];
  /** Series definitions for trend chart */
  trendSeries?: ChartSeriesDef[];
  /** Bar chart data */
  barData?: ChartDataPoint[];
  /** Bar chart series definitions */
  barSeries?: ChartSeriesDef[];
  /** Top events table data */
  topEvents?: ProjectOverviewTopEvent[];
  /** Recent events list */
  recentEvents?: ProjectOverviewRecentEvent[];
  /** Saved dashboards */
  dashboards?: ProjectOverviewDashboard[];
  /** Create dashboard callback */
  onDashboardCreate?: () => void;
  /** Dashboard click callback */
  onDashboardClick?: (id: string) => void;
  /** Dashboard delete callback */
  onDashboardDelete?: (id: string) => void;
  /** SDK integration code snippet */
  sdkSnippet?: string;
  /** Snippet language */
  sdkLanguage?: string;
  /** Date range value */
  timeRange?: DateRange;
  /** Date range change handler */
  onTimeRangeChange?: (range: DateRange) => void;
  /** Loading state */
  loading?: boolean;
  /** Custom header slot */
  header?: React.ReactNode;
  /** Custom actions slot */
  actions?: React.ReactNode;
  /** Custom labels */
  labels?: ProjectOverviewLabels;
  /** Root className override */
  className?: string;
}

/* ------------------------------------------------------------------ */
/*  Default columns                                                    */
/* ------------------------------------------------------------------ */

function buildTopEventColumns(
  labels: Required<ProjectOverviewLabels>,
): ColumnDef<ProjectOverviewTopEvent>[] {
  return [
    { id: "name", header: labels.eventName, accessor: "name" },
    {
      id: "count",
      header: labels.eventCount,
      accessor: (row) => row.count.toLocaleString(),
    },
    {
      id: "change",
      header: labels.eventChange,
      accessor: (row) =>
        row.change != null
          ? `${row.change > 0 ? "+" : ""}${row.change}%`
          : "\u2014",
    },
  ];
}

function buildRecentEventColumns(
  labels: Required<ProjectOverviewLabels>,
): ColumnDef<ProjectOverviewRecentEvent>[] {
  const cols: ColumnDef<ProjectOverviewRecentEvent>[] = [
    { id: "name", header: labels.recentEventName, accessor: "name" },
    { id: "timestamp", header: labels.recentTimestamp, accessor: "timestamp" },
  ];
  cols.push({
    id: "userId",
    header: labels.recentUserId,
    accessor: (row) => row.userId ?? "\u2014",
  });
  return cols;
}

/* ------------------------------------------------------------------ */
/*  Component                                                          */
/* ------------------------------------------------------------------ */

/** Renders the `ProjectOverview` component. */
export function ProjectOverview({
  stats = [],
  navCards = [],
  trendData = [],
  trendSeries = [],
  barData = [],
  barSeries = [],
  topEvents = [],
  recentEvents = [],
  dashboards = [],
  onDashboardCreate,
  onDashboardClick,
  onDashboardDelete,
  sdkSnippet,
  sdkLanguage = "typescript",
  timeRange,
  onTimeRangeChange,
  loading = false,
  header,
  actions,
  labels: customLabels,
  className,
}: ProjectOverviewProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();
  const analyticsId = React.useId();
  const trendsId = React.useId();
  const barId = React.useId();
  const topEventsId = React.useId();
  const recentEventsId = React.useId();
  const dashboardsId = React.useId();
  const sdkId = React.useId();

  const [pendingDeleteId, setPendingDeleteId] = React.useState<string | null>(
    null,
  );

  const topEventColumns = React.useMemo(
    () => buildTopEventColumns(labels),
    [labels.eventName, labels.eventCount, labels.eventChange],
  );

  const recentEventColumns = React.useMemo(
    () => buildRecentEventColumns(labels),
    [labels.recentEventName, labels.recentTimestamp, labels.recentUserId],
  );

  const dashboardColumns = React.useMemo<ColumnDef<ProjectOverviewDashboard>[]>(
    () => [
      { id: "name", header: labels.dashboardName, accessor: "name" },
      {
        id: "updatedAt",
        header: labels.dashboardUpdated,
        accessor: (row) => row.updatedAt ?? "\u2014",
      },
      ...(onDashboardDelete
        ? [
            {
              id: "actions" as const,
              header: labels.dashboardActions,
              accessor: () => "",
              cell: (row: ProjectOverviewDashboard) => (
                <Button
                  variant="ghost"
                  size="sm"
                  onClick={(e: React.MouseEvent) => {
                    e.stopPropagation();
                    setPendingDeleteId(row.id);
                  }}
                >
                  {labels.deleteDashboard}
                </Button>
              ),
            },
          ]
        : []),
    ],
    [
      labels.dashboardName,
      labels.dashboardUpdated,
      labels.dashboardActions,
      labels.deleteDashboard,
      onDashboardDelete,
    ],
  );

  const hasStats = stats.length > 0;
  const hasNavCards = navCards.length > 0;
  const hasTrend = trendData.length > 0 && trendSeries.length > 0;
  const hasBar = barData.length > 0 && barSeries.length > 0;
  const hasTopEvents = topEvents.length > 0;
  const hasRecentEvents = recentEvents.length > 0;
  const hasDashboards = dashboards.length > 0;
  const isEmpty = !hasStats && !hasNavCards && !hasTrend && !hasBar;

  const handleConfirmDelete = () => {
    if (pendingDeleteId != null) {
      onDashboardDelete?.(pendingDeleteId);
      setPendingDeleteId(null);
    }
  };

  return (
    <SurfaceLayout loading={loading} className={className}>
      {/* ---- Header ---- */}
      {header || actions ? (
        <div className="flex items-center justify-between">
          {header && <div>{header}</div>}
          <div className="gap-element flex items-center">
            <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
            {actions}
          </div>
        </div>
      ) : (
        <div className="flex items-center justify-between">
          <h2 id={titleId} className="text-text-primary text-lg font-semibold">
            {labels.overview}
          </h2>
          <TimeRangeSelector value={timeRange} onChange={onTimeRangeChange} />
        </div>
      )}

      {isEmpty ? (
        <div
          role="status"
          className="flex flex-col items-center justify-center py-16 text-center"
        >
          <h3 className="text-text-primary text-lg font-medium">
            {labels.emptyTitle}
          </h3>
          <p className="text-text-tertiary mt-1 text-sm">
            {labels.emptyDescription}
          </p>
        </div>
      ) : (
        <>
          {/* ---- Stats Cards ---- */}
          {hasStats && <SummaryRow stats={stats} columns={4} />}

          {/* ---- Navigation Cards ---- */}
          {hasNavCards && (
            <section aria-labelledby={analyticsId}>
              <h3
                id={analyticsId}
                className="text-text-tertiary mb-3 text-sm font-medium"
              >
                {labels.analytics}
              </h3>
              <nav
                aria-label={labels.analytics}
                className="gap-group grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4"
              >
                {navCards.map((card) => {
                  const content = (
                    <>
                      {card.icon && (
                        <span className="text-lg" aria-hidden="true">
                          {card.icon}
                        </span>
                      )}
                      <span className="text-text-primary text-sm font-medium">
                        {card.label}
                      </span>
                      {card.description && (
                        <span className="text-text-tertiary text-xs">
                          {card.description}
                        </span>
                      )}
                    </>
                  );

                  const cls =
                    "gap-element flex flex-col rounded-lg border border-border-subtle p-3 transition-colors hover:bg-bg-subtle";

                  if (card.href) {
                    return (
                      <a key={card.id} href={card.href} className={cls}>
                        {content}
                      </a>
                    );
                  }
                  return (
                    <button
                      key={card.id}
                      type="button"
                      onClick={card.onClick}
                      className={cn(cls, "text-left")}
                    >
                      {content}
                    </button>
                  );
                })}
              </nav>
            </section>
          )}

          {/* ---- Charts Section ---- */}
          {(hasTrend || hasBar) && (
            <div className="gap-section grid grid-cols-1 lg:grid-cols-2">
              {hasTrend && (
                <section aria-labelledby={trendsId}>
                  <h3
                    id={trendsId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.trends}
                  </h3>
                  <LineChart
                    data={trendData}
                    series={trendSeries}
                    aria-label={labels.trends}
                  />
                </section>
              )}
              {hasBar && (
                <section aria-labelledby={barId}>
                  <h3
                    id={barId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.barChart}
                  </h3>
                  <BarChart
                    data={barData}
                    series={barSeries}
                    aria-label={labels.barChart}
                  />
                </section>
              )}
            </div>
          )}

          {/* ---- Events Section ---- */}
          {(hasTopEvents || hasRecentEvents) && (
            <div className="gap-section grid grid-cols-1 lg:grid-cols-2">
              {hasTopEvents && (
                <section aria-labelledby={topEventsId}>
                  <h3
                    id={topEventsId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.topEvents}
                  </h3>
                  <DataTable
                    columns={topEventColumns}
                    data={topEvents}
                    keyExtractor={(r) => r.name}
                  />
                </section>
              )}
              {hasRecentEvents && (
                <section aria-labelledby={recentEventsId}>
                  <h3
                    id={recentEventsId}
                    className="text-text-tertiary mb-2 text-sm font-medium"
                  >
                    {labels.recentEvents}
                  </h3>
                  <DataTable
                    columns={recentEventColumns}
                    data={recentEvents}
                    keyExtractor={(r) => `${r.name}-${r.timestamp}`}
                  />
                </section>
              )}
            </div>
          )}

          {/* ---- Dashboards Section ---- */}
          {(hasDashboards || onDashboardCreate) && (
            <section aria-labelledby={dashboardsId}>
              <div className="mb-2 flex items-center justify-between">
                <h3
                  id={dashboardsId}
                  className="text-text-tertiary text-sm font-medium"
                >
                  {labels.dashboards}
                </h3>
                {onDashboardCreate && (
                  <Button
                    variant="primary"
                    size="sm"
                    onClick={onDashboardCreate}
                  >
                    + {labels.createDashboard}
                  </Button>
                )}
              </div>
              {hasDashboards ? (
                <DataTable
                  columns={dashboardColumns}
                  data={dashboards}
                  keyExtractor={(r) => r.id}
                  onRowClick={
                    onDashboardClick
                      ? (row) => onDashboardClick(row.id)
                      : undefined
                  }
                />
              ) : (
                <p className="text-text-tertiary py-4 text-center text-sm">
                  {labels.noDashboards}
                </p>
              )}
            </section>
          )}

          {/* ---- SDK Snippet Section ---- */}
          {sdkSnippet && (
            <section aria-labelledby={sdkId}>
              <h3
                id={sdkId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.sdk}
              </h3>
              <CodeSnippetViewer
                code={sdkSnippet}
                language={sdkLanguage}
                title={labels.installation}
              />
            </section>
          )}
        </>
      )}

      {/* ---- Delete Dashboard Confirmation ---- */}
      <ConfirmDialog
        open={pendingDeleteId != null}
        onConfirm={handleConfirmDelete}
        onCancel={() => setPendingDeleteId(null)}
        title={labels.deleteConfirmTitle}
        description={labels.deleteConfirmDescription}
        confirmLabel={labels.deleteConfirmButton}
        cancelLabel={labels.cancelButton}
        variant="danger"
      />
    </SurfaceLayout>
  );
}

ProjectOverview.displayName = "ProjectOverview";