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.

AcquisitionAnalysis

surface

획득 분석 Surface. 3탭(Overview, Channels, Campaigns) + StatCard + LineChart + DataTable 조합.

컴포넌트 의존 관계

깊이
▼ USES (8)AcquisitionAnalysisline-chartbar-chartfilter-bardata-tableloading-overlaysummary-rowtime-range-selectortabs
100%

기본 사용

Acquisition

Statistics
New Users2,345↑ +18%
Signups890↑ +12%
Activation67%↑ +3%
CAC$32↓ -$5
Acquisition details

테스트 커버리지

2026년 2월 4일

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

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

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

AcquisitionAnalysis Props

Prop타입기본값설명
stats*AcquisitionStat[]—통계 카드 배열
trendDataChartDataPoint[]—추세 차트 데이터
trendSeriesChartSeriesDef[]—추세 시리즈 정의
channelDataChannelRow[]—채널 테이블 데이터
campaignDataCampaignRow[]—캠페인 테이블 데이터

Surface 설치

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

bash
npx @reopt-ai/opt-cli surface add acquisition-analysis

Consumer target

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

tsx
import { AcquisitionAnalysis } from "@/components/surfaces/acquisition-analysis";

Registry metadata

설명
획득 분석 Surface. 3탭(Overview, Channels, Campaigns) + StatCard + LineChart + DataTable 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
chartfiltertableanalytics
Install notes
없음

포함 파일

  • acquisition-analysis.tsx→acquisition-analysis.tsx
Surface 소스 보기
acquisition-analysis.tsx
"use client";

import * as React from "react";
import {
  TabsRoot,
  TabList,
  Tab,
  TabPanel,
  FilterBar,
  DataTable,
  SurfaceLayout,
  SummaryRow,
  TimeRangeSelector,
  type ColumnDef,
  type FilterGroupDef,
  type DateRange,
} from "@reopt-ai/opt-ui";
import {
  LineChart,
  BarChart,
  type ChartDataPoint,
  type ChartSeriesDef,
} from "@reopt-ai/opt-charts";

interface ChannelRow {
  id: string;
  channel: string;
  users: string;
  conversion: string;
  [k: string]: unknown;
}
interface CampaignRow {
  id: string;
  campaign: string;
  impressions: string;
  clicks: string;
  ctr: string;
  [k: string]: unknown;
}
interface ReferrerRow {
  id: string;
  [k: string]: unknown;
}

/** Statistic shape for `Acquisition`. */
export interface AcquisitionStat {
  id: string;
  title: string;
  value: string;
  change: string;
  trend: "up" | "down" | "neutral";
}

/** Labels for `AcquisitionAnalysis`. */
export interface AcquisitionAnalysisLabels {
  title?: string;
  overview?: string;
  channels?: string;
  campaigns?: string;
  referrers?: string;
  attribution?: string;
  filtersLabel?: string;
  statsLabel?: string;
  detailsLabel?: string;
  emptyTitle?: string;
  emptyDescription?: string;
}

const defaultLabels: Required<AcquisitionAnalysisLabels> = {
  title: "Acquisition",
  overview: "Overview",
  channels: "Channels",
  campaigns: "Campaigns",
  referrers: "Referrers",
  attribution: "Attribution",
  filtersLabel: "Filters",
  statsLabel: "Statistics",
  detailsLabel: "Acquisition details",
  emptyTitle: "No acquisition data",
  emptyDescription: "There is no acquisition data to display yet.",
};

/** Props for `AcquisitionAnalysis`. */
export interface AcquisitionAnalysisProps {
  stats: AcquisitionStat[];
  trendData?: ChartDataPoint[];
  trendSeries?: ChartSeriesDef[];
  channelData?: ChannelRow[];
  channelColumns?: ColumnDef<ChannelRow>[];
  campaignData?: CampaignRow[];
  campaignColumns?: ColumnDef<CampaignRow>[];
  /** Referrer domain table data */
  referrerData?: ReferrerRow[];
  referrerColumns?: ColumnDef<ReferrerRow>[];
  /** Attribution bar chart data (e.g., first/last touch model) */
  attributionData?: ChartDataPoint[];
  attributionSeries?: ChartSeriesDef[];
  /** FilterBar definitions (e.g., conversion event, date range) */
  filters?: FilterGroupDef[];
  onFilterChange?: (
    filterId: string,
    value: string | string[] | boolean,
  ) => void;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: AcquisitionAnalysisLabels;
  loading?: boolean;
  timeRange?: DateRange;
  onTimeRangeChange?: (range: DateRange) => void;
  className?: string;
}

