reopt designreopt design
DocsExploreToolsPricingBuilder
시작하기
개요
시작하기
Next.js 설치
Private install
핵심 개념
아키텍처
컴포지션 패턴
접근성
키보드 패턴
스타일링
테마 시스템
고급 패턴
구축·운영
Skills
AI 연동
CLI (opt surface add)
의존 그래프
도구
Canvas 카탈로그
Theme Builder
Form Builder
템플릿
템플릿
릴리즈
릴리즈 노트
Oopt-ui
reopt designreopt design

AI 시대를 위한 디자인 시스템

  • 문서
  • 가격
  • 릴리즈 노트
  • GitHub
  • 서비스 약관
  • 개인정보처리방침

© 2026 reopt-ai. All rights reserved.

InsightsDashboard

surface

인사이트 대시보드 Surface. InsightsPanel 래핑 + 탐지 실행, 시간 범위 필터, 에러 표시, dismissed 토글, 탐지 메서드/파라미터 제어.

컴포넌트 의존 관계

깊이
▼ USES (4)InsightsDashboardinsights-panelswitchtime-range-selectorloading-overlay
100%

기본 사용

Insights

Show dismissed
Anomaly Detection: Detect statistical anomalies in metric trendsThreshold Alerts: Alert when metrics cross configured thresholdsTrend Analysis: Identify long-term trends and seasonal patterns

Detection Settings

!!
Signup conversion dropcritical

Signup-to-onboarding conversion dropped 18% compared to the previous 7 days.

conversion_rate: -18%
!
Session duration spikewarning

Average session duration increased by 32% on mobile devices.

session_duration: +32%
i
API error rate normalinfo

API error rate returned to baseline after yesterday's spike.

error_rate: 0.3%

테스트 커버리지

2026년 2월 4일

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

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

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

InsightsDashboard Props

Prop타입기본값설명
insights*InsightItem[]—인사이트 목록
statsInsightsStat[]—인사이트 통계 카드
detectionTypesDetectionTypeDef[]—탐지 유형 토글
onDetectionTypeToggle(typeId: string, enabled: boolean) => void—탐지 유형 변경 핸들러
detectionConfigDetectionConfig—탐지 파라미터 (days, z-score)
onDetectionConfigChange(config: DetectionConfig) => void—탐지 파라미터 변경 핸들러
detectionsArray<{ id: string; name: string; enabled: boolean; description?: string }>—탐지 메서드 정의 (detectionTypes 별칭, description 포함)
onDetectionToggle(detectionId: string, enabled: boolean) => void—탐지 메서드 토글 (onDetectionTypeToggle 별칭)
detectionParamsRecord<string, number>—탐지 파라미터 key-value (detectionConfig 별칭)
onDetectionParamChange(key: string, value: number) => void—탐지 파라미터 변경 핸들러 (onDetectionConfigChange 별칭)
showDismissedboolean—dismissed 인사이트 표시 여부 (제어 모드)
onShowDismissedChange(show: boolean) => void—dismissed 토글 변경 핸들러
onDismiss(id: string) => void—인사이트 무시 핸들러
onDismissInsight(insightId: string) => void—인사이트 무시 핸들러 (onDismiss 별칭)
onInsightClick(insight: InsightItem) => void—인사이트 클릭 핸들러
onStatClick(statId: string) => void—통계 카드 클릭 핸들러
onRun() => void—탐지 실행 핸들러
runningboolean—탐지 실행 중 상태
errorstring—에러 메시지
timeRangeDateRange—시간 범위 필터
onTimeRangeChange(range: DateRange) => void—시간 범위 변경 핸들러
loadingboolean—로딩 상태
headerReactNode—커스텀 헤더 슬롯
actionsReactNode—액션 버튼 슬롯
labelsInsightsDashboardLabels—커스텀 레이블
classNamestring—최외곽 CSS 클래스

Surface 설치

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

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

Consumer target

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

tsx
import { InsightsDashboard } from "@/components/surfaces/insights-dashboard";

Registry metadata

설명
인사이트 대시보드 Surface. InsightsPanel 래핑 + 탐지 실행, 시간 범위 필터, 에러 표시, dismissed 토글, 탐지 메서드/파라미터 제어.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
dashboard
Install notes
없음

