Migrate from Supabase to Appwrite with core functionality and UI components

This commit is contained in:
hansoo
2025-05-05 08:58:27 +09:00
parent fdfdf15166
commit f83bb384af
79 changed files with 2373 additions and 199 deletions

8
.env
View File

@@ -1,3 +1,4 @@
# Supabase 관련 설정 (이전 버전)
CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres
ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres # On-Prem 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_URL=http://localhost:9000
ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q 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 VITE_DISABLE_LOVABLE_BANNER=true

View File

@@ -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. 문제 발생 시 개발팀에 즉시 보고하세요.

View File

@@ -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만 증가할 것
- 버전 정보는 항상 설정 페이지에 표시하여 사용자와 개발자가 확인 가능하게 할 것

View File

@@ -1,141 +1,59 @@
# 적자 탈출 가계부 프로젝트 문서 # Zellyy Finance 프로젝트 문서
이 디렉토리는 적자 탈출 가계부 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱 개발 프로젝트입니다. 이 디렉토리는 Zellyy Finance 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱입니다.
## 프로젝트 개요 ## 프로젝트 개요
'적자 탈출 가계부'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. AI 기술을 활용한 개인화된 재정 관리 경험을 제공하고, 궁극적으로는 사용자들의 재정적 웰빙을 향상시키는 것을 목표로 합니다. 'Zellyy Finance'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. Appwrite 백엔드를 활용하여 안정적인 데이터 관리와 인증 시스템을 제공합니다.
## 폴더 구조 ## 폴더 구조
### 00_프로젝트_개요 ### 00_프로젝트_개요
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다. 프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
- `01_프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명 - `프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
- `02_핵심_문제_정의.md` - 해결하고자 하는 문제 정의 (예정) - `핵심_문제_정의.md` - 해결하고자 하는 문제 정의
- `03_사용자_페르소나.md` - 타겟 사용자 프로필 (예정) - `사용자_페르소나.md` - 타겟 사용자 프로필
- `04_사용자_스토리.md` - 사용자 관점의 요구사항 (예정)
- `05_비즈니스_모델.md` - 수익 모델 및 사업화 전략 (예정)
- `06_법률_규제_검토.md` - 금융 앱 관련 법규 및 규제 검토 (예정)
### 01_기획_및_설계 ### 01_기획_및_설계
프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다. 프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다.
- `01_요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석 - `요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
- `02_MVP_기능_목록.md` - 최소 기능 제품(MVP)의 기능 목록 (예정) - `UI_와이어프레임.md` - 핵심 화면 와이어프레임
- `03_주요_사용_시나리오.md` - 주요 사용 사례 시나리오 (예정) - `사용자_경험_전략.md` - 사용자 경험 설계 전략
- `04_UI_와이어프레임.md` - 핵심 화면 와이어프레임 (예정)
- `05_사용자_여정_맵.md` - 사용자 경험 흐름도 (예정)
- `06_정보_아키텍처.md` - 앱 구조 및 화면 흐름도 (예정)
### 02_기술_문서 ### 02_기술_문서
프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다. 프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
- `01_시스템_아키텍처.md` - 시스템 아키텍처 설계 문서 - `시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
- `02_데이터_모델_설계.md` - 데이터 모델 설계 문서 (예정) - `데이터_모델_설계.md` - 데이터베이스 스키마 및 모델 설계
- `03_API_명세서.md` - API 엔드포인트 명세 (예정) - `Appwrite_전환_가이드.md` - Supabase에서 Appwrite로의 전환 가이드
- `04_보안_설계.md` - 보안 및 개인정보 보호 설계 (예정)
- `05_성능_최적화_전략.md` - 앱 성능 최적화 전략 (예정)
- `06_CI_CD_파이프라인.md` - 지속적 통합/배포 전략 (예정)
- `07_AI_ML_구현_전략.md` - AI 기반 소비 패턴 분석 구현 방법 (예정)
### 03_개발_단계 ### 03_개발_단계
프로젝트 개발 단계별 문서가 포함되어 있습니다. 개발 과정과 관련된 문서가 포함되어 있습니다.
- `01_개발_로드맵.md` - 전체 개발 로드맵 및 일정 - `개발_가이드라인.md` - 코드 작성 원칙, iOS/Android 지원, Appwrite 통합 등에 관한 가이드라인
- `02_1단계_개발_계획.md` - 1단계(MVP) 개발 상세 계획 (예정)
- `03_테스트_전략.md` - 테스트 방법론 및 계획 (예정)
- `04_배포_전략.md` - 배포 및 운영 계획 (예정)
- `05_품질_보증_계획.md` - QA 전략 및 테스트 케이스 (예정)
- `06_유지보수_전략.md` - 출시 후 유지보수 및 업데이트 계획 (예정)
### 04_디자인_가이드 ### archive
UI/UX 디자인 관련 문서가 포함되어 있습니다. 더 이상 활발하게 사용되지 않는 레거시 문서들이 보관되어 있습니다.
- `01_디자인_시스템.md` - 디자인 언어 및 컴포넌트 정의 (예정) - `Supabase 관련 문서` - 이전에 사용하던 Supabase 관련 설정 및 가이드
- `02_색상_팔레트.md` - 앱 색상 가이드라인 (예정) - `개발 단계별 문서` - 이전 개발 단계의 계획 및 산출물 요약
- `03_타이포그래피.md` - 폰트 및 텍스트 스타일 가이드 (예정)
- `04_아이콘_및_이미지.md` - 아이콘 디자인 및 사용 가이드 (예정)
- `05_애니메이션_가이드.md` - UI 애니메이션 및 트랜지션 (예정)
- `06_접근성_지침.md` - 접근성 디자인 원칙 (예정)
### 05_프로젝트_관리 ## 주요 기술 스택
프로젝트 관리 및 협업 관련 문서가 포함되어 있습니다.
- `01_팀_구성.md` - 팀 구성원 및 역할 정의 (예정)
- `02_의사결정_프로세스.md` - 프로젝트 의사결정 체계 (예정)
- `03_커뮤니케이션_계획.md` - 팀 내 소통 방식 및 도구 (예정)
- `04_일정_및_마일스톤.md` - 주요 마일스톤 및 납기일 (예정)
- `05_위험_관리.md` - 잠재적 위험 요소 및 대응 계획 (예정)
### 06_참고자료 - **프론트엔드**: React Native, TypeScript
프로젝트 진행에 참고할 수 있는 자료들이 포함되어 있습니다. - **백엔드**: Appwrite
- `01_시장_조사_보고서.md` - 가계부 앱 시장 조사 보고서 - **상태 관리**: Context API
- `02_경쟁사_분석.md` - 주요 경쟁 앱 상세 분석 (예정) - **UI 컴포넌트**: Lovable UI
- `03_사용자_인터뷰.md` - 잠재 사용자 인터뷰 결과 (예정) - **네이티브 통합**: Capacitor
- `04_참고_리소스.md` - 유용한 참고 자료 및 링크 (예정)
- `05_금융_데이터_소스.md` - 재정 관리 데이터 참고 자료 (예정)
- `06_관련_연구_자료.md` - 소비 행동 및 금융 심리학 연구 (예정)
### 07_마케팅_및_성장 ## 개발 가이드라인
마케팅 및 사용자 확보 전략 관련 문서가 포함되어 있습니다.
- `01_마케팅_전략.md` - 출시 및 사용자 확보 전략 (예정)
- `02_ASO_전략.md` - 앱 스토어 최적화 전략 (예정)
- `03_콘텐츠_전략.md` - 콘텐츠 마케팅 계획 (예정)
- `04_사용자_유지_전략.md` - 사용자 참여 및 유지 방안 (예정)
- `05_파트너십_계획.md` - 잠재적 파트너십 및 협업 기회 (예정)
## 주요 기능 개발 가이드라인은 [03_개발_단계/개발_가이드라인.md](./03_개발_단계/개발_가이드라인.md) 문서를 참조하세요. 이 문서에는 다음 내용이 포함되어 있습니다:
1. **수입/지출 기록**: 간편한 UI로 일상 재정 활동 기록 1. 코드 작성 원칙
2. **카테고리 관리**: 사용자 정의 카테고리로 지출 분류 2. 트랜잭션 삭제 안전성
3. **예산 설정**: 카테고리별 월간/주간 예산 설정 및 알림 3. Appwrite 통합 원칙
4. **지출 분석**: 차트와 그래프로 소비 패턴 시각 4. 상태 관리 최적
5. **AI 기반 분석**: 소비 패턴 분석 및 맞춤형 절약 제안 5. iOS/Android 지원
6. **절약 챌린지**: 사용자 맞춤형 절약 목표 설정 및 달성 보상 6. 디버깅 및 로깅
7. **재정 건강 점수**: 사용자의 재정 상태를 점수화하여 개선 동기 부여
8. **구독 관리**: 정기 구독 서비스 추적 및 최적화 제안
9. **재정 목표 설정**: 단기/중기/장기 저축 목표 설정 및 진행 상황 추적
10. **알림 시스템**: 예산 초과, 주요 지출, 절약 기회에 대한 스마트 알림
11. **가계부 보고서**: 정기적인 재정 상태 요약 보고서 제공
12. **공유 기능**: 가족 또는 파트너와 특정 재정 정보 공유
## 기술 스택 ## Appwrite 전환
- **프론트엔드**: React, Vite, Tailwind CSS, Capacitor Supabase에서 Appwrite로의 전환에 관한 상세 정보는 [02_기술_문서/Appwrite_전환_가이드.md](./02_기술_문서/Appwrite_전환_가이드.md) 문서를 참조하세요.
- **백엔드**: 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로 전환

7
package-lock.json generated
View File

@@ -45,6 +45,7 @@
"@supabase/supabase-js": "^2.49.4", "@supabase/supabase-js": "^2.49.4",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"appwrite": "^17.0.2",
"browserslist": "^4.24.4", "browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@@ -3832,6 +3833,12 @@
"node": ">= 8" "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": { "node_modules/arg": {
"version": "5.0.2", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",

View File

@@ -48,6 +48,7 @@
"@supabase/supabase-js": "^2.49.4", "@supabase/supabase-js": "^2.49.4",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"appwrite": "^17.0.2",
"browserslist": "^4.24.4", "browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",

View File

@@ -17,6 +17,7 @@ import HelpSupport from './pages/HelpSupport';
import SecurityPrivacySettings from './pages/SecurityPrivacySettings'; import SecurityPrivacySettings from './pages/SecurityPrivacySettings';
import NotificationSettings from './pages/NotificationSettings'; import NotificationSettings from './pages/NotificationSettings';
import ForgotPassword from './pages/ForgotPassword'; import ForgotPassword from './pages/ForgotPassword';
import AppwriteSettingsPage from './pages/AppwriteSettingsPage';
function App() { function App() {
useEffect(() => { useEffect(() => {
@@ -40,6 +41,7 @@ function App() {
<Route path="/security-privacy" element={<SecurityPrivacySettings />} /> <Route path="/security-privacy" element={<SecurityPrivacySettings />} />
<Route path="/notifications" element={<NotificationSettings />} /> <Route path="/notifications" element={<NotificationSettings />} />
<Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
<Toaster /> <Toaster />

21
src/archive/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Archive 폴더
이 폴더는 Zellyy Finance 프로젝트에서 더 이상 활발하게 사용되지 않는 레거시 코드를 보관하는 곳입니다.
## 개요
Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환했습니다. 이 폴더에는 Supabase 관련 코드가 보관되어 있으며, 참조용으로만 유지됩니다.
## 폴더 구조
- `components/`: Supabase 관련 UI 컴포넌트
- `hooks/`: Supabase 관련 훅
- `integrations/`: Supabase 통합 코드
- `lib/`: Supabase 클라이언트 및 유틸리티
- `utils/`: Supabase 트랜잭션 유틸리티
## 주의사항
이 폴더의 코드는 더 이상 유지보수되지 않으며, 새로운 기능 개발에 사용해서는 안 됩니다. 모든 새로운 개발은 Appwrite 기반으로 진행해야 합니다.
마이그레이션이 완전히 완료되면 이 폴더는 삭제될 예정입니다.

View File

@@ -4,7 +4,7 @@ import { PlusIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용 import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용
import { useBudget } from '@/contexts/budget/BudgetContext'; 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 { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils';
import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm'; import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm';
import { Transaction } from '@/contexts/budget/types'; import { Transaction } from '@/contexts/budget/types';

View File

@@ -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 (
<div className={`mt-2 p-3 rounded-md text-sm ${testResults.connected ? 'bg-green-50' : 'bg-red-50'}`}>
<div className="flex items-start">
{testResults.connected ? (
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-medium ${testResults.connected ? 'text-green-800' : 'text-red-800'}`}>
{testResults.connected ? '연결됨' : '연결 실패'}
</p>
<p className={testResults.connected ? 'text-green-700' : 'text-red-700'}>
{testResults.message}
</p>
{testResults.details && (
<p className="text-gray-500 mt-1 text-xs">
{testResults.details}
</p>
)}
</div>
</div>
</div>
);
};
export default AppwriteConnectionStatus;

