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.

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";