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.

ProjectSettings

surface

프로젝트 설정 Surface. SettingsForm + DataTable + CodeSnippetViewer + Retention Policy + Cleanup Preview + Storage Usage 조합.

컴포넌트 의존 관계

깊이
▼ USES (7)ProjectSettingssettings-formdata-tablecode-snippet-viewerconfirm-dialogloading-overlaybuttonprogress
100%

기본 사용

Project Settings

Project Settings

API Keys

Data Table

NameAPI KeyCreated
Productionpk_live_abc123...2024-01-15
Developmentpk_test_xyz789...2024-02-01

SDK Integration

Installation
1npm install @reopt-ai/sdk\n\nimport { init } from '@reopt-ai/sdk';\ninit({ apiKey: 'pk_live_abc123' });

테스트 커버리지

2026년 2월 4일

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

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

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

ProjectSettings Props

Prop타입기본값설명
formFields*FormFieldDef[]—폼 필드 정의 배열
onFormSubmit(values: Record<string, unknown>) => void—폼 제출 핸들러
apiKeysApiKeyRow[]—API 키 배열
sdkSnippetstring—SDK 설치 코드 스니펫
retentionPolicyRetentionPolicyDef—데이터 보존 정책 옵션
onRetentionPolicyChange(policyId: string) => void—보존 정책 변경 핸들러
cleanupPreviewCleanupPreviewRow[]—클린업 미리보기 데이터
onCleanup() => void—클린업 실행 핸들러
cleanupConfirmRequiredbooleantrue클린업 확인 다이얼로그 필요 여부
storageUsageStorageUsageRow[]—스토리지 사용량 데이터

Surface 설치

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

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

Consumer target

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

tsx
import { ProjectSettings } from "@/components/surfaces/project-settings";

Registry metadata

설명
프로젝트 설정 Surface. SettingsForm + DataTable + CodeSnippetViewer + Retention Policy + Cleanup Preview + Storage Usage 조합.
파일 수
1개
Registry dependencies
없음
Package dependencies
없음
태그
formtablesettings
Install notes
없음

포함 파일

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

import * as React from "react";
import {
  Button,
  Progress,
  SettingsForm,
  DataTable,
  CodeSnippetViewer,
  ConfirmDialog,
  SurfaceLayout,
  type FormFieldDef,
  type ColumnDef,
} from "@reopt-ai/opt-ui";

interface ApiKeyRow {
  name: string;
  key: string;
  created: string;
}

/* ------------------------------------------------------------------ */
/*  Retention / Cleanup / Storage types                                */
/* ------------------------------------------------------------------ */

/** Option shape for `RetentionPolicy`. */
export interface RetentionPolicyOption {
  id: string;
  label: string;
  description?: string;
}

/** Definition shape for `RetentionPolicy`. */
export interface RetentionPolicyDef {
  current: string;
  options: RetentionPolicyOption[];
}

/** Row shape for `CleanupPreview`. */
export interface CleanupPreviewRow {
  table: string;
  rows: number;
  size: string;
}

/** Row shape for `StorageUsage`. */
export interface StorageUsageRow {
  table: string;
  rows: number;
  size: string;
  percentage: number;
}

/* ------------------------------------------------------------------ */
/*  Labels                                                             */
/* ------------------------------------------------------------------ */

/** Labels for `ProjectSettings`. */
export interface ProjectSettingsLabels {
  settings?: string;
  apiKeys?: string;
  sdk?: string;
  installation?: string;
  retentionTitle?: string;
  retentionDescription?: string;
  cleanupTitle?: string;
  cleanupButton?: string;
  cleanupWarning?: string;
  cleanupConfirmTitle?: string;
  cleanupConfirmDescription?: string;
  cleanupConfirmButton?: string;
  cancelButton?: string;
  storageTitle?: string;
  emptyTitle?: string;
  emptyDescription?: string;
}

