Refactor SupabaseConnectionTest component

The SupabaseConnectionTest component was refactored into smaller, more manageable components to improve readability and maintainability.
This commit is contained in:
gpt-engineer-app[bot]
2025-03-15 12:45:12 +00:00
parent 1e6d360d69
commit 971a1d29e5
10 changed files with 320 additions and 177 deletions

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
interface ConnectionTestButtonProps {
onClick: () => void;
isTesting: boolean;
}
const ConnectionTestButton: React.FC<ConnectionTestButtonProps> = ({ onClick, isTesting }) => {
return (
<Button
onClick={onClick}
disabled={isTesting}
variant="outline"
className="w-full mb-4"
>
{isTesting ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
</>
)}
</Button>
);
};
export default ConnectionTestButton;

View File

@@ -0,0 +1,99 @@
import React from 'react';
import { Info } from "lucide-react";
import { Button } from "@/components/ui/button";
import { TestResults } from '@/lib/supabase/tests/types';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger
} from "@/components/ui/collapsible";
interface DebugInfoCollapsibleProps {
testResults: TestResults;
showDebug: boolean;
setShowDebug: (show: boolean) => void;
}
const DebugInfoCollapsible: React.FC<DebugInfoCollapsibleProps> = ({
testResults,
showDebug,
setShowDebug
}) => {
if (!testResults.debugInfo) return null;
return (
<Collapsible
open={showDebug}
onOpenChange={setShowDebug}
className="border border-gray-200 rounded-md mt-4"
>
<div className="flex items-center justify-between px-4 py-2 bg-gray-50">
<h4 className="text-sm font-medium"> </h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<Info className="h-4 w-4" />
<span className="sr-only">
{showDebug ? '진단 정보 숨기기' : '진단 정보 표시'}
</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="px-4 py-2 text-xs">
<div className="space-y-2">
<div>
<span className="font-medium">Supabase URL:</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.url}
</div>
</div>
{testResults.usingProxy && (
<div>
<span className="font-medium"> URL:</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.proxyUrl}
</div>
<div className="mt-1">
: {testResults.proxyType || 'corsproxy.io'}
</div>
</div>
)}
<div>
<span className="font-medium"> :</span>
<div className="mt-1 text-green-600">
{testResults.client ? '성공' : '실패'}
</div>
</div>
{testResults.debugInfo.lastErrorDetails && (
<div>
<span className="font-medium"> :</span>
<div className="mt-1 bg-red-50 p-1 rounded break-all text-red-600">
{testResults.debugInfo.lastErrorDetails}
</div>
</div>
)}
<div>
<span className="font-medium"> :</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.debugInfo.browserInfo}
</div>
</div>
<div>
<span className="font-medium"> :</span>
<div className="mt-1">
{new Date(testResults.debugInfo.timestamp).toLocaleString()}
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
);
};
export default DebugInfoCollapsible;

View File

@@ -0,0 +1,20 @@
import React from 'react';
interface ErrorMessageCardProps {
errors: string[];
}
const ErrorMessageCard: React.FC<ErrorMessageCardProps> = ({ errors }) => {
if (errors.length === 0) return null;
return (
<div className="bg-red-50 border border-red-200 rounded p-2 mt-2">
{errors.map((error: string, index: number) => (
<p key={index} className="text-xs text-red-600 mb-1">{error}</p>
))}
</div>
);
};
export default ErrorMessageCard;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import { TestResults } from '@/lib/supabase/tests/types';
interface ProxyInfoCardProps {
testResults: TestResults;
}
const ProxyInfoCard: React.FC<ProxyInfoCardProps> = ({ testResults }) => {
if (!testResults.usingProxy) return null;
return (
<div className="bg-blue-50 border border-blue-200 rounded p-2 mt-2">
<p className="text-xs">CORS : {testResults.proxyType || 'corsproxy.io'}</p>
<p className="text-xs break-all mt-1">{testResults.proxyUrl}</p>
</div>
);
};
export default ProxyInfoCard;

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { AlertCircle } from "lucide-react";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
interface ProxyRecommendationAlertProps {
errors: string[];
}
const ProxyRecommendationAlert: React.FC<ProxyRecommendationAlertProps> = ({ errors }) => {
const hasProxyRecommendation = errors.some(err =>
err.includes('프록시 사용 시 정상 작동합니다') ||
err.includes('프록시를 선택해보세요')
);
if (!hasProxyRecommendation || errors.length === 0) return null;
const recommendationMessage = errors.find(err =>
err.includes('프록시 사용 시 정상 작동합니다') ||
err.includes('프록시를 선택해보세요')
);
return (
<Alert className="bg-amber-50 border-amber-200 mt-3">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-amber-800 text-xs font-medium"> </AlertTitle>
<AlertDescription className="text-amber-700 text-xs">
{recommendationMessage}
</AlertDescription>
</Alert>
);
};
export default ProxyRecommendationAlert;

