ProjectDashboard
surfaceVercel 스타일 프로젝트 개요 영역. DashboardGrid + DeploymentTimeline + EnvPanel 조합.
컴포넌트 의존 관계
깊이
100%
기본 사용
my-awesome-app
Next.js 애플리케이션
프로젝트 통계
총 배포1,234↑ +12.5%
에러율0.23%↓ -5.2%
평균 지연시간45ms→ +2ms
가동 시간99.9%↑ +0.1%
최근 배포
환경 변수
테스트 커버리지
2026년 2월 4일6/6 통과
6성공
0실패
6전체
- 통계 그리드를 렌더링한다
- 배포 타임라인을 렌더링한다
- 환경 변수 패널을 렌더링한다
- 커스텀 제목을 표시한다
- header와 actions 슬롯을 렌더링한다
- 빈 상태를 표시한다
ProjectDashboard Props
| Prop | 타입 | 기본값 | 설명 |
|---|---|---|---|
stats | StatCard[] | — | 통계 카드 데이터 배열 |
deployments | DeploymentStep[] | — | 배포 단계 배열 |
envGroups | EnvGroupDef[] | — | 환경 변수 그룹 배열 |
statsColumns | number | 3 | 통계 그리드 컬럼 수 |
statsTitle | string | "프로젝트 통계" | 통계 섹션 제목 |
deploymentsTitle | string | "최근 배포" | 배포 섹션 제목 |
envTitle | string | "환경 변수" | 환경 변수 섹션 제목 |
header | ReactNode | — | 상단 헤더 영역 콘텐츠 |
actions | ReactNode | — | 헤더 우측 액션 버튼 영역 |
className | string | — | 최외곽 컨테이너 CSS 클래스 |
Surface 설치
CLI가 공식 배포 채널입니다. 필요한 Surface를 프로젝트로 복사한 뒤 직접 수정할 수 있습니다.
bash
npx @reopt-ai/opt-cli surface add project-dashboardConsumer target
복사된 파일은 components/surfaces 아래에 저장됩니다.
tsx
import { ProjectDashboard } from "@/components/surfaces/project-dashboard";Registry metadata
- 설명
- Vercel 스타일 프로젝트 개요 영역. DashboardGrid + DeploymentTimeline + EnvPanel 조합.
- 파일 수
- 1개
- Registry dependencies
- 없음
- Package dependencies
- lucide-react
- 태그
- dashboard
- Install notes
- Install additional packages: lucide-react.
포함 파일
project-dashboard.tsxproject-dashboard.tsx
Surface 소스 보기
project-dashboard.tsx
import type { ReactNode } from "react";
import { ChartColumn } from "lucide-react";
import { DashboardGrid, DeploymentTimeline, EnvPanel, SurfaceLayout, cn, OPT_TEXT_PRIMARY, OPT_TEXT_SECONDARY } from "@reopt-ai/opt-ui";
import type { StatCard as StatCardType, DeploymentStep, EnvGroupDef } from "@reopt-ai/opt-ui";
/** Localized labels for the project dashboard component. */
export interface ProjectDashboardLabels {
stats?: string;
deployments?: string;
env?: string;
emptyTitle?: string;
emptyDescription?: string;
}
const defaultLabels: Required<ProjectDashboardLabels> = {
stats: "프로젝트 통계",
deployments: "최근 배포",
env: "환경 변수",
emptyTitle: "데이터가 없습니다",
emptyDescription:
"프로젝트 통계, 배포 이력, 환경 변수 중 하나 이상을 전달하세요.",
};
/** Props for the project dashboard component. */
export interface ProjectDashboardProps {
stats?: StatCardType[];
deployments?: DeploymentStep[];
envGroups?: EnvGroupDef[];
statsColumns?: number;
statsTitle?: string;
deploymentsTitle?: string;
envTitle?: string;
header?: ReactNode;
actions?: ReactNode;
/** Custom visualization section rendered below stats (e.g., TrendChart, CalendarHeatmap) */
chartSection?: ReactNode;
className?: string;
loading?: boolean;
labels?: ProjectDashboardLabels;
}
/** Renders the project dashboard component. */
export function ProjectDashboard({
stats,
deployments,
envGroups,
statsColumns = 3,
statsTitle,
deploymentsTitle,
envTitle,
header,
actions,
chartSection,
className,
loading = false,
labels: customLabels,
}: ProjectDashboardProps) {
const labels = { ...defaultLabels, ...customLabels };
const hasStats = stats && stats.length > 0;
const hasDeployments = deployments && deployments.length > 0;
const hasEnvGroups = envGroups && envGroups.length > 0;
return (
<SurfaceLayout
loading={loading}
className={className}
data-opt-id="3E50W"
data-opt-slug="project-dashboard.ProjectDashboard"
>
{/* 헤더 영역 */}
{(header || actions) && (
<div className="flex items-center justify-between">
{header && <div className="gap-element flex flex-col">{header}</div>}
{actions && (
<div className="gap-element flex items-center">{actions}</div>
)}
</div>
)}
{/* 통계 그리드 */}
{hasStats && (
<section aria-labelledby="stats-heading">
<h3
id="stats-heading"
className="text-text-primary mb-4 text-lg font-semibold"
>
{statsTitle ?? labels.stats}
</h3>
<DashboardGrid stats={stats} columns={statsColumns} />
</section>
)}
{/* 차트/시각화 영역 */}
{chartSection}
{/* 배포 및 환경변수 2컬럼 레이아웃 */}
{(hasDeployments || hasEnvGroups) && (
<div
className={cn(
"gap-section grid",
hasDeployments && hasEnvGroups
? "lg:grid-cols-2"
: "lg:grid-cols-1",
)}
>
{/* 배포 타임라인 */}
{hasDeployments && (
<section
aria-labelledby="deployments-heading"
className="border-border bg-surface rounded-xl border p-6"
>
<h3
id="deployments-heading"
className="text-text-primary mb-4 text-lg font-semibold"
>
{deploymentsTitle ?? labels.deployments}
</h3>
<DeploymentTimeline steps={deployments} title="" />
</section>
)}
{/* 환경변수 패널 */}
{hasEnvGroups && (
<section
aria-labelledby="env-heading"
className="border-border bg-surface rounded-xl border p-6"
>
<h3
id="env-heading"
className="text-text-primary mb-4 text-lg font-semibold"
>
{envTitle ?? labels.env}
</h3>
<EnvPanel groups={envGroups} />
</section>
)}
</div>
)}
{/* 빈 상태 */}
{!hasStats && !hasDeployments && !hasEnvGroups && !chartSection && (
<div className="flex flex-col items-center justify-center py-16 text-center">
<ChartColumn className="mb-4 size-10" aria-hidden />
<h3 className={cn("text-lg font-medium", OPT_TEXT_PRIMARY)}>
{labels.emptyTitle}
</h3>
<p className={cn("mt-1 text-sm", OPT_TEXT_SECONDARY)}>
{labels.emptyDescription}
</p>
</div>
)}
</SurfaceLayout>
);
}