View File

@@ -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<boolean>(false);
// 데이터베이스 설정 상태
const [dbSetupDone, setDbSetupDone] = useState<boolean>(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 (
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={testConnection}
disabled={loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
{testResults?.connected && !dbSetupDone && (
<Button
variant="outline"
size="sm"
onClick={setupDatabase}
disabled={loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
)}
</div>
<AppwriteConnectionStatus testResults={testResults} />
</div>
);
};
export default AppwriteConnectionTest;

View File

@@ -7,7 +7,7 @@ import { verifyServerConnection } from "@/contexts/auth/auth.utils";
import { ServerConnectionStatus } from "./types"; import { ServerConnectionStatus } from "./types";
import EmailConfirmation from "./EmailConfirmation"; import EmailConfirmation from "./EmailConfirmation";
import RegisterFormFields from "./RegisterFormFields"; import RegisterFormFields from "./RegisterFormFields";
import { supabase } from "@/lib/supabase"; import { supabase } from "@/archive/lib/supabase";
interface RegisterFormProps { interface RegisterFormProps {
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>; signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>;

View File

@@ -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 (
<div className="p-4 bg-yellow-50 rounded-md">
<p className="text-yellow-800">
.
</p>
</div>
);
}
return (
<div className="space-y-6 p-4 border rounded-md">
<div>
<h3 className="text-lg font-medium">Supabase에서 Appwrite로 </h3>
<p className="text-sm text-gray-500 mt-1">
Supabase의 Appwrite로 .
</p>
</div>
{/* 마이그레이션 상태 */}
{migrationStatus && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<Button
variant="ghost"
size="sm"
onClick={checkStatus}
disabled={migrationProgress.isRunning}
>
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="text-xs text-gray-500">Supabase </div>
<div className="text-lg font-semibold">{migrationStatus.supabaseCount}</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="text-xs text-gray-500">Appwrite </div>
<div className="text-lg font-semibold">{migrationStatus.appwriteCount}</div>
</div>
</div>
<div className="flex items-center mt-2">
{migrationStatus.isComplete ? (
<div className="flex items-center text-green-600 text-sm">
<CheckCircle className="h-4 w-4 mr-1" />
</div>
) : (
<div className="flex items-center text-amber-600 text-sm">
<AlertCircle className="h-4 w-4 mr-1" />
</div>
)}
</div>
{migrationStatus.error && (
<div className="text-sm text-red-600 mt-2">
: {migrationStatus.error}
</div>
)}
</div>
)}
{/* 마이그레이션 진행 상태 */}
{migrationProgress.isRunning && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<span className="text-xs text-gray-500">
{migrationProgress.current} / {migrationProgress.total} ({migrationProgress.percentage}%)
</span>
</div>
<Progress value={migrationProgress.percentage} className="h-2" />
</div>
)}
{/* 마이그레이션 결과 */}
{migrationResult && !migrationProgress.isRunning && (
<div className={`p-3 rounded-md ${migrationResult.success ? 'bg-green-50' : 'bg-red-50'}`}>
<div className="flex items-start">
{migrationResult.success ? (
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
) : (
<AlertCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-medium ${migrationResult.success ? 'text-green-800' : 'text-red-800'}`}>
{migrationResult.success ? '마이그레이션 성공' : '마이그레이션 실패'}
</p>
<p className={migrationResult.success ? 'text-green-700' : 'text-red-700'}>
{migrationResult.success
? `${migrationResult.migrated}개의 트랜잭션이 마이그레이션되었습니다.`
: migrationResult.error || '알 수 없는 오류가 발생했습니다.'
}
</p>
</div>
</div>
</div>
)}
{/* 마이그레이션 버튼 */}
<div className="flex justify-end">
<Button
onClick={runMigration}
disabled={migrationProgress.isRunning || migrationStatus?.isComplete}
>
{migrationProgress.isRunning && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{migrationStatus?.isComplete
? '이미 마이그레이션 완료됨'
: migrationProgress.isRunning
? '마이그레이션 중...'
: '마이그레이션 시작'
}
</Button>
</div>
</div>
);
};
export default SupabaseToAppwriteMigration;

View File

@@ -1,16 +1,16 @@
import React, { useEffect, useState } from 'react'; 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 { toast } from '@/hooks/useToast.wrapper';
import { AuthContextType } from './types'; import { AuthContextType } from './types';
import * as authActions from './authActions'; import * as authActions from './authActions';
import { clearAllToasts } from '@/hooks/toast/toastManager'; import { clearAllToasts } from '@/hooks/toast/toastManager';
import { AuthContext } from './AuthContext'; import { AuthContext } from './AuthContext';
import { account } from '@/lib/appwrite/client';
import { Models } from 'appwrite';
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session, setSession] = useState<Session | null>(null); const [session, setSession] = useState<Models.Session | null>(null);
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
@@ -22,19 +22,19 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve())); await new Promise<void>(resolve => queueMicrotask(() => resolve()));
const { data, error } = await supabase.auth.getSession(); try {
// Appwrite 세션 가져오기
const currentSession = await account.getSession('current');
const currentUser = await account.get();
if (error) {
console.error('세션 로딩 중 오류:', error);
} else if (data.session) {
// 상태 업데이트를 마이크로태스크로 지연 // 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => { queueMicrotask(() => {
setSession(data.session); setSession(currentSession);
setUser(data.session.user); setUser(currentUser);
console.log('세션 로딩 완료'); console.log('세션 로딩 완료');
}); });
} else { } catch (sessionError) {
console.log('활성 세션 없음'); console.error('세션 로딩 중 오류:', sessionError);
} }
} catch (error) { } catch (error) {
console.error('세션 확인 중 예외 발생:', error); console.error('세션 확인 중 예외 발생:', error);
@@ -51,21 +51,31 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
getSession(); getSession();
}, 100); }, 100);
// auth 상태 변경 리스너 - 최적화된 버전 // Appwrite 인증 상태 변경 리스너 설정
const { data: { subscription } } = supabase.auth.onAuthStateChange( // 참고: Appwrite는 직접적인 이벤트 리스너를 제공하지 않으므로 주기적으로 확인하는 방식 사용
async (event, session) => { const authCheckInterval = setInterval(async () => {
console.log('Supabase auth 이벤트:', event); try {
// 현재 로그인 상태 확인
const currentUser = await account.get();
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지 // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve())); await new Promise<void>(resolve => queueMicrotask(() => resolve()));
if (session) { // 사용자 정보가 변경되었는지 확인
if (currentUser && (!user || currentUser.$id !== user.$id)) {
// 세션 정보 가져오기
const currentSession = await account.getSession('current');
// 상태 업데이트를 마이크로태스크로 지연 // 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => { queueMicrotask(() => {
setSession(session); setSession(currentSession);
setUser(session.user); setUser(currentUser);
console.log('Appwrite 인증 상태 변경: 로그인됨');
}); });
} else if (event === 'SIGNED_OUT') { }
} catch (error) {
// 오류 발생 시 로그아웃 상태로 간주
if (user) {
// 상태 업데이트를 마이크로태스크로 지연 // 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => { queueMicrotask(() => {
setSession(null); setSession(null);
@@ -76,21 +86,17 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함 // 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
window.dispatchEvent(new Event('auth-state-changed')); window.dispatchEvent(new Event('auth-state-changed'));
console.log('Appwrite 인증 상태 변경: 로그아웃됨');
}); });
} }
// 로딩 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setLoading(false);
});
} }
); }, 5000); // 5초마다 확인
// 리스너 정리 // 리스너 정리
return () => { return () => {
subscription.unsubscribe(); clearInterval(authCheckInterval);
}; };
}, []); }, [user]);
// 인증 작업 메서드들 // 인증 작업 메서드들
const value: AuthContextType = { const value: AuthContextType = {

View File

@@ -1,4 +1,4 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { handleNetworkError, showAuthToast } from '@/utils/auth'; import { handleNetworkError, showAuthToast } from '@/utils/auth';
export const resetPassword = async (email: string) => { export const resetPassword = async (email: string) => {

View File

@@ -1,45 +1,44 @@
import { account } from '@/lib/appwrite/client';
import { supabase } from '@/lib/supabase';
import { showAuthToast } from '@/utils/auth'; import { showAuthToast } from '@/utils/auth';
/** /**
* 로그인 기능 - Supabase Cloud 환경에 최적화 * 로그인 기능 - Appwrite 환경에 최적화
*/ */
export const signIn = async (email: string, password: string) => { export const signIn = async (email: string, password: string) => {
try { try {
console.log('로그인 시도 중:', email); console.log('로그인 시도 중:', email);
// Supabase 인증 방식 시도 // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
const { data, error } = await supabase.auth.signInWithPassword({ await new Promise<void>(resolve => queueMicrotask(() => resolve()));
email,
password // Appwrite 인증 방식 시도
}); try {
const session = await account.createEmailSession(email, password);
const user = await account.get();
// 상태 업데이트를 마이크로태스크로 지연
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
if (!error && data.user) {
showAuthToast('로그인 성공', '환영합니다!'); showAuthToast('로그인 성공', '환영합니다!');
return { error: null, user: data.user }; return { error: null, user };
} else if (error) { } catch (authError: any) {
console.error('로그인 오류:', error.message); console.error('로그인 오류:', authError);
let errorMessage = error.message; let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
if (error.message.includes('Invalid login credentials')) {
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (authError.code === 401) {
errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.'; errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
} else if (error.message.includes('Email not confirmed')) { } else if (authError.code === 429) {
errorMessage = '이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.'; errorMessage = '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.';
} }
showAuthToast('로그인 실패', errorMessage, 'destructive'); showAuthToast('로그인 실패', errorMessage, 'destructive');
return { error: { message: errorMessage }, user: null }; return { error: authError, user: null };
} }
} catch (error) {
// 여기까지 왔다면 오류가 발생한 것 console.error('로그인 예외 발생:', error);
showAuthToast('로그인 실패', '로그인 처리 중 오류가 발생했습니다.', 'destructive'); showAuthToast('로그인 오류', '서버 연결 중 오류가 발생했습니다.', 'destructive');
return { error: { message: '로그인 처리 중 오류가 발생했습니다.' }, user: null }; return { error, user: null };
} catch (error: any) {
console.error('로그인 중 예외 발생:', error);
const errorMessage = error.message || '로그인 중 오류가 발생했습니다.';
showAuthToast('로그인 오류', errorMessage, 'destructive');
return { error: { message: errorMessage }, user: null };
} }
}; };

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { showAuthToast } from '@/utils/auth'; import { showAuthToast } from '@/utils/auth';
/** /**

View File

@@ -1,4 +1,4 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { showAuthToast } from '@/utils/auth'; import { showAuthToast } from '@/utils/auth';
export const signOut = async (): Promise<void> => { export const signOut = async (): Promise<void> => {

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { showAuthToast, verifyServerConnection } from '@/utils/auth'; import { showAuthToast, verifyServerConnection } from '@/utils/auth';
/** /**

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { parseResponse, showAuthToast } from '@/utils/auth'; import { parseResponse, showAuthToast } from '@/utils/auth';
/** /**

View File

@@ -1,9 +1,9 @@
import { Session, User } from '@supabase/supabase-js'; import { Models } from 'appwrite';
export type AuthContextType = { export type AuthContextType = {
session: Session | null; session: Models.Session | null;
user: User | null; user: Models.User<Models.Preferences> | null;
loading: boolean; loading: boolean;
signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>; signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>;
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>;

View File

@@ -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<AuthState>({
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;

View File

@@ -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<Transaction[]>(localTransactions);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
// 컴포넌트 마운트 상태 추적
const isMountedRef = useRef<boolean>(true);
// 진행 중인 작업 추적
const pendingOperations = useRef<Set<string>>(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;

View File

@@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import { useToast } from "@/hooks/useToast.wrapper"; import { useToast } from "@/hooks/useToast.wrapper";
import { createRequiredTables } from "@/lib/supabase/setup"; import { createRequiredTables } from "@/archive/lib/supabase/setup";
/** /**
* Supabase 테이블 설정을 처리하는 커스텀 훅 * Supabase 테이블 설정을 처리하는 커스텀 훅

View File

@@ -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<boolean> => {
try {
// 계정 서비스를 통해 현재 세션 상태 확인 (간단한 API 호출)
await account.get();
return true;
} catch (error) {
console.error('Appwrite 연결 확인 오류:', error);
return false;
}
};

View File

@@ -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");
};

30
src/lib/appwrite/index.ts Normal file
View File

@@ -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
};

View File

@@ -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 : '알 수 없는 오류'
};
}
};

172
src/lib/appwrite/setup.ts Normal file
View File

@@ -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<boolean> => {
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;
}
};

View File

@@ -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 (
<div className="container mx-auto py-6 space-y-8">
<div>
<h1 className="text-2xl font-bold">Appwrite </h1>
<p className="text-gray-500">
Appwrite
</p>
</div>
<Separator />
{/* 서버 연결 상태 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription>
Appwrite .
</CardDescription>
</CardHeader>
<CardContent>
<AppwriteConnectionTest />
</CardContent>
</Card>
{/* 인증 관리 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription>
Appwrite .
</CardDescription>
</CardHeader>
<CardContent>
{user ? (
<div className="space-y-4">
<div className="p-4 bg-gray-50 rounded-md">
<h3 className="text-sm font-medium"> </h3>
<p className="text-sm mt-1">
ID: <span className="font-mono">{user.$id}</span>
</p>
<p className="text-sm">
: {user.email}
</p>
{user.name && (
<p className="text-sm">
: {user.name}
</p>
)}
</div>
<div className="flex justify-end">
<Button onClick={handleLogout} variant="outline">
</Button>
</div>
</div>
) : (
<Tabs defaultValue="login">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login"></TabsTrigger>
<TabsTrigger value="signup"></TabsTrigger>
</TabsList>
<TabsContent value="login" className="space-y-4 mt-4">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
type="email"
placeholder="이메일 주소"
value={loginForm.email}
onChange={(e) => setLoginForm({ ...loginForm, email: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
placeholder="비밀번호"
value={loginForm.password}
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
</Button>
</form>
</TabsContent>
<TabsContent value="signup" className="space-y-4 mt-4">
<form onSubmit={handleSignup} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="signup-email"></Label>
<Input
id="signup-email"
type="email"
placeholder="이메일 주소"
value={signupForm.email}
onChange={(e) => setSignupForm({ ...signupForm, email: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="signup-password"></Label>
<Input
id="signup-password"
type="password"
placeholder="비밀번호"
value={signupForm.password}
onChange={(e) => setSignupForm({ ...signupForm, password: e.target.value })}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="name"> ()</Label>
<Input
id="name"
type="text"
placeholder="이름"
value={signupForm.name}
onChange={(e) => setSignupForm({ ...signupForm, name: e.target.value })}
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
</Button>
</form>
</TabsContent>
</Tabs>
)}
{error && (
<div className="mt-4 p-3 bg-red-50 rounded-md text-sm text-red-600">
{error.message}
</div>
)}
</CardContent>
</Card>
{/* 데이터 마이그레이션 */}
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription>
Supabase에서 Appwrite로 .
</CardDescription>
</CardHeader>
<CardContent>
<SupabaseToAppwriteMigration />
</CardContent>
</Card>
</div>
);
};
export default AppwriteSettingsPage;

View File

@@ -10,7 +10,7 @@ import RegisterForm from "@/components/auth/RegisterForm";
import LoginLink from "@/components/auth/LoginLink"; import LoginLink from "@/components/auth/LoginLink";
import ServerStatusAlert from "@/components/auth/ServerStatusAlert"; import ServerStatusAlert from "@/components/auth/ServerStatusAlert";
import TestConnectionSection from "@/components/auth/TestConnectionSection"; 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 RegisterErrorDisplay from "@/components/auth/RegisterErrorDisplay";
import { ServerConnectionStatus } from "@/components/auth/types"; import { ServerConnectionStatus } from "@/components/auth/types";

View File

@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
import NavBar from '@/components/NavBar'; import NavBar from '@/components/NavBar';
import SyncSettings from '@/components/SyncSettings'; import SyncSettings from '@/components/SyncSettings';
import AppVersionInfo from '@/components/AppVersionInfo'; 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 { cn } from '@/lib/utils';
import { useAuth } from '@/contexts/auth'; import { useAuth } from '@/contexts/auth';
import { useToast } from '@/hooks/useToast.wrapper'; import { useToast } from '@/hooks/useToast.wrapper';
@@ -105,6 +105,7 @@ const Settings = () => {
<div className="space-y-4 mb-8"> <div className="space-y-4 mb-8">
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2"> </h2> <h2 className="text-sm font-medium text-gray-500 mb-2 px-2"> </h2>
<SettingsOption icon={Lock} label="보안 및 개인정보" description="보안 및 데이터 설정" onClick={() => navigate('/security-privacy')} /> <SettingsOption icon={Lock} label="보안 및 개인정보" description="보안 및 데이터 설정" onClick={() => navigate('/security-privacy')} />
<SettingsOption icon={Database} label="Appwrite 설정" description="Appwrite 연결 및 데이터 마이그레이션" onClick={() => navigate('/appwrite-settings')} />
<SettingsOption icon={HelpCircle} label="도움말 및 지원" description="FAQ 및 고객 지원" onClick={() => navigate('/help-support')} /> <SettingsOption icon={HelpCircle} label="도움말 및 지원" description="FAQ 및 고객 지원" onClick={() => navigate('/help-support')} />
</div> </div>

View File

@@ -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<Transaction[]> => {
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<void> => {
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<void> => {
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<string, NodeJS.Timeout> = {};
// 디바운스된 트랜잭션 삭제 함수
export const debouncedDeleteTransaction = (
user: any,
transactionId: string,
delay: number = 300
): Promise<void> => {
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);
});
};

View File

@@ -1,5 +1,5 @@
import { getSupabaseUrl } from '@/lib/supabase/config'; import { getSupabaseUrl } from '@/archive/lib/supabase/config';
/** /**
* 기본 서버 연결 상태 검사 유틸리티 * 기본 서버 연결 상태 검사 유틸리티

View File

@@ -1,5 +1,5 @@
import { getSupabaseUrl } from '@/lib/supabase/config'; import { getSupabaseUrl } from '@/archive/lib/supabase/config';
/** /**
* 강화된 서버 연결 검사 - Supabase Cloud 환경에 최적화 * 강화된 서버 연결 검사 - Supabase Cloud 환경에 최적화

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { isSyncEnabled } from '@/utils/syncUtils'; import { isSyncEnabled } from '@/utils/syncUtils';
/** /**

View File

@@ -1,4 +1,4 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { isSyncEnabled } from '../syncSettings'; import { isSyncEnabled } from '../syncSettings';
import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker'; import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker';

View File

@@ -2,7 +2,7 @@
/** /**
* 카테고리 예산 업로드 기능 * 카테고리 예산 업로드 기능
*/ */
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { CategoryBudgets, CategoryBudgetRecord } from './types'; import { CategoryBudgets, CategoryBudgetRecord } from './types';
import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators'; import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators';

View File

@@ -2,7 +2,7 @@
/** /**
* 월간 예산 업로드 기능 * 월간 예산 업로드 기능
*/ */
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { BudgetData, BudgetRecord } from './types'; import { BudgetData, BudgetRecord } from './types';
import { isValidMonthlyBudget } from './validators'; import { isValidMonthlyBudget } from './validators';

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
/** /**
* 사용자의 모든 클라우드 데이터 초기화 * 사용자의 모든 클라우드 데이터 초기화

View File

@@ -2,7 +2,7 @@
/** /**
* 서버 및 로컬 데이터 상태 확인 기능 * 서버 및 로컬 데이터 상태 확인 기능
*/ */
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { DataStatus, ServerDataStatus, LocalDataStatus } from './types'; import { DataStatus, ServerDataStatus, LocalDataStatus } from './types';
/** /**

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { isSyncEnabled } from './syncSettings'; import { isSyncEnabled } from './syncSettings';
import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker'; import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker';

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { Transaction } from '@/components/TransactionCard'; import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from './syncSettings'; import { isSyncEnabled } from './syncSettings';
import { formatDateForDisplay } from './transaction/dateUtils'; import { formatDateForDisplay } from './transaction/dateUtils';

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { isSyncEnabled } from '../syncSettings'; import { isSyncEnabled } from '../syncSettings';
import { addToDeletedTransactions } from './deletedTransactionsTracker'; import { addToDeletedTransactions } from './deletedTransactionsTracker';

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { Transaction } from '@/components/TransactionCard'; import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from '../syncSettings'; import { isSyncEnabled } from '../syncSettings';
import { formatDateForDisplay } from './dateUtils'; import { formatDateForDisplay } from './dateUtils';

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase'; import { supabase } from '@/archive/lib/supabase';
import { Transaction } from '@/components/TransactionCard'; import { Transaction } from '@/components/TransactionCard';
import { isSyncEnabled } from '../syncSettings'; import { isSyncEnabled } from '../syncSettings';
import { normalizeDate } from './dateUtils'; import { normalizeDate } from './dateUtils';

15
update-supabase-imports.sh Executable file
View File

@@ -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 폴더로 변경되었습니다."