View File

@@ -1,12 +1,18 @@
import React, { useState } from 'react';
import { Button } from "@/components/ui/button";
import { RefreshCw, Info, HelpCircle, AlertCircle } from "lucide-react";
import { toast } from "@/hooks/useToast.wrapper";
import { testSupabaseConnection } from '@/lib/supabase';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { TestResults } from '@/lib/supabase/tests/types';
// 분리된 컴포넌트들 임포트
import ConnectionTestButton from './ConnectionTestButton';
import TestResultItem from './TestResultItem';
import ProxyInfoCard from './ProxyInfoCard';
import ProxyRecommendationAlert from './ProxyRecommendationAlert';
import ErrorMessageCard from './ErrorMessageCard';
import TroubleshootingTips from './TroubleshootingTips';
import DebugInfoCollapsible from './DebugInfoCollapsible';
const SupabaseConnectionTest: React.FC = () => {
const [testResults, setTestResults] = useState<TestResults | null>(null);
const [isTesting, setIsTesting] = useState(false);
@@ -42,175 +48,34 @@ const SupabaseConnectionTest: React.FC = () => {
}
};
const hasProxyRecommendation = (errors: string[]) => {
return errors.some(err =>
err.includes('프록시 사용 시 정상 작동합니다') ||
err.includes('프록시를 선택해보세요')
);
};
return (
<div>
<h2 className="text-lg font-semibold mb-4"> </h2>
<Button
onClick={runConnectionTest}
disabled={isTesting}
variant="outline"
className="w-full mb-4"
>
{isTesting ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
</>
)}
</Button>
<ConnectionTestButton
onClick={runConnectionTest}
isTesting={isTesting}
/>
{testResults && (
<div className="mt-4 space-y-3 text-sm">
<div className="flex justify-between">
<span className="font-medium">REST API:</span>
<span className={testResults.restApi ? 'text-green-500' : 'text-red-500'}>
{testResults.restApi ? '✅ 성공' : '❌ 실패'}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">:</span>
<span className={testResults.auth ? 'text-green-500' : 'text-red-500'}>
{testResults.auth ? '✅ 성공' : '❌ 실패'}
</span>
</div>
<div className="flex justify-between">
<span className="font-medium">:</span>
<span className={testResults.database ? 'text-green-500' : 'text-red-500'}>
{testResults.database ? '✅ 성공' : '❌ 실패'}
</span>
</div>
<TestResultItem label="REST API" success={testResults.restApi} />
<TestResultItem label="인증" success={testResults.auth} />
<TestResultItem label="데이터베이스" success={testResults.database} />
{testResults.usingProxy && (
<div className="bg-blue-50 border border-blue-200 rounded p-2 mt-2">
<p className="text-xs">CORS : {testResults.proxyType || 'corsproxy.io'}</p>
<p className="text-xs break-all mt-1">{testResults.proxyUrl}</p>
</div>
)}
<ProxyInfoCard testResults={testResults} />
{testResults.errors.length > 0 && hasProxyRecommendation(testResults.errors) && (
<Alert className="bg-amber-50 border-amber-200 mt-3">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-amber-800 text-xs font-medium"> </AlertTitle>
<AlertDescription className="text-amber-700 text-xs">
{testResults.errors.find(err =>
err.includes('프록시 사용 시 정상 작동합니다') ||
err.includes('프록시를 선택해보세요')
)}
</AlertDescription>
</Alert>
)}
<ProxyRecommendationAlert errors={testResults.errors} />
{testResults.errors.length > 0 && (
<div className="bg-red-50 border border-red-200 rounded p-2 mt-2">
{testResults.errors.map((error: string, index: number) => (
<p key={index} className="text-xs text-red-600 mb-1">{error}</p>
))}
</div>
)}
<ErrorMessageCard errors={testResults.errors} />
{/* 오류 해결 팁 */}
{!testResults.restApi && testResults.auth && (
<div className="bg-yellow-50 border border-yellow-200 rounded p-2 mt-2">
<div className="flex items-start gap-1">
<HelpCircle className="h-4 w-4 text-yellow-600 mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-yellow-800 font-medium"> API/DB </p>
<ul className="list-disc text-xs text-yellow-700 pl-4 mt-1">
<li> CORS </li>
<li>Supabase CORS </li>
<li> </li>
</ul>
</div>
</div>
</div>
)}
<TroubleshootingTips testResults={testResults} />
{/* 디버그 정보 섹션 추가 */}
{testResults.debugInfo && (
<Collapsible
open={showDebug}
onOpenChange={setShowDebug}
className="border border-gray-200 rounded-md mt-4"
>
<div className="flex items-center justify-between px-4 py-2 bg-gray-50">
<h4 className="text-sm font-medium"> </h4>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<Info className="h-4 w-4" />
<span className="sr-only">
{showDebug ? '진단 정보 숨기기' : '진단 정보 표시'}
</span>
</Button>
</CollapsibleTrigger>
</div>
<CollapsibleContent className="px-4 py-2 text-xs">
<div className="space-y-2">
<div>
<span className="font-medium">Supabase URL:</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.url}
</div>
</div>
{testResults.usingProxy && (
<div>
<span className="font-medium"> URL:</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.proxyUrl}
</div>
<div className="mt-1">
: {testResults.proxyType || 'corsproxy.io'}
</div>
</div>
)}
<div>
<span className="font-medium"> :</span>
<div className="mt-1 text-green-600">
{testResults.client ? '성공' : '실패'}
</div>
</div>
{testResults.debugInfo.lastErrorDetails && (
<div>
<span className="font-medium"> :</span>
<div className="mt-1 bg-red-50 p-1 rounded break-all text-red-600">
{testResults.debugInfo.lastErrorDetails}
</div>
</div>
)}
<div>
<span className="font-medium"> :</span>
<div className="mt-1 bg-gray-100 p-1 rounded break-all">
{testResults.debugInfo.browserInfo}
</div>
</div>
<div>
<span className="font-medium"> :</span>
<div className="mt-1">
{new Date(testResults.debugInfo.timestamp).toLocaleString()}
</div>
</div>
</div>
</CollapsibleContent>
</Collapsible>
)}
<DebugInfoCollapsible
testResults={testResults}
showDebug={showDebug}
setShowDebug={setShowDebug}
/>
</div>
)}
</div>