const defaultLabels: Required<ProjectSettingsLabels> = {
  settings: "Project Settings",
  apiKeys: "API Keys",
  sdk: "SDK Integration",
  installation: "Installation",
  retentionTitle: "Data Retention Policy",
  retentionDescription:
    "Choose how long to keep your project data before automatic cleanup.",
  cleanupTitle: "Data Cleanup Preview",
  cleanupButton: "Run Cleanup",
  cleanupWarning:
    "Running cleanup will permanently delete the data listed below. This action cannot be undone.",
  cleanupConfirmTitle: "Confirm Data Cleanup",
  cleanupConfirmDescription:
    "Are you sure you want to permanently delete the data listed above? This action cannot be undone.",
  cleanupConfirmButton: "Run Cleanup",
  cancelButton: "Cancel",
  storageTitle: "Storage Usage",
  emptyTitle: "No settings configured",
  emptyDescription: "Project settings will appear here once configured.",
};

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

/** Props for `ProjectSettings`. */
export interface ProjectSettingsProps {
  formFields: FormFieldDef[];
  onFormSubmit?: (values: Record<string, unknown>) => void;
  apiKeys?: ApiKeyRow[];
  apiKeyColumns?: ColumnDef<ApiKeyRow>[];
  sdkSnippet?: string;
  sdkLanguage?: string;
  /** Retention policy configuration */
  retentionPolicy?: RetentionPolicyDef;
  /** Retention policy change handler */
  onRetentionPolicyChange?: (policyId: string) => void;
  /** Cleanup preview data */
  cleanupPreview?: CleanupPreviewRow[];
  /** Manual cleanup trigger */
  onCleanup?: () => void;
  /** Require confirmation for cleanup (default: true) */
  cleanupConfirmRequired?: boolean;
  /** Storage usage breakdown */
  storageUsage?: StorageUsageRow[];
  /** Extra sections rendered after all built-in sections */
  extraSections?: React.ReactNode;
  header?: React.ReactNode;
  actions?: React.ReactNode;
  labels?: ProjectSettingsLabels;
  className?: string;
  loading?: boolean;
}

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

const defaultApiKeyColumns: ColumnDef<ApiKeyRow>[] = [
  { id: "name", header: "Name", accessor: "name" },
  { id: "key", header: "API Key", accessor: "key" },
  { id: "created", header: "Created", accessor: "created" },
];

const cleanupPreviewColumns: ColumnDef<CleanupPreviewRow>[] = [
  { id: "table", header: "Table", accessor: "table" },
  {
    id: "rows",
    header: "Rows",
    accessor: (row) => row.rows.toLocaleString(),
  },
  { id: "size", header: "Size", accessor: "size" },
];

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