포함 파일

  • insights-dashboard.tsx→insights-dashboard.tsx
Surface 소스 보기
insights-dashboard.tsx
"use client";

import * as React from "react";
import {
  InsightsPanel,
  Button,
  Alert,
  Switch,
  SurfaceLayout,
  TimeRangeSelector,
  type InsightItem,
  type InsightsStat,
  type DetectionTypeDef,
  type DetectionConfig,
  type InsightsPanelLabels,
  type DateRange,
} from "@reopt-ai/opt-ui";

/** Labels for `InsightsDashboard`. */
export interface InsightsDashboardLabels extends InsightsPanelLabels {
  dashboard?: string;
  runDetection?: string;
  running?: string;
  emptyTitle?: string;
  emptyDescription?: string;
  emptyRunAction?: string;
  /** Label for the show dismissed toggle at surface level */
  showDismissedLabel?: string;
  /** Title for the detection settings panel */
  detectionPanelTitle?: string;
  /** Label for dismiss insight action */
  dismissButton?: string;
}

const defaultLabels: Required<InsightsDashboardLabels> = {
  dashboard: "Insights",
  runDetection: "Run Detection",
  running: "Running...",
  emptyTitle: "No insights yet",
  emptyDescription: "Run anomaly detection to discover insights.",
  emptyRunAction: "Run Detection",
  showDismissedLabel: "Show dismissed",
  detectionPanelTitle: "Detection Settings",
  dismissButton: "Dismiss",
  title: "",
  dismiss: "Dismiss",
  showDismissed: "Show dismissed",
  hideDismissed: "Hide dismissed",
  detectionTitle: "Detection Settings",
  daysLabel: "Days",
  zScoreLabel: "Z-Score",
};

/** Props for `InsightsDashboard`. */
export interface InsightsDashboardProps {
  insights: InsightItem[];
  stats?: InsightsStat[];
  detectionTypes?: DetectionTypeDef[];
  onDetectionTypeToggle?: (typeId: string, enabled: boolean) => void;
  detectionConfig?: DetectionConfig;
  onDetectionConfigChange?: (config: DetectionConfig) => void;
  onDismiss?: (id: string) => void;
  onInsightClick?: (insight: InsightItem) => void;
  onStatClick?: (statId: string) => void;
  /** Trigger detection run */
  onRun?: () => void;
  /** Whether detection is currently running */
  running?: boolean;
  /** Error message to display */
  error?: string;
  /** Controlled show/hide dismissed insights toggle */
  showDismissed?: boolean;
  /** Callback when show dismissed toggle changes */
  onShowDismissedChange?: (show: boolean) => void;
  /** Alias for onDismiss — dismiss a specific insight */
  onDismissInsight?: (insightId: string) => void;
  /** Detection method definitions (alias for detectionTypes) */
  detections?: Array<{
    id: string;
    name: string;
    enabled: boolean;
    description?: string;
  }>;
  /** Toggle a detection method (alias for onDetectionTypeToggle) */
  onDetectionToggle?: (detectionId: string, enabled: boolean) => void;
  /** Detection parameters as key-value record */
  detectionParams?: Record<string, number>;
  /** Callback when a detection parameter changes */
  onDetectionParamChange?: (key: string, value: number) => void;
  loading?: boolean;
  timeRange?: DateRange;
  onTimeRangeChange?: (range: DateRange) => void;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: InsightsDashboardLabels;
  className?: string;
}

