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 (
+
+ {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 ? (
+