diff --git a/src/lib/supabase/client.ts b/src/lib/supabase/client.ts index 658cef1..098ceb4 100644 --- a/src/lib/supabase/client.ts +++ b/src/lib/supabase/client.ts @@ -1,6 +1,7 @@ import { createClient } from '@supabase/supabase-js'; import { getSupabaseUrl, getSupabaseKey } from './config'; +import { customFetch } from './customFetch'; const supabaseUrl = getSupabaseUrl(); const supabaseAnonKey = getSupabaseKey(); @@ -25,153 +26,10 @@ try { }, global: { // 커스텀 fetch 구현 - fetch: (...args) => { - // 첫 번째 인자는 URL 또는 Request 객체 - let requestToUse = args[0]; - let url = ''; - let headers = {}; - - // URL 형식 변환 및 헤더 수정 - if (typeof requestToUse === 'string') { - url = requestToUse; - - // Storage API 엔드포인트 경로 수정 (buckets → bucket) - if (url.includes('/storage/v1/buckets')) { - url = url.replace('/storage/v1/buckets', '/storage/v1/bucket'); - console.log('Storage API 경로 수정:', url); - // 문자열 URL인 경우 수정된 URL을 requestToUse에 할당 - requestToUse = url; - } - - // 두 번째 인자에서 헤더 가져오기 - if (args[1] && args[1].headers) { - headers = args[1].headers; - } - } else if (requestToUse instanceof Request) { - url = requestToUse.url; - - // Storage API 엔드포인트 경로 수정 (buckets → bucket) - if (url.includes('/storage/v1/buckets')) { - const newUrl = url.replace('/storage/v1/buckets', '/storage/v1/bucket'); - // Request 객체인 경우 새 Request 객체 생성 - const newRequest = new Request(newUrl, requestToUse); - requestToUse = newRequest; - url = newUrl; - console.log('Storage API Request 객체 경로 수정:', url); - } - } - - // Storage API 호출 감지 및 헤더 형식 수정 - if (url.includes('/storage/v1/')) { - console.log('Supabase Storage API 호출 감지'); - - // 두 번째 인자에서 헤더 가져오기 및 수정 - if (args[1] && args[1].headers) { - const originalHeaders = args[1].headers; - const apiKey = originalHeaders['apikey'] || originalHeaders['Apikey'] || supabaseAnonKey; - - // 새 헤더 객체 생성 - const newHeaders = new Headers(originalHeaders); - - // apikey 헤더가 있으면 삭제하고 Authorization 헤더로 교체 - if (newHeaders.has('apikey')) { - const apiKeyValue = newHeaders.get('apikey'); - newHeaders.delete('apikey'); - newHeaders.set('Authorization', `Bearer ${apiKeyValue}`); - } else if (newHeaders.has('Apikey')) { - const apiKeyValue = newHeaders.get('Apikey'); - newHeaders.delete('Apikey'); - newHeaders.set('Authorization', `Bearer ${apiKeyValue}`); - } else { - // apikey 헤더가 없지만 Storage API를 호출하는 경우 - newHeaders.set('Authorization', `Bearer ${supabaseAnonKey}`); - } - - // 수정된 헤더로 새 옵션 객체 생성 - args[1].headers = newHeaders; - console.log('Storage API 헤더 형식 수정 완료'); - } - } - - // URL 로깅 및 디버깅 - console.log('Supabase fetch 요청:', url); - - // 기본 fetch 호출 - return fetch(requestToUse, args[1]) - .then(response => { - console.log('Supabase 응답 상태:', response.status); - return response; - }) - .catch(err => { - console.error('Supabase fetch 오류:', err); - throw err; - }); - } + fetch: customFetch } }); - // CORS 문제 확인을 위한 기본 헤더 테스트 - (async () => { - try { - // 기본 서버 상태 확인 (CORS 테스트) - console.log('Supabase 서버 상태 확인 중...'); - const response = await fetch(`${supabaseUrl}/auth/v1/`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'apikey': supabaseAnonKey, - }, - }); - - if (response.ok || response.status === 401 || response.status === 404) { - console.log('Supabase 서버 연결 성공:', response.status); - } else { - console.warn('Supabase 서버 연결 실패:', response.status, response.statusText); - // 응답 세부 정보 로깅 - try { - const errorText = await response.text(); - console.warn('Supabase 서버 응답 내용:', errorText); - } catch (e) { - console.error('응답 내용 읽기 실패:', e); - } - } - } catch (err) { - console.error('Supabase 서버 상태 확인 중 오류 (CORS 문제 가능성):', err); - } - })(); - - // Supabase 연결 테스트 - (async () => { - try { - console.log('Supabase 인증 테스트 시도 중...'); - const { data, error } = await supabaseClient.auth.getSession(); - if (error) { - console.warn('Supabase 연결 테스트 실패:', error.message); - } else { - console.log('Supabase 연결 성공!', data); - } - - // 추가 테스트: 공개 데이터 조회 시도 - try { - console.log('Supabase 데이터베이스 공개 테이블 조회 시도...'); - const { data: tableData, error: tableError } = await supabaseClient - .from('transactions') - .select('*') - .limit(1); - - if (tableError) { - console.warn('Supabase 데이터베이스 테스트 실패:', tableError.message); - } else { - console.log('Supabase 데이터베이스 테스트 성공:', tableData); - } - } catch (dbErr) { - console.error('Supabase 데이터베이스 테스트 중 예외 발생:', dbErr); - } - } catch (err) { - console.error('Supabase 연결 확인 중 오류:', err); - } - })(); - // Supabase 연결 로그 console.log('Supabase 클라이언트가 생성되었습니다.'); @@ -179,6 +37,14 @@ try { if (!isValidUrl) { console.warn('경고: 기본 Supabase URL 또는 Anon Key가 감지되었습니다. Supabase 설정 페이지에서 온프레미스 설정을 구성하세요.'); } + + // 연결 테스트 - 별도 파일로 이동됨 + import('./connectionTest').then(module => { + module.testConnection(supabaseClient, supabaseUrl, supabaseAnonKey); + }).catch(err => { + console.error('연결 테스트 모듈 로딩 오류:', err); + }); + } catch (error) { console.error('Supabase 클라이언트 생성 오류:', error); diff --git a/src/lib/supabase/connectionTest.ts b/src/lib/supabase/connectionTest.ts new file mode 100644 index 0000000..7fadc2e --- /dev/null +++ b/src/lib/supabase/connectionTest.ts @@ -0,0 +1,69 @@ + +import { SupabaseClient } from '@supabase/supabase-js'; + +/** + * Supabase 연결 테스트 + */ +export async function testConnection( + supabaseClient: SupabaseClient, + supabaseUrl: string, + supabaseAnonKey: string +): Promise { + // CORS 문제 확인을 위한 기본 헤더 테스트 + try { + // 기본 서버 상태 확인 (CORS 테스트) + console.log('Supabase 서버 상태 확인 중...'); + const response = await fetch(`${supabaseUrl}/auth/v1/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'apikey': supabaseAnonKey, + }, + }); + + if (response.ok || response.status === 401 || response.status === 404) { + console.log('Supabase 서버 연결 성공:', response.status); + } else { + console.warn('Supabase 서버 연결 실패:', response.status, response.statusText); + // 응답 세부 정보 로깅 + try { + const errorText = await response.text(); + console.warn('Supabase 서버 응답 내용:', errorText); + } catch (e) { + console.error('응답 내용 읽기 실패:', e); + } + } + } catch (err) { + console.error('Supabase 서버 상태 확인 중 오류 (CORS 문제 가능성):', err); + } + + // Supabase 연결 테스트 + try { + console.log('Supabase 인증 테스트 시도 중...'); + const { data, error } = await supabaseClient.auth.getSession(); + if (error) { + console.warn('Supabase 연결 테스트 실패:', error.message); + } else { + console.log('Supabase 연결 성공!', data); + } + + // 추가 테스트: 공개 데이터 조회 시도 + try { + console.log('Supabase 데이터베이스 공개 테이블 조회 시도...'); + const { data: tableData, error: tableError } = await supabaseClient + .from('transactions') + .select('*') + .limit(1); + + if (tableError) { + console.warn('Supabase 데이터베이스 테스트 실패:', tableError.message); + } else { + console.log('Supabase 데이터베이스 테스트 성공:', tableData); + } + } catch (dbErr) { + console.error('Supabase 데이터베이스 테스트 중 예외 발생:', dbErr); + } + } catch (err) { + console.error('Supabase 연결 확인 중 오류:', err); + } +} diff --git a/src/lib/supabase/customFetch.ts b/src/lib/supabase/customFetch.ts new file mode 100644 index 0000000..56eac4a --- /dev/null +++ b/src/lib/supabase/customFetch.ts @@ -0,0 +1,78 @@ + +import { getSupabaseKey } from './config'; +import { modifyStorageApiRequest } from './storageUtils'; + +/** + * Supabase 클라이언트에서 사용할 커스텀 fetch 구현 + */ +export const customFetch = (...args: [RequestInfo | URL, RequestInit?]): Promise => { + // 첫 번째 인자는 URL 또는 Request 객체 + let requestToUse = args[0]; + let url = ''; + + // URL 형식 변환 + if (typeof requestToUse === 'string') { + url = requestToUse; + } else if (requestToUse instanceof Request) { + url = requestToUse.url; + } + + // Storage API 호출 감지 및 요청 수정 + if (url.includes('/storage/v1/')) { + requestToUse = modifyStorageApiRequest(requestToUse, args[1], getSupabaseKey()); + + // args[1]이 존재하는 경우 modifyStorageApiHeaders 함수를 통해 헤더 수정 + if (args[1]) { + args[1] = modifyStorageApiHeaders(args[1], getSupabaseKey()); + } + } + + // URL 로깅 및 디버깅 + console.log('Supabase fetch 요청:', url); + + // 기본 fetch 호출 + return fetch(requestToUse, args[1]) + .then(response => { + console.log('Supabase 응답 상태:', response.status); + return response; + }) + .catch(err => { + console.error('Supabase fetch 오류:', err); + throw err; + }); +}; + +/** + * Storage API 요청에 대한 헤더 수정 + */ +function modifyStorageApiHeaders(options: RequestInit, supabaseAnonKey: string): RequestInit { + if (!options.headers) { + return options; + } + + console.log('Storage API 호출 감지'); + + // 헤더 수정 + const originalHeaders = options.headers; + const newHeaders = new Headers(originalHeaders); + + // apikey 헤더가 있으면 삭제하고 Authorization 헤더로 교체 + if (newHeaders.has('apikey')) { + const apiKeyValue = newHeaders.get('apikey'); + newHeaders.delete('apikey'); + newHeaders.set('Authorization', `Bearer ${apiKeyValue}`); + } else if (newHeaders.has('Apikey')) { + const apiKeyValue = newHeaders.get('Apikey'); + newHeaders.delete('Apikey'); + newHeaders.set('Authorization', `Bearer ${apiKeyValue}`); + } else { + // apikey 헤더가 없지만 Storage API를 호출하는 경우 + newHeaders.set('Authorization', `Bearer ${supabaseAnonKey}`); + } + + // 수정된 헤더로 새 옵션 객체 생성 + const newOptions = { ...options, headers: newHeaders }; + console.log('Storage API 헤더 형식 수정 완료'); + + return newOptions; +} diff --git a/src/lib/supabase/index.ts b/src/lib/supabase/index.ts index 0cdf13b..aa712f1 100644 --- a/src/lib/supabase/index.ts +++ b/src/lib/supabase/index.ts @@ -2,11 +2,18 @@ import { supabase, isValidUrl } from './client'; import { testSupabaseConnection } from './tests'; import { createRequiredTables, checkTablesStatus } from './setup'; +import { customFetch } from './customFetch'; +import { modifyStorageApiRequest, getStorageApiHeaders } from './storageUtils'; +import { testConnection } from './connectionTest'; export { supabase, isValidUrl, testSupabaseConnection, createRequiredTables, - checkTablesStatus + checkTablesStatus, + customFetch, + modifyStorageApiRequest, + getStorageApiHeaders, + testConnection }; diff --git a/src/lib/supabase/storageUtils.ts b/src/lib/supabase/storageUtils.ts new file mode 100644 index 0000000..3e3e498 --- /dev/null +++ b/src/lib/supabase/storageUtils.ts @@ -0,0 +1,47 @@ + +/** + * Storage API 경로 수정 (buckets → bucket) + */ +export function modifyStorageApiRequest( + request: RequestInfo | URL, + options?: RequestInit, + supabaseAnonKey?: string +): RequestInfo | URL { + let url = ''; + + // URL 추출 + if (typeof request === 'string') { + url = request; + + // Storage API 엔드포인트 경로 수정 (buckets → bucket) + if (url.includes('/storage/v1/buckets')) { + url = url.replace('/storage/v1/buckets', '/storage/v1/bucket'); + console.log('Storage API 경로 수정:', url); + return url; + } + } else if (request instanceof Request) { + url = request.url; + + // Storage API 엔드포인트 경로 수정 (buckets → bucket) + if (url.includes('/storage/v1/buckets')) { + const newUrl = url.replace('/storage/v1/buckets', '/storage/v1/bucket'); + // Request 객체인 경우 새 Request 객체 생성 + const newRequest = new Request(newUrl, request); + console.log('Storage API Request 객체 경로 수정:', newUrl); + return newRequest; + } + } + + // 수정이 필요 없으면 원래 요청 반환 + return request; +} + +/** + * Storage API에 사용할 헤더 가져오기 + */ +export function getStorageApiHeaders(supabaseAnonKey: string): Headers { + const headers = new Headers(); + headers.set('Authorization', `Bearer ${supabaseAnonKey}`); + headers.set('Content-Type', 'application/json'); + return headers; +}