AcquisitionAnalysis
surface획득 분석 Surface. 3탭(Overview, Channels, Campaigns) + StatCard + LineChart + DataTable 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
Acquisition
New Users2,345↑ +18%
Signups890↑ +12%
Activation67%↑ +3%
CAC$32↓ -$5
테스트 커버리지
2026년 2월 4일생성된 테스트 결과를 찾지 못했습니다.
AcquisitionAnalysis 항목이 문서 메타에 연결되어 있지만 현재 생성 파일에는 없습니다.
테스트를 추가한 뒤 `bun run generate:test-results`를 실행하거나 `testDescribe` 매핑을 다시 확인하세요.
AcquisitionAnalysis Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
stats* | AcquisitionStat[] | — | 통계 카드 배열 |
trendData | ChartDataPoint[] | — | 추세 차트 데이터 |
trendSeries | ChartSeriesDef[] | — | 추세 시리즈 정의 |
channelData | ChannelRow[] | — | 채널 테이블 데이터 |
campaignData | CampaignRow[] | — | 캠페인 테이블 데이터 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add acquisition-analysisConsumer 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.tsxacquisition-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";