/** Renders the `AcquisitionAnalysis` component. */
export function AcquisitionAnalysis({
  stats,
  trendData = [],
  trendSeries = [],
  channelData = [],
  channelColumns = [
    { id: "channel", header: "Channel", accessor: "channel" as const },
    { id: "users", header: "Users", accessor: "users" as const },
    { id: "conversion", header: "Conversion", accessor: "conversion" as const },
  ],
  campaignData = [],
  campaignColumns = [
    { id: "campaign", header: "Campaign", accessor: "campaign" as const },
    {
      id: "impressions",
      header: "Impressions",
      accessor: "impressions" as const,
    },
    { id: "clicks", header: "Clicks", accessor: "clicks" as const },
    { id: "ctr", header: "CTR", accessor: "ctr" as const },
  ],
  referrerData,
  referrerColumns = [
    { id: "domain", header: "Domain", accessor: "domain" as keyof ReferrerRow },
    { id: "visits", header: "Visits", accessor: "visits" as keyof ReferrerRow },
    {
      id: "conversion",
      header: "Conversion",
      accessor: "conversion" as keyof ReferrerRow,
    },
  ],
  attributionData,
  attributionSeries,
  filters,
  onFilterChange,
  header,
  actions,
  labels: customLabels,
  loading = false,
  timeRange,
  onTimeRangeChange,
  className,
}: AcquisitionAnalysisProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const sectionId = React.useId();

  const isEmpty = stats.length === 0;
  const hasFilters = filters && filters.length > 0;
  const hasReferrers = referrerData && referrerData.length > 0;
  const hasAttribution =
    attributionData &&
    attributionData.length > 0 &&
    attributionSeries &&
    attributionSeries.length > 0;

  return (
    <SurfaceLayout loading={loading} className={className}>
      {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 className="text-text-primary text-lg font-semibold">
            {labels.title}
          </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>
      ) : (
        <>
          {hasFilters && (
            <section aria-labelledby={`${sectionId}-filters`}>
              <span id={`${sectionId}-filters`} className="sr-only">
                {labels.filtersLabel}
              </span>
              <FilterBar filters={filters} onFilterChange={onFilterChange} />
            </section>
          )}

          <section aria-labelledby={`${sectionId}-stats`}>
            <span id={`${sectionId}-stats`} className="sr-only">
              {labels.statsLabel}
            </span>
            <SummaryRow stats={stats} columns={4} />
          </section>

          <section aria-labelledby={`${sectionId}-tabs`}>
            <span id={`${sectionId}-tabs`} className="sr-only">
              {labels.detailsLabel}
            </span>
            <TabsRoot defaultSelectedId="overview">
              <TabList>
                <Tab id="overview">{labels.overview}</Tab>
                <Tab id="channels">{labels.channels}</Tab>
                <Tab id="campaigns">{labels.campaigns}</Tab>
                {hasReferrers && <Tab id="referrers">{labels.referrers}</Tab>}
                {hasAttribution && (
                  <Tab id="attribution">{labels.attribution}</Tab>
                )}
              </TabList>
              <TabPanel tabId="overview">
                {trendData.length > 0 && trendSeries.length > 0 && (
                  <div className="pt-4">
                    <LineChart
                      data={trendData}
                      series={trendSeries}
                      aria-label="Acquisition trend"
                    />
                  </div>
                )}
              </TabPanel>
              <TabPanel tabId="channels">
                <div className="pt-4">
                  <DataTable
                    columns={channelColumns}
                    data={channelData}
                    keyExtractor={(r) => r.id}
                  />
                </div>
              </TabPanel>
              <TabPanel tabId="campaigns">
                <div className="pt-4">
                  <DataTable
                    columns={campaignColumns}
                    data={campaignData}
                    keyExtractor={(r) => r.id}
                  />
                </div>
              </TabPanel>
              {hasReferrers && (
                <TabPanel tabId="referrers">
                  <div className="pt-4">
                    <DataTable
                      columns={referrerColumns}
                      data={referrerData}
                      keyExtractor={(r) => r.id}
                    />
                  </div>
                </TabPanel>
              )}
              {hasAttribution && (
                <TabPanel tabId="attribution">
                  <div className="pt-4">
                    <BarChart
                      data={attributionData}
                      series={attributionSeries}
                      layout="vertical"
                      aria-label={labels.attribution}
                    />
                  </div>
                </TabPanel>
              )}
            </TabsRoot>
          </section>
        </>
      )}
    </SurfaceLayout>
  );
}

AcquisitionAnalysis.displayName = "AcquisitionAnalysis";