Investigate Supabase connection issues
Investigate REST API and database connection failures despite successful authentication.
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw, Info } from "lucide-react";
|
||||||
import { toast } from "@/hooks/useToast.wrapper";
|
import { toast } from "@/hooks/useToast.wrapper";
|
||||||
import { testSupabaseConnection } from '@/lib/supabase';
|
import { testSupabaseConnection } from '@/lib/supabase';
|
||||||
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
|
||||||
|
|
||||||
interface TestResult {
|
interface TestResult {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -14,11 +15,13 @@ interface TestResult {
|
|||||||
auth: boolean;
|
auth: boolean;
|
||||||
database: boolean;
|
database: boolean;
|
||||||
errors: string[];
|
errors: string[];
|
||||||
|
debugInfo?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SupabaseConnectionTest: React.FC = () => {
|
const SupabaseConnectionTest: React.FC = () => {
|
||||||
const [testResults, setTestResults] = useState<TestResult | null>(null);
|
const [testResults, setTestResults] = useState<TestResult | null>(null);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [showDebug, setShowDebug] = useState(false);
|
||||||
|
|
||||||
const runConnectionTest = async () => {
|
const runConnectionTest = async () => {
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
@@ -107,6 +110,59 @@ const SupabaseConnectionTest: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 디버그 정보 섹션 추가 */}
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">클라이언트 초기화:</span>
|
||||||
|
<div className="mt-1 text-green-600">
|
||||||
|
{testResults.client ? '성공' : '실패'}
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
|
||||||
const SupabaseHelpSection: React.FC = () => {
|
const SupabaseHelpSection: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -17,12 +18,35 @@ const SupabaseHelpSection: React.FC = () => {
|
|||||||
<strong>Anon Key</strong>: Supabase 대시보드의 API 설정에서 찾을 수 있는 anon/public 키를 입력합니다.
|
<strong>Anon Key</strong>: Supabase 대시보드의 API 설정에서 찾을 수 있는 anon/public 키를 입력합니다.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>CORS 프록시</strong>: HTTP URL에 대한 브라우저 보안 제한을 우회하기 위해 사용합니다. HTTPS가 아닌 URL에 접근 문제가 있는 경우 활성화하세요.
|
<strong>CORS 프록시</strong>: HTTP URL에 대한 브라우저 보안 제한을 우회하기 위해 사용합니다.
|
||||||
|
HTTPS가 아닌 URL에 접근 문제가 있는 경우 활성화하세요.
|
||||||
</li>
|
</li>
|
||||||
<li className="text-amber-500 font-medium">
|
<li className="text-amber-500 font-medium">
|
||||||
가능하면 HTTPS URL을 사용하는 것이 좋습니다. HTTP URL을 사용해야 하는 경우 CORS 프록시를 활성화하세요.
|
가능하면 HTTPS URL을 사용하는 것이 좋습니다. HTTP URL을 사용해야 하는 경우 CORS 프록시를 활성화하세요.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{/* 오류 해결 팁 추가 */}
|
||||||
|
<div className="mt-6 border-t pt-4">
|
||||||
|
<h3 className="flex items-center text-sm font-semibold mb-2">
|
||||||
|
<AlertCircle className="h-4 w-4 mr-1 text-amber-500" />
|
||||||
|
<span>연결 문제 해결 팁</span>
|
||||||
|
</h3>
|
||||||
|
<ul className="list-disc pl-5 text-xs text-gray-500 space-y-2">
|
||||||
|
<li>
|
||||||
|
<strong>REST API 오류</strong>: 서버의 CORS 설정을 확인해보세요. Supabase 서버에서 CORS 허용 설정이 필요할 수 있습니다.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>인증 성공, API 실패</strong>: 이 경우는 일반적으로 CORS 문제를 의미합니다. CORS 프록시를 활성화하고 서버 설정도 확인해보세요.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>데이터베이스 오류</strong>: Supabase 관리자 대시보드에서 해당 테이블이 존재하는지, 그리고 익명 접근 권한이 있는지 확인하세요.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>HTTPS 필요</strong>: 로컬 개발 환경에서는 HTTP 접근이 가능하지만, 배포된 앱에서는 HTTPS Supabase URL이 필요할 수 있습니다.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ export const getSupabaseUrl = () => {
|
|||||||
if (useProxy) {
|
if (useProxy) {
|
||||||
// CORS 프록시 URL로 변환 (URL 구조 개선)
|
// CORS 프록시 URL로 변환 (URL 구조 개선)
|
||||||
const cleanUrl = storedUrl.trim();
|
const cleanUrl = storedUrl.trim();
|
||||||
return `https://corsproxy.io/?${encodeURIComponent(cleanUrl)}`;
|
// URL에 이미 프로토콜이 포함되어 있는지 확인
|
||||||
|
const hasProtocol = cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://');
|
||||||
|
const urlForProxy = hasProtocol ? cleanUrl : `http://${cleanUrl}`;
|
||||||
|
|
||||||
|
// 프록시 URL 생성 시 더 정확한 인코딩
|
||||||
|
const proxyUrl = `https://corsproxy.io/?${encodeURIComponent(urlForProxy)}`;
|
||||||
|
console.log('CORS 프록시 URL 생성:', proxyUrl);
|
||||||
|
return proxyUrl;
|
||||||
}
|
}
|
||||||
return storedUrl;
|
return storedUrl;
|
||||||
}
|
}
|
||||||
@@ -43,8 +50,13 @@ export const configureSupabase = (url: string, key: string, useProxy: boolean =
|
|||||||
// URL 정리 (앞뒤 공백 제거)
|
// URL 정리 (앞뒤 공백 제거)
|
||||||
const cleanUrl = url.trim();
|
const cleanUrl = url.trim();
|
||||||
|
|
||||||
|
// URL에 프로토콜이 없는 경우 http:// 추가
|
||||||
|
const normalizedUrl = (cleanUrl.startsWith('http://') || cleanUrl.startsWith('https://'))
|
||||||
|
? cleanUrl
|
||||||
|
: `http://${cleanUrl}`;
|
||||||
|
|
||||||
// 로컬 스토리지에 설정 저장
|
// 로컬 스토리지에 설정 저장
|
||||||
localStorage.setItem('supabase_url', cleanUrl);
|
localStorage.setItem('supabase_url', normalizedUrl);
|
||||||
localStorage.setItem('supabase_key', key);
|
localStorage.setItem('supabase_key', key);
|
||||||
localStorage.setItem('use_cors_proxy', useProxy.toString());
|
localStorage.setItem('use_cors_proxy', useProxy.toString());
|
||||||
|
|
||||||
|
|||||||
@@ -27,43 +27,130 @@ export const testSupabaseLogin = async (email: string, password: string) => {
|
|||||||
|
|
||||||
// API 테스트 도우미 함수
|
// API 테스트 도우미 함수
|
||||||
export const testSupabaseConnection = async () => {
|
export const testSupabaseConnection = async () => {
|
||||||
|
const originalUrl = getOriginalSupabaseUrl();
|
||||||
|
const proxyUrl = getSupabaseUrl();
|
||||||
|
const usingProxy = isCorsProxyEnabled();
|
||||||
|
const supabaseKey = getSupabaseKey();
|
||||||
|
|
||||||
const results = {
|
const results = {
|
||||||
url: getOriginalSupabaseUrl(), // 원본 URL
|
url: originalUrl, // 원본 URL
|
||||||
proxyUrl: getSupabaseUrl(), // 프록시 적용된 URL
|
proxyUrl: proxyUrl, // 프록시 적용된 URL
|
||||||
usingProxy: isCorsProxyEnabled(), // 프록시 사용 여부
|
usingProxy: usingProxy, // 프록시 사용 여부
|
||||||
client: !!supabase,
|
client: !!supabase,
|
||||||
restApi: false,
|
restApi: false,
|
||||||
auth: false,
|
auth: false,
|
||||||
database: false,
|
database: false,
|
||||||
errors: [] as string[]
|
errors: [] as string[],
|
||||||
|
debugInfo: {
|
||||||
|
originalUrl,
|
||||||
|
proxyUrl,
|
||||||
|
usingProxy,
|
||||||
|
keyLength: supabaseKey.length,
|
||||||
|
browserInfo: navigator.userAgent,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('연결 테스트 시작 - 설정 정보:', {
|
||||||
|
originalUrl,
|
||||||
|
proxyUrl,
|
||||||
|
usingProxy
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. REST API 접근 테스트
|
// 1. REST API 접근 테스트 - 다양한 URL 형식 시도
|
||||||
try {
|
try {
|
||||||
console.log('REST API 테스트 시작...');
|
console.log('REST API 테스트 시작...');
|
||||||
// 정확한 REST API 엔드포인트 구성
|
|
||||||
const apiUrl = results.proxyUrl.endsWith('/')
|
|
||||||
? `${results.proxyUrl}rest/v1/`
|
|
||||||
: `${results.proxyUrl}/rest/v1/`;
|
|
||||||
|
|
||||||
console.log('REST API 테스트 URL:', apiUrl);
|
// 여러 형태의 REST API 엔드포인트 URL 구성 시도
|
||||||
|
let apiUrl = '';
|
||||||
|
let response = null;
|
||||||
|
let errorBody = '';
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
// 1번째 시도: 기본 URL + /rest/v1/
|
||||||
method: 'GET',
|
apiUrl = proxyUrl.endsWith('/')
|
||||||
headers: {
|
? `${proxyUrl}rest/v1/`
|
||||||
'Content-Type': 'application/json',
|
: `${proxyUrl}/rest/v1/`;
|
||||||
'apikey': getSupabaseKey(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
results.restApi = response.ok;
|
console.log('REST API 테스트 URL (시도 1):', apiUrl);
|
||||||
if (!response.ok) {
|
|
||||||
const errorBody = await response.text();
|
try {
|
||||||
results.errors.push(`REST API 오류(${response.status} ${response.statusText}): ${errorBody || '응답 없음'}`);
|
response = await fetch(apiUrl, {
|
||||||
console.error('REST API 테스트 실패:', response.status, errorBody);
|
method: 'GET',
|
||||||
} else {
|
headers: {
|
||||||
console.log('REST API 테스트 성공');
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': supabaseKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
results.restApi = true;
|
||||||
|
console.log('REST API 테스트 성공 (시도 1)');
|
||||||
|
} else {
|
||||||
|
errorBody = await response.text();
|
||||||
|
console.warn(`REST API 테스트 실패 (시도 1) - 상태: ${response.status}, 오류: ${errorBody}`);
|
||||||
|
|
||||||
|
// 2번째 시도: corsproxy.io URL 직접 구성
|
||||||
|
if (usingProxy) {
|
||||||
|
const directProxyUrl = `https://corsproxy.io/?${encodeURIComponent(`${originalUrl}/rest/v1/`)}`;
|
||||||
|
console.log('REST API 테스트 URL (시도 2 - 직접 프록시):', directProxyUrl);
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(directProxyUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': supabaseKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
results.restApi = true;
|
||||||
|
console.log('REST API 테스트 성공 (시도 2)');
|
||||||
|
} else {
|
||||||
|
const error2 = await response.text();
|
||||||
|
console.warn(`REST API 테스트 실패 (시도 2) - 상태: ${response.status}, 오류: ${error2}`);
|
||||||
|
results.errors.push(`REST API 오류(${response.status}): ${error2 || errorBody || '응답 없음'}`);
|
||||||
|
}
|
||||||
|
} catch (innerErr: any) {
|
||||||
|
console.error('REST API 테스트 시도 2 예외:', innerErr);
|
||||||
|
results.errors.push(`REST API 예외 (시도 2): ${innerErr.message || '알 수 없는 오류'}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results.errors.push(`REST API 오류(${response.status}): ${errorBody || '응답 없음'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (outerErr: any) {
|
||||||
|
console.error('REST API 테스트 시도 1 예외:', outerErr);
|
||||||
|
results.errors.push(`REST API 예외 (시도 1): ${outerErr.message || '알 수 없는 오류'}`);
|
||||||
|
|
||||||
|
// 백업 시도
|
||||||
|
if (usingProxy) {
|
||||||
|
try {
|
||||||
|
// 백업 프록시 시도: 다른 CORS 프록시 서비스 사용
|
||||||
|
const backupProxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(`${originalUrl}/rest/v1/`)}`;
|
||||||
|
console.log('REST API 테스트 URL (백업 프록시):', backupProxyUrl);
|
||||||
|
|
||||||
|
response = await fetch(backupProxyUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'apikey': supabaseKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
results.restApi = true;
|
||||||
|
console.log('REST API 테스트 성공 (백업 프록시)');
|
||||||
|
results.debugInfo.backupProxySuccess = true;
|
||||||
|
} else {
|
||||||
|
const backupError = await response.text();
|
||||||
|
console.warn(`REST API 테스트 실패 (백업 프록시) - 상태: ${response.status}, 오류: ${backupError}`);
|
||||||
|
}
|
||||||
|
} catch (backupErr: any) {
|
||||||
|
console.error('REST API 백업 프록시 예외:', backupErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
results.errors.push(`REST API 예외: ${err.message || '알 수 없는 오류'}`);
|
results.errors.push(`REST API 예외: ${err.message || '알 수 없는 오류'}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user