/** Renders the `ProjectSettings` component. */
export function ProjectSettings({
  formFields,
  onFormSubmit,
  apiKeys = [],
  apiKeyColumns = defaultApiKeyColumns,
  sdkSnippet,
  sdkLanguage = "typescript",
  retentionPolicy,
  onRetentionPolicyChange,
  cleanupPreview,
  onCleanup,
  cleanupConfirmRequired = true,
  storageUsage,
  extraSections,
  header,
  actions,
  labels: customLabels,
  className,
  loading = false,
}: ProjectSettingsProps) {
  const labels = { ...defaultLabels, ...customLabels };
  const titleId = React.useId();
  const settingsId = React.useId();
  const apiKeysId = React.useId();
  const sdkId = React.useId();
  const retentionId = React.useId();
  const cleanupId = React.useId();
  const storageId = React.useId();

  const [showCleanupConfirm, setShowCleanupConfirm] = React.useState(false);

  const isEmpty = formFields.length === 0;

  const handleCleanup = () => {
    if (cleanupConfirmRequired) {
      setShowCleanupConfirm(true);
    } else {
      onCleanup?.();
    }
  };

  const handleCleanupConfirm = () => {
    setShowCleanupConfirm(false);
    onCleanup?.();
  };

  const storageColumns = React.useMemo<ColumnDef<StorageUsageRow>[]>(
    () => [
      { id: "table", header: "Table", accessor: "table" },
      {
        id: "rows",
        header: "Rows",
        accessor: (row) => row.rows.toLocaleString(),
      },
      { id: "size", header: "Size", accessor: "size" },
      {
        id: "usage",
        header: "Usage",
        accessor: () => "",
        cell: (row: StorageUsageRow) => (
          <div className="gap-element flex items-center">
            <Progress
              value={row.percentage}
              max={100}
              size="sm"
              variant={
                row.percentage >= 90
                  ? "error"
                  : row.percentage >= 70
                    ? "warning"
                    : "default"
              }
              className="w-20"
            />
            <span className="text-text-tertiary text-xs">
              {row.percentage}%
            </span>
          </div>
        ),
      },
    ],
    [],
  );

  const hasRetention = retentionPolicy && retentionPolicy.options.length > 0;
  const hasCleanup = cleanupPreview && cleanupPreview.length > 0;
  const hasStorage = storageUsage && storageUsage.length > 0;

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

      {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>
      ) : (
        <>
          <section aria-labelledby={settingsId}>
            <h3
              id={settingsId}
              className="text-text-tertiary mb-2 text-sm font-medium"
            >
              {labels.settings}
            </h3>
            <SettingsForm fields={formFields} onSubmit={onFormSubmit} />
          </section>

          {apiKeys.length > 0 && (
            <section aria-labelledby={apiKeysId}>
              <h3
                id={apiKeysId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.apiKeys}
              </h3>
              <DataTable
                columns={apiKeyColumns}
                data={apiKeys}
                keyExtractor={(r) => r.key}
              />
            </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>
          )}

          {/* ---- Retention Policy Section ---- */}
          {hasRetention && (
            <section aria-labelledby={retentionId}>
              <h3
                id={retentionId}
                className="text-text-tertiary mb-1 text-sm font-medium"
              >
                {labels.retentionTitle}
              </h3>
              <p className="text-text-tertiary mb-3 text-xs">
                {labels.retentionDescription}
              </p>
              <div
                className="gap-element flex flex-col"
                role="radiogroup"
                aria-labelledby={retentionId}
              >
                {retentionPolicy.options.map((option) => {
                  const isSelected = retentionPolicy.current === option.id;
                  return (
                    <button
                      key={option.id}
                      type="button"
                      role="radio"
                      aria-checked={isSelected}
                      onClick={() => onRetentionPolicyChange?.(option.id)}
                      className={`gap-group flex items-start rounded-lg border p-3 text-left transition-colors ${
                        isSelected
                          ? "border-accent bg-accent/5"
                          : "border-border-subtle hover:bg-bg-subtle"
                      }`}
                    >
                      <span
                        className={`mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border-2 ${
                          isSelected ? "border-accent" : "border-border"
                        }`}
                      >
                        {isSelected && (
                          <span className="bg-accent h-2 w-2 rounded-full" />
                        )}
                      </span>
                      <div className="flex flex-col">
                        <span className="text-text-primary text-sm font-medium">
                          {option.label}
                        </span>
                        {option.description && (
                          <span className="text-text-tertiary text-xs">
                            {option.description}
                          </span>
                        )}
                      </div>
                    </button>
                  );
                })}
              </div>
            </section>
          )}

          {/* ---- Cleanup Preview Section ---- */}
          {hasCleanup && (
            <section aria-labelledby={cleanupId}>
              <h3
                id={cleanupId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.cleanupTitle}
              </h3>
              <DataTable
                columns={cleanupPreviewColumns}
                data={cleanupPreview}
                keyExtractor={(r) => r.table}
              />
              {onCleanup && (
                <div className="mt-3">
                  <p className="text-text-tertiary mb-2 text-xs">
                    {labels.cleanupWarning}
                  </p>
                  <Button variant="danger" size="sm" onClick={handleCleanup}>
                    {labels.cleanupButton}
                  </Button>
                </div>
              )}
            </section>
          )}

          {/* ---- Storage Usage Section ---- */}
          {hasStorage && (
            <section aria-labelledby={storageId}>
              <h3
                id={storageId}
                className="text-text-tertiary mb-2 text-sm font-medium"
              >
                {labels.storageTitle}
              </h3>
              <DataTable
                columns={storageColumns}
                data={storageUsage}
                keyExtractor={(r) => r.table}
              />
            </section>
          )}

          {extraSections}
        </>
      )}

      {/* ---- Cleanup Confirmation Dialog ---- */}
      <ConfirmDialog
        open={showCleanupConfirm}
        onConfirm={handleCleanupConfirm}
        onCancel={() => setShowCleanupConfirm(false)}
        title={labels.cleanupConfirmTitle}
        description={labels.cleanupConfirmDescription}
        confirmLabel={labels.cleanupConfirmButton}
        cancelLabel={labels.cancelButton}
        variant="danger"
      />
    </SurfaceLayout>
  );
}

ProjectSettings.displayName = "ProjectSettings";