View File

@@ -0,0 +1,20 @@
import React from 'react';
interface TestResultItemProps {
label: string;
success: boolean;
}
const TestResultItem: React.FC<TestResultItemProps> = ({ label, success }) => {
return (
<div className="flex justify-between">
<span className="font-medium">{label}:</span>
<span className={success ? 'text-green-500' : 'text-red-500'}>
{success ? '✅ 성공' : '❌ 실패'}
</span>
</div>
);
};
export default TestResultItem;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { HelpCircle } from "lucide-react";
import { TestResults } from '@/lib/supabase/tests/types';
interface TroubleshootingTipsProps {
testResults: TestResults;
}
const TroubleshootingTips: React.FC<TroubleshootingTipsProps> = ({ testResults }) => {
if (!(!testResults.restApi && testResults.auth)) return null;
return (
<div className="bg-yellow-50 border border-yellow-200 rounded p-2 mt-2">
<div className="flex items-start gap-1">
<HelpCircle className="h-4 w-4 text-yellow-600 mt-0.5 flex-shrink-0" />
<div>
<p className="text-xs text-yellow-800 font-medium"> API/DB </p>
<ul className="list-disc text-xs text-yellow-700 pl-4 mt-1">
<li> CORS </li>
<li>Supabase CORS </li>
<li> </li>
</ul>
</div>
</div>
</div>
);
};
export default TroubleshootingTips;