/** Renders the `InsightsDashboard` component. */
export function InsightsDashboard({
  insights,
  stats,
  detectionTypes,
  onDetectionTypeToggle,
  detectionConfig,
  onDetectionConfigChange,
  onDismiss,
  onInsightClick,
  onStatClick,
  onRun,
  running = false,
  error,
  showDismissed: controlledShowDismissed,
  onShowDismissedChange,
  onDismissInsight,
  detections,
  onDetectionToggle,
  detectionParams,
  onDetectionParamChange,
  loading = false,
  timeRange,
  onTimeRangeChange,
  header,
  actions,
  labels: customLabels,
  className,
}: InsightsDashboardProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();

  // Merge detections alias into detectionTypes
  const mergedDetectionTypes: DetectionTypeDef[] | undefined =
    detectionTypes ??
    detections?.map((d) => ({ id: d.id, label: d.name, enabled: d.enabled }));
  const mergedDetectionToggle = onDetectionTypeToggle ?? onDetectionToggle;

  // Merge detectionParams into detectionConfig
  const mergedDetectionConfig: DetectionConfig | undefined =
    detectionConfig ??
    (detectionParams
      ? {
          days: detectionParams.days,
          zScoreThreshold: detectionParams.zScoreThreshold,
        }
      : undefined);
  const mergedDetectionConfigChange =
    onDetectionConfigChange ??
    (onDetectionParamChange
      ? (config: DetectionConfig) => {
          if (config.days != null) onDetectionParamChange("days", config.days);
          if (config.zScoreThreshold != null)
            onDetectionParamChange("zScoreThreshold", config.zScoreThreshold);
        }
      : undefined);

  // Merge dismiss callback
  const mergedDismiss = onDismiss ?? onDismissInsight;

  // Controlled vs uncontrolled showDismissed
  const [internalShowDismissed, setInternalShowDismissed] =
    React.useState(false);
  const isControlled = controlledShowDismissed !== undefined;
  const showDismissed = isControlled
    ? controlledShowDismissed
    : internalShowDismissed;
  const toggleShowDismissed = () => {
    const next = !showDismissed;
    if (onShowDismissedChange) {
      onShowDismissedChange(next);
    }
    if (!isControlled) {
      setInternalShowDismissed(next);
    }
  };

  const isEmpty =
    insights.length === 0 &&
    (!stats || stats.length === 0) &&
    (!mergedDetectionTypes || mergedDetectionTypes.length === 0) &&
    mergedDetectionConfig == null;

  const runButton = onRun ? (
    <Button variant="primary" size="sm" onClick={onRun} disabled={running}>
      {running ? labels.running : labels.runDetection}
    </Button>
  ) : null;

  const panelLabels: InsightsPanelLabels = {
    title: labels.title,
    dismiss: labels.dismissButton || labels.dismiss,
    showDismissed: labels.showDismissed,
    hideDismissed: labels.hideDismissed,
    emptyTitle: labels.emptyTitle,
    emptyDescription: labels.emptyDescription,
    detectionTitle: labels.detectionPanelTitle || labels.detectionTitle,
    daysLabel: labels.daysLabel,
    zScoreLabel: labels.zScoreLabel,
  };

  // Filter insights based on showDismissed state at surface level
  const visibleInsights = showDismissed
    ? insights
    : insights.filter((i) => !i.dismissed);

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

      {/* Show dismissed toggle (controlled or uncontrolled at surface level) */}
      {(onShowDismissedChange || !isControlled) && insights.length > 0 && (
        <div className="gap-element flex items-center">
          <Switch
            checked={showDismissed}
            onChange={toggleShowDismissed}
            size="sm"
          />
          <span className="text-text-secondary text-sm">
            {labels.showDismissedLabel}
          </span>
        </div>
      )}

      {/* Detection descriptions (from detections alias) */}
      {detections &&
        detections.some((d) => d.description) &&
        !detectionTypes && (
          <div className="gap-element flex flex-wrap">
            {detections
              .filter((d) => d.description)
              .map((d) => (
                <span
                  key={d.id}
                  className="text-text-tertiary text-xs"
                  title={d.description}
                >
                  <span className="font-medium">{d.name}:</span> {d.description}
                </span>
              ))}
          </div>
        )}

      {error && (
        <Alert variant="error" aria-live="polite">
          {error}
        </Alert>
      )}

      {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>
          {onRun && (
            <Button
              variant="primary"
              size="sm"
              className="mt-4"
              onClick={onRun}
              disabled={running}
            >
              {running ? labels.running : labels.emptyRunAction}
            </Button>
          )}
        </div>
      ) : (
        <InsightsPanel
          insights={visibleInsights}
          stats={stats}
          detectionTypes={mergedDetectionTypes}
          onDetectionTypeToggle={mergedDetectionToggle}
          detectionConfig={mergedDetectionConfig}
          onDetectionConfigChange={mergedDetectionConfigChange}
          onDismiss={mergedDismiss}
          onInsightClick={onInsightClick}
          onStatClick={onStatClick}
          labels={panelLabels}
        />
      )}
    </SurfaceLayout>
  );
}

InsightsDashboard.displayName = "InsightsDashboard";