From f83bb384af0c888233bf18376d5000a570e6e26a Mon Sep 17 00:00:00 2001 From: hansoo Date: Mon, 5 May 2025 08:58:27 +0900 Subject: [PATCH] Migrate from Supabase to Appwrite with core functionality and UI components --- .env | 8 + docs/02_기술_문서/Appwrite_전환_가이드.md | 276 ++++++++++++++++ docs/03_개발_단계/개발_가이드라인.md | 59 ++++ docs/README.md | 150 ++------- .../03_개발_단계/01_개발_로드맵.md | 0 .../03_개발_단계/1단계/1단계_산출물_요약.md | 0 .../03_개발_단계/1단계/1단계_첫주차_할일.md | 0 .../03_개발_단계/2단계/2단계_계획.md | 0 .../03_개발_단계/3단계/3단계_계획.md | 0 .../03_개발_단계/4단계/4단계_계획.md | 0 .../Nginx_Supabase_설치_가이드.md | 0 .../SUPABASE_ONPREM_MIGRATION_PLAN.md | 0 .../Supabase_설정_가이드.md | 0 .../Supabase_인증_정보.md | 0 package-lock.json | 7 + package.json | 1 + src/App.tsx | 2 + src/archive/README.md | 21 ++ .../components}/SupabaseConnectionStatus.tsx | 0 .../supabase/DebugInfoCollapsible.tsx | 0 .../components/supabase/ErrorMessageCard.tsx | 0 .../supabase/ProxyRecommendationAlert.tsx | 0 .../supabase/TroubleshootingTips.tsx | 0 .../hooks/transactions/supabaseUtils.ts | 0 .../integrations/supabase/client.ts | 0 .../integrations/supabase/types.ts | 0 src/{ => archive}/lib/supabase/client.ts | 0 src/{ => archive}/lib/supabase/config.ts | 0 src/{ => archive}/lib/supabase/customFetch.ts | 0 src/{ => archive}/lib/supabase/index.ts | 0 src/{ => archive}/lib/supabase/setup.ts | 0 src/{ => archive}/lib/supabase/setup/index.ts | 0 .../lib/supabase/setup/status.ts | 0 .../lib/supabase/setup/tables.ts | 0 .../lib/supabase/storageUtils.ts | 0 .../lib/supabase/tests/apiTests.ts | 0 .../lib/supabase/tests/authTests.ts | 0 .../lib/supabase/tests/databaseTests.ts | 0 src/{ => archive}/lib/supabase/tests/types.ts | 0 .../utils/supabaseTransactionUtils.ts | 0 src/components/AddTransactionButton.tsx | 2 +- .../auth/AppwriteConnectionStatus.tsx | 41 +++ .../auth/AppwriteConnectionTest.tsx | 146 +++++++++ src/components/auth/RegisterForm.tsx | 2 +- .../migration/SupabaseToAppwriteMigration.tsx | 300 ++++++++++++++++++ src/contexts/auth/AuthProvider.tsx | 64 ++-- src/contexts/auth/resetPassword.ts | 2 +- src/contexts/auth/signIn.ts | 55 ++-- src/contexts/auth/signInUtils.ts | 2 +- src/contexts/auth/signOut.ts | 2 +- src/contexts/auth/signUp.ts | 2 +- src/contexts/auth/signUpUtils.ts | 2 +- src/contexts/auth/types.ts | 6 +- src/hooks/auth/useAppwriteAuth.ts | 182 +++++++++++ .../transactions/useAppwriteTransactions.ts | 162 ++++++++++ src/hooks/useTableSetup.ts | 2 +- src/lib/appwrite/client.ts | 83 +++++ src/lib/appwrite/config.ts | 45 +++ src/lib/appwrite/index.ts | 30 ++ src/lib/appwrite/migrateFromSupabase.ts | 186 +++++++++++ src/lib/appwrite/setup.ts | 172 ++++++++++ src/pages/AppwriteSettingsPage.tsx | 256 +++++++++++++++ src/pages/Register.tsx | 2 +- src/pages/Settings.tsx | 3 +- src/utils/appwriteTransactionUtils.ts | 258 +++++++++++++++ src/utils/auth/network/connectionVerifier.ts | 2 +- src/utils/auth/network/enhancedVerifier.ts | 2 +- src/utils/categoryBudgetUtils.ts | 2 +- src/utils/sync/budget/downloadBudget.ts | 2 +- .../sync/budget/uploadCategoryBudgets.ts | 2 +- src/utils/sync/budget/uploadMonthlyBudget.ts | 2 +- src/utils/sync/clearCloudData.ts | 2 +- src/utils/sync/core/status.ts | 2 +- src/utils/sync/downloadBudget.ts | 2 +- src/utils/sync/downloadTransaction.ts | 2 +- .../sync/transaction/deleteTransaction.ts | 2 +- .../sync/transaction/downloadTransaction.ts | 2 +- .../sync/transaction/uploadTransaction.ts | 2 +- update-supabase-imports.sh | 15 + 79 files changed, 2373 insertions(+), 199 deletions(-) create mode 100644 docs/02_기술_문서/Appwrite_전환_가이드.md create mode 100644 docs/03_개발_단계/개발_가이드라인.md rename docs/{ => archive}/03_개발_단계/01_개발_로드맵.md (100%) rename docs/{ => archive}/03_개발_단계/1단계/1단계_산출물_요약.md (100%) rename docs/{ => archive}/03_개발_단계/1단계/1단계_첫주차_할일.md (100%) rename docs/{ => archive}/03_개발_단계/2단계/2단계_계획.md (100%) rename docs/{ => archive}/03_개발_단계/3단계/3단계_계획.md (100%) rename docs/{ => archive}/03_개발_단계/4단계/4단계_계획.md (100%) rename docs/{02_기술_문서 => archive}/Nginx_Supabase_설치_가이드.md (100%) rename docs/{ => archive}/SUPABASE_ONPREM_MIGRATION_PLAN.md (100%) rename docs/{02_기술_문서 => archive}/Supabase_설정_가이드.md (100%) rename docs/{02_기술_문서 => archive}/Supabase_인증_정보.md (100%) create mode 100644 src/archive/README.md rename src/{components/auth => archive/components}/SupabaseConnectionStatus.tsx (100%) rename src/{ => archive}/components/supabase/DebugInfoCollapsible.tsx (100%) rename src/{ => archive}/components/supabase/ErrorMessageCard.tsx (100%) rename src/{ => archive}/components/supabase/ProxyRecommendationAlert.tsx (100%) rename src/{ => archive}/components/supabase/TroubleshootingTips.tsx (100%) rename src/{ => archive}/hooks/transactions/supabaseUtils.ts (100%) rename src/{ => archive}/integrations/supabase/client.ts (100%) rename src/{ => archive}/integrations/supabase/types.ts (100%) rename src/{ => archive}/lib/supabase/client.ts (100%) rename src/{ => archive}/lib/supabase/config.ts (100%) rename src/{ => archive}/lib/supabase/customFetch.ts (100%) rename src/{ => archive}/lib/supabase/index.ts (100%) rename src/{ => archive}/lib/supabase/setup.ts (100%) rename src/{ => archive}/lib/supabase/setup/index.ts (100%) rename src/{ => archive}/lib/supabase/setup/status.ts (100%) rename src/{ => archive}/lib/supabase/setup/tables.ts (100%) rename src/{ => archive}/lib/supabase/storageUtils.ts (100%) rename src/{ => archive}/lib/supabase/tests/apiTests.ts (100%) rename src/{ => archive}/lib/supabase/tests/authTests.ts (100%) rename src/{ => archive}/lib/supabase/tests/databaseTests.ts (100%) rename src/{ => archive}/lib/supabase/tests/types.ts (100%) rename src/{ => archive}/utils/supabaseTransactionUtils.ts (100%) create mode 100644 src/components/auth/AppwriteConnectionStatus.tsx create mode 100644 src/components/auth/AppwriteConnectionTest.tsx create mode 100644 src/components/migration/SupabaseToAppwriteMigration.tsx create mode 100644 src/hooks/auth/useAppwriteAuth.ts create mode 100644 src/hooks/transactions/useAppwriteTransactions.ts create mode 100644 src/lib/appwrite/client.ts create mode 100644 src/lib/appwrite/config.ts create mode 100644 src/lib/appwrite/index.ts create mode 100644 src/lib/appwrite/migrateFromSupabase.ts create mode 100644 src/lib/appwrite/setup.ts create mode 100644 src/pages/AppwriteSettingsPage.tsx create mode 100644 src/utils/appwriteTransactionUtils.ts create mode 100755 update-supabase-imports.sh diff --git a/.env b/.env index 839f7e6..a6896ad 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ +# Supabase 관련 설정 (이전 버전) CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres # On-Prem Postgres 기본 @@ -9,4 +10,11 @@ CLOUD_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ ONPREM_SUPABASE_URL=http://localhost:9000 ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q + +# Appwrite 관련 설정 +VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1 +VITE_APPWRITE_PROJECT_ID=zellyy-finance +VITE_APPWRITE_DATABASE_ID=zellyy-finance +VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions + VITE_DISABLE_LOVABLE_BANNER=true diff --git a/docs/02_기술_문서/Appwrite_전환_가이드.md b/docs/02_기술_문서/Appwrite_전환_가이드.md new file mode 100644 index 0000000..7879ed4 --- /dev/null +++ b/docs/02_기술_문서/Appwrite_전환_가이드.md @@ -0,0 +1,276 @@ +# Appwrite 전환 가이드 + +## 개요 + +Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환합니다. 이 문서는 전환 과정과 새로운 코드 구조에 대한 가이드를 제공합니다. + +## 전환 이유 + +1. **더 나은 성능**: Appwrite는 경량화된 서비스로 더 빠른 응답 시간 제공 +2. **확장성**: 사용자 증가에 따른 확장성 개선 +3. **기능 세트**: Appwrite의 실시간 데이터베이스와 인증 시스템 활용 +4. **유지보수 용이성**: 단일 백엔드 서비스로 통합하여 유지보수 간소화 + +## 코드 구조 + +``` +src/ +├── lib/ +│ ├── appwrite/ (Appwrite 서비스) +│ │ ├── index.ts (단일 진입점) +│ │ ├── client.ts (클라이언트 설정) +│ │ ├── config.ts (환경 설정) +│ │ └── setup.ts (데이터베이스 설정) +│ └── capacitor/ (네이티브 기능) +│ ├── index.ts (단일 진입점) +│ ├── buildInfo.ts (빌드 정보 관련) +│ ├── notification.ts (알림 관련) +│ └── permissions.ts (권한 관련) +├── hooks/ +│ ├── auth/ +│ │ └── useAppwriteAuth.ts (인증 관련 훅) +│ └── transactions/ +│ └── useAppwriteTransactions.ts (트랜잭션 관련 훅) +├── components/ +│ ├── auth/ +│ │ └── AppwriteConnectionStatus.tsx (연결 상태 표시) +│ ├── migration/ +│ │ └── SupabaseToAppwriteMigration.tsx (마이그레이션 도구) +│ └── native/ +│ ├── PermissionRequest.tsx (권한 요청 UI) +│ └── NotificationSettings.tsx (알림 설정 UI) +└── utils/ + └── appwriteTransactionUtils.ts (트랜잭션 유틸리티) +``` + +## 환경 설정 + +`.env` 파일에 다음 환경 변수를 설정합니다: + +``` +# Appwrite 설정 +VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1 +VITE_APPWRITE_PROJECT_ID=zellyy-finance +VITE_APPWRITE_DATABASE_ID=zellyy-finance +VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions + +# 네이티브 설정 +VITE_ANDROID_MIN_API_LEVEL=21 +VITE_ANDROID_TARGET_API_LEVEL=33 +VITE_ANDROID_NOTIFICATION_CHANNEL_ID=zellyy_finance_notifications +``` + +## 마이그레이션 단계 + +1. **데이터베이스 설정** + - Appwrite 데이터베이스 및 컬렉션 생성 + - 필요한 인덱스 및 권한 설정 + +2. **인증 시스템 전환** + - Appwrite 인증 시스템 설정 + - 사용자 계정 마이그레이션 + +3. **데이터 마이그레이션** + - 트랜잭션 데이터 마이그레이션 + - 데이터 무결성 검증 + +4. **Supabase 코드 제거** + - 마이그레이션 완료 후 Supabase 관련 코드 제거 + - 환경 변수 정리 + +## 주요 컴포넌트 및 훅 + +### 1. Appwrite 클라이언트 설정 + +```typescript +// src/lib/appwrite/client.ts +import { Client, Account, Databases, Storage } from 'appwrite'; +import { config } from './config'; + +// 클라이언트 초기화 +export const client = new Client() + .setEndpoint(config.endpoint) + .setProject(config.projectId); + +// 서비스 초기화 +export const account = new Account(client); +export const databases = new Databases(client); +export const storage = new Storage(client); +``` + +### 2. 인증 훅 + +```typescript +// src/hooks/auth/useAppwriteAuth.ts +import { useState, useEffect } from 'react'; +import { account } from '../../lib/appwrite'; + +export const useAppwriteAuth = () => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + // 사용자 세션 확인 + useEffect(() => { + const checkSession = async () => { + try { + const session = await account.getSession('current'); + if (session) { + const currentUser = await account.get(); + setUser(currentUser); + } + } catch (error) { + console.error('세션 확인 오류:', error); + } finally { + setLoading(false); + } + }; + + checkSession(); + }, []); + + // 로그인 함수 + const login = async (email, password) => { + try { + await account.createEmailSession(email, password); + const currentUser = await account.get(); + setUser(currentUser); + return { success: true }; + } catch (error) { + console.error('로그인 오류:', error); + return { success: false, error }; + } + }; + + // 로그아웃 함수 + const logout = async () => { + try { + await account.deleteSession('current'); + setUser(null); + return { success: true }; + } catch (error) { + console.error('로그아웃 오류:', error); + return { success: false, error }; + } + }; + + return { user, loading, login, logout }; +}; +``` + +### 3. 트랜잭션 훅 + +```typescript +// src/hooks/transactions/useAppwriteTransactions.ts +import { useState, useEffect, useCallback } from 'react'; +import { databases } from '../../lib/appwrite'; +import { config } from '../../lib/appwrite/config'; +import { Query } from 'appwrite'; + +export const useAppwriteTransactions = (userId) => { + const [transactions, setTransactions] = useState([]); + const [loading, setLoading] = useState(true); + + // 트랜잭션 불러오기 + const fetchTransactions = useCallback(async () => { + if (!userId) return; + + try { + setLoading(true); + const response = await databases.listDocuments( + config.databaseId, + config.transactionsCollectionId, + [Query.equal('userId', userId)] + ); + setTransactions(response.documents); + } catch (error) { + console.error('트랜잭션 불러오기 오류:', error); + } finally { + setLoading(false); + } + }, [userId]); + + // 초기 데이터 로드 + useEffect(() => { + fetchTransactions(); + }, [fetchTransactions]); + + // 트랜잭션 추가 + const addTransaction = async (transaction) => { + try { + const newTransaction = await databases.createDocument( + config.databaseId, + config.transactionsCollectionId, + 'unique()', + { + ...transaction, + userId, + createdAt: new Date().toISOString(), + } + ); + setTransactions((prev) => [...prev, newTransaction]); + return { success: true, transaction: newTransaction }; + } catch (error) { + console.error('트랜잭션 추가 오류:', error); + return { success: false, error }; + } + }; + + // 트랜잭션 업데이트 + const updateTransaction = async (id, data) => { + try { + const updatedTransaction = await databases.updateDocument( + config.databaseId, + config.transactionsCollectionId, + id, + data + ); + setTransactions((prev) => + prev.map((t) => (t.$id === id ? updatedTransaction : t)) + ); + return { success: true, transaction: updatedTransaction }; + } catch (error) { + console.error('트랜잭션 업데이트 오류:', error); + return { success: false, error }; + } + }; + + // 트랜잭션 삭제 + const deleteTransaction = async (id) => { + try { + await databases.deleteDocument( + config.databaseId, + config.transactionsCollectionId, + id + ); + setTransactions((prev) => prev.filter((t) => t.$id !== id)); + return { success: true }; + } catch (error) { + console.error('트랜잭션 삭제 오류:', error); + return { success: false, error }; + } + }; + + return { + transactions, + loading, + fetchTransactions, + addTransaction, + updateTransaction, + deleteTransaction, + }; +}; +``` + +## 마이그레이션 도구 사용법 + +1. 설정 페이지에서 "Appwrite 설정" 메뉴 선택 +2. "Supabase에서 Appwrite로 마이그레이션" 섹션에서 "마이그레이션 시작" 버튼 클릭 +3. 마이그레이션 진행 상황 확인 +4. 완료 후 데이터 검증 + +## 주의사항 + +1. 마이그레이션 중에는 데이터 변경을 최소화하세요. +2. 마이그레이션 전에 데이터 백업을 수행하세요. +3. 마이그레이션 후 모든 기능이 정상 작동하는지 테스트하세요. +4. 문제 발생 시 개발팀에 즉시 보고하세요. diff --git a/docs/03_개발_단계/개발_가이드라인.md b/docs/03_개발_단계/개발_가이드라인.md new file mode 100644 index 0000000..168a925 --- /dev/null +++ b/docs/03_개발_단계/개발_가이드라인.md @@ -0,0 +1,59 @@ +# Zellyy Finance 개발 가이드라인 + +## 1. 코드 작성 원칙 +- 모든 컴포넌트는 함수형 컴포넌트로 작성할 것 +- Hook 명명 규칙은 'use'로 시작하는 camelCase 사용할 것 +- 비즈니스 로직은 훅으로 분리하여 재사용성 높일 것 +- 주석은 한국어로 작성하여 가독성 높일 것 +- prop 타입은 모두 TypeScript 인터페이스로 정의할 것 + +## 2. 트랜잭션 삭제 안전성 +- 트랜잭션 삭제 작업은 UI 스레드를 차단하지 않도록 비동기로 처리할 것 +- 상태 업데이트 전/후에 try-catch 블록으로 오류 처리할 것 +- 가능한 requestAnimationFrame 또는 queueMicrotask를 사용하여 UI 업데이트 최적화할 것 +- 컴포넌트 언마운트 상태를 추적하여 메모리 누수 방지할 것 +- 이벤트 핸들러는 성능 병목 지점이 될 수 있으므로 디바운스/스로틀링 적용할 것 + +## 3. Appwrite 통합 원칙 +- Appwrite 클라이언트는 앱 시작 시 한 번만 초기화 +- 인증 및 데이터 동기화는 전용 훅 사용 +- 오류 처리 및 사용자 피드백 제공 +- 트랜잭션 작업은 비동기로 처리 +- 네트워크 오류 시 적절한 재시도 메커니즘 구현 + +## 4. 상태 관리 최적화 +- 컴포넌트 간 상태 공유는 Context API나 상태 관리 라이브러리 사용할 것 +- 큰 상태 객체는 여러 작은 조각으로 분리하여 불필요한 리렌더링 방지할 것 +- 불변성을 유지하여 React의 상태 업데이트 최적화 활용할 것 +- useCallback, useMemo를 적극 활용하여 함수와 값 메모이제이션할 것 +- 기본 데이터 로딩은 상위 컴포넌트에서 처리하고 하위 컴포넌트로 전달할 것 + +## 5. 디버깅 및 로깅 +- 중요 작업(특히 트랜잭션 삭제와 같은 위험 작업)은 상세한 로그 남길 것 +- 개발 모드에서는 상태 변화를 추적할 수 있는 로그 포함할 것 +- 사용자에게 영향을 주는 오류는 UI 피드백(토스트 등)으로 표시할 것 +- 백그라운드 작업 실패는 적절히 로깅하고 필요시 재시도 메커니즘 구현할 것 + +## 6. iOS 지원 +- iOS 안전 영역(Safe Area) 고려한 UI 레이아웃 설계 +- iOS 특유의 제스처와 상호작용 패턴 지원 (스와이프, 핀치 등) +- iOS 다크 모드 대응을 위한 동적 색상 시스템 활용 +- iOS 기기별 화면 크기 및 노치(Notch) 대응 +- iOS 앱 배포 시 필요한 인증서 및 프로비저닝 프로파일 관리 + +## 7. Android 지원 +- BuildInfo와 같은 네이티브 플러그인은 반드시 MainActivity에 등록할 것 +- 안드로이드 빌드 정보는 Capacitor 플러그인을 통해 JS로 전달할 것 +- 플러그인 호출 시 항상 오류 처리 로직 포함할 것 +- 네이티브 기능 실패 시 대체 방법(fallback) 제공할 것 +- 안드로이드 버전별 호환성 고려 (API 레벨 차이) +- 다양한 화면 크기 및 해상도 대응 (태블릿 포함) +- 안드로이드 백 버튼 처리 및 생명주기 관리 +- 권한 요청 및 처리 로직 구현 +- 안드로이드 알림 채널 설정 및 관리 + +## 8. 버전 관리 +- 모든 빌드는 자동으로 빌드 번호가 증가되도록 설정할 것 +- 릴리즈 빌드는 versionCode와 buildNumber 모두 증가할 것 +- 디버그 빌드는 buildNumber만 증가할 것 +- 버전 정보는 항상 설정 페이지에 표시하여 사용자와 개발자가 확인 가능하게 할 것 diff --git a/docs/README.md b/docs/README.md index 5f73643..d7614c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,141 +1,59 @@ -# 적자 탈출 가계부 프로젝트 문서 +# Zellyy Finance 프로젝트 문서 -이 디렉토리는 적자 탈출 가계부 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱 개발 프로젝트입니다. +이 디렉토리는 Zellyy Finance 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱입니다. ## 프로젝트 개요 -'적자 탈출 가계부'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. AI 기술을 활용한 개인화된 재정 관리 경험을 제공하고, 궁극적으로는 사용자들의 재정적 웰빙을 향상시키는 것을 목표로 합니다. +'Zellyy Finance'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. Appwrite 백엔드를 활용하여 안정적인 데이터 관리와 인증 시스템을 제공합니다. ## 폴더 구조 ### 00_프로젝트_개요 프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다. -- `01_프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명 -- `02_핵심_문제_정의.md` - 해결하고자 하는 문제 정의 (예정) -- `03_사용자_페르소나.md` - 타겟 사용자 프로필 (예정) -- `04_사용자_스토리.md` - 사용자 관점의 요구사항 (예정) -- `05_비즈니스_모델.md` - 수익 모델 및 사업화 전략 (예정) -- `06_법률_규제_검토.md` - 금융 앱 관련 법규 및 규제 검토 (예정) +- `프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명 +- `핵심_문제_정의.md` - 해결하고자 하는 문제 정의 +- `사용자_페르소나.md` - 타겟 사용자 프로필 ### 01_기획_및_설계 프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다. -- `01_요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석 -- `02_MVP_기능_목록.md` - 최소 기능 제품(MVP)의 기능 목록 (예정) -- `03_주요_사용_시나리오.md` - 주요 사용 사례 시나리오 (예정) -- `04_UI_와이어프레임.md` - 핵심 화면 와이어프레임 (예정) -- `05_사용자_여정_맵.md` - 사용자 경험 흐름도 (예정) -- `06_정보_아키텍처.md` - 앱 구조 및 화면 흐름도 (예정) +- `요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석 +- `UI_와이어프레임.md` - 핵심 화면 와이어프레임 +- `사용자_경험_전략.md` - 사용자 경험 설계 전략 ### 02_기술_문서 프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다. -- `01_시스템_아키텍처.md` - 시스템 아키텍처 설계 문서 -- `02_데이터_모델_설계.md` - 데이터 모델 설계 문서 (예정) -- `03_API_명세서.md` - API 엔드포인트 명세 (예정) -- `04_보안_설계.md` - 보안 및 개인정보 보호 설계 (예정) -- `05_성능_최적화_전략.md` - 앱 성능 최적화 전략 (예정) -- `06_CI_CD_파이프라인.md` - 지속적 통합/배포 전략 (예정) -- `07_AI_ML_구현_전략.md` - AI 기반 소비 패턴 분석 구현 방법 (예정) +- `시스템_아키텍처.md` - 시스템 아키텍처 설계 문서 +- `데이터_모델_설계.md` - 데이터베이스 스키마 및 모델 설계 +- `Appwrite_전환_가이드.md` - Supabase에서 Appwrite로의 전환 가이드 ### 03_개발_단계 -프로젝트 개발 단계별 문서가 포함되어 있습니다. -- `01_개발_로드맵.md` - 전체 개발 로드맵 및 일정 -- `02_1단계_개발_계획.md` - 1단계(MVP) 개발 상세 계획 (예정) -- `03_테스트_전략.md` - 테스트 방법론 및 계획 (예정) -- `04_배포_전략.md` - 배포 및 운영 계획 (예정) -- `05_품질_보증_계획.md` - QA 전략 및 테스트 케이스 (예정) -- `06_유지보수_전략.md` - 출시 후 유지보수 및 업데이트 계획 (예정) +개발 과정과 관련된 문서가 포함되어 있습니다. +- `개발_가이드라인.md` - 코드 작성 원칙, iOS/Android 지원, Appwrite 통합 등에 관한 가이드라인 -### 04_디자인_가이드 -UI/UX 디자인 관련 문서가 포함되어 있습니다. -- `01_디자인_시스템.md` - 디자인 언어 및 컴포넌트 정의 (예정) -- `02_색상_팔레트.md` - 앱 색상 가이드라인 (예정) -- `03_타이포그래피.md` - 폰트 및 텍스트 스타일 가이드 (예정) -- `04_아이콘_및_이미지.md` - 아이콘 디자인 및 사용 가이드 (예정) -- `05_애니메이션_가이드.md` - UI 애니메이션 및 트랜지션 (예정) -- `06_접근성_지침.md` - 접근성 디자인 원칙 (예정) +### archive +더 이상 활발하게 사용되지 않는 레거시 문서들이 보관되어 있습니다. +- `Supabase 관련 문서` - 이전에 사용하던 Supabase 관련 설정 및 가이드 +- `개발 단계별 문서` - 이전 개발 단계의 계획 및 산출물 요약 -### 05_프로젝트_관리 -프로젝트 관리 및 협업 관련 문서가 포함되어 있습니다. -- `01_팀_구성.md` - 팀 구성원 및 역할 정의 (예정) -- `02_의사결정_프로세스.md` - 프로젝트 의사결정 체계 (예정) -- `03_커뮤니케이션_계획.md` - 팀 내 소통 방식 및 도구 (예정) -- `04_일정_및_마일스톤.md` - 주요 마일스톤 및 납기일 (예정) -- `05_위험_관리.md` - 잠재적 위험 요소 및 대응 계획 (예정) +## 주요 기술 스택 -### 06_참고자료 -프로젝트 진행에 참고할 수 있는 자료들이 포함되어 있습니다. -- `01_시장_조사_보고서.md` - 가계부 앱 시장 조사 보고서 -- `02_경쟁사_분석.md` - 주요 경쟁 앱 상세 분석 (예정) -- `03_사용자_인터뷰.md` - 잠재 사용자 인터뷰 결과 (예정) -- `04_참고_리소스.md` - 유용한 참고 자료 및 링크 (예정) -- `05_금융_데이터_소스.md` - 재정 관리 데이터 참고 자료 (예정) -- `06_관련_연구_자료.md` - 소비 행동 및 금융 심리학 연구 (예정) +- **프론트엔드**: React Native, TypeScript +- **백엔드**: Appwrite +- **상태 관리**: Context API +- **UI 컴포넌트**: Lovable UI +- **네이티브 통합**: Capacitor -### 07_마케팅_및_성장 -마케팅 및 사용자 확보 전략 관련 문서가 포함되어 있습니다. -- `01_마케팅_전략.md` - 출시 및 사용자 확보 전략 (예정) -- `02_ASO_전략.md` - 앱 스토어 최적화 전략 (예정) -- `03_콘텐츠_전략.md` - 콘텐츠 마케팅 계획 (예정) -- `04_사용자_유지_전략.md` - 사용자 참여 및 유지 방안 (예정) -- `05_파트너십_계획.md` - 잠재적 파트너십 및 협업 기회 (예정) +## 개발 가이드라인 -## 주요 기능 +개발 가이드라인은 [03_개발_단계/개발_가이드라인.md](./03_개발_단계/개발_가이드라인.md) 문서를 참조하세요. 이 문서에는 다음 내용이 포함되어 있습니다: -1. **수입/지출 기록**: 간편한 UI로 일상 재정 활동 기록 -2. **카테고리 관리**: 사용자 정의 카테고리로 지출 분류 -3. **예산 설정**: 카테고리별 월간/주간 예산 설정 및 알림 -4. **지출 분석**: 차트와 그래프로 소비 패턴 시각화 -5. **AI 기반 분석**: 소비 패턴 분석 및 맞춤형 절약 제안 -6. **절약 챌린지**: 사용자 맞춤형 절약 목표 설정 및 달성 보상 -7. **재정 건강 점수**: 사용자의 재정 상태를 점수화하여 개선 동기 부여 -8. **구독 관리**: 정기 구독 서비스 추적 및 최적화 제안 -9. **재정 목표 설정**: 단기/중기/장기 저축 목표 설정 및 진행 상황 추적 -10. **알림 시스템**: 예산 초과, 주요 지출, 절약 기회에 대한 스마트 알림 -11. **가계부 보고서**: 정기적인 재정 상태 요약 보고서 제공 -12. **공유 기능**: 가족 또는 파트너와 특정 재정 정보 공유 +1. 코드 작성 원칙 +2. 트랜잭션 삭제 안전성 +3. Appwrite 통합 원칙 +4. 상태 관리 최적화 +5. iOS/Android 지원 +6. 디버깅 및 로깅 -## 기술 스택 +## Appwrite 전환 -- **프론트엔드**: React, Vite, Tailwind CSS, Capacitor -- **백엔드**: Node.js, Express, Supabase(PostgreSQL) -- **AI/ML**: TensorFlow, Python -- **클라우드**: Supabase On-Premise -- **데이터 시각화**: D3.js, Chart.js -- **인증/보안**: JWT, OAuth 2.0, 데이터 암호화 -- **테스트**: Jest, Cypress -- **CI/CD**: GitHub Actions -- **분석**: Supabase Analytics - -## 문서 작성 가이드라인 -- 모든 문서는 마크다운(.md) 형식으로 작성합니다. -- 파일명은 내용을 명확히 나타내는 한글 또는 영문으로 작성합니다. -- 이미지나 다이어그램은 가능한 마크다운 내에 포함시킵니다. -- 문서 간 연결이 필요한 경우 상대 경로를 사용하여 링크합니다. -- 코드 예시는 적절한 구문 강조와 함께 코드 블록으로 포함합니다. -- 변경 사항은 문서 하단의 업데이트 이력에 기록합니다. -- 중요 결정사항은 의사결정 배경과 함께 기록합니다. - -## 개발 워크플로우 -1. **기능 기획**: 사용자 스토리 및 요구사항 정의 -2. **설계**: UI/UX 디자인 및 기술 아키텍처 설계 -3. **개발**: 기능 구현 및 단위 테스트 -4. **코드 리뷰**: 팀원 간 코드 품질 검토 -5. **테스트**: QA 및 사용성 테스트 -6. **배포**: 스테이징 및 프로덕션 환경 배포 -7. **모니터링**: 성능 및 사용자 피드백 모니터링 -8. **반복**: 피드백을 바탕으로 기능 개선 - -## 출시 계획 -- **알파 버전**: 내부 테스트 (2024년 4월 초) -- **베타 버전**: 제한적 사용자 테스트 (2024년 4월 중순) -- **MVP 출시**: 앱스토어 및 플레이스토어 공개 (2024년 4월 말) -- **기능 업데이트**: 사용자 피드백 기반 주요 기능 추가 (2024년 5월 초) -- **확장 계획**: 웹 버전 및 추가 기능 확장 (2024년 5월 중순부터) - -## 업데이트 이력 -- 2024-03-15: 프로젝트 문서 초기 구성 완료 -- 2024-03-15: 프로젝트 소개, 요구사항 분석, 시스템 아키텍처, 개발 로드맵, 시장 조사 보고서 추가 -- 2024-04-01: 폴더 구조 개선 및 추가 섹션(디자인 가이드, 프로젝트 관리, 마케팅) 추가 -- 2024-04-05: 일정 조정 - 모든 개발 계획을 4월 말까지 완료하도록 수정 -- 2025-03-09: 개발 방법 변경 - Flutter에서 React, Tailwind CSS, Capacitor 기반 웹 앱으로 전환, Lovable UI 컴포넌트 스타일 적용 -- 2025-03-09: 데이터베이스 변경 - MongoDB에서 Supabase(PostgreSQL) On-Premise로 전환 +Supabase에서 Appwrite로의 전환에 관한 상세 정보는 [02_기술_문서/Appwrite_전환_가이드.md](./02_기술_문서/Appwrite_전환_가이드.md) 문서를 참조하세요. diff --git a/docs/03_개발_단계/01_개발_로드맵.md b/docs/archive/03_개발_단계/01_개발_로드맵.md similarity index 100% rename from docs/03_개발_단계/01_개발_로드맵.md rename to docs/archive/03_개발_단계/01_개발_로드맵.md diff --git a/docs/03_개발_단계/1단계/1단계_산출물_요약.md b/docs/archive/03_개발_단계/1단계/1단계_산출물_요약.md similarity index 100% rename from docs/03_개발_단계/1단계/1단계_산출물_요약.md rename to docs/archive/03_개발_단계/1단계/1단계_산출물_요약.md diff --git a/docs/03_개발_단계/1단계/1단계_첫주차_할일.md b/docs/archive/03_개발_단계/1단계/1단계_첫주차_할일.md similarity index 100% rename from docs/03_개발_단계/1단계/1단계_첫주차_할일.md rename to docs/archive/03_개발_단계/1단계/1단계_첫주차_할일.md diff --git a/docs/03_개발_단계/2단계/2단계_계획.md b/docs/archive/03_개발_단계/2단계/2단계_계획.md similarity index 100% rename from docs/03_개발_단계/2단계/2단계_계획.md rename to docs/archive/03_개발_단계/2단계/2단계_계획.md diff --git a/docs/03_개발_단계/3단계/3단계_계획.md b/docs/archive/03_개발_단계/3단계/3단계_계획.md similarity index 100% rename from docs/03_개발_단계/3단계/3단계_계획.md rename to docs/archive/03_개발_단계/3단계/3단계_계획.md diff --git a/docs/03_개발_단계/4단계/4단계_계획.md b/docs/archive/03_개발_단계/4단계/4단계_계획.md similarity index 100% rename from docs/03_개발_단계/4단계/4단계_계획.md rename to docs/archive/03_개발_단계/4단계/4단계_계획.md diff --git a/docs/02_기술_문서/Nginx_Supabase_설치_가이드.md b/docs/archive/Nginx_Supabase_설치_가이드.md similarity index 100% rename from docs/02_기술_문서/Nginx_Supabase_설치_가이드.md rename to docs/archive/Nginx_Supabase_설치_가이드.md diff --git a/docs/SUPABASE_ONPREM_MIGRATION_PLAN.md b/docs/archive/SUPABASE_ONPREM_MIGRATION_PLAN.md similarity index 100% rename from docs/SUPABASE_ONPREM_MIGRATION_PLAN.md rename to docs/archive/SUPABASE_ONPREM_MIGRATION_PLAN.md diff --git a/docs/02_기술_문서/Supabase_설정_가이드.md b/docs/archive/Supabase_설정_가이드.md similarity index 100% rename from docs/02_기술_문서/Supabase_설정_가이드.md rename to docs/archive/Supabase_설정_가이드.md diff --git a/docs/02_기술_문서/Supabase_인증_정보.md b/docs/archive/Supabase_인증_정보.md similarity index 100% rename from docs/02_기술_문서/Supabase_인증_정보.md rename to docs/archive/Supabase_인증_정보.md diff --git a/package-lock.json b/package-lock.json index 8f46949..99b9cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,7 @@ "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.56.2", "@types/uuid": "^10.0.0", + "appwrite": "^17.0.2", "browserslist": "^4.24.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -3832,6 +3833,12 @@ "node": ">= 8" } }, + "node_modules/appwrite": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/appwrite/-/appwrite-17.0.2.tgz", + "integrity": "sha512-h8frLDRYzFDLS9xA2s8ZSlH/prPFq/ma5477fgQHHLcE/t9RDxNImpq9AleRUb9Oh1YJiP49HCObxgSTGW5AQA==", + "license": "BSD-3-Clause" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", diff --git a/package.json b/package.json index d973358..4d79502 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@supabase/supabase-js": "^2.49.4", "@tanstack/react-query": "^5.56.2", "@types/uuid": "^10.0.0", + "appwrite": "^17.0.2", "browserslist": "^4.24.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/App.tsx b/src/App.tsx index 8313301..7466a41 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,6 +17,7 @@ import HelpSupport from './pages/HelpSupport'; import SecurityPrivacySettings from './pages/SecurityPrivacySettings'; import NotificationSettings from './pages/NotificationSettings'; import ForgotPassword from './pages/ForgotPassword'; +import AppwriteSettingsPage from './pages/AppwriteSettingsPage'; function App() { useEffect(() => { @@ -40,6 +41,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/src/archive/README.md b/src/archive/README.md new file mode 100644 index 0000000..149b9d6 --- /dev/null +++ b/src/archive/README.md @@ -0,0 +1,21 @@ +# Archive 폴더 + +이 폴더는 Zellyy Finance 프로젝트에서 더 이상 활발하게 사용되지 않는 레거시 코드를 보관하는 곳입니다. + +## 개요 + +Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환했습니다. 이 폴더에는 Supabase 관련 코드가 보관되어 있으며, 참조용으로만 유지됩니다. + +## 폴더 구조 + +- `components/`: Supabase 관련 UI 컴포넌트 +- `hooks/`: Supabase 관련 훅 +- `integrations/`: Supabase 통합 코드 +- `lib/`: Supabase 클라이언트 및 유틸리티 +- `utils/`: Supabase 트랜잭션 유틸리티 + +## 주의사항 + +이 폴더의 코드는 더 이상 유지보수되지 않으며, 새로운 기능 개발에 사용해서는 안 됩니다. 모든 새로운 개발은 Appwrite 기반으로 진행해야 합니다. + +마이그레이션이 완전히 완료되면 이 폴더는 삭제될 예정입니다. diff --git a/src/components/auth/SupabaseConnectionStatus.tsx b/src/archive/components/SupabaseConnectionStatus.tsx similarity index 100% rename from src/components/auth/SupabaseConnectionStatus.tsx rename to src/archive/components/SupabaseConnectionStatus.tsx diff --git a/src/components/supabase/DebugInfoCollapsible.tsx b/src/archive/components/supabase/DebugInfoCollapsible.tsx similarity index 100% rename from src/components/supabase/DebugInfoCollapsible.tsx rename to src/archive/components/supabase/DebugInfoCollapsible.tsx diff --git a/src/components/supabase/ErrorMessageCard.tsx b/src/archive/components/supabase/ErrorMessageCard.tsx similarity index 100% rename from src/components/supabase/ErrorMessageCard.tsx rename to src/archive/components/supabase/ErrorMessageCard.tsx diff --git a/src/components/supabase/ProxyRecommendationAlert.tsx b/src/archive/components/supabase/ProxyRecommendationAlert.tsx similarity index 100% rename from src/components/supabase/ProxyRecommendationAlert.tsx rename to src/archive/components/supabase/ProxyRecommendationAlert.tsx diff --git a/src/components/supabase/TroubleshootingTips.tsx b/src/archive/components/supabase/TroubleshootingTips.tsx similarity index 100% rename from src/components/supabase/TroubleshootingTips.tsx rename to src/archive/components/supabase/TroubleshootingTips.tsx diff --git a/src/hooks/transactions/supabaseUtils.ts b/src/archive/hooks/transactions/supabaseUtils.ts similarity index 100% rename from src/hooks/transactions/supabaseUtils.ts rename to src/archive/hooks/transactions/supabaseUtils.ts diff --git a/src/integrations/supabase/client.ts b/src/archive/integrations/supabase/client.ts similarity index 100% rename from src/integrations/supabase/client.ts rename to src/archive/integrations/supabase/client.ts diff --git a/src/integrations/supabase/types.ts b/src/archive/integrations/supabase/types.ts similarity index 100% rename from src/integrations/supabase/types.ts rename to src/archive/integrations/supabase/types.ts diff --git a/src/lib/supabase/client.ts b/src/archive/lib/supabase/client.ts similarity index 100% rename from src/lib/supabase/client.ts rename to src/archive/lib/supabase/client.ts diff --git a/src/lib/supabase/config.ts b/src/archive/lib/supabase/config.ts similarity index 100% rename from src/lib/supabase/config.ts rename to src/archive/lib/supabase/config.ts diff --git a/src/lib/supabase/customFetch.ts b/src/archive/lib/supabase/customFetch.ts similarity index 100% rename from src/lib/supabase/customFetch.ts rename to src/archive/lib/supabase/customFetch.ts diff --git a/src/lib/supabase/index.ts b/src/archive/lib/supabase/index.ts similarity index 100% rename from src/lib/supabase/index.ts rename to src/archive/lib/supabase/index.ts diff --git a/src/lib/supabase/setup.ts b/src/archive/lib/supabase/setup.ts similarity index 100% rename from src/lib/supabase/setup.ts rename to src/archive/lib/supabase/setup.ts diff --git a/src/lib/supabase/setup/index.ts b/src/archive/lib/supabase/setup/index.ts similarity index 100% rename from src/lib/supabase/setup/index.ts rename to src/archive/lib/supabase/setup/index.ts diff --git a/src/lib/supabase/setup/status.ts b/src/archive/lib/supabase/setup/status.ts similarity index 100% rename from src/lib/supabase/setup/status.ts rename to src/archive/lib/supabase/setup/status.ts diff --git a/src/lib/supabase/setup/tables.ts b/src/archive/lib/supabase/setup/tables.ts similarity index 100% rename from src/lib/supabase/setup/tables.ts rename to src/archive/lib/supabase/setup/tables.ts diff --git a/src/lib/supabase/storageUtils.ts b/src/archive/lib/supabase/storageUtils.ts similarity index 100% rename from src/lib/supabase/storageUtils.ts rename to src/archive/lib/supabase/storageUtils.ts diff --git a/src/lib/supabase/tests/apiTests.ts b/src/archive/lib/supabase/tests/apiTests.ts similarity index 100% rename from src/lib/supabase/tests/apiTests.ts rename to src/archive/lib/supabase/tests/apiTests.ts diff --git a/src/lib/supabase/tests/authTests.ts b/src/archive/lib/supabase/tests/authTests.ts similarity index 100% rename from src/lib/supabase/tests/authTests.ts rename to src/archive/lib/supabase/tests/authTests.ts diff --git a/src/lib/supabase/tests/databaseTests.ts b/src/archive/lib/supabase/tests/databaseTests.ts similarity index 100% rename from src/lib/supabase/tests/databaseTests.ts rename to src/archive/lib/supabase/tests/databaseTests.ts diff --git a/src/lib/supabase/tests/types.ts b/src/archive/lib/supabase/tests/types.ts similarity index 100% rename from src/lib/supabase/tests/types.ts rename to src/archive/lib/supabase/tests/types.ts diff --git a/src/utils/supabaseTransactionUtils.ts b/src/archive/utils/supabaseTransactionUtils.ts similarity index 100% rename from src/utils/supabaseTransactionUtils.ts rename to src/archive/utils/supabaseTransactionUtils.ts diff --git a/src/components/AddTransactionButton.tsx b/src/components/AddTransactionButton.tsx index d1e29d7..3173884 100644 --- a/src/components/AddTransactionButton.tsx +++ b/src/components/AddTransactionButton.tsx @@ -4,7 +4,7 @@ import { PlusIcon } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용 import { useBudget } from '@/contexts/budget/BudgetContext'; -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils'; import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm'; import { Transaction } from '@/contexts/budget/types'; diff --git a/src/components/auth/AppwriteConnectionStatus.tsx b/src/components/auth/AppwriteConnectionStatus.tsx new file mode 100644 index 0000000..e230e2a --- /dev/null +++ b/src/components/auth/AppwriteConnectionStatus.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { CheckCircle, XCircle } from 'lucide-react'; + +interface AppwriteConnectionStatusProps { + testResults: { + connected: boolean; + message: string; + details?: string; + } | null; +} + +const AppwriteConnectionStatus = ({ testResults }: AppwriteConnectionStatusProps) => { + if (!testResults) return null; + + return ( +
+
+ {testResults.connected ? ( + + ) : ( + + )} +
+

