Compare commits
8 Commits
4f1705c3b7
...
appwrite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61492549f6 | ||
|
|
8efa986ed5 | ||
|
|
5305c98970 | ||
|
|
f83bb384af | ||
|
|
fdfdf15166 | ||
|
|
2bfe52fb7b | ||
|
|
cdf2de5d9f | ||
|
|
c089195ea9 |
20
.env
20
.env
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
284
app_build.log
284
app_build.log
@@ -1,284 +0,0 @@
|
||||
===== 빌드 시작: Sat Apr 5 19:45:45 KST 2025 =====
|
||||
[1;33m1. 웹 앱 빌드 중...[0m
|
||||
실행 명령어: 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
|
||||
[0;32m웹 앱 빌드 완료[0m
|
||||
[1;33m2. Capacitor에 웹 코드 동기화 중...[0m
|
||||
실행 명령어: 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
|
||||
[0;32mCapacitor 동기화 완료[0m
|
||||
[1;33m3. 안드로이드 빌드 시작 (release-aab)...[0m
|
||||
실행 명령어: ./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
|
||||
[0;32m릴리즈 AAB 빌드 완료[0m
|
||||
[0;32mAAB 파일 생성 완료: app/build/outputs/bundle/release/app-release.aab[0m
|
||||
[0;32mAAB 파일 크기: 3.8M[0m
|
||||
[0;32mAAB 파일이 릴리즈 디렉토리에 복사되었습니다: release/zellyy_release_v1.1.8_20250405_194612.aab[0m
|
||||
@@ -1 +0,0 @@
|
||||
===== 오류 로그: Sat Apr 5 19:45:45 KST 2025 =====
|
||||
0
archive/sql/supabase_cloud_data.sql
Normal file
0
archive/sql/supabase_cloud_data.sql
Normal file
0
archive/sql/supabase_schema.sql
Normal file
0
archive/sql/supabase_schema.sql
Normal file
1
archive/supabase/.temp/cli-latest
Normal file
1
archive/supabase/.temp/cli-latest
Normal file
@@ -0,0 +1 @@
|
||||
v2.22.6
|
||||
@@ -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분
|
||||
|
||||
@@ -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}"
|
||||
|
||||
86
docs/02_기술_문서/02_기술스택.md
Normal file
86
docs/02_기술_문서/02_기술스택.md
Normal 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 (개발/테스트 환경)
|
||||
276
docs/02_기술_문서/Appwrite_전환_가이드.md
Normal file
276
docs/02_기술_문서/Appwrite_전환_가이드.md
Normal 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. 문제 발생 시 개발팀에 즉시 보고하세요.
|
||||
59
docs/03_개발_단계/개발_가이드라인.md
Normal file
59
docs/03_개발_단계/개발_가이드라인.md
Normal 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만 증가할 것
|
||||
- 버전 정보는 항상 설정 페이지에 표시하여 사용자와 개발자가 확인 가능하게 할 것
|
||||
150
docs/README.md
150
docs/README.md
@@ -1,141 +1,59 @@
|
||||
# 적자 탈출 가계부 프로젝트 문서
|
||||
# Zellyy Finance 프로젝트 문서
|
||||
|
||||
이 디렉토리는 적자 탈출 가계부 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱 개발 프로젝트입니다.
|
||||
이 디렉토리는 Zellyy Finance 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱입니다.
|
||||
|
||||
## 프로젝트 개요
|
||||
|
||||
'적자 탈출 가계부'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. AI 기술을 활용한 개인화된 재정 관리 경험을 제공하고, 궁극적으로는 사용자들의 재정적 웰빙을 향상시키는 것을 목표로 합니다.
|
||||
'Zellyy Finance'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. Appwrite 백엔드를 활용하여 안정적인 데이터 관리와 인증 시스템을 제공합니다.
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
### 00_프로젝트_개요
|
||||
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
|
||||
- `01_프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
|
||||
- `02_핵심_문제_정의.md` - 해결하고자 하는 문제 정의 (예정)
|
||||
- `03_사용자_페르소나.md` - 타겟 사용자 프로필 (예정)
|
||||
- `04_사용자_스토리.md` - 사용자 관점의 요구사항 (예정)
|
||||
- `05_비즈니스_모델.md` - 수익 모델 및 사업화 전략 (예정)
|
||||
- `06_법률_규제_검토.md` - 금융 앱 관련 법규 및 규제 검토 (예정)
|
||||
- `프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
|
||||
- `핵심_문제_정의.md` - 해결하고자 하는 문제 정의
|
||||
- `사용자_페르소나.md` - 타겟 사용자 프로필
|
||||
|
||||
### 01_기획_및_설계
|
||||
프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다.
|
||||
- `01_요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
|
||||
- `02_MVP_기능_목록.md` - 최소 기능 제품(MVP)의 기능 목록 (예정)
|
||||
- `03_주요_사용_시나리오.md` - 주요 사용 사례 시나리오 (예정)
|
||||
- `04_UI_와이어프레임.md` - 핵심 화면 와이어프레임 (예정)
|
||||
- `05_사용자_여정_맵.md` - 사용자 경험 흐름도 (예정)
|
||||
- `06_정보_아키텍처.md` - 앱 구조 및 화면 흐름도 (예정)
|
||||
- `요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
|
||||
- `UI_와이어프레임.md` - 핵심 화면 와이어프레임
|
||||
- `사용자_경험_전략.md` - 사용자 경험 설계 전략
|
||||
|
||||
### 02_기술_문서
|
||||
프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
|
||||
- `01_시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
|
||||
- `02_데이터_모델_설계.md` - 데이터 모델 설계 문서 (예정)
|
||||
- `03_API_명세서.md` - API 엔드포인트 명세 (예정)
|
||||
- `04_보안_설계.md` - 보안 및 개인정보 보호 설계 (예정)
|
||||
- `05_성능_최적화_전략.md` - 앱 성능 최적화 전략 (예정)
|
||||
- `06_CI_CD_파이프라인.md` - 지속적 통합/배포 전략 (예정)
|
||||
- `07_AI_ML_구현_전략.md` - AI 기반 소비 패턴 분석 구현 방법 (예정)
|
||||
- `시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
|
||||
- `데이터_모델_설계.md` - 데이터베이스 스키마 및 모델 설계
|
||||
- `Appwrite_전환_가이드.md` - Supabase에서 Appwrite로의 전환 가이드
|
||||
|
||||
### 03_개발_단계
|
||||
프로젝트 개발 단계별 문서가 포함되어 있습니다.
|
||||
- `01_개발_로드맵.md` - 전체 개발 로드맵 및 일정
|
||||
- `02_1단계_개발_계획.md` - 1단계(MVP) 개발 상세 계획 (예정)
|
||||
- `03_테스트_전략.md` - 테스트 방법론 및 계획 (예정)
|
||||
- `04_배포_전략.md` - 배포 및 운영 계획 (예정)
|
||||
- `05_품질_보증_계획.md` - QA 전략 및 테스트 케이스 (예정)
|
||||
- `06_유지보수_전략.md` - 출시 후 유지보수 및 업데이트 계획 (예정)
|
||||
개발 과정과 관련된 문서가 포함되어 있습니다.
|
||||
- `개발_가이드라인.md` - 코드 작성 원칙, iOS/Android 지원, Appwrite 통합 등에 관한 가이드라인
|
||||
|
||||
### 04_디자인_가이드
|
||||
UI/UX 디자인 관련 문서가 포함되어 있습니다.
|
||||
- `01_디자인_시스템.md` - 디자인 언어 및 컴포넌트 정의 (예정)
|
||||
- `02_색상_팔레트.md` - 앱 색상 가이드라인 (예정)
|
||||
- `03_타이포그래피.md` - 폰트 및 텍스트 스타일 가이드 (예정)
|
||||
- `04_아이콘_및_이미지.md` - 아이콘 디자인 및 사용 가이드 (예정)
|
||||
- `05_애니메이션_가이드.md` - UI 애니메이션 및 트랜지션 (예정)
|
||||
- `06_접근성_지침.md` - 접근성 디자인 원칙 (예정)
|
||||
### archive
|
||||
더 이상 활발하게 사용되지 않는 레거시 문서들이 보관되어 있습니다.
|
||||
- `Supabase 관련 문서` - 이전에 사용하던 Supabase 관련 설정 및 가이드
|
||||
- `개발 단계별 문서` - 이전 개발 단계의 계획 및 산출물 요약
|
||||
|
||||
### 05_프로젝트_관리
|
||||
프로젝트 관리 및 협업 관련 문서가 포함되어 있습니다.
|
||||
- `01_팀_구성.md` - 팀 구성원 및 역할 정의 (예정)
|
||||
- `02_의사결정_프로세스.md` - 프로젝트 의사결정 체계 (예정)
|
||||
- `03_커뮤니케이션_계획.md` - 팀 내 소통 방식 및 도구 (예정)
|
||||
- `04_일정_및_마일스톤.md` - 주요 마일스톤 및 납기일 (예정)
|
||||
- `05_위험_관리.md` - 잠재적 위험 요소 및 대응 계획 (예정)
|
||||
## 주요 기술 스택
|
||||
|
||||
### 06_참고자료
|
||||
프로젝트 진행에 참고할 수 있는 자료들이 포함되어 있습니다.
|
||||
- `01_시장_조사_보고서.md` - 가계부 앱 시장 조사 보고서
|
||||
- `02_경쟁사_분석.md` - 주요 경쟁 앱 상세 분석 (예정)
|
||||
- `03_사용자_인터뷰.md` - 잠재 사용자 인터뷰 결과 (예정)
|
||||
- `04_참고_리소스.md` - 유용한 참고 자료 및 링크 (예정)
|
||||
- `05_금융_데이터_소스.md` - 재정 관리 데이터 참고 자료 (예정)
|
||||
- `06_관련_연구_자료.md` - 소비 행동 및 금융 심리학 연구 (예정)
|
||||
- **프론트엔드**: React Native, TypeScript
|
||||
- **백엔드**: Appwrite
|
||||
- **상태 관리**: Context API
|
||||
- **UI 컴포넌트**: Lovable UI
|
||||
- **네이티브 통합**: Capacitor
|
||||
|
||||
### 07_마케팅_및_성장
|
||||
마케팅 및 사용자 확보 전략 관련 문서가 포함되어 있습니다.
|
||||
- `01_마케팅_전략.md` - 출시 및 사용자 확보 전략 (예정)
|
||||
- `02_ASO_전략.md` - 앱 스토어 최적화 전략 (예정)
|
||||
- `03_콘텐츠_전략.md` - 콘텐츠 마케팅 계획 (예정)
|
||||
- `04_사용자_유지_전략.md` - 사용자 참여 및 유지 방안 (예정)
|
||||
- `05_파트너십_계획.md` - 잠재적 파트너십 및 협업 기회 (예정)
|
||||
## 개발 가이드라인
|
||||
|
||||
## 주요 기능
|
||||
개발 가이드라인은 [03_개발_단계/개발_가이드라인.md](./03_개발_단계/개발_가이드라인.md) 문서를 참조하세요. 이 문서에는 다음 내용이 포함되어 있습니다:
|
||||
|
||||
1. **수입/지출 기록**: 간편한 UI로 일상 재정 활동 기록
|
||||
2. **카테고리 관리**: 사용자 정의 카테고리로 지출 분류
|
||||
3. **예산 설정**: 카테고리별 월간/주간 예산 설정 및 알림
|
||||
4. **지출 분석**: 차트와 그래프로 소비 패턴 시각화
|
||||
5. **AI 기반 분석**: 소비 패턴 분석 및 맞춤형 절약 제안
|
||||
6. **절약 챌린지**: 사용자 맞춤형 절약 목표 설정 및 달성 보상
|
||||
7. **재정 건강 점수**: 사용자의 재정 상태를 점수화하여 개선 동기 부여
|
||||
8. **구독 관리**: 정기 구독 서비스 추적 및 최적화 제안
|
||||
9. **재정 목표 설정**: 단기/중기/장기 저축 목표 설정 및 진행 상황 추적
|
||||
10. **알림 시스템**: 예산 초과, 주요 지출, 절약 기회에 대한 스마트 알림
|
||||
11. **가계부 보고서**: 정기적인 재정 상태 요약 보고서 제공
|
||||
12. **공유 기능**: 가족 또는 파트너와 특정 재정 정보 공유
|
||||
1. 코드 작성 원칙
|
||||
2. 트랜잭션 삭제 안전성
|
||||
3. Appwrite 통합 원칙
|
||||
4. 상태 관리 최적화
|
||||
5. iOS/Android 지원
|
||||
6. 디버깅 및 로깅
|
||||
|
||||
## 기술 스택
|
||||
## Appwrite 전환
|
||||
|
||||
- **프론트엔드**: React, Vite, Tailwind CSS, Capacitor
|
||||
- **백엔드**: Node.js, Express, Supabase(PostgreSQL)
|
||||
- **AI/ML**: TensorFlow, Python
|
||||
- **클라우드**: Supabase On-Premise
|
||||
- **데이터 시각화**: D3.js, Chart.js
|
||||
- **인증/보안**: JWT, OAuth 2.0, 데이터 암호화
|
||||
- **테스트**: Jest, Cypress
|
||||
- **CI/CD**: GitHub Actions
|
||||
- **분석**: Supabase Analytics
|
||||
|
||||
## 문서 작성 가이드라인
|
||||
- 모든 문서는 마크다운(.md) 형식으로 작성합니다.
|
||||
- 파일명은 내용을 명확히 나타내는 한글 또는 영문으로 작성합니다.
|
||||
- 이미지나 다이어그램은 가능한 마크다운 내에 포함시킵니다.
|
||||
- 문서 간 연결이 필요한 경우 상대 경로를 사용하여 링크합니다.
|
||||
- 코드 예시는 적절한 구문 강조와 함께 코드 블록으로 포함합니다.
|
||||
- 변경 사항은 문서 하단의 업데이트 이력에 기록합니다.
|
||||
- 중요 결정사항은 의사결정 배경과 함께 기록합니다.
|
||||
|
||||
## 개발 워크플로우
|
||||
1. **기능 기획**: 사용자 스토리 및 요구사항 정의
|
||||
2. **설계**: UI/UX 디자인 및 기술 아키텍처 설계
|
||||
3. **개발**: 기능 구현 및 단위 테스트
|
||||
4. **코드 리뷰**: 팀원 간 코드 품질 검토
|
||||
5. **테스트**: QA 및 사용성 테스트
|
||||
6. **배포**: 스테이징 및 프로덕션 환경 배포
|
||||
7. **모니터링**: 성능 및 사용자 피드백 모니터링
|
||||
8. **반복**: 피드백을 바탕으로 기능 개선
|
||||
|
||||
## 출시 계획
|
||||
- **알파 버전**: 내부 테스트 (2024년 4월 초)
|
||||
- **베타 버전**: 제한적 사용자 테스트 (2024년 4월 중순)
|
||||
- **MVP 출시**: 앱스토어 및 플레이스토어 공개 (2024년 4월 말)
|
||||
- **기능 업데이트**: 사용자 피드백 기반 주요 기능 추가 (2024년 5월 초)
|
||||
- **확장 계획**: 웹 버전 및 추가 기능 확장 (2024년 5월 중순부터)
|
||||
|
||||
## 업데이트 이력
|
||||
- 2024-03-15: 프로젝트 문서 초기 구성 완료
|
||||
- 2024-03-15: 프로젝트 소개, 요구사항 분석, 시스템 아키텍처, 개발 로드맵, 시장 조사 보고서 추가
|
||||
- 2024-04-01: 폴더 구조 개선 및 추가 섹션(디자인 가이드, 프로젝트 관리, 마케팅) 추가
|
||||
- 2024-04-05: 일정 조정 - 모든 개발 계획을 4월 말까지 완료하도록 수정
|
||||
- 2025-03-09: 개발 방법 변경 - Flutter에서 React, Tailwind CSS, Capacitor 기반 웹 앱으로 전환, Lovable UI 컴포넌트 스타일 적용
|
||||
- 2025-03-09: 데이터베이스 변경 - MongoDB에서 Supabase(PostgreSQL) On-Premise로 전환
|
||||
Supabase에서 Appwrite로의 전환에 관한 상세 정보는 [02_기술_문서/Appwrite_전환_가이드.md](./02_기술_문서/Appwrite_전환_가이드.md) 문서를 참조하세요.
|
||||
|
||||
116
docs/WEB_SERVER_SETUP.md
Normal file
116
docs/WEB_SERVER_SETUP.md
Normal 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
|
||||
```
|
||||
80
docs/archive/SUPABASE_ONPREM_MIGRATION_PLAN.md
Normal file
80
docs/archive/SUPABASE_ONPREM_MIGRATION_PLAN.md
Normal 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`*
|
||||
@@ -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>
|
||||
|
||||
@@ -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
152
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
128
src/App.tsx
128
src/App.tsx
@@ -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,16 +16,132 @@ 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() {
|
||||
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const [appwriteEnabled, setAppwriteEnabled] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "적자 탈출 가계부";
|
||||
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 (
|
||||
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
|
||||
<AuthProvider>
|
||||
<BudgetProvider>
|
||||
<div className="App">
|
||||
<BasicLayout>
|
||||
<Routes>
|
||||
<Route path="/" element={<Index />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
@@ -40,12 +155,13 @@ function App() {
|
||||
<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>
|
||||
<Toaster />
|
||||
</div>
|
||||
</BasicLayout>
|
||||
</BudgetProvider>
|
||||
</AuthProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
21
src/archive/README.md
Normal file
21
src/archive/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Archive 폴더
|
||||
|
||||
이 폴더는 Zellyy Finance 프로젝트에서 더 이상 활발하게 사용되지 않는 레거시 코드를 보관하는 곳입니다.
|
||||
|
||||
## 개요
|
||||
|
||||
Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환했습니다. 이 폴더에는 Supabase 관련 코드가 보관되어 있으며, 참조용으로만 유지됩니다.
|
||||
|
||||
## 폴더 구조
|
||||
|
||||
- `components/`: Supabase 관련 UI 컴포넌트
|
||||
- `hooks/`: Supabase 관련 훅
|
||||
- `integrations/`: Supabase 통합 코드
|
||||
- `lib/`: Supabase 클라이언트 및 유틸리티
|
||||
- `utils/`: Supabase 트랜잭션 유틸리티
|
||||
|
||||
## 주의사항
|
||||
|
||||
이 폴더의 코드는 더 이상 유지보수되지 않으며, 새로운 기능 개발에 사용해서는 안 됩니다. 모든 새로운 개발은 Appwrite 기반으로 진행해야 합니다.
|
||||
|
||||
마이그레이션이 완전히 완료되면 이 폴더는 삭제될 예정입니다.
|
||||
300
src/archive/components/migration/SupabaseToAppwriteMigration.tsx
Normal file
300
src/archive/components/migration/SupabaseToAppwriteMigration.tsx
Normal 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;
|
||||
@@ -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";
|
||||
186
src/archive/lib/appwrite/migrateFromSupabase.ts
Normal file
186
src/archive/lib/appwrite/migrateFromSupabase.ts
Normal 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 : '알 수 없는 오류'
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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 환경에서는 필요 없지만 호환성을 위해 유지
|
||||
@@ -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';
|
||||
|
||||
41
src/components/auth/AppwriteConnectionStatus.tsx
Normal file
41
src/components/auth/AppwriteConnectionStatus.tsx
Normal 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;
|
||||
146
src/components/auth/AppwriteConnectionTest.tsx
Normal file
146
src/components/auth/AppwriteConnectionTest.tsx
Normal 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;
|
||||
@@ -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 }>;
|
||||
|
||||
@@ -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();
|
||||
// Appwrite 초기화 상태 확인
|
||||
const isInitialized = await checkAppwriteInitialization();
|
||||
if (!isInitialized) {
|
||||
console.warn('Appwrite 초기화 상태가 정상적이지 않습니다. 비로그인 상태로 처리합니다.');
|
||||
queueMicrotask(() => {
|
||||
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;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('세션 로딩 중 오류:', error);
|
||||
} else if (data.session) {
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
queueMicrotask(() => {
|
||||
setSession(data.session);
|
||||
setUser(data.session.user);
|
||||
console.log('세션 로딩 완료');
|
||||
setUser(currentUser);
|
||||
setSession(currentSession);
|
||||
console.log('세션 로딩 완료 - 사용자:', currentUser.$id);
|
||||
});
|
||||
} else {
|
||||
console.log('활성 세션 없음');
|
||||
// 사용자 정보가 없으면 비로그인 상태로 처리
|
||||
queueMicrotask(() => {
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
console.log('비로그인 상태로 처리');
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('세션 확인 중 예외 발생:', error);
|
||||
// 예상치 못한 오류 처리
|
||||
console.error('세션 처리 중 예상치 못한 오류:', error);
|
||||
handleAuthError(error);
|
||||
|
||||
// 오류 발생 시 로그아웃 상태로 처리
|
||||
queueMicrotask(() => {
|
||||
setSession(null);
|
||||
setUser(null);
|
||||
});
|
||||
}
|
||||
} catch (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) {
|
||||
// 사용자 정보가 있고, 이전과 다르면 세션 정보 가져오기
|
||||
if (currentUser && (!user || currentUser.$id !== user.$id)) {
|
||||
try {
|
||||
// 세션 정보 가져오기 시도 - 안전하게 처리
|
||||
const currentSession = await account.getSession('current').catch(err => {
|
||||
console.log('세션 정보 가져오기 실패:', err);
|
||||
return null;
|
||||
});
|
||||
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
queueMicrotask(() => {
|
||||
setSession(session);
|
||||
setUser(session.user);
|
||||
setUser(currentUser);
|
||||
setSession(currentSession);
|
||||
console.log('Appwrite 인증 상태 변경: 로그인됨 - 사용자:', currentUser.$id);
|
||||
});
|
||||
} else if (event === 'SIGNED_OUT') {
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
} 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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
console.log('비밀번호 재설정 시도 중:', email);
|
||||
|
||||
if (error) {
|
||||
console.error('비밀번호 재설정 오류:', error);
|
||||
showAuthToast('비밀번호 재설정 실패', error.message, 'destructive');
|
||||
return { error };
|
||||
}
|
||||
// 비동기 작업을 마이크로태스크로 지연하여 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 };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('비밀번호 재설정 중 예외 발생:', error);
|
||||
|
||||
// 네트워크 오류 확인
|
||||
const errorMessage = handleNetworkError(error);
|
||||
const errorMessage = error.message && error.message.includes('network')
|
||||
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
|
||||
: '예상치 못한 오류가 발생했습니다.';
|
||||
|
||||
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
|
||||
return { error };
|
||||
|
||||
@@ -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()));
|
||||
|
||||
// Appwrite 인증 방식 시도
|
||||
try {
|
||||
const session = await account.createSession(email, password);
|
||||
const user = await account.get();
|
||||
|
||||
// 상태 업데이트를 마이크로태스크로 지연
|
||||
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);
|
||||
return { error: null, user };
|
||||
} catch (authError: any) {
|
||||
console.error('로그인 오류:', authError);
|
||||
|
||||
let errorMessage = error.message;
|
||||
if (error.message.includes('Invalid login credentials')) {
|
||||
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);
|
||||
// 대체 로직도 실패한 경우 원래 오류 반환
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackMode) {
|
||||
showAuthToast('로그인 실패', errorMessage, 'destructive');
|
||||
return { error: { message: errorMessage }, 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 };
|
||||
return { error: authError, user: null };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 예외 발생:', error);
|
||||
showAuthToast('로그인 오류', '서버 연결 중 오류가 발생했습니다.', 'destructive');
|
||||
return { error, user: null };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabase } from '@/archive/lib/supabase';
|
||||
import { showAuthToast } from '@/utils/auth';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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('로그아웃 시도 중');
|
||||
|
||||
// 비동기 작업을 마이크로태스크로 지연하여 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'));
|
||||
|
||||
if (error) {
|
||||
console.error('로그아웃 오류:', error);
|
||||
showAuthToast('로그아웃 실패', error.message, 'destructive');
|
||||
} else {
|
||||
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')
|
||||
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
|
||||
: '예상치 못한 오류가 발생했습니다.';
|
||||
|
||||
|
||||
@@ -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({
|
||||
try {
|
||||
// Appwrite로 회원가입 요청
|
||||
const user = await account.create(
|
||||
ID.unique(),
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
username, // 사용자 이름을 메타데이터에 저장
|
||||
},
|
||||
emailRedirectTo: redirectUrl
|
||||
}
|
||||
});
|
||||
username
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('회원가입 오류:', error);
|
||||
// 이메일 인증 메일 발송
|
||||
await account.createVerification(window.location.origin + '/login');
|
||||
|
||||
// 오류 메시지 처리
|
||||
let errorMessage = error.message;
|
||||
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||
|
||||
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 = '이메일 링크가 유효하지 않습니다.';
|
||||
}
|
||||
|
||||
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,
|
||||
user,
|
||||
message: '이메일 인증 필요',
|
||||
emailConfirmationRequired: true
|
||||
};
|
||||
} else {
|
||||
showAuthToast('회원가입 성공', '환영합니다!', 'default');
|
||||
return { error: null, user: data.user };
|
||||
}
|
||||
} catch (authError: any) {
|
||||
console.error('회원가입 오류:', authError);
|
||||
|
||||
// 오류 메시지 처리
|
||||
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
|
||||
|
||||
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
|
||||
if (authError.code === 409) {
|
||||
errorMessage = '이미 등록된 이메일입니다.';
|
||||
} else if (authError.code === 400) {
|
||||
errorMessage = '유효하지 않은 이메일 또는 비밀번호입니다.';
|
||||
} else if (authError.code === 429) {
|
||||
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||
}
|
||||
|
||||
// 사용자 데이터가 없는 경우 (드물게 발생)
|
||||
console.warn('회원가입 응답은 성공했지만 사용자 데이터가 없습니다');
|
||||
showAuthToast('회원가입 성공', '계정이 생성되었습니다. 이메일 인증을 완료한 후 로그인해주세요.', 'default');
|
||||
return {
|
||||
error: null,
|
||||
user: { email },
|
||||
message: '회원가입 완료',
|
||||
emailConfirmationRequired: true
|
||||
};
|
||||
showAuthToast('회원가입 실패', errorMessage, 'destructive');
|
||||
return { error: { message: errorMessage }, user: null };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('회원가입 전역 예외:', error);
|
||||
showAuthToast('회원가입 오류', error.message || '알 수 없는 오류', 'destructive');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabase } from '@/archive/lib/supabase';
|
||||
import { parseResponse, showAuthToast } from '@/utils/auth';
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
|
||||
182
src/hooks/auth/useAppwriteAuth.ts
Normal file
182
src/hooks/auth/useAppwriteAuth.ts
Normal 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;
|
||||
162
src/hooks/transactions/useAppwriteTransactions.ts
Normal file
162
src/hooks/transactions/useAppwriteTransactions.ts
Normal 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;
|
||||
@@ -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
153
src/lib/appwrite/client.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
63
src/lib/appwrite/config.ts
Normal file
63
src/lib/appwrite/config.ts
Normal 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");
|
||||
};
|
||||
28
src/lib/appwrite/defaultUser.ts
Normal file
28
src/lib/appwrite/defaultUser.ts
Normal 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
29
src/lib/appwrite/index.ts
Normal 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
172
src/lib/appwrite/setup.ts
Normal 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
80
src/lib/fullMigrate.js
Executable 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
263
src/lib/migrateData.js
Normal 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
63
src/lib/migrateData.ts
Normal 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);
|
||||
});
|
||||
106
src/main.tsx
106
src/main.tsx
@@ -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(
|
||||
// 전역 오류 핸들러 추가
|
||||
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>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
256
src/pages/AppwriteSettingsPage.tsx
Normal file
256
src/pages/AppwriteSettingsPage.tsx
Normal 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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
86
src/test-appwrite-user.ts
Normal 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
88
src/test-appwrite.ts
Normal 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);
|
||||
});
|
||||
258
src/utils/appwriteTransactionUtils.ts
Normal file
258
src/utils/appwriteTransactionUtils.ts
Normal 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);
|
||||
});
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { getSupabaseUrl } from '@/lib/supabase/config';
|
||||
import { getSupabaseUrl } from '@/archive/lib/supabase/config';
|
||||
|
||||
/**
|
||||
* 기본 서버 연결 상태 검사 유틸리티
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { getSupabaseUrl } from '@/lib/supabase/config';
|
||||
import { getSupabaseUrl } from '@/archive/lib/supabase/config';
|
||||
|
||||
/**
|
||||
* 강화된 서버 연결 검사 - Supabase Cloud 환경에 최적화
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabase } from '@/archive/lib/supabase';
|
||||
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { supabase } from '@/archive/lib/supabase';
|
||||
import { isSyncEnabled } from '../syncSettings';
|
||||
import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker';
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user