Compare commits

..

8 Commits

109 changed files with 4078 additions and 743 deletions

20
.env
View File

@@ -1 +1,21 @@
# 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 기본
VITE_SUPABASE_URL=http://localhost:9000
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
CLOUD_SUPABASE_URL=https://qnerebtvwwfobfzdoftx.supabase.co
CLOUD_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8
CLOUD_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MjA1MTQzOCwiZXhwIjoyMDU3NjI3NDM4fQ.3G9UksB-kE-ChGQrz6YrSZqQSqvzYsnhvZyCnE99Ifc
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=68182a300039f6d700a6
VITE_APPWRITE_DATABASE_ID=default
VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions
VITE_APPWRITE_API_KEY=standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea
VITE_DISABLE_LOVABLE_BANNER=true

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.0'
classpath 'com.android.tools.build:gradle:8.9.1'
classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -22,7 +22,8 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true
# Java 및 Kotlin 버전 설정
org.gradle.java.home=/opt/homebrew/Cellar/openjdk@17/17.0.14/libexec/openjdk.jdk/Contents/Home
# org.gradle.java.home is commented out to allow default or JAVA_HOME usage
# org.gradle.java.home=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false

View File

@@ -1,284 +0,0 @@
===== 빌드 시작: Sat Apr 5 19:45:45 KST 2025 =====
1. 웹 앱 빌드 중...
실행 명령어: npm run build
> vite_react_shadcn_ts@0.0.0 build
> vite build
vite v5.4.10 building for production...
transforming...
✓ 3636 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 0.72 kB │ gzip: 0.46 kB
dist/assets/index-BgIUBQkk.css 74.73 kB │ gzip: 12.82 kB
dist/assets/browser-Q2e0CuoM.js 0.30 kB │ gzip: 0.25 kB
dist/assets/index-ukZ_MYNA.js 1,155.73 kB │ gzip: 335.16 kB
(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 3.88s
웹 앱 빌드 완료
2. Capacitor에 웹 코드 동기화 중...
실행 명령어: npx cap sync android
✔ Copying web assets from dist to android/app/src/main/assets/public in 5.47ms
✔ Creating capacitor.config.json in android/app/src/main/assets in 1.94ms
✔ copy android in 30.43ms
✔ Updating Android plugins in 6.52ms
[info] Found 2 Capacitor plugins for android:
@capacitor/keyboard@7.0.0
@capacitor/splash-screen@7.0.0
✔ update android in 88.42ms
[info] Sync finished in 0.152s
Capacitor 동기화 완료
3. 안드로이드 빌드 시작 (release-aab)...
실행 명령어: ./gradlew bundleRelease
> Configure project :app
WARNING: The option setting 'android.defaults.buildfeatures.buildconfig=true' is deprecated.
The current default is 'false'.
It will be removed in version 10.0 of the Android Gradle plugin.
To keep using this feature, add the following to your module-level build.gradle files:
android.buildFeatures.buildConfig = true
or from Android Studio, click: `Refactor` > `Migrate BuildConfig to Gradle Build Files`.
버전 정보 로드: versionName=1.1.8, versionCode=9, buildNumber=9
최종 버전 정보: versionName=1.1.8, versionCode=9, buildNumber=9
WARNING: Using flatDir should be avoided because it doesn't support any meta-data formats.
> Configure project :capacitor-cordova-android-plugins
WARNING: Using flatDir should be avoided because it doesn't support any meta-data formats.
> Task :app:preBuild UP-TO-DATE
> Task :app:preReleaseBuild UP-TO-DATE
> Task :app:generateReleaseResValues
> Task :capacitor-android:preBuild UP-TO-DATE
> Task :capacitor-android:preReleaseBuild UP-TO-DATE
> Task :capacitor-android:generateReleaseResValues
> Task :capacitor-android:generateReleaseResources
> Task :capacitor-android:packageReleaseResources
> Task :capacitor-cordova-android-plugins:preBuild UP-TO-DATE
> Task :capacitor-cordova-android-plugins:preReleaseBuild UP-TO-DATE
> Task :capacitor-cordova-android-plugins:generateReleaseResValues
> Task :capacitor-cordova-android-plugins:generateReleaseResources
> Task :capacitor-cordova-android-plugins:packageReleaseResources
> Task :capacitor-keyboard:preBuild UP-TO-DATE
> Task :capacitor-keyboard:preReleaseBuild UP-TO-DATE
> Task :capacitor-keyboard:generateReleaseResValues
> Task :capacitor-keyboard:generateReleaseResources
> Task :capacitor-keyboard:packageReleaseResources
> Task :capacitor-splash-screen:preBuild UP-TO-DATE
> Task :capacitor-splash-screen:preReleaseBuild UP-TO-DATE
> Task :capacitor-splash-screen:generateReleaseResValues
> Task :capacitor-splash-screen:generateReleaseResources
> Task :capacitor-splash-screen:packageReleaseResources
> Task :app:mapReleaseSourceSetPaths
> Task :app:generateReleaseResources
> Task :app:createReleaseCompatibleScreenManifests
> Task :app:extractDeepLinksRelease
> Task :capacitor-android:extractDeepLinksRelease
> Task :capacitor-cordova-android-plugins:extractDeepLinksRelease
> Task :capacitor-keyboard:extractDeepLinksRelease
> Task :capacitor-cordova-android-plugins:processReleaseManifest
> Task :capacitor-splash-screen:extractDeepLinksRelease
> Task :capacitor-android:processReleaseManifest
> Task :capacitor-keyboard:processReleaseManifest
> Task :capacitor-splash-screen:writeReleaseAarMetadata
> Task :capacitor-keyboard:writeReleaseAarMetadata
> Task :capacitor-android:writeReleaseAarMetadata
> Task :capacitor-cordova-android-plugins:writeReleaseAarMetadata
> Task :capacitor-splash-screen:processReleaseManifest
> Task :capacitor-cordova-android-plugins:compileReleaseLibraryResources
> Task :capacitor-cordova-android-plugins:parseReleaseLocalResources
> Task :app:checkReleaseAarMetadata
> Task :capacitor-keyboard:compileReleaseLibraryResources
> Task :capacitor-android:parseReleaseLocalResources
> Task :capacitor-android:compileReleaseLibraryResources
> Task :app:processReleaseMainManifest
> Task :app:processReleaseManifest
> Task :app:processApplicationManifestReleaseForBundle
> Task :app:processReleaseManifestForPackage
> Task :capacitor-android:generateReleaseRFile
> Task :app:extractReleaseVersionControlInfo
> Task :capacitor-android:generateReleaseBuildConfig
> Task :capacitor-cordova-android-plugins:generateReleaseRFile
> Task :capacitor-android:javaPreCompileRelease
> Task :capacitor-splash-screen:parseReleaseLocalResources
> Task :capacitor-keyboard:parseReleaseLocalResources
> Task :capacitor-splash-screen:compileReleaseLibraryResources
> Task :app:mergeReleaseResources
> Task :capacitor-android:compileReleaseJavaWithJavac
Note: /Users/hansoo./Dev/zellyy-finance/node_modules/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Note: Some input files use unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
> Task :capacitor-keyboard:generateReleaseRFile
> Task :capacitor-keyboard:generateReleaseBuildConfig
> Task :capacitor-keyboard:javaPreCompileRelease
> Task :capacitor-splash-screen:generateReleaseRFile
> Task :capacitor-android:bundleLibCompileToJarRelease
> Task :capacitor-android:bundleLibRuntimeToJarRelease
> Task :capacitor-keyboard:compileReleaseJavaWithJavac
> Task :capacitor-keyboard:bundleLibRuntimeToJarRelease
> Task :capacitor-splash-screen:generateReleaseBuildConfig
> Task :capacitor-splash-screen:javaPreCompileRelease
> Task :capacitor-cordova-android-plugins:generateReleaseBuildConfig
> Task :app:processReleaseResources
> Task :capacitor-splash-screen:compileReleaseJavaWithJavac
> Task :capacitor-splash-screen:bundleLibRuntimeToJarRelease
> Task :capacitor-cordova-android-plugins:javaPreCompileRelease
> Task :app:checkReleaseDuplicateClasses
> Task :capacitor-cordova-android-plugins:compileReleaseJavaWithJavac
> Task :capacitor-cordova-android-plugins:bundleLibRuntimeToJarRelease
> Task :app:generateReleaseBuildConfig
> Task :app:javaPreCompileRelease
> Task :capacitor-cordova-android-plugins:bundleLibCompileToJarRelease
> Task :capacitor-keyboard:bundleLibCompileToJarRelease
> Task :capacitor-splash-screen:bundleLibCompileToJarRelease
> Task :app:desugarReleaseFileDependencies
> Task :app:bundleReleaseResources
> Task :app:compileReleaseJavaWithJavac
> Task :app:dexBuilderRelease
> Task :app:mergeReleaseStartupProfile
> Task :app:mergeReleaseShaders
> Task :app:compileReleaseShaders NO-SOURCE
> Task :app:generateReleaseAssets UP-TO-DATE
> Task :capacitor-android:mergeReleaseShaders
> Task :capacitor-android:compileReleaseShaders NO-SOURCE
> Task :capacitor-android:generateReleaseAssets UP-TO-DATE
> Task :capacitor-android:mergeReleaseAssets
> Task :capacitor-cordova-android-plugins:mergeReleaseShaders
> Task :capacitor-cordova-android-plugins:compileReleaseShaders NO-SOURCE
> Task :capacitor-cordova-android-plugins:generateReleaseAssets UP-TO-DATE
> Task :capacitor-cordova-android-plugins:mergeReleaseAssets
> Task :capacitor-keyboard:mergeReleaseShaders
> Task :capacitor-keyboard:compileReleaseShaders NO-SOURCE
> Task :capacitor-keyboard:generateReleaseAssets UP-TO-DATE
> Task :capacitor-keyboard:mergeReleaseAssets
> Task :capacitor-splash-screen:mergeReleaseShaders
> Task :capacitor-splash-screen:compileReleaseShaders NO-SOURCE
> Task :capacitor-splash-screen:generateReleaseAssets UP-TO-DATE
> Task :capacitor-splash-screen:mergeReleaseAssets
> Task :app:mergeReleaseAssets
> Task :app:processReleaseJavaRes NO-SOURCE
> Task :capacitor-android:processReleaseJavaRes NO-SOURCE
> Task :capacitor-cordova-android-plugins:processReleaseJavaRes NO-SOURCE
> Task :capacitor-keyboard:processReleaseJavaRes NO-SOURCE
> Task :capacitor-splash-screen:processReleaseJavaRes NO-SOURCE
> Task :app:mergeReleaseJniLibFolders
> Task :capacitor-android:mergeReleaseJniLibFolders
> Task :capacitor-android:mergeReleaseNativeLibs NO-SOURCE
> Task :capacitor-android:copyReleaseJniLibsProjectOnly
> Task :capacitor-cordova-android-plugins:mergeReleaseJniLibFolders
> Task :capacitor-cordova-android-plugins:mergeReleaseNativeLibs NO-SOURCE
> Task :capacitor-cordova-android-plugins:copyReleaseJniLibsProjectOnly
> Task :capacitor-keyboard:mergeReleaseJniLibFolders
> Task :capacitor-keyboard:mergeReleaseNativeLibs NO-SOURCE
> Task :capacitor-keyboard:copyReleaseJniLibsProjectOnly
> Task :capacitor-splash-screen:mergeReleaseJniLibFolders
> Task :capacitor-splash-screen:mergeReleaseNativeLibs NO-SOURCE
> Task :capacitor-splash-screen:copyReleaseJniLibsProjectOnly
> Task :app:writeReleaseAppMetadata
> Task :app:mergeReleaseNativeLibs NO-SOURCE
> Task :app:stripReleaseDebugSymbols NO-SOURCE
> Task :capacitor-android:prepareReleaseArtProfile
> Task :capacitor-cordova-android-plugins:prepareReleaseArtProfile
> Task :capacitor-keyboard:prepareReleaseArtProfile
> Task :capacitor-splash-screen:prepareReleaseArtProfile
> Task :app:mergeReleaseArtProfile
> Task :app:collectReleaseDependencies
> Task :app:configureReleaseDependencies
> Task :app:extractReleaseNativeSymbolTables NO-SOURCE
> Task :app:extractProguardFiles
> Task :capacitor-android:createFullJarRelease
> Task :capacitor-android:extractProguardFiles
> Task :app:mergeReleaseJavaResource
> Task :capacitor-android:generateReleaseLintModel
> Task :capacitor-android:prepareLintJarForPublish
> Task :capacitor-cordova-android-plugins:createFullJarRelease
> Task :capacitor-cordova-android-plugins:extractProguardFiles
> Task :capacitor-cordova-android-plugins:generateReleaseLintModel
> Task :capacitor-cordova-android-plugins:prepareLintJarForPublish
> Task :capacitor-keyboard:createFullJarRelease
> Task :capacitor-keyboard:extractProguardFiles
> Task :app:mergeReleaseGlobalSynthetics
> Task :capacitor-keyboard:generateReleaseLintModel
> Task :capacitor-keyboard:prepareLintJarForPublish
> Task :capacitor-splash-screen:createFullJarRelease
> Task :capacitor-splash-screen:extractProguardFiles
> Task :capacitor-splash-screen:generateReleaseLintModel
> Task :capacitor-splash-screen:prepareLintJarForPublish
> Task :app:generateReleaseLintVitalReportModel
> Task :capacitor-keyboard:stripReleaseDebugSymbols NO-SOURCE
> Task :capacitor-keyboard:copyReleaseJniLibsProjectAndLocalJars
> Task :capacitor-keyboard:extractDeepLinksForAarRelease
> Task :capacitor-keyboard:extractReleaseAnnotations
> Task :capacitor-keyboard:mergeReleaseGeneratedProguardFiles
> Task :capacitor-keyboard:mergeReleaseConsumerProguardFiles
> Task :capacitor-splash-screen:stripReleaseDebugSymbols NO-SOURCE
> Task :capacitor-keyboard:mergeReleaseJavaResource
> Task :capacitor-splash-screen:copyReleaseJniLibsProjectAndLocalJars
> Task :capacitor-keyboard:syncReleaseLibJars
> Task :capacitor-keyboard:bundleReleaseLocalLintAar
> Task :capacitor-splash-screen:extractDeepLinksForAarRelease
> Task :capacitor-splash-screen:extractReleaseAnnotations
> Task :capacitor-splash-screen:mergeReleaseGeneratedProguardFiles
> Task :capacitor-splash-screen:mergeReleaseConsumerProguardFiles
> Task :capacitor-android:stripReleaseDebugSymbols NO-SOURCE
> Task :capacitor-splash-screen:mergeReleaseJavaResource
> Task :capacitor-android:copyReleaseJniLibsProjectAndLocalJars
> Task :capacitor-splash-screen:syncReleaseLibJars
> Task :capacitor-splash-screen:bundleReleaseLocalLintAar
> Task :capacitor-android:extractDeepLinksForAarRelease
> Task :capacitor-android:extractReleaseAnnotations
> Task :capacitor-android:mergeReleaseGeneratedProguardFiles
> Task :capacitor-android:mergeReleaseConsumerProguardFiles
> Task :capacitor-cordova-android-plugins:stripReleaseDebugSymbols NO-SOURCE
> Task :capacitor-android:mergeReleaseJavaResource
> Task :capacitor-cordova-android-plugins:copyReleaseJniLibsProjectAndLocalJars
> Task :capacitor-android:syncReleaseLibJars
> Task :capacitor-android:bundleReleaseLocalLintAar
> Task :capacitor-cordova-android-plugins:extractDeepLinksForAarRelease
> Task :capacitor-cordova-android-plugins:extractReleaseAnnotations
> Task :capacitor-cordova-android-plugins:mergeReleaseGeneratedProguardFiles
> Task :capacitor-cordova-android-plugins:mergeReleaseConsumerProguardFiles
> Task :capacitor-cordova-android-plugins:mergeReleaseJavaResource
> Task :capacitor-cordova-android-plugins:syncReleaseLibJars
> Task :capacitor-cordova-android-plugins:bundleReleaseLocalLintAar
> Task :capacitor-android:writeReleaseLintModelMetadata
> Task :capacitor-cordova-android-plugins:writeReleaseLintModelMetadata
> Task :capacitor-keyboard:writeReleaseLintModelMetadata
> Task :capacitor-splash-screen:writeReleaseLintModelMetadata
> Task :capacitor-android:generateReleaseLintVitalModel
> Task :capacitor-cordova-android-plugins:generateReleaseLintVitalModel
> Task :capacitor-keyboard:generateReleaseLintVitalModel
> Task :capacitor-splash-screen:generateReleaseLintVitalModel
> Task :app:parseReleaseIntegrityConfig
> Task :app:validateSigningRelease
> Task :capacitor-keyboard:lintVitalAnalyzeRelease
> Task :capacitor-splash-screen:lintVitalAnalyzeRelease
> Task :app:mergeExtDexRelease
> Task :capacitor-android:lintVitalAnalyzeRelease
> Task :app:mergeDexRelease
> Task :app:buildReleasePreBundle
> Task :app:compileReleaseArtProfile
> Task :capacitor-cordova-android-plugins:lintVitalAnalyzeRelease
> Task :app:lintVitalAnalyzeRelease
> Task :app:lintVitalReportRelease
> Task :app:lintVitalRelease
> Task :app:packageReleaseBundle
> Task :app:signReleaseBundle
> Task :app:produceReleaseBundleIdeListingFile
> Task :app:createReleaseBundleListingFileRedirect
> Task :app:bundleRelease
[Incubating] Problems report is available at: file:///Users/hansoo./Dev/zellyy-finance/android/build/reports/problems/problems-report.html
BUILD SUCCESSFUL in 11s
181 actionable tasks: 181 executed
릴리즈 AAB 빌드 완료
AAB 파일 생성 완료: app/build/outputs/bundle/release/app-release.aab
AAB 파일 크기: 3.8M
AAB 파일이 릴리즈 디렉토리에 복사되었습니다: release/zellyy_release_v1.1.8_20250405_194612.aab

View File

@@ -1 +0,0 @@
===== 오류 로그: Sat Apr 5 19:45:45 KST 2025 =====

View File

View File

View File

@@ -0,0 +1 @@
v2.22.6

View File

@@ -12,9 +12,11 @@ YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# 로그 파일 설정
LOG_FILE="app_build.log"
ERROR_LOG_FILE="app_error.log"
# 로그 파일 설정 (log 폴더로 이동)
LOG_DIR="log"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/app_build.log"
ERROR_LOG_FILE="$LOG_DIR/app_error.log"
# 타임아웃 설정 (초 단위)
BUILD_TIMEOUT=600 # 10분

View File

@@ -18,6 +18,15 @@ if [ -f "$CONFIG_FILE" ]; then
source "$CONFIG_FILE"
fi
# 로그 폴더 설정
LOG_DIR="log"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/app_build.log"
ERROR_LOG_FILE="$LOG_DIR/app_error.log"
# 모든 출력을 로그로 리다이렉트
exec > >(tee -a "$LOG_FILE") 2> >(tee -a "$ERROR_LOG_FILE" >&2)
# 캐시 삭제 함수
clean_cache() {
echo -e "${YELLOW}캐시 삭제 중...${NC}"

View File

@@ -0,0 +1,86 @@
# 기술 스택
Zellyy Finance 프로젝트 개발에 사용된 전체 기술 스택을 정리합니다.
## Frontend
- 언어: TypeScript, JavaScript
- 프레임워크: React 18.x
- 번들러/빌드 도구: Vite
- UI 라이브러리: Shadcn UI, Radix UI
- 스타일링: Tailwind CSS
- 라우팅: React Router DOM
- 상태 관리: React Context, @tanstack/react-query
- 폼 관리: React Hook Form (@hookform/resolvers)
- 알림: Radix UI Toast, 사용자 정의 토스트 훅
## Backend
- Backend-as-a-Service: Appwrite 17.x
- 인증/인가: Appwrite Auth
- 데이터베이스: Appwrite Databases (컬렉션)
- 스토리지: Appwrite Storage
- API: Appwrite SDK (RESTful)
## Mobile (Cross-platform)
- 플랫폼: Capacitor
- 패키지: @capacitor/core, @capacitor/cli, @capacitor/android, @capacitor/ios
- 플러그인: Keyboard, Splash Screen 등
## Utilities & Tools
- 코드 스타일 및 검사: ESLint
- HTTP 클라이언트: Appwrite SDK, fetch
- 데이터 페칭: @tanstack/react-query
- UUID 생성: uuid (@types/uuid)
## 배포 및 운영
- 개발 서버: Vite dev server
- 패키지 매니저: npm
## 개발 환경 및 도구
- IDE: Visual Studio Code
- 버전 관리: Git (GitHub)
## 테스트 및 품질 관리
- Unit/Integration 테스트: Jest + React Testing Library 또는 Vitest
- E2E 테스트: Cypress 또는 Playwright
## 코드 품질 및 포맷팅
- Prettier
- Husky + lint-staged + commitlint
- TypeScript strict 모드
## CI/CD
- GitHub Actions 또는 Gitea Actions: lint → test → build → deploy 자동화
- Drone CI (Gitea 연동): CI/CD 파이프라인 자동화
- 커버리지 리포트 업로드
## 문서화
- Storybook (UI 컴포넌트 카탈로그)
- OpenAPI/Swagger (API 문서 자동 생성)
## 보안
- Dependabot, Snyk (dependency 스캔)
- 환경변수 관리: dotenv, Vault
## 성능 및 PWA
- Lighthouse 성능 측정
- Service Worker (오프라인, 푸시 지원)
## 국제화 (i18n)
- react-i18next 등
## 컨테이너화
- Dockerfile, docker-compose (개발/테스트 환경)

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_프로젝트_개요
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
- `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) 문서를 참조하세요.

116
docs/WEB_SERVER_SETUP.md Normal file
View File

@@ -0,0 +1,116 @@
# 웹 서버 설치 & 배포 가이드
## 1. Next.js 앱 단일 배포 (Ubuntu 22.04 + Nginx)
### 1.1 서버 준비
```bash
ssh your_user@your_server_ip
# Node.js 18.x 설치
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs build-essential
```
### 1.2 코드 클론
```bash
cd /var/www
git clone https://github.com/your-org/your-repo.git my-nextjs
cd my-nextjs
```
### 1.3 의존성 설치 & 빌드
```bash
# npm
npm install
npm run build
# 또는 yarn
# yarn
yarn build
```
### 1.4 PM2로 서비스 등록
```bash
sudo npm install -g pm2
pm2 start npm --name "my-nextjs" -- start
pm2 save
pm2 startup # 출력된 명령 복사 후 실행
```
### 1.5 Nginx 리버스 프록시 설정
```nginx
# /etc/nginx/sites-available/my-nextjs.conf
server {
listen 80;
server_name your.domain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
```bash
sudo ln -s /etc/nginx/sites-available/my-nextjs.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
## 2. 한 서버에 여러 사이트 호스팅
### 2.1 앱별 포트 분리
- 디렉터리별 배치: `/var/www/site-alpha`, `/var/www/site-beta`
- `package.json` start 스크립트에 포트 지정
```jsonc
"scripts": {
"start": "next start -p 3000"
}
```
### 2.2 PM2로 프로세스 분리
```bash
# site-alpha
cd /var/www/site-alpha
npm install
npm run build
pm2 start npm --name site-alpha -- start
# site-beta
cd /var/www/site-beta
npm install
npm run build
pm2 start npm --name site-beta -- start
pm2 save
pm2 startup
```
### 2.3 Nginx 도메인별 서버블록 설정
```nginx
# /etc/nginx/sites-available/site-alpha.conf
server {
listen 80;
server_name alpha.example.com;
location / { proxy_pass http://127.0.0.1:3000; }
}
# /etc/nginx/sites-available/site-beta.conf
server {
listen 80;
server_name beta.example.com;
location / { proxy_pass http://127.0.0.1:3001; }
}
```
```bash
sudo ln -s /etc/nginx/sites-available/{site-alpha.conf,site-beta.conf} /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
### 2.4 (선택) SSL 자동 발급
- Let's Encrypt + Certbot 설치
```bash
sudo apt-get install certbot python3-certbot-nginx
sudo certbot --nginx -d alpha.example.com -d beta.example.com
```

View File

@@ -0,0 +1,80 @@
# Supabase Cloud → On-Premise Migration Plan
이 문서에는 기존 Supabase Cloud 프로젝트를 On-Premise Self-Host 환경으로 이전하기 위한 단계별 절차를 정리합니다.
## 1. 개요
- 목적: 클라우드 종속성을 제거하고 자체 운영 가능하도록 Supabase 스택을 온프레미스 환경에 배포
- 범위: 인증(Gotrue), 실시간(Realtime), 스토리지(Storage), 데이터베이스(Postgres)
## 2. 사전 준비
1. 현재 환경 문서화
- 프로젝트 구조, 스키마, RLS 규칙, 함수, 트리거 등
- 사용 중인 Supabase CLI/SDK 버전 및 환경 변수 목록
2. 인프라 준비
- Docker 엔진 또는 Kubernetes 클러스터
- 도메인, TLS 인증서 준비
- 최소 요구 사양 확인 (CPU, 메모리, 디스크)
3. 백업 계획 수립
- Cloud DB Dump 스케줄링 방법
- 백업 보관 위치 및 암호화 방침
## 3. On-Premise Supabase Self-Host 배포
1. Supabase CLI 설치
```bash
npm install -g supabase
```
2. 초기화 및 컨테이너 실행
```bash
supabase init # 프로젝트 디렉토리에 구성 파일 작성
supabase start # Postgres, Kong, Realtime, GoTrue, Storage 컨테이너 실행
```
3. 버전 호환성 확인
- `supabase version`으로 CLI, API, DB 버전 일치 여부 점검
## 4. 데이터 마이그레이션
1. Cloud DB Dump
```bash
supabase db dump --project-ref <PROJECT_REF> --file cloud_dump.sql
```
2. On-Premise DB Restore
```bash
supabase db restore --file cloud_dump.sql
```
3. 데이터 무결성 검증
- 주요 테이블 row 수 체크
- RLS 규칙·함수 정상 동작 여부 테스트
## 5. 애플리케이션 구성 변경
1. 환경 변수 업데이트
```env
SUPABASE_URL=http://<ONPREM_HOST>:8000
SUPABASE_ANON_KEY=<새_ANON_KEY>
SUPABASE_SERVICE_KEY=<새_SERVICE_KEY>
```
2. SDK 초기화 점검
- `client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)` 정상 연결 확인
## 6. 테스트 및 검증
1. 로컬 개발서버 테스트
- 프론트·백엔드 실행 후 CRUD 기능 확인
2. 스테이징 환경 배포
- Blue-Green 또는 Canary 배포로 트래픽 분할
- 모니터링(로그, 메트릭) 이상 유무 확인
## 7. 프로덕션 전환
1. DNS 레코드 변경 또는 로드밸런서 설정
2. SSL/TLS 인증서 적용
3. 전환 후 롤백 플랜 준비
## 8. 롤백 계획
- 문제가 발생 시 이전 Cloud 인스턴스로 트래픽 리디렉션
- 신규 데이터 백업 및 차분 마이그레이션
## 9. 모니터링 및 운영
- 백그라운드 컨테이너 상태 감시 (Prometheus, Grafana)
- 정기 백업·복원 테스트 자동화
- 보안 패치 및 버전 업데이트 정책 수립
---
*문서 위치: `docs/SUPABASE_ONPREM_MIGRATION_PLAN.md`*

View File

@@ -12,8 +12,6 @@
<body>
<div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -24,10 +24,10 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/splash-screen"
SPEC CHECKSUMS:
Capacitor: 68ff8eabbcce387e69767c13b5fbcc1c5399eabc
Capacitor: bceb785fb78f5e81e4a9e37843bc1c24bd9c7194
CapacitorCordova: 866217f32c1d25b326c568a10ea3ed0c36b13e29
CapacitorKeyboard: 2c26c6fccde35023c579fc37d4cae6326d5e6343
CapacitorSplashScreen: f4e58cc02aafd91c7cbaf32a3d1b44d02a115125
CapacitorKeyboard: 4db71e694e7afb5d7c0be09b05495c19f7d6c914
CapacitorSplashScreen: 7e7a0a1113833032f196b3af6fa437baccacf5bc
PODFILE CHECKSUM: 7376e84e32edf2d1753401ce95b6db45439d33ff

152
package-lock.json generated
View File

@@ -42,14 +42,16 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@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",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",
@@ -92,7 +94,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -138,9 +139,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.9.tgz",
"integrity": "sha512-4zpTHZ9Cm6L9L+uIqghQX8ZXg8HKFcjYO3qHoO8zTmRm6HQUJ8SSJ+KRvbMBZn0EGVlT4DRYeQ/6hjlyXBh+Kg==",
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
@@ -1136,7 +1137,6 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
@@ -1151,7 +1151,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1161,7 +1160,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1171,14 +1169,12 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1189,7 +1185,6 @@
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
"integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "2.0.5",
@@ -1203,7 +1198,6 @@
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
"integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -1213,7 +1207,6 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
"integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.scandir": "2.1.5",
@@ -1227,7 +1220,6 @@
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@@ -3001,9 +2993,9 @@
]
},
"node_modules/@supabase/auth-js": {
"version": "2.68.0",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz",
"integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==",
"version": "2.69.1",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.69.1.tgz",
"integrity": "sha512-FILtt5WjCNzmReeRLq5wRs3iShwmnWgBvxHfqapC/VoljJl+W8hDAyFmf1NVw3zH+ZjZ05AKxiKxVeb0HNWRMQ==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
@@ -3031,9 +3023,9 @@
}
},
"node_modules/@supabase/postgrest-js": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz",
"integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==",
"version": "1.19.4",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.4.tgz",
"integrity": "sha512-O4soKqKtZIW3olqmbXXbKugUtByD2jPa8kL2m2c1oozAO11uCcGrRhkZL0kVxjBLrXHE0mdSkFsMj7jDSfyNpw==",
"license": "MIT",
"dependencies": {
"@supabase/node-fetch": "^2.6.14"
@@ -3061,15 +3053,15 @@
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.49.1",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz",
"integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==",
"version": "2.49.4",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.4.tgz",
"integrity": "sha512-jUF0uRUmS8BKt37t01qaZ88H9yV1mbGYnqLeuFWLcdV+x1P4fl0yP9DGtaEhFPZcwSom7u16GkLEH9QJZOqOkw==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.68.0",
"@supabase/auth-js": "2.69.1",
"@supabase/functions-js": "2.4.4",
"@supabase/node-fetch": "2.6.15",
"@supabase/postgrest-js": "1.19.2",
"@supabase/postgrest-js": "1.19.4",
"@supabase/realtime-js": "2.11.2",
"@supabase/storage-js": "2.7.1"
}
@@ -3459,14 +3451,14 @@
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -3477,7 +3469,7 @@
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"dev": true,
"devOptional": true,
"license": "MIT",
"dependencies": {
"@types/react": "*"
@@ -3826,14 +3818,12 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
"dev": true,
"license": "MIT"
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
@@ -3843,11 +3833,16 @@
"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",
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"dev": true,
"license": "MIT"
},
"node_modules/argparse": {
@@ -3964,7 +3959,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4000,7 +3994,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
@@ -4064,7 +4057,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -4111,7 +4103,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
@@ -4136,7 +4127,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -4574,7 +4564,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -4604,7 +4593,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
@@ -4741,9 +4729,9 @@
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
@@ -4799,14 +4787,12 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
"integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-helpers": {
@@ -4819,6 +4805,18 @@
"csstype": "^3.0.2"
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -5161,7 +5159,6 @@
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"@nodelib/fs.stat": "^2.0.2",
@@ -5178,7 +5175,6 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
@@ -5205,7 +5201,6 @@
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
"integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
"dev": true,
"license": "ISC",
"dependencies": {
"reusify": "^1.0.4"
@@ -5237,7 +5232,6 @@
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"license": "MIT",
"dependencies": {
"to-regex-range": "^5.0.1"
@@ -5356,7 +5350,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -5371,7 +5364,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -5390,7 +5382,6 @@
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -5411,7 +5402,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
@@ -5424,7 +5414,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -5434,7 +5423,6 @@
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -5486,7 +5474,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -5579,7 +5566,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
@@ -5592,7 +5578,6 @@
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
@@ -5623,7 +5608,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5642,7 +5626,6 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
@@ -5655,7 +5638,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.12.0"
@@ -5683,7 +5665,6 @@
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
"integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
@@ -5699,7 +5680,6 @@
"version": "1.21.6",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
"dev": true,
"license": "MIT",
"bin": {
"jiti": "bin/jiti.js"
@@ -5794,7 +5774,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
@@ -5807,7 +5786,6 @@
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -6317,7 +6295,6 @@
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"dev": true,
"license": "ISC"
},
"node_modules/lucide-react": {
@@ -6342,7 +6319,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 8"
@@ -6352,7 +6328,6 @@
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
@@ -6431,7 +6406,6 @@
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
"integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0",
@@ -6440,10 +6414,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -6510,7 +6483,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6539,7 +6511,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -6654,14 +6625,12 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
"integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"lru-cache": "^10.2.0",
@@ -6690,7 +6659,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
@@ -6703,7 +6671,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -6713,7 +6680,6 @@
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz",
"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 6"
@@ -6737,7 +6703,6 @@
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6766,7 +6731,6 @@
"version": "15.1.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
"integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-value-parser": "^4.0.0",
@@ -6784,7 +6748,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
"integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
"dev": true,
"license": "MIT",
"dependencies": {
"camelcase-css": "^2.0.1"
@@ -6804,7 +6767,6 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
"integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6840,7 +6802,6 @@
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
"integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -6866,7 +6827,6 @@
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
@@ -6880,7 +6840,6 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prelude-ls": {
@@ -6946,7 +6905,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
"dev": true,
"funding": [
{
"type": "github",
@@ -7171,7 +7129,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
"integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"pify": "^2.3.0"
@@ -7195,7 +7152,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
@@ -7246,7 +7202,6 @@
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
@@ -7274,7 +7229,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
"dev": true,
"license": "MIT",
"engines": {
"iojs": ">=1.0.0",
@@ -7427,7 +7381,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
"integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
"dev": true,
"funding": [
{
"type": "github",
@@ -7564,7 +7517,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -7701,7 +7653,6 @@
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
"integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.2",
@@ -7737,7 +7688,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -7760,7 +7710,6 @@
"version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -7840,7 +7789,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
@@ -7850,7 +7798,6 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
@@ -7878,7 +7825,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-number": "^7.0.0"
@@ -7919,7 +7865,6 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/tslib": {
@@ -8141,9 +8086,9 @@
}
},
"node_modules/vite": {
"version": "5.4.10",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
"integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
"version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
"integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8391,7 +8336,6 @@
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"

View File

@@ -45,14 +45,16 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@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",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",

View File

@@ -1,5 +1,4 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState, Suspense, Component, ErrorInfo, ReactNode } from 'react';
import { Routes, Route } from 'react-router-dom';
import { BudgetProvider } from './contexts/budget/BudgetContext';
import { AuthProvider } from './contexts/auth/AuthProvider';
@@ -17,35 +16,152 @@ 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';
// 간단한 오류 경계 컴포넌트 구현
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error('애플리케이션 오류:', error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
// 오류 발생 시 대체 UI 표시
return this.props.fallback || (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="mb-4"> .</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
}
return this.props.children;
}
}
// 로딩 상태 표시 컴포넌트
const LoadingScreen: React.FC = () => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<h2 className="text-xl font-bold mb-2">Zellyy Finance</h2>
<p className="text-gray-600"> ...</p>
</div>
);
// 오류 화면 컴포넌트
const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ error, retry }) => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="text-red-500 text-5xl mb-4"></div>
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="text-center mb-6">{error?.message || '애플리케이션 로딩 중 오류가 발생했습니다.'}</p>
<button
onClick={retry}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
// 기본 레이아웃 컴포넌트 - 인증 없이도 표시 가능
const BasicLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="App">
{children}
<Toaster />
</div>
);
function App() {
useEffect(() => {
document.title = "적자 탈출 가계부";
}, []);
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
const [error, setError] = useState<Error | null>(null);
const [appwriteEnabled, setAppwriteEnabled] = useState(true);
useEffect(() => {
document.title = "Zellyy Finance";
// 애플리케이션 초기화 시간 지연 설정
const timer = setTimeout(() => {
setAppState('ready');
}, 1500); // 1.5초 후 로딩 상태 해제
return () => clearTimeout(timer);
}, []);
// 재시도 기능
const handleRetry = () => {
setAppState('loading');
setError(null);
// 재시도 시 지연 후 상태 변경
setTimeout(() => {
setAppState('ready');
}, 1500);
};
// 로딩 상태 표시
if (appState === 'loading') {
return (
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
<LoadingScreen />
</ErrorBoundary>
);
}
// 오류 상태 표시
if (appState === 'error') {
return <ErrorScreen error={error} retry={handleRetry} />;
}
return (
<AuthProvider>
<BudgetProvider>
<div className="App">
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/settings" element={<Settings />} />
<Route path="/transactions" element={<Transactions />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/profile" element={<ProfileManagement />} />
<Route path="/payment-methods" element={<PaymentMethods />} />
<Route path="/help-support" element={<HelpSupport />} />
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
<Route path="/notifications" element={<NotificationSettings />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />
</div>
</BudgetProvider>
</AuthProvider>
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
<AuthProvider>
<BudgetProvider>
<BasicLayout>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/settings" element={<Settings />} />
<Route path="/transactions" element={<Transactions />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/profile" element={<ProfileManagement />} />
<Route path="/payment-methods" element={<PaymentMethods />} />
<Route path="/help-support" element={<HelpSupport />} />
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
<Route path="/notifications" element={<NotificationSettings />} />
<Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</BasicLayout>
</BudgetProvider>
</AuthProvider>
</ErrorBoundary>
);
}

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

@@ -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,10 +1,18 @@
// This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
const SUPABASE_URL = "https://qnerebtvwwfobfzdoftx.supabase.co";
const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8";
const SUPABASE_URL = (() => {
const url = import.meta.env.VITE_SUPABASE_URL;
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
return url;
})();
const SUPABASE_PUBLISHABLE_KEY = (() => {
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
return key;
})();
// Import the supabase client like this:
// import { supabase } from "@/integrations/supabase/client";

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

View File

@@ -1,16 +1,19 @@
// Supabase Cloud URL과 anon key 설정
export const getSupabaseUrl = () => {
return "https://qnerebtvwwfobfzdoftx.supabase.co";
const url = import.meta.env.VITE_SUPABASE_URL;
if (!url) throw new Error("VITE_SUPABASE_URL is not set");
return url;
};
export const getSupabaseKey = () => {
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8";
const key = import.meta.env.VITE_SUPABASE_ANON_KEY;
if (!key) throw new Error("VITE_SUPABASE_ANON_KEY is not set");
return key;
};
// Supabase 키 유효성 검사 - Cloud 환경에서는 항상 유효함
export const isValidSupabaseKey = () => {
return true;
return Boolean(import.meta.env.VITE_SUPABASE_ANON_KEY);
};
// 다음 함수들은 Cloud 환경에서는 필요 없지만 호환성을 위해 유지

View File

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

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 EmailConfirmation from "./EmailConfirmation";
import RegisterFormFields from "./RegisterFormFields";
import { supabase } from "@/lib/supabase";
import { supabase } from "@/archive/lib/supabase";
interface RegisterFormProps {
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>;

View File

@@ -1,17 +1,59 @@
import React, { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabase';
import { Session, User } from '@supabase/supabase-js';
import React, { useEffect, useState, useCallback } from 'react';
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, getInitializationStatus, reinitializeAppwriteClient, isValidConnection } from '@/lib/appwrite/client';
import { Models } from 'appwrite';
import { getDefaultUserId } from '@/lib/appwrite/defaultUser';
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [session, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Models.Session | null>(null);
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [appwriteInitialized, setAppwriteInitialized] = useState<boolean>(false);
// 오류 발생 시 안전하게 처리하는 함수
const handleAuthError = useCallback((err: any) => {
console.error('인증 처리 중 오류 발생:', err);
setError(err instanceof Error ? err : new Error(String(err)));
// 오류가 발생해도 로딩 상태는 해제하여 UI가 차단되지 않도록 함
setLoading(false);
}, []);
// Appwrite 초기화 상태 확인
const checkAppwriteInitialization = useCallback(async () => {
try {
const status = getInitializationStatus();
console.log('Appwrite 초기화 상태:', status.isInitialized ? '성공' : '실패');
if (!status.isInitialized) {
// 초기화 실패 시 재시도
console.log('Appwrite 초기화 재시도 중...');
const retryStatus = reinitializeAppwriteClient();
setAppwriteInitialized(retryStatus.isInitialized);
if (!retryStatus.isInitialized && retryStatus.error) {
handleAuthError(retryStatus.error);
}
} else {
setAppwriteInitialized(true);
}
// 연결 상태 확인
const connectionValid = await isValidConnection();
console.log('Appwrite 연결 상태:', connectionValid ? '정상' : '연결 문제');
return status.isInitialized;
} catch (error) {
console.error('Appwrite 초기화 상태 확인 오류:', error);
handleAuthError(error);
return false;
}
}, [handleAuthError]);
useEffect(() => {
// 현재 세션 체크 - 최적화된 버전
@@ -22,22 +64,67 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
const { data, error } = await supabase.auth.getSession();
if (error) {
console.error('세션 로딩 중 오류:', error);
} else if (data.session) {
// 상태 업데이트를 마이크로태스크로 지연
// Appwrite 초기화 상태 확인
const isInitialized = await checkAppwriteInitialization();
if (!isInitialized) {
console.warn('Appwrite 초기화 상태가 정상적이지 않습니다. 비로그인 상태로 처리합니다.');
queueMicrotask(() => {
setSession(data.session);
setUser(data.session.user);
console.log('세션 로딩 완료');
setSession(null);
setUser(null);
setLoading(false);
});
return;
}
// 사용자 정보 가져오기 시도 - 안전한 방식으로 처리
try {
// 사용자 정보 가져오기 시도
const currentUser = await account.get().catch(err => {
// 401 오류는 비로그인 상태로 정상적인 경우
if (err && (err as any).code === 401) {
console.log('사용자 정보 가져오기 실패, 비로그인 상태로 간주');
} else {
console.error('사용자 정보 가져오기 오류:', err);
}
return null;
});
if (currentUser) {
// 사용자 정보가 있으면 세션 정보 가져오기 시도
const currentSession = await account.getSession('current').catch(err => {
console.log('세션 정보 가져오기 실패:', err);
return null;
});
// 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setUser(currentUser);
setSession(currentSession);
console.log('세션 로딩 완료 - 사용자:', currentUser.$id);
});
} else {
// 사용자 정보가 없으면 비로그인 상태로 처리
queueMicrotask(() => {
setSession(null);
setUser(null);
console.log('비로그인 상태로 처리');
});
}
} catch (error) {
// 예상치 못한 오류 처리
console.error('세션 처리 중 예상치 못한 오류:', error);
handleAuthError(error);
// 오류 발생 시 로그아웃 상태로 처리
queueMicrotask(() => {
setSession(null);
setUser(null);
});
} else {
console.log('활성 세션 없음');
}
} catch (error) {
console.error('세션 확인 중 예외 발생:', error);
// 최상위 예외 처리
console.error('세션 확인 중 최상위 예외 발생:', error);
handleAuthError(error);
} finally {
// 로딩 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
@@ -46,27 +133,64 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
}
};
// 초기 세션 로딩 - 약간 지연시 UI 렌더링 우선시
// 초기 세션 로딩 - 약간 지연시 UI 렌더링 우선시
setTimeout(() => {
getSession();
}, 100);
// auth 상태 변경 리스너 - 최적화된 버전
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
console.log('Supabase auth 이벤트:', event);
// Appwrite 인증 상태 변경 리스너 설정
// 참고: Appwrite는 직접적인 이벤트 리스너를 제공하지 않으므로 주기적으로 확인하는 방식 사용
const authCheckInterval = setInterval(async () => {
// 오류가 발생해도 애플리케이션이 중단되지 않도록 try-catch로 감싸기
try {
// Appwrite 초기화 상태 확인
if (!appwriteInitialized) {
const isInitialized = await checkAppwriteInitialization();
if (!isInitialized) {
console.warn('Appwrite 초기화 상태가 여전히 정상적이지 않습니다. 다음 간격에서 재시도합니다.');
return;
}
}
// 사용자 정보 가져오기 시도 - 안전하게 처리
const currentUser = await account.get().catch(err => {
// 401 오류는 비로그인 상태로 정상적인 경우
if (err && (err as any).code === 401) {
console.log('사용자 정보 가져오기 실패, 비로그인 상태로 간주');
} else {
console.error('사용자 정보 가져오기 오류:', err);
}
return null;
});
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
if (session) {
// 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setSession(session);
setUser(session.user);
});
} else if (event === 'SIGNED_OUT') {
// 상태 업데이트를 마이크로태스크로 지연
// 사용자 정보가 있고, 이전과 다르면 세션 정보 가져오기
if (currentUser && (!user || currentUser.$id !== user.$id)) {
try {
// 세션 정보 가져오기 시도 - 안전하게 처리
const currentSession = await account.getSession('current').catch(err => {
console.log('세션 정보 가져오기 실패:', err);
return null;
});
// 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setUser(currentUser);
setSession(currentSession);
console.log('Appwrite 인증 상태 변경: 로그인됨 - 사용자:', currentUser.$id);
});
} catch (sessionError) {
console.error('세션 정보 가져오기 중 오류:', sessionError);
// 오류 발생해도 사용자 정보는 업데이트
queueMicrotask(() => {
setUser(currentUser);
setSession(null);
});
}
} else if (!currentUser && user) {
// 이전에는 사용자 정보가 있었지만 지금은 없는 경우 (로그아웃 상태)
queueMicrotask(() => {
setSession(null);
setUser(null);
@@ -76,20 +200,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
window.dispatchEvent(new Event('auth-state-changed'));
console.log('Appwrite 인증 상태 변경: 로그아웃됨');
});
}
// 로딩 상태 업데이트를 마이크로태스크로 지연
queueMicrotask(() => {
setLoading(false);
});
} catch (error) {
// 예상치 못한 오류 발생 시에도 애플리케이션이 중단되지 않도록 처리
console.error('Appwrite 인증 상태 검사 중 예상치 못한 오류:', error);
handleAuthError(error);
}
);
}, 5000); // 5초마다 확인
// 리스너 정리
return () => {
subscription.unsubscribe();
clearInterval(authCheckInterval);
};
}, [user]);
// Appwrite 재초기화 함수
const reinitializeAppwrite = useCallback(() => {
console.log('Appwrite 재초기화 요청됨');
return reinitializeAppwriteClient();
}, []);
// 인증 작업 메서드들
@@ -97,11 +227,20 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
session,
user,
loading,
error,
appwriteInitialized,
reinitializeAppwrite,
signIn: authActions.signIn,
signUp: authActions.signUp,
signOut: authActions.signOut,
resetPassword: authActions.resetPassword,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
// 로딩 중이 아니고 오류가 없을 때만 자식 컴포넌트 렌더링
// 오류가 있어도 애플리케이션이 중단되지 않도록 처리
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};

View File

@@ -1,25 +1,48 @@
import { supabase } from '@/lib/supabase';
import { handleNetworkError, showAuthToast } from '@/utils/auth';
import { account } from '@/lib/appwrite/client';
import { showAuthToast } from '@/utils/auth';
export const resetPassword = async (email: string) => {
try {
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: window.location.origin + '/reset-password',
});
if (error) {
console.error('비밀번호 재설정 오류:', error);
showAuthToast('비밀번호 재설정 실패', error.message, 'destructive');
return { error };
console.log('비밀번호 재설정 시도 중:', email);
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
try {
// Appwrite로 비밀번호 재설정 이메일 발송
await account.createRecovery(
email,
window.location.origin + '/reset-password'
);
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.');
return { error: null };
} catch (recoveryError: any) {
console.error('비밀번호 재설정 이메일 전송 오류:', recoveryError);
// 오류 메시지 처리
let errorMessage = recoveryError.message || '알 수 없는 오류가 발생했습니다.';
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (recoveryError.code === 404) {
errorMessage = '등록되지 않은 이메일입니다.';
} else if (recoveryError.code === 429) {
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
}
showAuthToast('비밀번호 재설정 실패', errorMessage, 'destructive');
return { error: recoveryError };
}
showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.');
return { error: null };
} catch (error: any) {
console.error('비밀번호 재설정 중 예외 발생:', error);
// 네트워크 오류 확인
const errorMessage = handleNetworkError(error);
const errorMessage = error.message && error.message.includes('network')
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
: '예상치 못한 오류가 발생했습니다.';
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
return { error };

View File

@@ -1,45 +1,78 @@
import { supabase } from '@/lib/supabase';
import { account } from '@/lib/appwrite/client';
import { showAuthToast } from '@/utils/auth';
import { getDefaultUserId } from '@/lib/appwrite/defaultUser';
/**
* 로그인 기능 - 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<void>(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.createSession(email, password);
const user = await account.get();
let errorMessage = error.message;
if (error.message.includes('Invalid login credentials')) {
// 상태 업데이트를 마이크로태스크로 지연
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
showAuthToast('로그인 성공', '환영합니다!');
return { error: null, user };
} catch (authError: any) {
console.error('로그인 오류:', authError);
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
let fallbackMode = false;
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (authError.code === 401) {
errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
} else if (error.message.includes('Email not confirmed')) {
errorMessage = '이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.';
} else if (authError.code === 429) {
errorMessage = '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.';
} else if (authError.code === 404 || authError.code === 503) {
// 서버 연결 문제인 경우 기본 사용자 ID를 활용한 대체 로직 시도
errorMessage = '서버 연결에 문제가 있어 일반 모드로 접속합니다.';
fallbackMode = true;
try {
// 기본 사용자 ID를 활용한 대체 로직
const defaultUserId = getDefaultUserId();
console.log('기본 사용자 ID를 활용한 대체 로직 시도:', defaultUserId);
// 일반 모드로 접속하는 경우 사용자에게 알림
showAuthToast('일반 모드 접속', '일반 모드로 접속합니다. 일부 기능이 제한될 수 있습니다.', 'default');
// 기본 사용자 정보를 가진 가상의 사용자 객체 생성
const fallbackUser = {
$id: defaultUserId,
name: '일반 사용자',
email: email,
$createdAt: new Date().toISOString(),
$updatedAt: new Date().toISOString(),
status: true,
isFallbackUser: true // 기본 사용자임을 표시하는 플래그
};
return { error: null, user: fallbackUser, isFallbackMode: true };
} catch (fallbackError) {
console.error('기본 사용자 대체 로직 오류:', fallbackError);
// 대체 로직도 실패한 경우 원래 오류 반환
}
}
showAuthToast('로그인 실패', errorMessage, 'destructive');
return { error: { message: errorMessage }, user: null };
if (!fallbackMode) {
showAuthToast('로그인 실패', errorMessage, 'destructive');
}
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 };
}
};

View File

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

View File

@@ -1,21 +1,46 @@
import { supabase } from '@/lib/supabase';
import { account } from '@/lib/appwrite/client';
import { showAuthToast } from '@/utils/auth';
import { clearAllToasts } from '@/hooks/toast/toastManager';
export const signOut = async (): Promise<void> => {
try {
const { error } = await supabase.auth.signOut();
console.log('로그아웃 시도 중');
if (error) {
console.error('로그아웃 오류:', error);
showAuthToast('로그아웃 실패', error.message, 'destructive');
} else {
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
try {
// 현재 세션 아이디 가져오기
const currentSession = await account.getSession('current');
// 현재 세션 삭제
await account.deleteSession(currentSession.$id);
// 로그아웃 시 열려있는 모든 토스트 제거
clearAllToasts();
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
window.dispatchEvent(new Event('auth-state-changed'));
showAuthToast('로그아웃 성공', '다음에 또 만나요!');
} catch (sessionError: any) {
console.error('세션 삭제 중 오류:', sessionError);
// 오류 메시지 생성
let errorMessage = sessionError.message || '알 수 없는 오류가 발생했습니다.';
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (sessionError.code === 401) {
errorMessage = '이미 로그아웃되었습니다.';
}
showAuthToast('로그아웃 실패', errorMessage, 'destructive');
}
} catch (error: any) {
console.error('로그아웃 중 예외 발생:', error);
// 네트워크 오류 확인
const errorMessage = error.message && error.message.includes('fetch')
const errorMessage = error.message && error.message.includes('network')
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
: '예상치 못한 오류가 발생했습니다.';

View File

@@ -1,90 +1,69 @@
import { supabase } from '@/lib/supabase';
import { showAuthToast, verifyServerConnection } from '@/utils/auth';
import { account, client } from '@/lib/appwrite/client';
import { ID } from 'appwrite';
import { showAuthToast } from '@/utils/auth';
import { isValidConnection } from '@/lib/appwrite/client';
/**
* 회원가입 기능 - Supabase Cloud 환경에 최적화
* 회원가입 기능 - Appwrite 환경에 최적화
*/
export const signUp = async (email: string, password: string, username: string) => {
try {
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
// 서버 연결 상태 확인
const connectionStatus = await verifyServerConnection();
if (!connectionStatus.connected) {
console.error('서버 연결 실패:', connectionStatus.message);
showAuthToast('회원가입 오류', `서버 연결 실패: ${connectionStatus.message}`, 'destructive');
return { error: { message: connectionStatus.message }, user: null };
const connected = await isValidConnection();
if (!connected) {
console.error('서버 연결 실패');
showAuthToast('회원가입 오류', '서버 연결 실패했습니다. 네트워크 연결을 확인해주세요.', 'destructive');
return { error: { message: '서버 연결 실패' }, user: null };
}
console.log('회원가입 시도:', email);
// 현재 브라우저 URL 가져오기
const currentUrl = window.location.origin;
const redirectUrl = `${currentUrl}/login?auth_callback=true`;
console.log('이메일 인증 리디렉션 URL:', redirectUrl);
// 회원가입 요청
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
username, // 사용자 이름을 메타데이터에 저장
},
emailRedirectTo: redirectUrl
}
});
if (error) {
console.error('회원가입 오류:', error);
try {
// Appwrite로 회원가입 요청
const user = await account.create(
ID.unique(),
email,
password,
username
);
// 이메일 인증 메일 발송
await account.createVerification(window.location.origin + '/login');
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default');
console.log('인증 메일 발송됨:', email);
return {
error: null,
user,
message: '이메일 인증 필요',
emailConfirmationRequired: true
};
} catch (authError: any) {
console.error('회원가입 오류:', authError);
// 오류 메시지 처리
let errorMessage = error.message;
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
if (error.message.includes('User already registered')) {
errorMessage = '이미 등록된 사용자입니다.';
} else if (error.message.includes('Signup not allowed')) {
errorMessage = '회원가입이 허용되지 않습니다.';
} else if (error.message.includes('Email link invalid')) {
errorMessage = '이메일 링크가 유효하지 않습니다.';
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
if (authError.code === 409) {
errorMessage = '이미 등록된 이메일입니다.';
} else if (authError.code === 400) {
errorMessage = '유효하지 않은 이메일 또는 비밀번호입니다.';
} else if (authError.code === 429) {
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
}
showAuthToast('회원가입 실패', errorMessage, 'destructive');
return { error: { message: errorMessage }, user: null };
}
// 회원가입 성공
if (data && data.user) {
// 이메일 확인이 필요한지 확인
const isEmailConfirmationRequired = data.user.identities &&
data.user.identities.length > 0 &&
!data.user.identities[0].identity_data?.email_verified;
if (isEmailConfirmationRequired) {
showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default');
console.log('인증 메일 발송됨:', email);
return {
error: null,
user: data.user,
message: '이메일 인증 필요',
emailConfirmationRequired: true
};
} else {
showAuthToast('회원가입 성공', '환영합니다!', 'default');
return { error: null, user: data.user };
}
}
// 사용자 데이터가 없는 경우 (드물게 발생)
console.warn('회원가입 응답은 성공했지만 사용자 데이터가 없습니다');
showAuthToast('회원가입 성공', '계정이 생성되었습니다. 이메일 인증을 완료한 후 로그인해주세요.', 'default');
return {
error: null,
user: { email },
message: '회원가입 완료',
emailConfirmationRequired: true
};
} catch (error: any) {
console.error('회원가입 전역 예외:', error);
showAuthToast('회원가입 오류', error.message || '알 수 없는 오류', 'destructive');

View File

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

View File

@@ -1,10 +1,24 @@
import { Session, User } from '@supabase/supabase-js';
import { Models } from 'appwrite';
/**
* Appwrite 초기화 상태 반환 타입
*/
export type AppwriteInitializationStatus = {
isInitialized: boolean;
error: Error | null;
};
/**
* 인증 컨텍스트 타입
*/
export type AuthContextType = {
session: Session | null;
user: User | null;
session: Models.Session | null;
user: Models.User<Models.Preferences> | null;
loading: boolean;
error: Error | null;
appwriteInitialized: boolean;
reinitializeAppwrite: () => AppwriteInitializationStatus;
signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>;
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>;
signOut: () => Promise<void>;

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 { useToast } from "@/hooks/useToast.wrapper";
import { createRequiredTables } from "@/lib/supabase/setup";
import { createRequiredTables } from "@/archive/lib/supabase/setup";
/**
* Supabase 테이블 설정을 처리하는 커스텀 훅

153
src/lib/appwrite/client.ts Normal file
View File

@@ -0,0 +1,153 @@
/**
* 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;
}
// 클라이언트 초기화 상태 추적
let isInitialized = false;
let initializationError: Error | null = null;
// Appwrite 클라이언트 초기화
let appwriteClient: Client;
let accountService: Account;
let databasesService: Databases;
let storageService: Storage;
let avatarsService: Avatars;
/**
* Appwrite 클라이언트 초기화 함수
* UI 스레드를 차단하지 않도록 비동기적으로 초기화합니다.
*/
const initializeAppwriteClient = () => {
try {
// 설정 유효성 검증
validateConfig();
console.log(`Appwrite 클라이언트 생성 중: ${config.endpoint}`);
console.log(`프로젝트 ID: ${config.projectId}`);
// Appwrite 클라이언트 생성
appwriteClient = new Client();
appwriteClient
.setEndpoint(config.endpoint)
.setProject(config.projectId);
// API 키가 있는 경우 설정
if (config.apiKey) {
console.log('API 키 설정 중...');
// 최신 Appwrite SDK에서는 JWT 토큰을 사용하거나 세션 기반 인증을 사용합니다.
// 서버에서는 API 키를 사용하지만 클라이언트에서는 사용하지 않습니다.
// 클라이언트에서 API 키를 사용하는 것은 보안 위험이 있어 권장되지 않습니다.
console.log('API 키가 설정되었지만 클라이언트에서는 사용하지 않습니다.');
} else {
console.warn('API 키가 설정되지 않았습니다. 일부 기능이 제한될 수 있습니다.');
}
// 서비스 초기화
accountService = new Account(appwriteClient);
databasesService = new Databases(appwriteClient);
storageService = new Storage(appwriteClient);
avatarsService = new Avatars(appwriteClient);
isInitialized = true;
console.log('Appwrite 클라이언트가 성공적으로 생성되었습니다.');
// 세션 확인 (선택적)
queueMicrotask(async () => {
try {
await accountService.get();
console.log('Appwrite 세션 확인 성공');
} catch (sessionError) {
// 로그인되지 않은 상태는 정상적인 경우이므로 오류로 처리하지 않음
console.log('Appwrite 세션 없음 (정상)');
}
});
} catch (error) {
console.error('Appwrite 클라이언트 생성 오류:', error);
initializationError = error as 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(() => {
console.warn('Appwrite 서버 연결에 실패했습니다. 환경 설정을 확인해주세요.');
});
}
}
};
// 클라이언트 초기화 실행
initializeAppwriteClient();
// 서비스 내보내기
export const client = appwriteClient;
export const account = accountService;
export const databases = databasesService;
export const storage = storageService;
export const avatars = avatarsService;
/**
* 초기화 상태 확인
* @returns 초기화 상태
*/
export const getInitializationStatus = () => {
return {
isInitialized,
error: initializationError
};
};
/**
* Appwrite 클라이언트 재초기화 시도
* 오류 발생 시 재시도하기 위한 함수
*/
export const reinitializeAppwriteClient = () => {
console.log('Appwrite 클라이언트 재초기화 시도');
isInitialized = false;
initializationError = null;
initializeAppwriteClient();
return getInitializationStatus();
};
// 연결 상태 확인
export const isValidConnection = async (): Promise<boolean> => {
if (!isInitialized) {
return false;
}
try {
// 계정 서비스를 통해 현재 세션 상태 확인 (간단한 API 호출)
await account.get();
return true;
} catch (error) {
// 401 오류는 로그인되지 않은 상태로 정상적인 경우
if (error && (error as any).code === 401) {
return true; // 서버 연결은 정상이지만 로그인되지 않은 상태
}
console.error('Appwrite 연결 확인 오류:', error);
return false;
}
};

View File

@@ -0,0 +1,63 @@
/**
* Appwrite 설정
*
* 이 파일은 Appwrite 서비스에 필요한 모든 설정 값을 정의합니다.
* 환경 변수에서 값을 가져오며, 기본값을 제공합니다.
*/
// Appwrite 설정 타입 정의
export interface AppwriteConfig {
endpoint: string;
projectId: string;
databaseId: string;
transactionsCollectionId: string;
apiKey: string;
}
// 환경 변수에서 설정 값 가져오기
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://a11.ism.kr/v1';
const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || '68182a300039f6d700a6';
const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'default';
const transactionsCollectionId = import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || 'transactions';
const apiKey = import.meta.env.VITE_APPWRITE_API_KEY || '';
// 개발 모드에서 설정 값 로깅
console.log('현재 Appwrite 설정:', {
endpoint,
projectId,
databaseId,
transactionsCollectionId,
apiKey: apiKey ? '설정됨' : '설정되지 않음' // API 키는 안전을 위해 완전한 값을 로깅하지 않음
});
// 설정 객체 생성
export const config: AppwriteConfig = {
endpoint,
projectId,
databaseId,
transactionsCollectionId,
apiKey,
};
// Getter functions for config values
export const getAppwriteEndpoint = (): string => endpoint;
export const getAppwriteProjectId = (): string => projectId;
export const getAppwriteDatabaseId = (): string => databaseId;
export const getAppwriteTransactionsCollectionId = (): string => 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");
};

View File

@@ -0,0 +1,28 @@
/**
* Appwrite 기본 사용자 정보
*
* 이 파일은 Appwrite 서비스에 연결할 때 사용할 기본 사용자 정보를 제공합니다.
* 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다.
*/
// 기본 사용자 ID
export const DEFAULT_USER_ID = '68183aa4002a6f19542b';
/**
* 기본 사용자 정보를 가져오는 함수
*
* @returns 기본 사용자 ID
*/
export const getDefaultUserId = (): string => {
return DEFAULT_USER_ID;
};
/**
* 사용자 ID가 기본 사용자인지 확인하는 함수
*
* @param userId 확인할 사용자 ID
* @returns 기본 사용자 여부
*/
export const isDefaultUser = (userId: string): boolean => {
return userId === DEFAULT_USER_ID;
};

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

@@ -0,0 +1,29 @@
import { client, account, databases, storage, avatars, isValidConnection } from './client';
import {
getAppwriteEndpoint,
getAppwriteProjectId,
getAppwriteDatabaseId,
getAppwriteTransactionsCollectionId,
isValidAppwriteConfig
} from './config';
import { setupAppwriteDatabase } from './setup';
export {
// 클라이언트 및 서비스
client,
account,
databases,
storage,
avatars,
// 설정 및 유틸리티
getAppwriteEndpoint,
getAppwriteProjectId,
getAppwriteDatabaseId,
getAppwriteTransactionsCollectionId,
isValidAppwriteConfig,
isValidConnection,
// 데이터베이스 설정
setupAppwriteDatabase
};

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

80
src/lib/fullMigrate.js Executable file
View File

@@ -0,0 +1,80 @@
#!/usr/bin/env node
// 스키마 및 데이터를 Supabase Cloud -> On-Prem(a11)으로 완전 복제
import 'dotenv/config';
import { execSync } from 'child_process';
import { URL, fileURLToPath } from 'url';
import path from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CLOUD_DATABASE_URL = process.env.CLOUD_DATABASE_URL;
const ONPREM_SSH_HOST = process.env.ONPREM_SSH_HOST || 'a11';
const ONPREM_REMOTE_TMP_DIR = process.env.ONPREM_REMOTE_TMP_DIR || '/root';
if (!CLOUD_DATABASE_URL) {
console.error('환경 변수 CLOUD_DATABASE_URL이 설정되지 않았습니다.');
process.exit(1);
}
// Cloud DB 비밀번호 추출
const cloudUrlObj = new URL(CLOUD_DATABASE_URL);
const CLOUD_DATABASE_PASSWORD = cloudUrlObj.password;
if (!CLOUD_DATABASE_PASSWORD) {
console.error('Cloud DB URL에서 비밀번호를 찾을 수 없습니다.');
process.exit(1);
}
// 원격 Postgres 컨테이너 이름 조회
console.log('원격 Postgres 컨테이너 조회 (ssh a11)...');
let containerName = execSync(
`ssh ${ONPREM_SSH_HOST} "docker ps --format '{{.Names}} {{.Image}}' | grep supabase/postgres | awk '{print \\$1}'"`,
{ encoding: 'utf8' }
).trim();
if (!containerName) {
console.error('원격 Postgres 컨테이너를 찾을 수 없습니다. docker ps 결과를 확인하세요.');
process.exit(1);
}
console.log(`발견된 컨테이너: ${containerName}`);
// 1) 원격 a11에서 Cloud DB 스키마 덤프
console.log('원격에서 Cloud DB 스키마 덤프 시작...');
execSync(
`ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` +
`pg_dump --schema-only --no-owner --no-privileges ` +
`-h ${cloudUrlObj.hostname} ` +
`-p ${cloudUrlObj.port} ` +
`-U ${cloudUrlObj.username} ` +
`${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`,
{ stdio: 'inherit' }
);
// 2) 원격 a11에서 Cloud DB 데이터 덤프
console.log('원격에서 Cloud DB 데이터 덤프 시작...');
execSync(
`ssh ${ONPREM_SSH_HOST} "docker run --rm --network host -e PGPASSWORD='${CLOUD_DATABASE_PASSWORD}' postgres:15 ` +
`pg_dump --data-only --column-inserts --no-owner --no-privileges ` +
`-h ${cloudUrlObj.hostname} ` +
`-p ${cloudUrlObj.port} ` +
`-U ${cloudUrlObj.username} ` +
`${cloudUrlObj.pathname.slice(1)} > ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`,
{ stdio: 'inherit' }
);
// 4) 원격에 복원 (스키마)
console.log('원격 스키마 복원...');
execSync(
`ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` +
`psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_schema.sql"`,
{ stdio: 'inherit' }
);
// 5) 원격에 복원 (데이터)
console.log('원격 데이터 복원...');
execSync(
`ssh ${ONPREM_SSH_HOST} "docker exec -i ${containerName} ` +
`psql -U postgres -d postgres < ${ONPREM_REMOTE_TMP_DIR}/supabase_data.sql"`,
{ stdio: 'inherit' }
);
console.log('Cloud → On-Prem 전체 마이그레이션 완료.');

263
src/lib/migrateData.js Normal file
View File

@@ -0,0 +1,263 @@
import dotenv from 'dotenv';
import { createClient } from '@supabase/supabase-js';
dotenv.config();
const cloudUrl = process.env.CLOUD_SUPABASE_URL;
const cloudKey = process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || process.env.CLOUD_SUPABASE_ANON_KEY;
const onpremUrl = process.env.ONPREM_SUPABASE_URL;
const onpremKey = process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || process.env.ONPREM_SUPABASE_ANON_KEY;
if (!cloudUrl || !cloudKey || !onpremUrl || !onpremKey) {
console.error('환경 변수 설정 오류: CLOUD/ONPREM URL 또는 키가 누락되었습니다.');
process.exit(1);
}
const cloud = createClient(cloudUrl, cloudKey);
const onprem = createClient(onpremUrl, onpremKey);
// 마이그레이션할 테이블 목록
const tables = ['transactions', 'budgets', '_tests'];
// 테이블 스키마 정의
const tableSchemas = {
transactions: `
CREATE TABLE IF NOT EXISTS transactions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) NOT NULL,
title TEXT NOT NULL,
amount NUMERIC NOT NULL,
category TEXT NOT NULL,
date TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
type TEXT NOT NULL,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Row Level Security 설정
ALTER TABLE transactions ENABLE ROW LEVEL SECURITY;
-- 사용자 정책 설정 (읽기)
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 볼 수 있음" ON transactions;
CREATE POLICY "사용자는 자신의 트랜잭션만 볼 수 있음"
ON transactions FOR SELECT
USING (auth.uid() = user_id);
-- 사용자 정책 설정 (쓰기)
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 추가할 수 있음" ON transactions;
CREATE POLICY "사용자는自己的 트랜잭션만 추가할 수 있음"
ON transactions FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 사용자 정책 설정 (업데이트)
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 업데이트할 수 있음" ON transactions;
CREATE POLICY "사용자는自己的 트랜잭션만 업데이트할 수 있음"
ON transactions FOR UPDATE
USING (auth.uid() = user_id);
-- 사용자 정책 설정 (삭제)
DROP POLICY IF EXISTS "사용자는 자신의 트랜잭션만 삭제할 수 있음" ON transactions;
CREATE POLICY "사용자는自己的 트랜잭션만 삭제할 수 있음"
ON transactions FOR DELETE
USING (auth.uid() = user_id);
`,
budgets: `
CREATE TABLE IF NOT EXISTS budgets (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id) NOT NULL,
month INTEGER NOT NULL,
year INTEGER NOT NULL,
total_budget NUMERIC NOT NULL DEFAULT 0,
categories JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE (user_id, month, year)
);
-- Row Level Security 설정
ALTER TABLE budgets ENABLE ROW LEVEL SECURITY;
-- 사용자 정책 설정 (읽기)
DROP POLICY IF EXISTS "사용자는自己的 예산만 볼 수 있음" ON budgets;
CREATE POLICY "사용자는自己的 예산만 볼 수 있음"
ON budgets FOR SELECT
USING (auth.uid() = user_id);
-- 사용자 정책 설정 (쓰기)
DROP POLICY IF EXISTS "사용자는自己的 예산만 추가할 수 있음" ON budgets;
CREATE POLICY "사용자는自己的 예산만 추가할 수 있음"
ON budgets FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 사용자 정책 설정 (업데이트)
DROP POLICY IF EXISTS "사용자는自己的 예산만 업데이트할 수 있음" ON budgets;
CREATE POLICY "사용자는自己的 예산만 업데이트할 수 있음"
ON budgets FOR UPDATE
USING (auth.uid() = user_id);
-- 사용자 정책 설정 (삭제)
DROP POLICY IF EXISTS "사용자는自己的 예산만 삭제할 수 있음" ON budgets;
CREATE POLICY "사용자는自己的 예산만 삭제할 수 있음"
ON budgets FOR DELETE
USING (auth.uid() = user_id);
`,
_tests: `
CREATE TABLE IF NOT EXISTS _tests (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
test_name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 모든 사용자가 접근 가능하도록 설정
ALTER TABLE _tests ENABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS "모든 사용자가 테스트 테이블에 접근 가능" ON _tests;
CREATE POLICY "모든 사용자가 테스트 테이블에 접근 가능"
ON _tests FOR SELECT
USING (true);
`
};
/**
* 헬퍼 함수 생성
*/
async function createHelperFunctions() {
console.log('헬퍼 함수 생성 중...');
// execute_sql 함수 생성
const executeSqlSQL = `
CREATE OR REPLACE FUNCTION execute_sql(sql_query TEXT)
RETURNS VOID AS $$
BEGIN
EXECUTE sql_query;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
`;
const { error: execFnError } = await onprem.rpc('execute_sql', { sql_query: executeSqlSQL });
if (execFnError) {
console.error('execute_sql 함수 생성 실패:', execFnError);
return false;
}
console.log('헬퍼 함수 생성 완료');
return true;
}
/**
* 테이블 생성
*/
async function createTable(tableName) {
console.log(`테이블 생성 중: ${tableName}`);
if (!tableSchemas[tableName]) {
console.warn(`${tableName} 테이블의 스키마 정보가 없습니다.`);
return false;
}
try {
// 테이블 생성 SQL 실행
const { error } = await onprem.rpc('execute_sql', { sql_query: tableSchemas[tableName] });
if (error) {
console.error(`${tableName} 테이블 생성 실패:`, error);
return false;
}
console.log(`${tableName} 테이블 생성 완료`);
return true;
} catch (error) {
console.error(`${tableName} 테이블 생성 중 오류:`, error);
return false;
}
}
/**
* 테이블 데이터 마이그레이션
*/
async function migrateTableData(tableName) {
console.log(`테이블 데이터 마이그레이션 중: ${tableName}`);
try {
// Cloud DB에서 데이터 가져오기
const { data, error } = await cloud.from(tableName).select('*');
if (error) {
if (error.code === '42P01') {
console.warn(`Cloud DB에 ${tableName} 테이블이 없습니다. 건너뜁니다.`);
return true;
}
console.error(`Cloud DB에서 ${tableName} 데이터 가져오기 실패:`, error);
return false;
}
if (!data || data.length === 0) {
console.log(`${tableName} 테이블에 마이그레이션할 데이터가 없습니다.`);
return true;
}
console.log(`${tableName} 테이블에서 ${data.length}개 행을 가져왔습니다.`);
// 데이터를 작은 배치로 나누어 삽입 (트랜잭션 삭제 안전성 고려)
const batchSize = 100;
for (let i = 0; i < data.length; i += batchSize) {
const batch = data.slice(i, i + batchSize);
const { error: insertError } = await onprem.from(tableName).upsert(batch);
if (insertError) {
console.error(`${tableName} 테이블에 데이터 삽입 실패:`, insertError);
return false;
}
console.log(`${tableName} 테이블에 ${batch.length}개 행 삽입 완료 (${i + batch.length}/${data.length})`);
// 비동기 작업 사이에 짧은 지연 추가 (UI 스레드 차단 방지)
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log(`${tableName} 테이블 데이터 마이그레이션 완료`);
return true;
} catch (error) {
console.error(`${tableName} 테이블 데이터 마이그레이션 중 오류:`, error);
return false;
}
}
/**
* 메인 마이그레이션 함수
*/
async function main() {
try {
console.log('Supabase Cloud → On-Prem 마이그레이션 시작');
// 헬퍼 함수 생성
const helperCreated = await createHelperFunctions();
if (!helperCreated) {
console.warn('헬퍼 함수 생성에 실패했습니다. 계속 진행합니다.');
}
// 각 테이블에 대해 스키마 생성 및 데이터 마이그레이션 수행
for (const tableName of tables) {
// 테이블 생성
const tableCreated = await createTable(tableName);
if (!tableCreated) {
console.warn(`${tableName} 테이블 생성에 실패했습니다. 데이터 마이그레이션을 건너뜁니다.`);
continue;
}
// 데이터 마이그레이션
const dataMigrated = await migrateTableData(tableName);
if (!dataMigrated) {
console.warn(`${tableName} 테이블 데이터 마이그레이션에 실패했습니다.`);
}
}
console.log('Supabase Cloud → On-Prem 마이그레이션 완료');
} catch (error) {
console.error('마이그레이션 중 오류 발생:', error);
process.exit(1);
}
}
// 마이그레이션 실행
main();

63
src/lib/migrateData.ts Normal file
View File

@@ -0,0 +1,63 @@
import dotenv from 'dotenv';
import { createClient } from '@supabase/supabase-js';
dotenv.config();
const cloudUrl = process.env.CLOUD_SUPABASE_URL;
// 서비스 역할 키가 유효하지 않으면 CLOUD_SUPABASE_ANON_KEY 또는 VITE_SUPABASE_ANON_KEY를 사용합니다.
const cloudKey = process.env.CLOUD_SUPABASE_SERVICE_ROLE_KEY || process.env.CLOUD_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
const onpremUrl = process.env.ONPREM_SUPABASE_URL;
// 서비스 역할 키가 유효하지 않으면 ONPREM_SUPABASE_ANON_KEY 또는 VITE_SUPABASE_ANON_KEY를 사용합니다.
const onpremKey = process.env.ONPREM_SUPABASE_SERVICE_ROLE_KEY || process.env.ONPREM_SUPABASE_ANON_KEY || process.env.VITE_SUPABASE_ANON_KEY;
if (!cloudUrl || !cloudKey || !onpremUrl || !onpremKey) {
console.error('환경 변수가 설정되지 않았습니다. .env 파일을 확인하세요.');
process.exit(1);
}
const cloud = createClient(cloudUrl, cloudKey);
const onprem = createClient(onpremUrl, onpremKey);
// 복사할 테이블 목록을 정의하세요.
const tables = [
'users',
'accounts',
'transactions',
// 필요에 따라 추가 테이블을 여기에 입력
];
async function migrateTable(table: string) {
console.log(`Migrating table: ${table}`);
const { data, error } = await cloud.from(table).select('*');
if (error) {
// 테이블이 없으면 스킵
if (error.code === '42P01') {
console.warn(`Table ${table} not found in Cloud DB, skipping.`);
return;
}
console.error(`Error fetching ${table}:`, error);
return;
}
if (!data || data.length === 0) {
console.log(`${table} has no data to migrate.`);
return;
}
const { error: insertError } = await onprem.from(table).upsert(data);
if (insertError) {
console.error(`Error inserting into ${table}:`, insertError);
} else {
console.log(`Migrated ${data.length} rows into ${table}`);
}
}
async function main() {
for (const table of tables) {
await migrateTable(table);
}
console.log('Migration complete.');
}
main().catch(err => {
console.error('Migration failed:', err);
process.exit(1);
});

View File

@@ -1,9 +1,10 @@
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.tsx';
import './index.css';
console.log('main.tsx loaded');
// iOS 안전 영역 메타 태그 추가
const setViewportMetaTag = () => {
// 기존 viewport 메타 태그 찾기
@@ -23,9 +24,108 @@ const setViewportMetaTag = () => {
// 메타 태그 설정 적용
setViewportMetaTag();
// 앱 렌더링 - BrowserRouter로 감싸기
createRoot(document.getElementById("root")!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
// 전역 오류 핸들러 추가
window.onerror = function(message, source, lineno, colno, error) {
console.error('전역 오류 발생:', { message, source, lineno, colno, error });
// 오류 발생 시 기본 오류 화면 표시
const rootElement = document.getElementById('root');
if (rootElement) {
rootElement.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">애플리케이션 로딩 중 오류가 발생했습니다.</p>
<pre style="max-width: 80%; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">${message}</pre>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
return false;
};
// 처리되지 않은 Promise 오류 핸들러 추가
window.addEventListener('unhandledrejection', function(event) {
console.error('처리되지 않은 Promise 오류:', event.reason);
// 오류 발생 시 기본 오류 화면 표시
const rootElement = document.getElementById('root');
if (rootElement) {
rootElement.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">비동기 작업 중 오류가 발생했습니다.</p>
<pre style="max-width: 80%; overflow: auto; background: #f5f5f5; padding: 10px; border-radius: 4px; margin-bottom: 20px;">${event.reason}</pre>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
});
// 디버깅 정보 출력
console.log('환경 변수:', {
NODE_ENV: import.meta.env.MODE,
BASE_URL: import.meta.env.BASE_URL,
APPWRITE_ENDPOINT: import.meta.env.VITE_APPWRITE_ENDPOINT,
APPWRITE_PROJECT_ID: import.meta.env.VITE_APPWRITE_PROJECT_ID,
});
// 상태 확인
// TypeScript에서 window 객체에 사용자 정의 속성 추가
declare global {
interface Window {
appwriteEnabled: boolean;
}
}
// 기본적으로 Appwrite 비활성화
window.appwriteEnabled = false;
try {
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Root element not found');
}
const root = createRoot(rootElement);
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
console.log('애플리케이션 렌더링 성공');
} catch (error) {
console.error('애플리케이션 렌더링 오류:', error);
// 오류 발생 시 기본 오류 화면 표시
const rootElement = document.getElementById('root');
if (rootElement) {
rootElement.innerHTML = `
<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; font-family: Arial, sans-serif;">
<div style="color: red; font-size: 48px; margin-bottom: 20px;">⚠️</div>
<h1 style="margin-bottom: 20px;">Zellyy Finance 오류</h1>
<p style="margin-bottom: 20px; text-align: center;">애플리케이션 로딩 중 오류가 발생했습니다.</p>
<button
onclick="window.location.reload()"
style="padding: 10px 20px; background-color: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer;"
>
새로고침
</button>
</div>
`;
}
};

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

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import NavBar from '@/components/NavBar';
import AddTransactionButton from '@/components/AddTransactionButton';
import WelcomeDialog from '@/components/onboarding/WelcomeDialog';
@@ -11,6 +11,8 @@ import SafeAreaContainer from '@/components/SafeAreaContainer';
import { useInitialDataLoading } from '@/hooks/useInitialDataLoading';
import { useAppFocusEvents } from '@/hooks/useAppFocusEvents';
import { useWelcomeNotification } from '@/hooks/useWelcomeNotification';
import { useAuth } from '@/contexts/auth';
import { isValidConnection } from '@/lib/appwrite/client';
/**
* 애플리케이션의 메인 인덱스 페이지 컴포넌트
@@ -19,20 +21,108 @@ const Index = () => {
const { resetBudgetData } = useBudget();
const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog();
const { isInitialized } = useDataInitialization(resetBudgetData);
const { loading: authLoading, error: authError, appwriteInitialized, reinitializeAppwrite } = useAuth();
// 애플리케이션 상태 관리
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
const [connectionError, setConnectionError] = useState<string | null>(null);
// 커스텀 훅 사용으로 코드 분리
useInitialDataLoading();
useAppFocusEvents();
useWelcomeNotification(isInitialized);
// Appwrite 연결 상태 확인
useEffect(() => {
const checkConnection = async () => {
try {
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
// Appwrite 초기화 상태 확인
if (!appwriteInitialized) {
console.log('Appwrite 초기화 상태 확인 중...');
const status = reinitializeAppwrite();
if (!status.isInitialized) {
setConnectionError('서버 연결에 문제가 있습니다. 재시도해주세요.');
setAppState('error');
return;
}
}
// 연결 상태 확인
const connectionValid = await isValidConnection();
if (!connectionValid) {
console.warn('Appwrite 연결 문제 발생');
setConnectionError('서버 연결에 문제가 있습니다. 재시도해주세요.');
setAppState('error');
return;
}
// 인증 오류 확인
if (authError) {
console.error('Appwrite 인증 오류:', authError);
setConnectionError('인증 처리 중 오류가 발생했습니다.');
setAppState('error');
return;
}
// 모든 검사 통과 시 준비 상태로 전환
setAppState('ready');
} catch (error) {
console.error('연결 확인 중 오류:', error);
setConnectionError('서버 연결 확인 중 오류가 발생했습니다.');
setAppState('error');
}
};
// 앱 상태가 로딩 상태일 때만 연결 확인
if (appState === 'loading' && !authLoading) {
checkConnection();
}
}, [appState, authLoading, authError, appwriteInitialized, reinitializeAppwrite]);
// 초기화 후 환영 메시지 표시 상태 확인
useEffect(() => {
if (isInitialized) {
if (isInitialized && appState === 'ready') {
const timeoutId = setTimeout(checkWelcomeDialogState, 500);
return () => clearTimeout(timeoutId);
}
}, [isInitialized, checkWelcomeDialogState]);
}, [isInitialized, appState, checkWelcomeDialogState]);
// 로딩 상태 표시
if (appState === 'loading' || authLoading) {
return (
<SafeAreaContainer className="min-h-screen bg-neuro-background flex flex-col items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<h2 className="text-xl font-bold mb-2">Zellyy Finance</h2>
<p className="text-gray-600"> ...</p>
</SafeAreaContainer>
);
}
// 오류 상태 표시
if (appState === 'error') {
return (
<SafeAreaContainer className="min-h-screen bg-neuro-background flex flex-col items-center justify-center p-4">
<div className="text-red-500 text-5xl mb-4"></div>
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="text-center mb-6">{connectionError || '서버 연결에 문제가 발생했습니다.'}</p>
<button
onClick={() => {
setAppState('loading');
reinitializeAppwrite();
}}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</SafeAreaContainer>
);
}
// 준비 완료 시 일반 UI 표시
return (
<SafeAreaContainer className="min-h-screen bg-neuro-background pb-24" extraBottomPadding={true}>
<IndexContent />

View File

@@ -10,7 +10,7 @@ import RegisterForm from "@/components/auth/RegisterForm";
import LoginLink from "@/components/auth/LoginLink";
import ServerStatusAlert from "@/components/auth/ServerStatusAlert";
import TestConnectionSection from "@/components/auth/TestConnectionSection";
import SupabaseConnectionStatus from "@/components/auth/SupabaseConnectionStatus";
import SupabaseConnectionStatus from "@/archive/components/SupabaseConnectionStatus";
import RegisterErrorDisplay from "@/components/auth/RegisterErrorDisplay";
import { ServerConnectionStatus } from "@/components/auth/types";

View File

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

86
src/test-appwrite-user.ts Normal file
View File

@@ -0,0 +1,86 @@
/**
* Appwrite 사용자 연결 테스트 스크립트
*
* 이 파일은 Appwrite 서비스와의 사용자 연결을 테스트하기 위한 스크립트입니다.
* 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다.
*/
import { Client, Account, ID } from 'appwrite';
// 설정 값 직접 지정
const endpoint = 'https://a11.ism.kr/v1';
const projectId = '68182a300039f6d700a6'; // 프로젝트 ID
const userId = '68183aa4002a6f19542b'; // 사용자 ID
// 테스트 함수
async function testAppwriteUserConnection() {
console.log('Appwrite 사용자 연결 테스트 시작...');
console.log('설정 정보:', {
endpoint,
projectId,
userId
});
try {
// Appwrite 클라이언트 생성
const client = new Client();
client
.setEndpoint(endpoint)
.setProject(projectId);
// 계정 서비스 초기화
const account = new Account(client);
// 이메일/비밀번호 로그인 테스트
try {
console.log('이메일/비밀번호 로그인 테스트...');
// 참고: 실제 로그인 정보는 보안상의 이유로 하드코딩하지 않습니다.
// 이 부분은 실제 애플리케이션에서 사용자 입력을 통해 처리해야 합니다.
console.log('로그인은 실제 애플리케이션에서 수행해야 합니다.');
// JWT 세션 테스트 (선택적)
try {
console.log('JWT 세션 테스트...');
// JWT 세션 생성은 서버 측에서 수행해야 하는 작업입니다.
console.log('JWT 세션 생성은 서버 측에서 수행해야 합니다.');
} catch (jwtError) {
console.error('JWT 세션 테스트 실패:', jwtError);
}
} catch (loginError) {
console.error('로그인 테스트 실패:', loginError);
}
// 익명 세션 테스트
try {
console.log('익명 세션 테스트...');
const anonymousSession = await account.createAnonymousSession();
console.log('익명 세션 생성 성공:', anonymousSession.$id);
// 세션 삭제
try {
await account.deleteSession(anonymousSession.$id);
console.log('익명 세션 삭제 성공');
} catch (deleteError) {
console.error('익명 세션 삭제 실패:', deleteError);
}
} catch (anonymousError) {
console.error('익명 세션 테스트 실패:', anonymousError);
}
} catch (error) {
console.error('Appwrite 클라이언트 생성 오류:', error);
}
console.log('Appwrite 사용자 연결 테스트 완료');
}
// 테스트 실행
testAppwriteUserConnection()
.then(() => {
console.log('테스트가 완료되었습니다.');
})
.catch((error) => {
console.error('테스트 중 예외 발생:', error);
});

88
src/test-appwrite.ts Normal file
View File

@@ -0,0 +1,88 @@
/**
* Appwrite 연결 테스트 스크립트
*
* 이 파일은 Appwrite 서비스와의 연결을 테스트하기 위한 스크립트입니다.
* 개발 가이드라인에 따라 UI 스레드를 차단하지 않도록 비동기 처리와 오류 처리를 포함합니다.
*/
import { Client, Account } from 'appwrite';
// 설정 값 직접 지정
const endpoint = 'https://a11.ism.kr/v1';
const projectId = '68182a300039f6d700a6'; // 올바른 프로젝트 ID
const apiKey = 'standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea';
// 테스트 함수
async function testAppwriteConnection() {
console.log('Appwrite 연결 테스트 시작...');
console.log('설정 정보:', {
endpoint,
projectId,
apiKey: apiKey ? '설정됨' : '설정되지 않음'
});
try {
// Appwrite 클라이언트 생성
const client = new Client();
client
.setEndpoint(endpoint)
.setProject(projectId);
// 계정 서비스 초기화
const account = new Account(client);
// 연결 테스트 (익명 세션 생성 시도)
try {
console.log('익명 세션 생성 시도...');
const session = await account.createAnonymousSession();
console.log('익명 세션 생성 성공:', session.$id);
// 세션 정보 확인
try {
const user = await account.get();
console.log('사용자 정보 확인 성공:', user.$id);
} catch (userError) {
console.error('사용자 정보 확인 실패:', userError);
}
// 세션 삭제
try {
await account.deleteSession(session.$id);
console.log('세션 삭제 성공');
} catch (deleteError) {
console.error('세션 삭제 실패:', deleteError);
}
} catch (sessionError) {
console.error('익명 세션 생성 실패:', sessionError);
// 프로젝트 정보 확인 시도
try {
console.log('프로젝트 정보 확인 시도...');
// 프로젝트 정보는 API 키가 있어야 확인 가능
if (!apiKey) {
console.error('API 키가 없어 프로젝트 정보를 확인할 수 없습니다.');
} else {
console.log('API 키가 있지만 클라이언트에서는 사용할 수 없습니다.');
}
} catch (projectError) {
console.error('프로젝트 정보 확인 실패:', projectError);
}
}
} catch (error) {
console.error('Appwrite 클라이언트 생성 오류:', error);
}
console.log('Appwrite 연결 테스트 완료');
}
// 테스트 실행
testAppwriteConnection()
.then(() => {
console.log('테스트가 완료되었습니다.');
})
.catch((error) => {
console.error('테스트 중 예외 발생:', error);
});

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 환경에 최적화

View File

@@ -1,5 +1,5 @@
import { supabase } from '@/lib/supabase';
import { supabase } from '@/archive/lib/supabase';
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 { 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 { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators';

Some files were not shown because too many files have changed in this diff Show More