+ {testResults.connected ? '연결됨' : '연결 실패'} +

+

+ {testResults.message} +

+ {testResults.details && ( +

+ {testResults.details} +

+ )} +
+
+
+ ); +}; + +export default AppwriteConnectionStatus; diff --git a/src/components/auth/AppwriteConnectionTest.tsx b/src/components/auth/AppwriteConnectionTest.tsx new file mode 100644 index 0000000..83dd0b9 --- /dev/null +++ b/src/components/auth/AppwriteConnectionTest.tsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Loader2 } from 'lucide-react'; +import AppwriteConnectionStatus from './AppwriteConnectionStatus'; +import { client, account, isValidAppwriteConfig, getAppwriteEndpoint } from '@/lib/appwrite'; +import { setupAppwriteDatabase } from '@/lib/appwrite/setup'; + +/** + * Appwrite 연결 테스트 컴포넌트 + * 서버 연결 상태를 확인하고 데이터베이스 설정을 진행합니다. + */ +const AppwriteConnectionTest = () => { + // 연결 테스트 결과 상태 + const [testResults, setTestResults] = useState<{ + connected: boolean; + message: string; + details?: string; + } | null>(null); + + // 로딩 상태 + const [loading, setLoading] = useState(false); + + // 데이터베이스 설정 상태 + const [dbSetupDone, setDbSetupDone] = useState(false); + + // 연결 테스트 함수 + const testConnection = useCallback(async () => { + setLoading(true); + setTestResults(null); + + try { + // 설정 유효성 검사 + if (!isValidAppwriteConfig()) { + setTestResults({ + connected: false, + message: 'Appwrite 설정이 완료되지 않았습니다.', + details: '환경 변수 VITE_APPWRITE_ENDPOINT 및 VITE_APPWRITE_PROJECT_ID를 확인하세요.' + }); + return; + } + + // 서버 연결 테스트 + try { + await account.get(); + + setTestResults({ + connected: true, + message: 'Appwrite 서버에 성공적으로 연결되었습니다.', + details: `서버: ${getAppwriteEndpoint()}` + }); + } catch (error: any) { + // 인증 오류는 연결 성공으로 간주 (로그인 필요) + if (error.code === 401) { + setTestResults({ + connected: true, + message: 'Appwrite 서버에 연결되었지만 로그인이 필요합니다.', + details: `서버: ${getAppwriteEndpoint()}` + }); + } else { + setTestResults({ + connected: false, + message: '서버 연결에 실패했습니다.', + details: error.message + }); + } + } + } catch (error: any) { + setTestResults({ + connected: false, + message: '연결 테스트 중 오류가 발생했습니다.', + details: error.message + }); + } finally { + setLoading(false); + } + }, []); + + // 데이터베이스 설정 함수 + const setupDatabase = useCallback(async () => { + setLoading(true); + + try { + const success = await setupAppwriteDatabase(); + + if (success) { + setDbSetupDone(true); + setTestResults({ + connected: true, + message: '데이터베이스 설정이 완료되었습니다.', + details: '트랜잭션 컬렉션이 준비되었습니다.' + }); + } else { + setTestResults({ + connected: false, + message: '데이터베이스 설정에 실패했습니다.', + details: '로그를 확인하세요.' + }); + } + } catch (error: any) { + setTestResults({ + connected: false, + message: '데이터베이스 설정 중 오류가 발생했습니다.', + details: error.message + }); + } finally { + setLoading(false); + } + }, []); + + // 컴포넌트 마운트 시 자동 테스트 + useEffect(() => { + testConnection(); + }, [testConnection]); + + return ( +
+
+ + + {testResults?.connected && !dbSetupDone && ( + + )} +
+ + +
+ ); +}; + +export default AppwriteConnectionTest; diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx index f59a98c..549cd1c 100644 --- a/src/components/auth/RegisterForm.tsx +++ b/src/components/auth/RegisterForm.tsx @@ -7,7 +7,7 @@ import { verifyServerConnection } from "@/contexts/auth/auth.utils"; import { ServerConnectionStatus } from "./types"; import EmailConfirmation from "./EmailConfirmation"; import RegisterFormFields from "./RegisterFormFields"; -import { supabase } from "@/lib/supabase"; +import { supabase } from "@/archive/lib/supabase"; interface RegisterFormProps { signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>; diff --git a/src/components/migration/SupabaseToAppwriteMigration.tsx b/src/components/migration/SupabaseToAppwriteMigration.tsx new file mode 100644 index 0000000..18a4ee7 --- /dev/null +++ b/src/components/migration/SupabaseToAppwriteMigration.tsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Loader2, CheckCircle, AlertCircle } from 'lucide-react'; +import { + migrateTransactionsFromSupabase, + checkMigrationStatus +} from '@/lib/appwrite/migrateFromSupabase'; +import { useAppwriteAuth } from '@/hooks/auth/useAppwriteAuth'; +import { toast } from '@/hooks/useToast.wrapper'; + +/** + * Supabase에서 Appwrite로 마이그레이션 컴포넌트 + * 데이터 마이그레이션 상태 확인 및 마이그레이션 실행 + */ +const SupabaseToAppwriteMigration: React.FC = () => { + // 인증 상태 + const { user } = useAppwriteAuth(); + + // 마이그레이션 상태 + const [migrationStatus, setMigrationStatus] = useState<{ + supabaseCount: number; + appwriteCount: number; + isComplete: boolean; + error?: string; + } | null>(null); + + // 마이그레이션 진행 상태 + const [migrationProgress, setMigrationProgress] = useState<{ + isRunning: boolean; + current: number; + total: number; + percentage: number; + }>({ + isRunning: false, + current: 0, + total: 0, + percentage: 0 + }); + + // 마이그레이션 결과 + const [migrationResult, setMigrationResult] = useState<{ + success: boolean; + migrated: number; + total: number; + error?: string; + } | null>(null); + + // 컴포넌트 마운트 상태 추적 + const [isMounted, setIsMounted] = useState(true); + + // 마이그레이션 상태 확인 + const checkStatus = useCallback(async () => { + if (!user || !isMounted) return; + + try { + const status = await checkMigrationStatus(user); + if (isMounted) { + setMigrationStatus(status); + } + } catch (error) { + console.error('마이그레이션 상태 확인 오류:', error); + if (isMounted) { + toast({ + title: '상태 확인 실패', + description: '마이그레이션 상태를 확인하는 중 오류가 발생했습니다.', + variant: 'destructive' + }); + } + } + }, [user, isMounted]); + + // 마이그레이션 실행 + const runMigration = useCallback(async () => { + if (!user || !isMounted) return; + + // 진행 상태 초기화 + setMigrationProgress({ + isRunning: true, + current: 0, + total: migrationStatus?.supabaseCount || 0, + percentage: 0 + }); + + // 결과 초기화 + setMigrationResult(null); + + try { + // 진행 상황 콜백 + const progressCallback = (current: number, total: number) => { + if (!isMounted) return; + + // requestAnimationFrame을 사용하여 UI 업데이트 최적화 + requestAnimationFrame(() => { + if (!isMounted) return; + + setMigrationProgress({ + isRunning: true, + current, + total, + percentage: Math.round((current / total) * 100) + }); + }); + }; + + // 마이그레이션 실행 + const result = await migrateTransactionsFromSupabase(user, progressCallback); + + if (!isMounted) return; + + // 결과 설정 + setMigrationResult(result); + + // 성공 메시지 + if (result.success) { + toast({ + title: '마이그레이션 완료', + description: `${result.migrated}개의 트랜잭션이 성공적으로 마이그레이션되었습니다.`, + variant: 'default' + }); + } else { + toast({ + title: '마이그레이션 실패', + description: result.error || '알 수 없는 오류가 발생했습니다.', + variant: 'destructive' + }); + } + + // 상태 다시 확인 + checkStatus(); + } catch (error) { + console.error('마이그레이션 오류:', error); + + if (!isMounted) return; + + // 오류 메시지 + toast({ + title: '마이그레이션 실패', + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', + variant: 'destructive' + }); + + // 결과 설정 + setMigrationResult({ + success: false, + migrated: 0, + total: migrationStatus?.supabaseCount || 0, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }); + } finally { + // 진행 상태 종료 + if (isMounted) { + setMigrationProgress(prev => ({ + ...prev, + isRunning: false + })); + } + } + }, [user, migrationStatus, checkStatus, isMounted]); + + // 컴포넌트 마운트 시 상태 확인 + useEffect(() => { + setIsMounted(true); + + if (user) { + checkStatus(); + } + + return () => { + setIsMounted(false); + }; + }, [user, checkStatus]); + + // 사용자가 로그인하지 않은 경우 + if (!user) { + return ( +
+

+ 마이그레이션을 시작하려면 로그인이 필요합니다. +

+
+ ); + } + + return ( +
+
+

Supabase에서 Appwrite로 데이터 마이그레이션

+

+ Supabase의 트랜잭션 데이터를 Appwrite로 이전합니다. +

+
+ + {/* 마이그레이션 상태 */} + {migrationStatus && ( +
+
+ 마이그레이션 상태 + +
+ +
+
+
Supabase 트랜잭션
+
{migrationStatus.supabaseCount}
+
+
+
Appwrite 트랜잭션
+
{migrationStatus.appwriteCount}
+
+
+ +
+ {migrationStatus.isComplete ? ( +
+ + 마이그레이션 완료 +
+ ) : ( +
+ + 마이그레이션 필요 +
+ )} +
+ + {migrationStatus.error && ( +
+ 오류: {migrationStatus.error} +
+ )} +
+ )} + + {/* 마이그레이션 진행 상태 */} + {migrationProgress.isRunning && ( +
+
+ 진행 상황 + + {migrationProgress.current} / {migrationProgress.total} ({migrationProgress.percentage}%) + +
+ +
+ )} + + {/* 마이그레이션 결과 */} + {migrationResult && !migrationProgress.isRunning && ( +
+
+ {migrationResult.success ? ( + + ) : ( + + )} +
+

+ {migrationResult.success ? '마이그레이션 성공' : '마이그레이션 실패'} +

+

+ {migrationResult.success + ? `${migrationResult.migrated}개의 트랜잭션이 마이그레이션되었습니다.` + : migrationResult.error || '알 수 없는 오류가 발생했습니다.' + } +

+
+
+
+ )} + + {/* 마이그레이션 버튼 */} +
+ +
+
+ ); +}; + +export default SupabaseToAppwriteMigration; diff --git a/src/contexts/auth/AuthProvider.tsx b/src/contexts/auth/AuthProvider.tsx index e1f68a8..fed1680 100644 --- a/src/contexts/auth/AuthProvider.tsx +++ b/src/contexts/auth/AuthProvider.tsx @@ -1,16 +1,16 @@ import React, { useEffect, useState } from 'react'; -import { supabase } from '@/lib/supabase'; -import { Session, User } from '@supabase/supabase-js'; import { toast } from '@/hooks/useToast.wrapper'; import { AuthContextType } from './types'; import * as authActions from './authActions'; import { clearAllToasts } from '@/hooks/toast/toastManager'; import { AuthContext } from './AuthContext'; +import { account } from '@/lib/appwrite/client'; +import { Models } from 'appwrite'; export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [session, setSession] = useState(null); - const [user, setUser] = useState(null); + const [session, setSession] = useState(null); + const [user, setUser] = useState | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -22,19 +22,19 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 await new Promise(resolve => queueMicrotask(() => resolve())); - const { data, error } = await supabase.auth.getSession(); - - if (error) { - console.error('세션 로딩 중 오류:', error); - } else if (data.session) { + try { + // Appwrite 세션 가져오기 + const currentSession = await account.getSession('current'); + const currentUser = await account.get(); + // 상태 업데이트를 마이크로태스크로 지연 queueMicrotask(() => { - setSession(data.session); - setUser(data.session.user); + setSession(currentSession); + setUser(currentUser); console.log('세션 로딩 완료'); }); - } else { - console.log('활성 세션 없음'); + } catch (sessionError) { + console.error('세션 로딩 중 오류:', sessionError); } } catch (error) { console.error('세션 확인 중 예외 발생:', error); @@ -51,21 +51,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children getSession(); }, 100); - // auth 상태 변경 리스너 - 최적화된 버전 - const { data: { subscription } } = supabase.auth.onAuthStateChange( - async (event, session) => { - console.log('Supabase auth 이벤트:', event); + // Appwrite 인증 상태 변경 리스너 설정 + // 참고: Appwrite는 직접적인 이벤트 리스너를 제공하지 않으므로 주기적으로 확인하는 방식 사용 + const authCheckInterval = setInterval(async () => { + try { + // 현재 로그인 상태 확인 + const currentUser = await account.get(); // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 await new Promise(resolve => queueMicrotask(() => resolve())); - if (session) { + // 사용자 정보가 변경되었는지 확인 + if (currentUser && (!user || currentUser.$id !== user.$id)) { + // 세션 정보 가져오기 + const currentSession = await account.getSession('current'); + // 상태 업데이트를 마이크로태스크로 지연 queueMicrotask(() => { - setSession(session); - setUser(session.user); + setSession(currentSession); + setUser(currentUser); + console.log('Appwrite 인증 상태 변경: 로그인됨'); }); - } else if (event === 'SIGNED_OUT') { + } + } catch (error) { + // 오류 발생 시 로그아웃 상태로 간주 + if (user) { // 상태 업데이트를 마이크로태스크로 지연 queueMicrotask(() => { setSession(null); @@ -76,21 +86,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children // 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함 window.dispatchEvent(new Event('auth-state-changed')); + console.log('Appwrite 인증 상태 변경: 로그아웃됨'); }); } - - // 로딩 상태 업데이트를 마이크로태스크로 지연 - queueMicrotask(() => { - setLoading(false); - }); } - ); + }, 5000); // 5초마다 확인 // 리스너 정리 return () => { - subscription.unsubscribe(); + clearInterval(authCheckInterval); }; - }, []); + }, [user]); // 인증 작업 메서드들 const value: AuthContextType = { diff --git a/src/contexts/auth/resetPassword.ts b/src/contexts/auth/resetPassword.ts index 1b5a916..d40e82d 100644 --- a/src/contexts/auth/resetPassword.ts +++ b/src/contexts/auth/resetPassword.ts @@ -1,4 +1,4 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { handleNetworkError, showAuthToast } from '@/utils/auth'; export const resetPassword = async (email: string) => { diff --git a/src/contexts/auth/signIn.ts b/src/contexts/auth/signIn.ts index 59f577b..f7022c7 100644 --- a/src/contexts/auth/signIn.ts +++ b/src/contexts/auth/signIn.ts @@ -1,45 +1,44 @@ - -import { supabase } from '@/lib/supabase'; +import { account } from '@/lib/appwrite/client'; import { showAuthToast } from '@/utils/auth'; /** - * 로그인 기능 - Supabase Cloud 환경에 최적화 + * 로그인 기능 - Appwrite 환경에 최적화 */ export const signIn = async (email: string, password: string) => { try { console.log('로그인 시도 중:', email); - // Supabase 인증 방식 시도 - const { data, error } = await supabase.auth.signInWithPassword({ - email, - password - }); + // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 + await new Promise(resolve => queueMicrotask(() => resolve())); - if (!error && data.user) { - showAuthToast('로그인 성공', '환영합니다!'); - return { error: null, user: data.user }; - } else if (error) { - console.error('로그인 오류:', error.message); + // Appwrite 인증 방식 시도 + try { + const session = await account.createEmailSession(email, password); + const user = await account.get(); - let errorMessage = error.message; - if (error.message.includes('Invalid login credentials')) { + // 상태 업데이트를 마이크로태스크로 지연 + await new Promise(resolve => queueMicrotask(() => resolve())); + + showAuthToast('로그인 성공', '환영합니다!'); + return { error: null, user }; + } catch (authError: any) { + console.error('로그인 오류:', authError); + + let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.'; + + // Appwrite 오류 코드에 따른 사용자 친화적 메시지 + if (authError.code === 401) { errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.'; - } else if (error.message.includes('Email not confirmed')) { - errorMessage = '이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.'; + } else if (authError.code === 429) { + errorMessage = '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.'; } showAuthToast('로그인 실패', errorMessage, 'destructive'); - return { error: { message: errorMessage }, user: null }; + return { error: authError, user: null }; } - - // 여기까지 왔다면 오류가 발생한 것 - showAuthToast('로그인 실패', '로그인 처리 중 오류가 발생했습니다.', 'destructive'); - return { error: { message: '로그인 처리 중 오류가 발생했습니다.' }, user: null }; - } catch (error: any) { - console.error('로그인 중 예외 발생:', error); - const errorMessage = error.message || '로그인 중 오류가 발생했습니다.'; - - showAuthToast('로그인 오류', errorMessage, 'destructive'); - return { error: { message: errorMessage }, user: null }; + } catch (error) { + console.error('로그인 예외 발생:', error); + showAuthToast('로그인 오류', '서버 연결 중 오류가 발생했습니다.', 'destructive'); + return { error, user: null }; } }; diff --git a/src/contexts/auth/signInUtils.ts b/src/contexts/auth/signInUtils.ts index cc2f9a9..cf2af46 100644 --- a/src/contexts/auth/signInUtils.ts +++ b/src/contexts/auth/signInUtils.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { showAuthToast } from '@/utils/auth'; /** diff --git a/src/contexts/auth/signOut.ts b/src/contexts/auth/signOut.ts index 9321fa8..85437af 100644 --- a/src/contexts/auth/signOut.ts +++ b/src/contexts/auth/signOut.ts @@ -1,4 +1,4 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { showAuthToast } from '@/utils/auth'; export const signOut = async (): Promise => { diff --git a/src/contexts/auth/signUp.ts b/src/contexts/auth/signUp.ts index 1adc2c1..036e116 100644 --- a/src/contexts/auth/signUp.ts +++ b/src/contexts/auth/signUp.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { showAuthToast, verifyServerConnection } from '@/utils/auth'; /** diff --git a/src/contexts/auth/signUpUtils.ts b/src/contexts/auth/signUpUtils.ts index 27cfc9e..074a2a3 100644 --- a/src/contexts/auth/signUpUtils.ts +++ b/src/contexts/auth/signUpUtils.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { parseResponse, showAuthToast } from '@/utils/auth'; /** diff --git a/src/contexts/auth/types.ts b/src/contexts/auth/types.ts index 647dddc..df523e3 100644 --- a/src/contexts/auth/types.ts +++ b/src/contexts/auth/types.ts @@ -1,9 +1,9 @@ -import { Session, User } from '@supabase/supabase-js'; +import { Models } from 'appwrite'; export type AuthContextType = { - session: Session | null; - user: User | null; + session: Models.Session | null; + user: Models.User | null; loading: boolean; signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>; signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; diff --git a/src/hooks/auth/useAppwriteAuth.ts b/src/hooks/auth/useAppwriteAuth.ts new file mode 100644 index 0000000..72ad9f3 --- /dev/null +++ b/src/hooks/auth/useAppwriteAuth.ts @@ -0,0 +1,182 @@ +import { useState, useEffect, useCallback } from 'react'; +import { account } from '@/lib/appwrite'; +import { ID } from 'appwrite'; + +// 인증 상태 인터페이스 +interface AuthState { + user: any | null; + loading: boolean; + error: Error | null; +} + +// 로그인 입력값 인터페이스 +interface LoginCredentials { + email: string; + password: string; +} + +// 회원가입 입력값 인터페이스 +interface SignupCredentials extends LoginCredentials { + name?: string; +} + +/** + * Appwrite 인증 관련 훅 + * 로그인, 로그아웃, 회원가입 및 사용자 상태 관리 + */ +export const useAppwriteAuth = () => { + // 인증 상태 관리 + const [authState, setAuthState] = useState({ + user: null, + loading: true, + error: null + }); + + // 컴포넌트 마운트 상태 추적 + const [isMounted, setIsMounted] = useState(true); + + // 사용자 정보 가져오기 + const getCurrentUser = useCallback(async () => { + try { + const user = await account.get(); + if (isMounted) { + setAuthState({ + user, + loading: false, + error: null + }); + } + return user; + } catch (error) { + if (isMounted) { + setAuthState({ + user: null, + loading: false, + error: error as Error + }); + } + return null; + } + }, [isMounted]); + + // 이메일/비밀번호로 로그인 + const login = useCallback(async ({ email, password }: LoginCredentials) => { + try { + setAuthState(prev => ({ ...prev, loading: true, error: null })); + + // 비동기 작업 시작 전 UI 스레드 차단 방지 + await new Promise(resolve => setTimeout(resolve, 0)); + + const session = await account.createEmailPasswordSession(email, password); + const user = await account.get(); + + if (isMounted) { + setAuthState({ + user, + loading: false, + error: null + }); + } + + return { user, session }; + } catch (error) { + if (isMounted) { + setAuthState(prev => ({ + ...prev, + loading: false, + error: error as Error + })); + } + throw error; + } + }, [isMounted]); + + // 회원가입 + const signup = useCallback(async ({ email, password, name }: SignupCredentials) => { + try { + setAuthState(prev => ({ ...prev, loading: true, error: null })); + + // 비동기 작업 시작 전 UI 스레드 차단 방지 + await new Promise(resolve => setTimeout(resolve, 0)); + + const user = await account.create( + ID.unique(), + email, + password, + name + ); + + // 회원가입 후 자동 로그인 + await account.createEmailPasswordSession(email, password); + + if (isMounted) { + setAuthState({ + user, + loading: false, + error: null + }); + } + + return user; + } catch (error) { + if (isMounted) { + setAuthState(prev => ({ + ...prev, + loading: false, + error: error as Error + })); + } + throw error; + } + }, [isMounted]); + + // 로그아웃 + const logout = useCallback(async () => { + try { + setAuthState(prev => ({ ...prev, loading: true })); + + // 현재 세션 삭제 + await account.deleteSession('current'); + + if (isMounted) { + setAuthState({ + user: null, + loading: false, + error: null + }); + } + } catch (error) { + if (isMounted) { + setAuthState(prev => ({ + ...prev, + loading: false, + error: error as Error + })); + } + throw error; + } + }, [isMounted]); + + // 초기 사용자 정보 로드 + useEffect(() => { + setIsMounted(true); + getCurrentUser(); + + // 정리 함수 + return () => { + setIsMounted(false); + }; + }, [getCurrentUser]); + + return { + user: authState.user, + loading: authState.loading, + error: authState.error, + login, + signup, + logout, + getCurrentUser + }; +}; + +export default useAppwriteAuth; diff --git a/src/hooks/transactions/useAppwriteTransactions.ts b/src/hooks/transactions/useAppwriteTransactions.ts new file mode 100644 index 0000000..f2bff18 --- /dev/null +++ b/src/hooks/transactions/useAppwriteTransactions.ts @@ -0,0 +1,162 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { Transaction } from '@/components/TransactionCard'; +import { + syncTransactionsWithAppwrite, + updateTransactionInAppwrite, + deleteTransactionFromAppwrite, + debouncedDeleteTransaction +} from '@/utils/appwriteTransactionUtils'; +import { toast } from '@/hooks/useToast.wrapper'; +import { isSyncEnabled } from '@/utils/syncUtils'; + +/** + * Appwrite 트랜잭션 관리 훅 + * 트랜잭션 동기화, 추가, 수정, 삭제 기능 제공 + */ +export const useAppwriteTransactions = (user: any, localTransactions: Transaction[]) => { + // 트랜잭션 상태 관리 + const [transactions, setTransactions] = useState(localTransactions); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 컴포넌트 마운트 상태 추적 + const isMountedRef = useRef(true); + + // 진행 중인 작업 추적 + const pendingOperations = useRef>(new Set()); + + // 트랜잭션 동기화 + const syncTransactions = useCallback(async () => { + if (!user || !isSyncEnabled()) return localTransactions; + + try { + setLoading(true); + setError(null); + + // UI 스레드 차단 방지 + await new Promise(resolve => setTimeout(resolve, 0)); + + const syncedTransactions = await syncTransactionsWithAppwrite(user, localTransactions); + + if (isMountedRef.current) { + setTransactions(syncedTransactions); + setLoading(false); + } + + return syncedTransactions; + } catch (err) { + console.error('트랜잭션 동기화 오류:', err); + + if (isMountedRef.current) { + setError(err as Error); + setLoading(false); + } + + return localTransactions; + } + }, [user, localTransactions]); + + // 트랜잭션 추가/수정 + const saveTransaction = useCallback(async (transaction: Transaction) => { + if (!user || !isSyncEnabled()) return; + + try { + // 작업 추적 시작 + pendingOperations.current.add(transaction.id); + + // UI 스레드 차단 방지 + await new Promise(resolve => requestAnimationFrame(resolve)); + + await updateTransactionInAppwrite(user, transaction); + + if (!isMountedRef.current) return; + + // 로컬 상태 업데이트 + setTransactions(prev => { + const index = prev.findIndex(t => t.id === transaction.id); + if (index >= 0) { + const updated = [...prev]; + updated[index] = transaction; + return updated; + } else { + return [...prev, transaction]; + } + }); + + } catch (err) { + console.error('트랜잭션 저장 오류:', err); + + if (isMountedRef.current) { + toast({ + title: '저장 실패', + description: '트랜잭션을 저장하는 중 오류가 발생했습니다.', + variant: 'destructive' + }); + } + } finally { + // 작업 추적 종료 + pendingOperations.current.delete(transaction.id); + } + }, [user]); + + // 트랜잭션 삭제 + const removeTransaction = useCallback(async (transactionId: string) => { + if (!user || !isSyncEnabled()) return; + + try { + // 작업 추적 시작 + pendingOperations.current.add(transactionId); + + // 로컬 상태 먼저 업데이트 (낙관적 UI 업데이트) + setTransactions(prev => prev.filter(t => t.id !== transactionId)); + + // 디바운스된 삭제 작업 실행 (여러 번 연속 호출 방지) + await debouncedDeleteTransaction(user, transactionId); + + } catch (err) { + console.error('트랜잭션 삭제 오류:', err); + + if (isMountedRef.current) { + toast({ + title: '삭제 실패', + description: '트랜잭션을 삭제하는 중 오류가 발생했습니다.', + variant: 'destructive' + }); + + // 실패 시 트랜잭션 복원 (서버에서 가져오기) + syncTransactions(); + } + } finally { + // 작업 추적 종료 + pendingOperations.current.delete(transactionId); + } + }, [user, syncTransactions]); + + // 초기 동기화 + useEffect(() => { + if (user && isSyncEnabled()) { + syncTransactions(); + } else { + setTransactions(localTransactions); + } + }, [user, localTransactions, syncTransactions]); + + // 컴포넌트 언마운트 시 정리 + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + return { + transactions, + loading, + error, + syncTransactions, + saveTransaction, + removeTransaction, + hasPendingOperations: pendingOperations.current.size > 0 + }; +}; + +export default useAppwriteTransactions; diff --git a/src/hooks/useTableSetup.ts b/src/hooks/useTableSetup.ts index 64044b1..78f1410 100644 --- a/src/hooks/useTableSetup.ts +++ b/src/hooks/useTableSetup.ts @@ -1,7 +1,7 @@ import { useState } from "react"; import { useToast } from "@/hooks/useToast.wrapper"; -import { createRequiredTables } from "@/lib/supabase/setup"; +import { createRequiredTables } from "@/archive/lib/supabase/setup"; /** * Supabase 테이블 설정을 처리하는 커스텀 훅 diff --git a/src/lib/appwrite/client.ts b/src/lib/appwrite/client.ts new file mode 100644 index 0000000..ee44906 --- /dev/null +++ b/src/lib/appwrite/client.ts @@ -0,0 +1,83 @@ +/** + * Appwrite 클라이언트 설정 + * + * 이 파일은 Appwrite 서비스와의 연결을 설정하고 필요한 서비스 인스턴스를 생성합니다. + * 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다. + */ + +import { Client, Account, Databases, Storage, Avatars } from 'appwrite'; +import { config, validateConfig } from './config'; + +// 서비스 타입 정의 +export interface AppwriteServices { + client: Client; + account: Account; + databases: Databases; + storage: Storage; + avatars: Avatars; +} + +// Appwrite 클라이언트 초기화 +let appwriteClient: Client; +let accountService: Account; +let databasesService: Databases; +let storageService: Storage; +let avatarsService: Avatars; + +try { + // 설정 유효성 검증 + validateConfig(); + + console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`); + + // Appwrite 클라이언트 생성 + appwriteClient = new Client(); + + appwriteClient + .setEndpoint(config.endpoint) + .setProject(config.projectId); + + // 서비스 초기화 + accountService = new Account(appwriteClient); + databasesService = new Databases(appwriteClient); + storageService = new Storage(appwriteClient); + avatarsService = new Avatars(appwriteClient); + + console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.'); + +} catch (error) { + console.error('Appwrite 클라이언트 생성 오류:', error); + + // 더미 클라이언트 생성 (앱이 완전히 실패하지 않도록) + appwriteClient = new Client(); + accountService = new Account(appwriteClient); + databasesService = new Databases(appwriteClient); + storageService = new Storage(appwriteClient); + avatarsService = new Avatars(appwriteClient); + + // 사용자에게 오류 알림 (개발 모드에서만) + if (import.meta.env.DEV) { + queueMicrotask(() => { + alert('Appwrite 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.'); + }); + } +} + +// 서비스 내보내기 +export const client = appwriteClient; +export const account = accountService; +export const databases = databasesService; +export const storage = storageService; +export const avatars = avatarsService; + +// 연결 상태 확인 +export const isValidConnection = async (): Promise => { + try { + // 계정 서비스를 통해 현재 세션 상태 확인 (간단한 API 호출) + await account.get(); + return true; + } catch (error) { + console.error('Appwrite 연결 확인 오류:', error); + return false; + } +}; diff --git a/src/lib/appwrite/config.ts b/src/lib/appwrite/config.ts new file mode 100644 index 0000000..f862143 --- /dev/null +++ b/src/lib/appwrite/config.ts @@ -0,0 +1,45 @@ +/** + * Appwrite 설정 + * + * 이 파일은 Appwrite 서비스에 필요한 모든 설정 값을 정의합니다. + * 환경 변수에서 값을 가져오며, 기본값을 제공합니다. + */ + +// Appwrite 설정 타입 정의 +export interface AppwriteConfig { + endpoint: string; + projectId: string; + databaseId: string; + transactionsCollectionId: string; +} + +// 환경 변수에서 설정 값 가져오기 +const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://a11.ism.kr/v1'; +const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || 'zellyy-finance'; +const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'zellyy-finance'; +const transactionsCollectionId = import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || 'transactions'; + +// 설정 객체 생성 +export const config: AppwriteConfig = { + endpoint, + projectId, + databaseId, + transactionsCollectionId +}; + +/** + * 서버 연결 유효성 검사 + * @returns 유효한 설정인지 여부 + */ +export const isValidAppwriteConfig = (): boolean => { + return Boolean(endpoint && projectId); +}; + +/** + * 설정 값 검증 및 오류 발생 + * @throws 필수 설정이 없는 경우 오류 발생 + */ +export const validateConfig = (): void => { + if (!endpoint) throw new Error("VITE_APPWRITE_ENDPOINT is not set"); + if (!projectId) throw new Error("VITE_APPWRITE_PROJECT_ID is not set"); +}; diff --git a/src/lib/appwrite/index.ts b/src/lib/appwrite/index.ts new file mode 100644 index 0000000..7df148b --- /dev/null +++ b/src/lib/appwrite/index.ts @@ -0,0 +1,30 @@ +import { client, account, databases, storage, avatars, realtime, isValidConnection } from './client'; +import { + getAppwriteEndpoint, + getAppwriteProjectId, + getAppwriteDatabaseId, + getAppwriteTransactionsCollectionId, + isValidAppwriteConfig +} from './config'; +import { setupAppwriteDatabase } from './setup'; + +export { + // 클라이언트 및 서비스 + client, + account, + databases, + storage, + avatars, + realtime, + + // 설정 및 유틸리티 + getAppwriteEndpoint, + getAppwriteProjectId, + getAppwriteDatabaseId, + getAppwriteTransactionsCollectionId, + isValidAppwriteConfig, + isValidConnection, + + // 데이터베이스 설정 + setupAppwriteDatabase +}; diff --git a/src/lib/appwrite/migrateFromSupabase.ts b/src/lib/appwrite/migrateFromSupabase.ts new file mode 100644 index 0000000..debd1b9 --- /dev/null +++ b/src/lib/appwrite/migrateFromSupabase.ts @@ -0,0 +1,186 @@ +import { ID, Query } from 'appwrite'; +import { supabase } from '@/archive/lib/supabase'; +import { databases, account } from './client'; +import { config } from './config'; +import { setupAppwriteDatabase } from './setup'; + +/** + * Supabase에서 Appwrite로 트랜잭션 데이터 마이그레이션 + * 1. Appwrite 데이터베이스 설정 + * 2. Supabase에서 트랜잭션 데이터 가져오기 + * 3. Appwrite에 트랜잭션 데이터 저장 + */ +export const migrateTransactionsFromSupabase = async ( + user: any, + progressCallback?: (progress: number, total: number) => void +): Promise<{ + success: boolean; + migrated: number; + total: number; + error?: string; +}> => { + try { + // 1. Appwrite 데이터베이스 설정 + const setupSuccess = await setupAppwriteDatabase(); + if (!setupSuccess) { + return { + success: false, + migrated: 0, + total: 0, + error: 'Appwrite 데이터베이스 설정에 실패했습니다.' + }; + } + + // 2. Supabase에서 트랜잭션 데이터 가져오기 + const { data: supabaseTransactions, error } = await supabase + .from('transactions') + .select('*') + .eq('user_id', user.id); + + if (error) { + console.error('Supabase 데이터 조회 오류:', error); + return { + success: false, + migrated: 0, + total: 0, + error: `Supabase 데이터 조회 실패: ${error.message}` + }; + } + + if (!supabaseTransactions || supabaseTransactions.length === 0) { + return { + success: true, + migrated: 0, + total: 0 + }; + } + + // 3. Appwrite에 트랜잭션 데이터 저장 + const databaseId = config.databaseId; + const collectionId = config.transactionsCollectionId; + + // 현재 Appwrite에 있는 트랜잭션 확인 (중복 방지) + const { documents: existingTransactions } = await databases.listDocuments( + databaseId, + collectionId, + [Query.equal('user_id', user.$id)] + ); + + // 이미 마이그레이션된 트랜잭션 ID 목록 + const existingTransactionIds = existingTransactions.map( + doc => doc.transaction_id + ); + + let migratedCount = 0; + const totalCount = supabaseTransactions.length; + + // 트랜잭션 데이터 마이그레이션 + for (let i = 0; i < supabaseTransactions.length; i++) { + const transaction = supabaseTransactions[i]; + + // 이미 마이그레이션된 트랜잭션은 건너뛰기 + if (existingTransactionIds.includes(transaction.transaction_id)) { + // 진행 상황 콜백 + if (progressCallback) { + progressCallback(i + 1, totalCount); + } + continue; + } + + try { + // 트랜잭션 데이터 Appwrite에 저장 + await databases.createDocument( + databaseId, + collectionId, + ID.unique(), + { + user_id: user.$id, + transaction_id: transaction.transaction_id, + title: transaction.title, + amount: transaction.amount, + date: transaction.date, + category: transaction.category, + type: transaction.type + } + ); + + migratedCount++; + } catch (docError) { + console.error('트랜잭션 마이그레이션 오류:', docError); + } + + // 진행 상황 콜백 + if (progressCallback) { + progressCallback(i + 1, totalCount); + } + } + + return { + success: true, + migrated: migratedCount, + total: totalCount + }; + } catch (error) { + console.error('마이그레이션 오류:', error); + return { + success: false, + migrated: 0, + total: 0, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }; + } +}; + +/** + * 마이그레이션 상태 확인 + * Supabase와 Appwrite의 트랜잭션 수를 비교 + */ +export const checkMigrationStatus = async ( + user: any +): Promise<{ + supabaseCount: number; + appwriteCount: number; + isComplete: boolean; + error?: string; +}> => { + try { + // Supabase 트랜잭션 수 확인 + const { count: supabaseCount, error: supabaseError } = await supabase + .from('transactions') + .select('*', { count: 'exact', head: true }) + .eq('user_id', user.id); + + if (supabaseError) { + return { + supabaseCount: 0, + appwriteCount: 0, + isComplete: false, + error: `Supabase 데이터 조회 실패: ${supabaseError.message}` + }; + } + + // Appwrite 트랜잭션 수 확인 + const databaseId = config.databaseId; + const collectionId = config.transactionsCollectionId; + + const { total: appwriteCount } = await databases.listDocuments( + databaseId, + collectionId, + [Query.equal('user_id', user.$id)] + ); + + return { + supabaseCount: supabaseCount || 0, + appwriteCount, + isComplete: (supabaseCount || 0) <= appwriteCount + }; + } catch (error) { + console.error('마이그레이션 상태 확인 오류:', error); + return { + supabaseCount: 0, + appwriteCount: 0, + isComplete: false, + error: error instanceof Error ? error.message : '알 수 없는 오류' + }; + } +}; diff --git a/src/lib/appwrite/setup.ts b/src/lib/appwrite/setup.ts new file mode 100644 index 0000000..82c213a --- /dev/null +++ b/src/lib/appwrite/setup.ts @@ -0,0 +1,172 @@ +import { ID, Query, Permission, Role } from 'appwrite'; +import { databases, account } from './client'; +import { config } from './config'; + +/** + * Appwrite 데이터베이스 및 컬렉션 설정 + * 필요한 데이터베이스와 컬렉션이 없으면 생성합니다. + */ +export const setupAppwriteDatabase = async (): Promise => { + try { + const databaseId = config.databaseId; + const transactionsCollectionId = config.transactionsCollectionId; + + // 현재 사용자 정보 가져오기 + const user = await account.get(); + + // 1. 데이터베이스 존재 확인 또는 생성 + let database: any; + + try { + // 기존 데이터베이스 가져오기 시도 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + database = await databases.getDatabase(databaseId); + console.log('기존 데이터베이스를 찾았습니다:', database.name); + } catch (error) { + // 데이터베이스가 없으면 생성 + console.log('데이터베이스를 생성합니다...'); + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + database = await databases.createDatabase(databaseId, 'Zellyy Finance'); + console.log('데이터베이스가 생성되었습니다:', database.name); + } + + // 2. 트랜잭션 컬렉션 존재 확인 또는 생성 + let collection: any; + + try { + // 기존 컬렉션 가져오기 시도 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + collection = await databases.getCollection(databaseId, transactionsCollectionId); + console.log('기존 트랜잭션 컬렉션을 찾았습니다:', collection.name); + } catch (error) { + // 컬렉션이 없으면 생성 + console.log('트랜잭션 컬렉션을 생성합니다...'); + + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + collection = await databases.createCollection( + databaseId, + transactionsCollectionId, + { + name: '거래 내역', + permissions: [ + // 사용자만 자신의 데이터에 접근 가능하도록 설정 + Permission.read(Role.user(user.$id)), + Permission.update(Role.user(user.$id)), + Permission.delete(Role.user(user.$id)), + Permission.create(Role.user(user.$id)) + ] + } + ); + + console.log('트랜잭션 컬렉션이 생성되었습니다:', collection.name); + + // 3. 필요한 속성(필드) 생성 + await Promise.all([ + // 사용자 ID 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'user_id', + { + size: 255, + required: true, + default: user.$id, + array: false + } + ), + + // 트랜잭션 ID 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'transaction_id', + { + size: 255, + required: true, + default: null, + array: false + } + ), + + // 제목 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'title', + { + size: 255, + required: true, + default: null, + array: false + } + ), + + // 금액 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createFloatAttribute( + databaseId, + transactionsCollectionId, + 'amount', + { + required: true, + default: 0, + min: null, + max: null + } + ), + + // 날짜 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'date', + { + size: 255, + required: true, + default: null, + array: false + } + ), + + // 카테고리 필드 + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'category', + { + size: 255, + required: false, + default: null, + array: false + } + ), + + // 유형 필드 (수입/지출) + // @ts-ignore - Appwrite SDK 17.0.2 버전 호환성 문제 + databases.createStringAttribute( + databaseId, + transactionsCollectionId, + 'type', + { + size: 50, + required: true, + default: 'expense', + array: false + } + ) + ]); + + console.log('트랜잭션 컬렉션 속성이 생성되었습니다.'); + } + + return true; + } catch (error) { + console.error('Appwrite 데이터베이스 설정 오류:', error); + return false; + } +}; diff --git a/src/pages/AppwriteSettingsPage.tsx b/src/pages/AppwriteSettingsPage.tsx new file mode 100644 index 0000000..e6a69de --- /dev/null +++ b/src/pages/AppwriteSettingsPage.tsx @@ -0,0 +1,256 @@ +import React, { useState } from 'react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Separator } from '@/components/ui/separator'; +import AppwriteConnectionTest from '@/components/auth/AppwriteConnectionTest'; +import SupabaseToAppwriteMigration from '@/components/migration/SupabaseToAppwriteMigration'; +import { useAppwriteAuth } from '@/hooks/auth/useAppwriteAuth'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { toast } from '@/hooks/useToast.wrapper'; + +/** + * Appwrite 설정 페이지 + * - 연결 설정 및 테스트 + * - 데이터베이스 설정 + * - Supabase에서 데이터 마이그레이션 + */ +const AppwriteSettingsPage: React.FC = () => { + // 인증 상태 + const { user, login, signup, logout, loading, error } = useAppwriteAuth(); + + // 로그인 폼 상태 + const [loginForm, setLoginForm] = useState({ + email: '', + password: '' + }); + + // 회원가입 폼 상태 + const [signupForm, setSignupForm] = useState({ + email: '', + password: '', + name: '' + }); + + // 로그인 처리 + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await login(loginForm); + toast({ + title: '로그인 성공', + description: '성공적으로 로그인되었습니다.', + variant: 'default' + }); + } catch (error) { + console.error('로그인 오류:', error); + toast({ + title: '로그인 실패', + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', + variant: 'destructive' + }); + } + }; + + // 회원가입 처리 + const handleSignup = async (e: React.FormEvent) => { + e.preventDefault(); + + try { + await signup(signupForm); + toast({ + title: '회원가입 성공', + description: '성공적으로 가입되었습니다.', + variant: 'default' + }); + } catch (error) { + console.error('회원가입 오류:', error); + toast({ + title: '회원가입 실패', + description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.', + variant: 'destructive' + }); + } + }; + + // 로그아웃 처리 + const handleLogout = async () => { + try { + await logout(); + toast({ + title: '로그아웃', + description: '성공적으로 로그아웃되었습니다.', + variant: 'default' + }); + } catch (error) { + console.error('로그아웃 오류:', error); + } + }; + + return ( +
+
+

Appwrite 설정

+

+ Appwrite 서버 연결 설정 및 데이터 마이그레이션 +

+
+ + + + {/* 서버 연결 상태 */} + + + 서버 연결 상태 + + Appwrite 서버 연결 상태를 확인하고 테스트합니다. + + + + + + + + {/* 인증 관리 */} + + + 인증 관리 + + Appwrite 계정에 로그인하거나 새 계정을 생성합니다. + + + + {user ? ( +
+
+

로그인 정보

+

+ 사용자 ID: {user.$id} +

+

+ 이메일: {user.email} +

+ {user.name && ( +

+ 이름: {user.name} +

+ )} +
+ +
+ +
+
+ ) : ( + + + 로그인 + 회원가입 + + + +
+
+ + setLoginForm({ ...loginForm, email: e.target.value })} + required + /> +
+ +
+ + setLoginForm({ ...loginForm, password: e.target.value })} + required + /> +
+ + +
+
+ + +
+
+ + setSignupForm({ ...signupForm, email: e.target.value })} + required + /> +
+ +
+ + setSignupForm({ ...signupForm, password: e.target.value })} + required + /> +
+ +
+ + setSignupForm({ ...signupForm, name: e.target.value })} + /> +
+ + +
+
+
+ )} + + {error && ( +
+ {error.message} +
+ )} +
+
+ + {/* 데이터 마이그레이션 */} + + + 데이터 마이그레이션 + + Supabase에서 Appwrite로 데이터를 마이그레이션합니다. + + + + + + +
+ ); +}; + +export default AppwriteSettingsPage; diff --git a/src/pages/Register.tsx b/src/pages/Register.tsx index 1aa566f..a2cc480 100644 --- a/src/pages/Register.tsx +++ b/src/pages/Register.tsx @@ -10,7 +10,7 @@ import RegisterForm from "@/components/auth/RegisterForm"; import LoginLink from "@/components/auth/LoginLink"; import ServerStatusAlert from "@/components/auth/ServerStatusAlert"; import TestConnectionSection from "@/components/auth/TestConnectionSection"; -import SupabaseConnectionStatus from "@/components/auth/SupabaseConnectionStatus"; +import SupabaseConnectionStatus from "@/archive/components/SupabaseConnectionStatus"; import RegisterErrorDisplay from "@/components/auth/RegisterErrorDisplay"; import { ServerConnectionStatus } from "@/components/auth/types"; diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 8029018..2f85363 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom'; import NavBar from '@/components/NavBar'; import SyncSettings from '@/components/SyncSettings'; import AppVersionInfo from '@/components/AppVersionInfo'; -import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react'; +import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight, Database } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useAuth } from '@/contexts/auth'; import { useToast } from '@/hooks/useToast.wrapper'; @@ -105,6 +105,7 @@ const Settings = () => {

앱 설정

navigate('/security-privacy')} /> + navigate('/appwrite-settings')} /> navigate('/help-support')} />
diff --git a/src/utils/appwriteTransactionUtils.ts b/src/utils/appwriteTransactionUtils.ts new file mode 100644 index 0000000..9da02ef --- /dev/null +++ b/src/utils/appwriteTransactionUtils.ts @@ -0,0 +1,258 @@ +import { ID, Query } from 'appwrite'; +import { databases, account } from '@/lib/appwrite'; +import { Transaction } from '@/components/TransactionCard'; +import { isSyncEnabled } from '@/utils/syncUtils'; +import { formatISO } from 'date-fns'; +import { getAppwriteDatabaseId, getAppwriteTransactionsCollectionId } from '@/lib/appwrite/config'; + +// ISO 형식으로 날짜 변환 (Appwrite 저장용) +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()); + } +}; + +// Appwrite와 트랜잭션 동기화 +export const syncTransactionsWithAppwrite = async ( + user: any, + transactions: Transaction[] +): Promise => { + if (!user || !isSyncEnabled()) return transactions; + + try { + const databaseId = getAppwriteDatabaseId(); + const collectionId = getAppwriteTransactionsCollectionId(); + + const { documents } = await databases.listDocuments( + databaseId, + collectionId, + [ + Query.equal('user_id', user.$id) + ] + ); + + if (documents && documents.length > 0) { + // Appwrite 데이터 로컬 형식으로 변환 + const appwriteTransactions = documents.map(doc => ({ + id: doc.transaction_id, + title: doc.title, + amount: doc.amount, + date: doc.date, + category: doc.category, + type: doc.type + })); + + // 로컬 데이터와 병합 (중복 ID 제거) + const mergedTransactions = [...transactions]; + + appwriteTransactions.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('Appwrite 동기화 오류:', err); + } + + return transactions; +}; + +// Appwrite에 트랜잭션 업데이트 +export const updateTransactionInAppwrite = async ( + user: any, + transaction: Transaction +): Promise => { + if (!user || !isSyncEnabled()) return; + + try { + const databaseId = getAppwriteDatabaseId(); + const collectionId = getAppwriteTransactionsCollectionId(); + + // 날짜를 ISO 형식으로 변환 + const isoDate = convertDateToISO(transaction.date); + + // 기존 문서 찾기 + const { documents } = await databases.listDocuments( + databaseId, + collectionId, + [ + Query.equal('transaction_id', transaction.id) + ] + ); + + if (documents && documents.length > 0) { + // 기존 문서 업데이트 + await databases.updateDocument( + databaseId, + collectionId, + documents[0].$id, + { + title: transaction.title, + amount: transaction.amount, + date: isoDate, + category: transaction.category, + type: transaction.type + } + ); + console.log('Appwrite 트랜잭션 업데이트 성공:', transaction.id); + } else { + // 새 문서 생성 + await databases.createDocument( + databaseId, + collectionId, + ID.unique(), + { + user_id: user.$id, + transaction_id: transaction.id, + title: transaction.title, + amount: transaction.amount, + date: isoDate, + category: transaction.category, + type: transaction.type + } + ); + console.log('Appwrite 트랜잭션 생성 성공:', transaction.id); + } + } catch (error) { + console.error('Appwrite 업데이트 오류:', error); + } +}; + +// Appwrite에서 트랜잭션 삭제 - UI 스레드 차단 방지를 위한 비동기 처리 +export const deleteTransactionFromAppwrite = async ( + user: any, + transactionId: string +): Promise => { + if (!user || !isSyncEnabled()) return; + + // 컴포넌트 마운트 상태 추적을 위한 변수 + let isMounted = true; + + // 비동기 작업 래퍼 함수 + const performDelete = async () => { + try { + const databaseId = getAppwriteDatabaseId(); + const collectionId = getAppwriteTransactionsCollectionId(); + + // 기존 문서 찾기 + const { documents } = await databases.listDocuments( + databaseId, + collectionId, + [ + Query.equal('transaction_id', transactionId) + ] + ); + + if (!isMounted) return; // 컴포넌트가 언마운트되었으면 중단 + + if (documents && documents.length > 0) { + // requestAnimationFrame을 사용하여 UI 업데이트 최적화 + requestAnimationFrame(async () => { + try { + await databases.deleteDocument( + databaseId, + collectionId, + documents[0].$id + ); + + if (!isMounted) return; // 컴포넌트가 언마운트되었으면 중단 + + console.log('Appwrite 트랜잭션 삭제 성공:', transactionId); + } catch (innerError) { + console.error('Appwrite 삭제 내부 오류:', innerError); + } + }); + } + } catch (error) { + console.error('Appwrite 삭제 오류:', error); + } + }; + + // 비동기 작업 시작 + performDelete(); + + // 정리 함수 반환은 해제 (이 함수는 void를 반환해야 함) +}; + +// 컴포넌트에서 사용할 수 있는 삭제 함수 (정리 함수 반환) +export const deleteTransactionWithCleanup = ( + user: any, + transactionId: string +): () => void => { + let isMounted = true; + + // 삭제 작업 시작 + deleteTransactionFromAppwrite(user, transactionId); + + // 정리 함수 반환 + return () => { + isMounted = false; + }; +}; + +// 트랜잭션 삭제 작업을 디바운스하기 위한 유틸리티 +let deleteTimeouts: Record = {}; + +// 디바운스된 트랜잭션 삭제 함수 +export const debouncedDeleteTransaction = ( + user: any, + transactionId: string, + delay: number = 300 +): Promise => { + return new Promise((resolve) => { + // 이전 타임아웃이 있으면 취소 + if (deleteTimeouts[transactionId]) { + clearTimeout(deleteTimeouts[transactionId]); + } + + // 새 타임아웃 설정 + deleteTimeouts[transactionId] = setTimeout(async () => { + try { + await deleteTransactionFromAppwrite(user, transactionId); + resolve(); + } catch (error) { + console.error('디바운스된 삭제 작업 오류:', error); + resolve(); + } finally { + delete deleteTimeouts[transactionId]; + } + }, delay); + }); +}; diff --git a/src/utils/auth/network/connectionVerifier.ts b/src/utils/auth/network/connectionVerifier.ts index cbcd604..a31305a 100644 --- a/src/utils/auth/network/connectionVerifier.ts +++ b/src/utils/auth/network/connectionVerifier.ts @@ -1,5 +1,5 @@ -import { getSupabaseUrl } from '@/lib/supabase/config'; +import { getSupabaseUrl } from '@/archive/lib/supabase/config'; /** * 기본 서버 연결 상태 검사 유틸리티 diff --git a/src/utils/auth/network/enhancedVerifier.ts b/src/utils/auth/network/enhancedVerifier.ts index bb27cb3..fa37e8e 100644 --- a/src/utils/auth/network/enhancedVerifier.ts +++ b/src/utils/auth/network/enhancedVerifier.ts @@ -1,5 +1,5 @@ -import { getSupabaseUrl } from '@/lib/supabase/config'; +import { getSupabaseUrl } from '@/archive/lib/supabase/config'; /** * 강화된 서버 연결 검사 - Supabase Cloud 환경에 최적화 diff --git a/src/utils/categoryBudgetUtils.ts b/src/utils/categoryBudgetUtils.ts index f1f63fa..5bf6260 100644 --- a/src/utils/categoryBudgetUtils.ts +++ b/src/utils/categoryBudgetUtils.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { isSyncEnabled } from '@/utils/syncUtils'; /** diff --git a/src/utils/sync/budget/downloadBudget.ts b/src/utils/sync/budget/downloadBudget.ts index a5cc798..449a0cd 100644 --- a/src/utils/sync/budget/downloadBudget.ts +++ b/src/utils/sync/budget/downloadBudget.ts @@ -1,4 +1,4 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { isSyncEnabled } from '../syncSettings'; import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker'; diff --git a/src/utils/sync/budget/uploadCategoryBudgets.ts b/src/utils/sync/budget/uploadCategoryBudgets.ts index 0751c97..293b24c 100644 --- a/src/utils/sync/budget/uploadCategoryBudgets.ts +++ b/src/utils/sync/budget/uploadCategoryBudgets.ts @@ -2,7 +2,7 @@ /** * 카테고리 예산 업로드 기능 */ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { CategoryBudgets, CategoryBudgetRecord } from './types'; import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators'; diff --git a/src/utils/sync/budget/uploadMonthlyBudget.ts b/src/utils/sync/budget/uploadMonthlyBudget.ts index 0e9108f..60be4e2 100644 --- a/src/utils/sync/budget/uploadMonthlyBudget.ts +++ b/src/utils/sync/budget/uploadMonthlyBudget.ts @@ -2,7 +2,7 @@ /** * 월간 예산 업로드 기능 */ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { BudgetData, BudgetRecord } from './types'; import { isValidMonthlyBudget } from './validators'; diff --git a/src/utils/sync/clearCloudData.ts b/src/utils/sync/clearCloudData.ts index 96783f8..e7f46e9 100644 --- a/src/utils/sync/clearCloudData.ts +++ b/src/utils/sync/clearCloudData.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; /** * 사용자의 모든 클라우드 데이터 초기화 diff --git a/src/utils/sync/core/status.ts b/src/utils/sync/core/status.ts index a765d9e..bf5222d 100644 --- a/src/utils/sync/core/status.ts +++ b/src/utils/sync/core/status.ts @@ -2,7 +2,7 @@ /** * 서버 및 로컬 데이터 상태 확인 기능 */ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { DataStatus, ServerDataStatus, LocalDataStatus } from './types'; /** diff --git a/src/utils/sync/downloadBudget.ts b/src/utils/sync/downloadBudget.ts index d53c273..6011f4c 100644 --- a/src/utils/sync/downloadBudget.ts +++ b/src/utils/sync/downloadBudget.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { isSyncEnabled } from './syncSettings'; import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker'; diff --git a/src/utils/sync/downloadTransaction.ts b/src/utils/sync/downloadTransaction.ts index 819d943..83b5ef0 100644 --- a/src/utils/sync/downloadTransaction.ts +++ b/src/utils/sync/downloadTransaction.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { Transaction } from '@/components/TransactionCard'; import { isSyncEnabled } from './syncSettings'; import { formatDateForDisplay } from './transaction/dateUtils'; diff --git a/src/utils/sync/transaction/deleteTransaction.ts b/src/utils/sync/transaction/deleteTransaction.ts index e8f5d2e..56b5271 100644 --- a/src/utils/sync/transaction/deleteTransaction.ts +++ b/src/utils/sync/transaction/deleteTransaction.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { isSyncEnabled } from '../syncSettings'; import { addToDeletedTransactions } from './deletedTransactionsTracker'; diff --git a/src/utils/sync/transaction/downloadTransaction.ts b/src/utils/sync/transaction/downloadTransaction.ts index c49d021..319caf3 100644 --- a/src/utils/sync/transaction/downloadTransaction.ts +++ b/src/utils/sync/transaction/downloadTransaction.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { Transaction } from '@/components/TransactionCard'; import { isSyncEnabled } from '../syncSettings'; import { formatDateForDisplay } from './dateUtils'; diff --git a/src/utils/sync/transaction/uploadTransaction.ts b/src/utils/sync/transaction/uploadTransaction.ts index b277167..7f5f53a 100644 --- a/src/utils/sync/transaction/uploadTransaction.ts +++ b/src/utils/sync/transaction/uploadTransaction.ts @@ -1,5 +1,5 @@ -import { supabase } from '@/lib/supabase'; +import { supabase } from '@/archive/lib/supabase'; import { Transaction } from '@/components/TransactionCard'; import { isSyncEnabled } from '../syncSettings'; import { normalizeDate } from './dateUtils'; diff --git a/update-supabase-imports.sh b/update-supabase-imports.sh new file mode 100755 index 0000000..248d9d8 --- /dev/null +++ b/update-supabase-imports.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Supabase import 경로를 archive 폴더로 변경하는 스크립트 +echo "Supabase import 경로를 archive 폴더로 변경합니다..." + +# 프로젝트 루트 디렉토리 설정 +PROJECT_ROOT="/Users/hansoo./Dev/zellyy-finance" + +# 모든 .ts 및 .tsx 파일에서 Supabase import 경로 변경 +find "$PROJECT_ROOT/src" -type f \( -name "*.ts" -o -name "*.tsx" \) -not -path "*/archive/*" | xargs grep -l "from '@/lib/supabase'" | while read -r file; do + echo "파일 수정 중: $file" + sed -i '' "s|from '@/lib/supabase'|from '@/archive/lib/supabase'|g" "$file" +done + +echo "완료! Supabase import 경로가 archive 폴더로 변경되었습니다."