Migrate from Supabase to Appwrite with core functionality and UI components
This commit is contained in:
21
src/archive/README.md
Normal file
21
src/archive/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Archive 폴더
|
||||
|
||||
이 폴더는 Zellyy Finance 프로젝트에서 더 이상 활발하게 사용되지 않는 레거시 코드를 보관하는 곳입니다.
|
||||
|
||||
## 개요
|
||||
|
||||
Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환했습니다. 이 폴더에는 Supabase 관련 코드가 보관되어 있으며, 참조용으로만 유지됩니다.
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
- `components/`: Supabase 관련 UI 컴포넌트
|
||||
- `hooks/`: Supabase 관련 훅
|
||||
- `integrations/`: Supabase 통합 코드
|
||||
- `lib/`: Supabase 클라이언트 및 유틸리티
|
||||
- `utils/`: Supabase 트랜잭션 유틸리티
|
||||
|
||||
## 주의사항
|
||||
|
||||
이 폴더의 코드는 더 이상 유지보수되지 않으며, 새로운 기능 개발에 사용해서는 안 됩니다. 모든 새로운 개발은 Appwrite 기반으로 진행해야 합니다.
|
||||
|
||||
마이그레이션이 완전히 완료되면 이 폴더는 삭제될 예정입니다.
|
||||
42
src/archive/components/SupabaseConnectionStatus.tsx
Normal file
42
src/archive/components/SupabaseConnectionStatus.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
import React from 'react';
|
||||
import { CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
interface SupabaseConnectionStatusProps {
|
||||
testResults: {
|
||||
connected: boolean;
|
||||
message: string;
|
||||
details?: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
const SupabaseConnectionStatus = ({ testResults }: SupabaseConnectionStatusProps) => {
|
||||
if (!testResults) return null;
|
||||
|
||||
return (
|
||||
<div className={`mt-2 p-3 rounded-md text-sm ${testResults.connected ? 'bg-green-50' : 'bg-red-50'}`}>
|
||||
<div className="flex items-start">
|
||||
{testResults.connected ? (
|
||||
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0 mt-0.5" />
|
||||
)}
|
||||
<div>
|
||||
<p className={`font-medium ${testResults.connected ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{testResults.connected ? '연결됨' : '연결 실패'}
|
||||
</p>
|
||||
<p className={testResults.connected ? 'text-green-700' : 'text-red-700'}>
|
||||
{testResults.message}
|
||||
</p>
|
||||
{testResults.details && (
|
||||
<p className="text-gray-500 mt-1 text-xs">
|
||||
{testResults.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupabaseConnectionStatus;
|
||||
99
src/archive/components/supabase/DebugInfoCollapsible.tsx
Normal file
99
src/archive/components/supabase/DebugInfoCollapsible.tsx
Normal 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;
|
||||
20
src/archive/components/supabase/ErrorMessageCard.tsx
Normal file
20
src/archive/components/supabase/ErrorMessageCard.tsx
Normal 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;
|
||||
35
src/archive/components/supabase/ProxyRecommendationAlert.tsx
Normal file
35
src/archive/components/supabase/ProxyRecommendationAlert.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
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 hasCorsError = errors.some(err =>
|
||||
err.includes('CORS') ||
|
||||
err.includes('Failed to fetch') ||
|
||||
err.includes('프록시 사용시 정상 작동') ||
|
||||
err.includes('프록시를 활성화')
|
||||
);
|
||||
|
||||
if (!hasCorsError || errors.length === 0) return null;
|
||||
|
||||
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">CORS 오류 감지됨</AlertTitle>
|
||||
<AlertDescription className="text-amber-700 text-xs">
|
||||
<p>HTTP URL에 대한 브라우저 보안 제한으로 인해 연결에 실패했습니다.</p>
|
||||
<ul className="list-disc pl-4 mt-1">
|
||||
<li className="mt-1">CORS 프록시를 활성화하고 프록시 유형을 변경해보세요.</li>
|
||||
<li className="mt-1">또는 HTTPS URL로 변경하는 것을 고려하세요.</li>
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxyRecommendationAlert;
|
||||
30
src/archive/components/supabase/TroubleshootingTips.tsx
Normal file
30
src/archive/components/supabase/TroubleshootingTips.tsx
Normal 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;
|
||||
138
src/archive/hooks/transactions/supabaseUtils.ts
Normal file
138
src/archive/hooks/transactions/supabaseUtils.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||
import { formatISO } from 'date-fns';
|
||||
|
||||
// ISO 형식으로 날짜 변환 (Supabase 저장용)
|
||||
const convertDateToISO = (dateStr: string): string => {
|
||||
try {
|
||||
// 이미 ISO 형식인 경우 그대로 반환
|
||||
if (dateStr.match(/^\d{4}-\d{2}-\d{2}T/)) {
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
// "오늘, 시간" 형식 처리
|
||||
if (dateStr.includes('오늘')) {
|
||||
const today = new Date();
|
||||
|
||||
// 시간 추출 시도
|
||||
const timeMatch = dateStr.match(/(\d{1,2}):(\d{2})/);
|
||||
if (timeMatch) {
|
||||
const hours = parseInt(timeMatch[1], 10);
|
||||
const minutes = parseInt(timeMatch[2], 10);
|
||||
today.setHours(hours, minutes, 0, 0);
|
||||
}
|
||||
|
||||
return formatISO(today);
|
||||
}
|
||||
|
||||
// 일반 날짜 문자열은 그대로 Date 객체로 변환 시도
|
||||
const date = new Date(dateStr);
|
||||
if (!isNaN(date.getTime())) {
|
||||
return formatISO(date);
|
||||
}
|
||||
|
||||
// 변환 실패 시 현재 시간 반환
|
||||
console.warn(`날짜 변환 오류: "${dateStr}"를 ISO 형식으로 변환할 수 없습니다.`);
|
||||
return formatISO(new Date());
|
||||
} catch (error) {
|
||||
console.error(`날짜 변환 오류: "${dateStr}"`, error);
|
||||
return formatISO(new Date());
|
||||
}
|
||||
};
|
||||
|
||||
// Supabase와 트랜잭션 동기화 - Cloud 최적화 버전
|
||||
export const syncTransactionsWithSupabase = async (user: any, transactions: Transaction[]): Promise<Transaction[]> => {
|
||||
if (!user || !isSyncEnabled()) return transactions;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase 데이터 조회 오류:', error);
|
||||
return transactions;
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Supabase 데이터 로컬 형식으로 변환
|
||||
const supabaseTransactions = data.map(t => ({
|
||||
id: t.transaction_id || t.id,
|
||||
title: t.title,
|
||||
amount: t.amount,
|
||||
date: t.date,
|
||||
category: t.category,
|
||||
type: t.type
|
||||
}));
|
||||
|
||||
// 로컬 데이터와 병합 (중복 ID 제거)
|
||||
const mergedTransactions = [...transactions];
|
||||
|
||||
supabaseTransactions.forEach(newTx => {
|
||||
const existingIndex = mergedTransactions.findIndex(t => t.id === newTx.id);
|
||||
if (existingIndex >= 0) {
|
||||
mergedTransactions[existingIndex] = newTx;
|
||||
} else {
|
||||
mergedTransactions.push(newTx);
|
||||
}
|
||||
});
|
||||
|
||||
return mergedTransactions;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Supabase 동기화 오류:', err);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
};
|
||||
|
||||
// Supabase에 트랜잭션 업데이트 - Cloud 최적화 버전
|
||||
export const updateTransactionInSupabase = async (user: any, transaction: Transaction): Promise<void> => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
// 날짜를 ISO 형식으로 변환
|
||||
const isoDate = convertDateToISO(transaction.date);
|
||||
|
||||
const { error } = await supabase.from('transactions')
|
||||
.upsert({
|
||||
user_id: user.id,
|
||||
title: transaction.title,
|
||||
amount: transaction.amount,
|
||||
date: isoDate, // ISO 형식 사용
|
||||
category: transaction.category,
|
||||
type: transaction.type,
|
||||
transaction_id: transaction.id
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('트랜잭션 업데이트 오류:', error);
|
||||
} else {
|
||||
console.log('Supabase 트랜잭션 업데이트 성공:', transaction.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Supabase 업데이트 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Supabase에서 트랜잭션 삭제 - Cloud 최적화 버전
|
||||
export const deleteTransactionFromSupabase = async (user: any, transactionId: string): Promise<void> => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('transactions')
|
||||
.delete()
|
||||
.eq('transaction_id', transactionId);
|
||||
|
||||
if (error) {
|
||||
console.error('트랜잭션 삭제 오류:', error);
|
||||
} else {
|
||||
console.log('Supabase 트랜잭션 삭제 성공:', transactionId);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Supabase 삭제 오류:', error);
|
||||
}
|
||||
};
|
||||
20
src/archive/integrations/supabase/client.ts
Normal file
20
src/archive/integrations/supabase/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// This file is automatically generated. Do not edit it directly.
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { Database } from './types';
|
||||
|
||||
const SUPABASE_URL = (() => {
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
|
||||
return url;
|
||||
})();
|
||||
|
||||
const SUPABASE_PUBLISHABLE_KEY = (() => {
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
|
||||
return key;
|
||||
})();
|
||||
|
||||
// Import the supabase client like this:
|
||||
// import { supabase } from "@/integrations/supabase/client";
|
||||
|
||||
export const supabase = createClient<Database>(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY);
|
||||
225
src/archive/integrations/supabase/types.ts
Normal file
225
src/archive/integrations/supabase/types.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
export type Json =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
budgets: {
|
||||
Row: {
|
||||
categories: Json
|
||||
created_at: string | null
|
||||
id: string
|
||||
month: number
|
||||
total_budget: number
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
year: number
|
||||
}
|
||||
Insert: {
|
||||
categories?: Json
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
month: number
|
||||
total_budget?: number
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
year: number
|
||||
}
|
||||
Update: {
|
||||
categories?: Json
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
month?: number
|
||||
total_budget?: number
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
year?: number
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
category_budgets: {
|
||||
Row: {
|
||||
amount: number
|
||||
category: string
|
||||
created_at: string | null
|
||||
id: string
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
amount?: number
|
||||
category: string
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
amount?: number
|
||||
category?: string
|
||||
created_at?: string | null
|
||||
id?: string
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
transactions: {
|
||||
Row: {
|
||||
amount: number
|
||||
category: string
|
||||
created_at: string | null
|
||||
date: string | null
|
||||
id: string
|
||||
notes: string | null
|
||||
title: string
|
||||
transaction_id: string | null
|
||||
type: string
|
||||
updated_at: string | null
|
||||
user_id: string
|
||||
}
|
||||
Insert: {
|
||||
amount: number
|
||||
category: string
|
||||
created_at?: string | null
|
||||
date?: string | null
|
||||
id?: string
|
||||
notes?: string | null
|
||||
title: string
|
||||
transaction_id?: string | null
|
||||
type: string
|
||||
updated_at?: string | null
|
||||
user_id: string
|
||||
}
|
||||
Update: {
|
||||
amount?: number
|
||||
category?: string
|
||||
created_at?: string | null
|
||||
date?: string | null
|
||||
id?: string
|
||||
notes?: string | null
|
||||
title?: string
|
||||
transaction_id?: string | null
|
||||
type?: string
|
||||
updated_at?: string | null
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
Enums: {
|
||||
[_ in never]: never
|
||||
}
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type PublicSchema = Database[Extract<keyof Database, "public">]
|
||||
|
||||
export type Tables<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])
|
||||
? (PublicSchema["Tables"] &
|
||||
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesInsert<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never
|
||||
|
||||
export type TablesUpdate<
|
||||
PublicTableNameOrOptions extends
|
||||
| keyof PublicSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never
|
||||
|
||||
export type Enums<
|
||||
PublicEnumNameOrOptions extends
|
||||
| keyof PublicSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
||||
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
||||
: never
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof PublicSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"]
|
||||
? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
45
src/archive/lib/supabase/client.ts
Normal file
45
src/archive/lib/supabase/client.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { getSupabaseUrl, getSupabaseKey } from './config';
|
||||
|
||||
const supabaseUrl = getSupabaseUrl();
|
||||
const supabaseAnonKey = getSupabaseKey();
|
||||
|
||||
let supabaseClient;
|
||||
|
||||
try {
|
||||
console.log(`Supabase 클라이언트 생성 중: ${supabaseUrl}`);
|
||||
|
||||
// Supabase 클라이언트 생성 - Cloud 환경에 최적화
|
||||
supabaseClient = createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Supabase 클라이언트가 성공적으로 생성되었습니다.');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Supabase 클라이언트 생성 오류:', error);
|
||||
|
||||
// 더미 클라이언트 생성 (앱이 완전히 실패하지 않도록)
|
||||
supabaseClient = {
|
||||
auth: {
|
||||
getUser: () => Promise.resolve({ data: { user: null } }),
|
||||
getSession: () => Promise.resolve({ data: { session: null } }),
|
||||
signInWithPassword: () => Promise.reject(new Error('Supabase 설정이 필요합니다')),
|
||||
signUp: () => Promise.reject(new Error('Supabase 설정이 필요합니다')),
|
||||
signOut: () => Promise.resolve({ error: null }),
|
||||
onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }),
|
||||
},
|
||||
from: () => ({
|
||||
select: () => ({ eq: () => ({ data: null, error: new Error('Supabase 설정이 필요합니다') }) }),
|
||||
insert: () => ({ error: new Error('Supabase 설정이 필요합니다') }),
|
||||
delete: () => ({ eq: () => ({ error: new Error('Supabase 설정이 필요합니다') }) }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export const supabase = supabaseClient;
|
||||
export const isValidUrl = true; // Supabase Cloud 환경에서는 항상 true
|
||||
30
src/archive/lib/supabase/config.ts
Normal file
30
src/archive/lib/supabase/config.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// Supabase Cloud URL과 anon key 설정
|
||||
export const getSupabaseUrl = () => {
|
||||
const url = import.meta.env.VITE_SUPABASE_URL;
|
||||
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getSupabaseKey = () => {
|
||||
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
|
||||
return key;
|
||||
};
|
||||
|
||||
// Supabase 키 유효성 검사 - Cloud 환경에서는 항상 유효함
|
||||
export const isValidSupabaseKey = () => {
|
||||
return Boolean(import.meta.env.VITE_SUPABASE_ANON_KEY);
|
||||
};
|
||||
|
||||
// 다음 함수들은 Cloud 환경에서는 필요 없지만 호환성을 위해 유지
|
||||
export const isCorsProxyEnabled = () => {
|
||||
return false;
|
||||
};
|
||||
|
||||
export const getProxyType = () => {
|
||||
return 'none';
|
||||
};
|
||||
|
||||
export const getOriginalSupabaseUrl = () => {
|
||||
return getSupabaseUrl();
|
||||
};
|
||||
78
src/archive/lib/supabase/customFetch.ts
Normal file
78
src/archive/lib/supabase/customFetch.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
|
||||
import { getSupabaseKey } from './config';
|
||||
import { modifyStorageApiRequest } from './storageUtils';
|
||||
|
||||
/**
|
||||
* Supabase 클라이언트에서 사용할 커스텀 fetch 구현
|
||||
*/
|
||||
export const customFetch = (...args: [RequestInfo | URL, RequestInit?]): Promise<Response> => {
|
||||
// 첫 번째 인자는 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;
|
||||
}
|
||||
15
src/archive/lib/supabase/index.ts
Normal file
15
src/archive/lib/supabase/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import { supabase, isValidUrl } from './client';
|
||||
import { createRequiredTables, checkTablesStatus } from './setup';
|
||||
import { customFetch } from './customFetch';
|
||||
import { modifyStorageApiRequest, getStorageApiHeaders } from './storageUtils';
|
||||
|
||||
export {
|
||||
supabase,
|
||||
isValidUrl,
|
||||
createRequiredTables,
|
||||
checkTablesStatus,
|
||||
customFetch,
|
||||
modifyStorageApiRequest,
|
||||
getStorageApiHeaders
|
||||
};
|
||||
3
src/archive/lib/supabase/setup.ts
Normal file
3
src/archive/lib/supabase/setup.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
// 리팩토링된 파일로 리다이렉션
|
||||
export { createRequiredTables, checkTablesStatus } from './setup/index';
|
||||
37
src/archive/lib/supabase/setup/index.ts
Normal file
37
src/archive/lib/supabase/setup/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
import { supabase } from '../client';
|
||||
import { checkTablesStatus } from './status';
|
||||
|
||||
/**
|
||||
* Supabase 데이터베이스에 필요한 테이블이 있는지 확인합니다.
|
||||
* 이 함수는 로그인 후 실행되어야 합니다.
|
||||
*/
|
||||
export const createRequiredTables = async (): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
console.log('데이터베이스 테이블 확인 시작...');
|
||||
|
||||
// 테이블 상태 확인
|
||||
const tablesStatus = await checkTablesStatus();
|
||||
|
||||
if (tablesStatus.transactions && tablesStatus.budgets && tablesStatus.category_budgets) {
|
||||
return {
|
||||
success: true,
|
||||
message: '필요한 테이블이 이미 존재합니다.'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: '일부 필요한 테이블이 없습니다. Supabase 대시보드에서 확인해주세요.'
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('테이블 확인 중 오류 발생:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `테이블 확인 실패: ${error.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 기존 checkTablesStatus 함수 내보내기
|
||||
export { checkTablesStatus };
|
||||
45
src/archive/lib/supabase/setup/status.ts
Normal file
45
src/archive/lib/supabase/setup/status.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
|
||||
import { supabase } from '../client';
|
||||
|
||||
/**
|
||||
* 테이블 상태를 확인합니다.
|
||||
*/
|
||||
export const checkTablesStatus = async (): Promise<{
|
||||
transactions: boolean;
|
||||
budgets: boolean;
|
||||
category_budgets: boolean;
|
||||
}> => {
|
||||
const tables = {
|
||||
transactions: false,
|
||||
budgets: false,
|
||||
category_budgets: false
|
||||
};
|
||||
|
||||
try {
|
||||
// transactions 테이블 확인
|
||||
const { data: transactionsData, error: transactionsError } = await supabase
|
||||
.from('transactions')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
tables.transactions = !transactionsError;
|
||||
|
||||
// budgets 테이블 확인
|
||||
const { data: budgetsData, error: budgetsError } = await supabase
|
||||
.from('budgets')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
tables.budgets = !budgetsError;
|
||||
|
||||
// category_budgets 테이블 확인
|
||||
const { data: categoryBudgetsData, error: categoryBudgetsError } = await supabase
|
||||
.from('category_budgets')
|
||||
.select('*', { count: 'exact', head: true });
|
||||
|
||||
tables.category_budgets = !categoryBudgetsError;
|
||||
|
||||
return tables;
|
||||
} catch (error) {
|
||||
console.error('테이블 상태 확인 중 오류 발생:', error);
|
||||
return tables;
|
||||
}
|
||||
};
|
||||
175
src/archive/lib/supabase/setup/tables.ts
Normal file
175
src/archive/lib/supabase/setup/tables.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
|
||||
import { supabase } from '../client';
|
||||
|
||||
/**
|
||||
* 트랜잭션 테이블을 생성합니다.
|
||||
*/
|
||||
export const createTransactionsTable = async (): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
const { error } = await supabase.rpc('execute_sql', {
|
||||
sql_query: `
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
amount NUMERIC NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
type TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Row Level Security 설정
|
||||
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자 정책 설정 (읽기)
|
||||
CREATE POLICY "사용자는 자신의 트랜잭션만 볼 수 있음"
|
||||
ON transactions FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (쓰기)
|
||||
CREATE POLICY "사용자는 자신의 트랜잭션만 추가할 수 있음"
|
||||
ON transactions FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (업데이트)
|
||||
CREATE POLICY "사용자는 자신의 트랜잭션만 업데이트할 수 있음"
|
||||
ON transactions FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (삭제)
|
||||
CREATE POLICY "사용자는 자신의 트랜잭션만 삭제할 수 있음"
|
||||
ON transactions FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('transactions 테이블 생성 실패:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `transactions 테이블 생성 실패: ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'transactions 테이블 생성 성공'
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('트랜잭션 테이블 생성 중 오류 발생:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `트랜잭션 테이블 생성 실패: ${error.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 예산 테이블을 생성합니다.
|
||||
*/
|
||||
export const createBudgetsTable = async (): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
const { error } = await supabase.rpc('execute_sql', {
|
||||
sql_query: `
|
||||
CREATE TABLE IF NOT EXISTS budgets (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
||||
month INTEGER NOT NULL,
|
||||
year INTEGER NOT NULL,
|
||||
total_budget NUMERIC NOT NULL DEFAULT 0,
|
||||
categories JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
UNIQUE (user_id, month, year)
|
||||
);
|
||||
|
||||
-- Row Level Security 설정
|
||||
ALTER TABLE budgets ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- 사용자 정책 설정 (읽기)
|
||||
CREATE POLICY "사용자는 자신의 예산만 볼 수 있음"
|
||||
ON budgets FOR SELECT
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (쓰기)
|
||||
CREATE POLICY "사용자는 자신의 예산만 추가할 수 있음"
|
||||
ON budgets FOR INSERT
|
||||
WITH CHECK (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (업데이트)
|
||||
CREATE POLICY "사용자는 자신의 예산만 업데이트할 수 있음"
|
||||
ON budgets FOR UPDATE
|
||||
USING (auth.uid() = user_id);
|
||||
|
||||
-- 사용자 정책 설정 (삭제)
|
||||
CREATE POLICY "사용자는 자신의 예산만 삭제할 수 있음"
|
||||
ON budgets FOR DELETE
|
||||
USING (auth.uid() = user_id);
|
||||
`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('budgets 테이블 생성 실패:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `budgets 테이블 생성 실패: ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'budgets 테이블 생성 성공'
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('예산 테이블 생성 중 오류 발생:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `예산 테이블 생성 실패: ${error.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 테스트 테이블을 생성합니다.
|
||||
*/
|
||||
export const createTestsTable = async (): Promise<{ success: boolean; message: string }> => {
|
||||
try {
|
||||
const { error } = await supabase.rpc('execute_sql', {
|
||||
sql_query: `
|
||||
CREATE TABLE IF NOT EXISTS _tests (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
test_name TEXT NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 모든 사용자가 접근 가능하도록 설정
|
||||
ALTER TABLE _tests ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY "모든 사용자가 테스트 테이블에 접근 가능"
|
||||
ON _tests FOR SELECT
|
||||
USING (true);
|
||||
`
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('_tests 테이블 생성 실패:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `_tests 테이블 생성 실패: ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '_tests 테이블 생성 성공'
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('테스트 테이블 생성 중 오류 발생:', error);
|
||||
return {
|
||||
success: false,
|
||||
message: `테스트 테이블 생성 실패: ${error.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
47
src/archive/lib/supabase/storageUtils.ts
Normal file
47
src/archive/lib/supabase/storageUtils.ts
Normal file
@@ -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;
|
||||
}
|
||||
76
src/archive/lib/supabase/tests/apiTests.ts
Normal file
76
src/archive/lib/supabase/tests/apiTests.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { TestResult } from './types';
|
||||
import { getSupabaseUrl, getSupabaseKey } from '../config';
|
||||
|
||||
// REST API 테스트
|
||||
export const testRestApi = async (
|
||||
supabase: SupabaseClient
|
||||
): Promise<TestResult> => {
|
||||
console.log('REST API 테스트 시작...');
|
||||
|
||||
try {
|
||||
const originalUrl = getSupabaseUrl();
|
||||
const supabaseKey = getSupabaseKey();
|
||||
|
||||
if (!originalUrl || !supabaseKey) {
|
||||
return {
|
||||
success: false,
|
||||
error: '유효하지 않은 Supabase URL 또는 API 키'
|
||||
};
|
||||
}
|
||||
|
||||
// HTTP URL 감지
|
||||
const isHttpUrl = originalUrl.startsWith('http:') && !originalUrl.startsWith('http://localhost');
|
||||
|
||||
// 클라이언트 인스턴스로 간단한 API 호출 수행
|
||||
const { data, error } = await supabase
|
||||
.from('_tests')
|
||||
.select('*')
|
||||
.limit(1);
|
||||
|
||||
if (error) {
|
||||
// CORS 관련 오류인지 확인
|
||||
if (error.message && error.message.includes('fetch failed') && isHttpUrl) {
|
||||
return {
|
||||
success: false,
|
||||
error: `REST API 요청 실패: ${error.message} (CORS 오류 가능성 높음. CORS 프록시 사용을 권장합니다)`
|
||||
};
|
||||
}
|
||||
|
||||
// 테이블이 없는 경우 정상 (테스트 테이블이 없을 수 있음)
|
||||
if (error.code === '42P01' || error.code === 'PGRST116') {
|
||||
return {
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `REST API 요청 실패: ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
} catch (err: any) {
|
||||
// HTTP URL을 사용하고 있는지 확인
|
||||
const url = getSupabaseUrl();
|
||||
const isHttpUrl = url.startsWith('http:') && !url.startsWith('http://localhost');
|
||||
|
||||
if (err.message && err.message.includes('Failed to fetch') && isHttpUrl) {
|
||||
return {
|
||||
success: false,
|
||||
error: `REST API 테스트 실패: ${err.message} (CORS 프록시 사용시 정상 작동합니다. 설정에서 프록시를 활성화해보세요)`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `REST API 테스트 예외: ${err.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
71
src/archive/lib/supabase/tests/authTests.ts
Normal file
71
src/archive/lib/supabase/tests/authTests.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { supabase } from '../client';
|
||||
import { LoginTestResult, TestResult } from './types';
|
||||
|
||||
// Supabase 인증 서비스 테스트
|
||||
export const testAuth = async (
|
||||
supabase: SupabaseClient,
|
||||
url: string
|
||||
): Promise<TestResult> => {
|
||||
try {
|
||||
console.log('인증 서비스 테스트 시작...');
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('인증 테스트 실패:', error);
|
||||
return { success: false, error: `인증 서비스 오류: ${error.message}` };
|
||||
}
|
||||
|
||||
console.log('인증 테스트 성공');
|
||||
return { success: true, error: null };
|
||||
} catch (err: any) {
|
||||
console.error('인증 테스트 중 예외:', err);
|
||||
return {
|
||||
success: false,
|
||||
error: `인증 테스트 중 예외 발생: ${err.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// 테스트용 직접 로그인 함수 (디버깅 전용)
|
||||
export const testSupabaseLogin = async (email: string, password: string): Promise<LoginTestResult> => {
|
||||
try {
|
||||
console.log('테스트 로그인 시도:', email);
|
||||
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('테스트 로그인 오류:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
console.log('테스트 로그인 성공:', data);
|
||||
return { success: true, data };
|
||||
} catch (err) {
|
||||
console.error('테스트 로그인 중 예외 발생:', err);
|
||||
return { success: false, error: err };
|
||||
}
|
||||
};
|
||||
|
||||
// 인증 서비스 테스트
|
||||
export const testAuthService = async (): Promise<{ success: boolean; error?: any }> => {
|
||||
try {
|
||||
console.log('인증 서비스 테스트 시작...');
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
console.error('인증 테스트 실패:', error);
|
||||
return { success: false, error };
|
||||
}
|
||||
|
||||
console.log('인증 테스트 성공');
|
||||
return { success: true };
|
||||
} catch (err) {
|
||||
console.error('인증 테스트 중 예외:', err);
|
||||
return { success: false, error: err };
|
||||
}
|
||||
};
|
||||
55
src/archive/lib/supabase/tests/databaseTests.ts
Normal file
55
src/archive/lib/supabase/tests/databaseTests.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
import { TestResult } from './types';
|
||||
import { getSupabaseUrl } from '../config';
|
||||
|
||||
export const testDatabaseConnection = async (
|
||||
supabase: SupabaseClient
|
||||
): Promise<TestResult> => {
|
||||
try {
|
||||
// 간단한 쿼리 실행으로 데이터베이스 연결 테스트
|
||||
const { data, error } = await supabase
|
||||
.from('_tests')
|
||||
.select('*')
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
// 테이블이 없는 경우 정상 (테스트 테이블이 없을 수 있음)
|
||||
if (error && (error.code === '42P01' || error.code === 'PGRST116')) {
|
||||
return {
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
// 다른 오류가 있으면 실패로 처리
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: `데이터베이스 연결 오류: ${error.message}`
|
||||
};
|
||||
}
|
||||
|
||||
// 성공
|
||||
return {
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
} catch (error: any) {
|
||||
// HTTP URL 관련 CORS 오류 확인
|
||||
const url = getSupabaseUrl();
|
||||
const isHttpUrl = url.startsWith('http:') && !url.startsWith('http://localhost');
|
||||
|
||||
if (error.message && error.message.includes('Failed to fetch') && isHttpUrl) {
|
||||
return {
|
||||
success: false,
|
||||
error: `데이터베이스 테스트 실패: ${error.message} (CORS 프록시 사용시 정상 작동합니다. 설정에서 프록시를 활성화해보세요)`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: `데이터베이스 테스트 중 예외 발생: ${error.message || '알 수 없는 오류'}`
|
||||
};
|
||||
}
|
||||
};
|
||||
51
src/archive/lib/supabase/tests/types.ts
Normal file
51
src/archive/lib/supabase/tests/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
// 테스트 결과를 위한 공통 타입 정의
|
||||
|
||||
export interface TestResult {
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface TestDebugInfo {
|
||||
originalUrl: string;
|
||||
proxyUrl: string;
|
||||
usingProxy: boolean;
|
||||
proxyType: string;
|
||||
keyLength: number;
|
||||
browserInfo: string;
|
||||
timestamp: string;
|
||||
backupProxySuccess: boolean;
|
||||
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;
|
||||
usingProxy: boolean;
|
||||
proxyType: string;
|
||||
client: boolean;
|
||||
restApi: boolean;
|
||||
auth: boolean;
|
||||
database: boolean;
|
||||
errors: string[];
|
||||
debugInfo: TestDebugInfo;
|
||||
}
|
||||
|
||||
export interface LoginTestResult {
|
||||
success: boolean;
|
||||
data?: any;
|
||||
error?: any;
|
||||
}
|
||||
102
src/archive/utils/supabaseTransactionUtils.ts
Normal file
102
src/archive/utils/supabaseTransactionUtils.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Transaction } from '@/components/TransactionCard';
|
||||
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||
import { toast } from '@/hooks/useToast.wrapper';
|
||||
|
||||
// Supabase와 거래 데이터 동기화
|
||||
export const syncTransactionsWithSupabase = async (
|
||||
user: any,
|
||||
transactions: Transaction[]
|
||||
): Promise<Transaction[]> => {
|
||||
if (!user || !isSyncEnabled()) return transactions;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('transactions')
|
||||
.select('*')
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Supabase 데이터 조회 오류:', error);
|
||||
return transactions;
|
||||
}
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// Supabase 데이터 로컬 형식으로 변환
|
||||
const supabaseTransactions = data.map(t => ({
|
||||
id: t.transaction_id || t.id,
|
||||
title: t.title,
|
||||
amount: t.amount,
|
||||
date: t.date,
|
||||
category: t.category,
|
||||
type: t.type
|
||||
}));
|
||||
|
||||
// 로컬 데이터와 병합 (중복 ID 제거)
|
||||
const mergedTransactions = [...transactions];
|
||||
|
||||
supabaseTransactions.forEach(newTx => {
|
||||
const existingIndex = mergedTransactions.findIndex(t => t.id === newTx.id);
|
||||
if (existingIndex >= 0) {
|
||||
mergedTransactions[existingIndex] = newTx;
|
||||
} else {
|
||||
mergedTransactions.push(newTx);
|
||||
}
|
||||
});
|
||||
|
||||
return mergedTransactions;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Supabase 동기화 오류:', err);
|
||||
}
|
||||
|
||||
return transactions;
|
||||
};
|
||||
|
||||
// Supabase에 트랜잭션 업데이트
|
||||
export const updateTransactionInSupabase = async (
|
||||
user: any,
|
||||
transaction: Transaction
|
||||
): Promise<void> => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('transactions')
|
||||
.upsert({
|
||||
user_id: user.id,
|
||||
title: transaction.title,
|
||||
amount: transaction.amount,
|
||||
date: transaction.date,
|
||||
category: transaction.category,
|
||||
type: transaction.type,
|
||||
transaction_id: transaction.id
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('트랜잭션 업데이트 오류:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Supabase 업데이트 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Supabase에서 트랜잭션 삭제
|
||||
export const deleteTransactionFromSupabase = async (
|
||||
user: any,
|
||||
transactionId: string
|
||||
): Promise<void> => {
|
||||
if (!user || !isSyncEnabled()) return;
|
||||
|
||||
try {
|
||||
const { error } = await supabase.from('transactions')
|
||||
.delete()
|
||||
.eq('transaction_id', transactionId);
|
||||
|
||||
if (error) {
|
||||
console.error('트랜잭션 삭제 오류:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Supabase 삭제 오류:', error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user