View File

@@ -2,7 +2,7 @@
import { testAuth } from './authTests';
import { testRestApi } from './apiTests';
import { testDatabaseConnection } from './databaseTests';
import { TestResults } from './types';
import { TestResults, TestDebugInfo } from './types';
import { supabase, isValidUrl } from '../client';
import { getSupabaseUrl, getSupabaseKey, isCorsProxyEnabled, getProxyType } from '../config';
@@ -17,7 +17,18 @@ export const testSupabaseConnection = async (): Promise<TestResults> => {
restApi: false,
auth: false,
database: false,
errors: []
errors: [],
debugInfo: {
originalUrl: getSupabaseUrl(),
proxyUrl: '',
usingProxy: isCorsProxyEnabled(),
proxyType: getProxyType(),
keyLength: getSupabaseKey().length,
browserInfo: navigator.userAgent,
timestamp: new Date().toISOString(),
backupProxySuccess: false,
lastErrorDetails: ''
}
};
try {
@@ -40,12 +51,16 @@ export const testSupabaseConnection = async (): Promise<TestResults> => {
} else {
results.proxyUrl = baseUrl; // 기본값
}
// debugInfo에도 proxyUrl 설정
results.debugInfo.proxyUrl = results.proxyUrl;
} else {
results.proxyUrl = results.url; // 프록시 사용 안 함
results.debugInfo.proxyUrl = results.url;
}
// 테스트 실행
const authResults = await testAuth(supabase, results.url);
const authResults = await testAuth(supabase);
const apiResults = await testRestApi(supabase);
const dbResults = await testDatabaseConnection(supabase);
@@ -57,15 +72,20 @@ export const testSupabaseConnection = async (): Promise<TestResults> => {
// 오류 수집
if (!authResults.success && authResults.error) {
results.errors.push(`인증 테스트 실패: ${authResults.error}`);
results.debugInfo.lastErrorDetails += `인증: ${authResults.error}; `;
}
if (!apiResults.success && apiResults.error) {
results.errors.push(`REST API 테스트 실패: ${apiResults.error}`);
results.debugInfo.lastErrorDetails += `API: ${apiResults.error}; `;
}
if (!dbResults.success && dbResults.error) {
results.errors.push(`DB 테스트 실패: ${dbResults.error}`);
results.debugInfo.lastErrorDetails += `DB: ${dbResults.error}; `;
}
} catch (error: any) {
results.errors.push(`테스트 실행 오류: ${error.message || '알 수 없는 오류'}`);
const errorMsg = `테스트 실행 오류: ${error.message || '알 수 없는 오류'}`;
results.errors.push(errorMsg);
results.debugInfo.lastErrorDetails = errorMsg;
}
return results;

View File

@@ -6,18 +6,6 @@ export interface TestResult {
error: string | null;
}
export interface TestResults {
url: string;
proxyUrl: string; // 추가된 필드
usingProxy: boolean;
proxyType: string;
client: boolean;
restApi: boolean;
auth: boolean;
database: boolean;
errors: string[];
}
export interface TestDebugInfo {
originalUrl: string;
proxyUrl: string;
@@ -30,6 +18,19 @@ export interface TestDebugInfo {
lastErrorDetails: string;
}
export interface TestResults {
url: string;
proxyUrl: string;
usingProxy: boolean;
proxyType: string;
client: boolean;
restApi: boolean;
auth: boolean;
database: boolean;
errors: string[];
debugInfo: TestDebugInfo;
}
export interface ConnectionTestResult {
url: string;
proxyUrl: string;