Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61492549f6 | ||
|
|
8efa986ed5 | ||
|
|
5305c98970 | ||
|
|
f83bb384af |
9
.env
9
.env
@@ -1,3 +1,4 @@
|
|||||||
|
# Supabase 관련 설정 (이전 버전)
|
||||||
CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres
|
CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres
|
||||||
ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres # On-Prem Postgres 기본
|
ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres # On-Prem Postgres 기본
|
||||||
|
|
||||||
@@ -9,4 +10,12 @@ CLOUD_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ
|
|||||||
ONPREM_SUPABASE_URL=http://localhost:9000
|
ONPREM_SUPABASE_URL=http://localhost:9000
|
||||||
ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||||
ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
|
||||||
|
|
||||||
|
# Appwrite 관련 설정
|
||||||
|
VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=68182a300039f6d700a6
|
||||||
|
VITE_APPWRITE_DATABASE_ID=default
|
||||||
|
VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions
|
||||||
|
VITE_APPWRITE_API_KEY=standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea
|
||||||
|
|
||||||
VITE_DISABLE_LOVABLE_BANNER=true
|
VITE_DISABLE_LOVABLE_BANNER=true
|
||||||
|
|||||||
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 =====
|
|
||||||
@@ -12,9 +12,11 @@ YELLOW='\033[1;33m'
|
|||||||
RED='\033[0;31m'
|
RED='\033[0;31m'
|
||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# 로그 파일 설정
|
# 로그 파일 설정 (log 폴더로 이동)
|
||||||
LOG_FILE="app_build.log"
|
LOG_DIR="log"
|
||||||
ERROR_LOG_FILE="app_error.log"
|
mkdir -p "$LOG_DIR"
|
||||||
|
LOG_FILE="$LOG_DIR/app_build.log"
|
||||||
|
ERROR_LOG_FILE="$LOG_DIR/app_error.log"
|
||||||
|
|
||||||
# 타임아웃 설정 (초 단위)
|
# 타임아웃 설정 (초 단위)
|
||||||
BUILD_TIMEOUT=600 # 10분
|
BUILD_TIMEOUT=600 # 10분
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ if [ -f "$CONFIG_FILE" ]; then
|
|||||||
source "$CONFIG_FILE"
|
source "$CONFIG_FILE"
|
||||||
fi
|
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() {
|
clean_cache() {
|
||||||
echo -e "${YELLOW}캐시 삭제 중...${NC}"
|
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_프로젝트_개요
|
### 00_프로젝트_개요
|
||||||
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
|
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
|
||||||
- `01_프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
|
- `프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
|
||||||
- `02_핵심_문제_정의.md` - 해결하고자 하는 문제 정의 (예정)
|
- `핵심_문제_정의.md` - 해결하고자 하는 문제 정의
|
||||||
- `03_사용자_페르소나.md` - 타겟 사용자 프로필 (예정)
|
- `사용자_페르소나.md` - 타겟 사용자 프로필
|
||||||
- `04_사용자_스토리.md` - 사용자 관점의 요구사항 (예정)
|
|
||||||
- `05_비즈니스_모델.md` - 수익 모델 및 사업화 전략 (예정)
|
|
||||||
- `06_법률_규제_검토.md` - 금융 앱 관련 법규 및 규제 검토 (예정)
|
|
||||||
|
|
||||||
### 01_기획_및_설계
|
### 01_기획_및_설계
|
||||||
프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다.
|
프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다.
|
||||||
- `01_요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
|
- `요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
|
||||||
- `02_MVP_기능_목록.md` - 최소 기능 제품(MVP)의 기능 목록 (예정)
|
- `UI_와이어프레임.md` - 핵심 화면 와이어프레임
|
||||||
- `03_주요_사용_시나리오.md` - 주요 사용 사례 시나리오 (예정)
|
- `사용자_경험_전략.md` - 사용자 경험 설계 전략
|
||||||
- `04_UI_와이어프레임.md` - 핵심 화면 와이어프레임 (예정)
|
|
||||||
- `05_사용자_여정_맵.md` - 사용자 경험 흐름도 (예정)
|
|
||||||
- `06_정보_아키텍처.md` - 앱 구조 및 화면 흐름도 (예정)
|
|
||||||
|
|
||||||
### 02_기술_문서
|
### 02_기술_문서
|
||||||
프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
|
프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
|
||||||
- `01_시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
|
- `시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
|
||||||
- `02_데이터_모델_설계.md` - 데이터 모델 설계 문서 (예정)
|
- `데이터_모델_설계.md` - 데이터베이스 스키마 및 모델 설계
|
||||||
- `03_API_명세서.md` - API 엔드포인트 명세 (예정)
|
- `Appwrite_전환_가이드.md` - Supabase에서 Appwrite로의 전환 가이드
|
||||||
- `04_보안_설계.md` - 보안 및 개인정보 보호 설계 (예정)
|
|
||||||
- `05_성능_최적화_전략.md` - 앱 성능 최적화 전략 (예정)
|
|
||||||
- `06_CI_CD_파이프라인.md` - 지속적 통합/배포 전략 (예정)
|
|
||||||
- `07_AI_ML_구현_전략.md` - AI 기반 소비 패턴 분석 구현 방법 (예정)
|
|
||||||
|
|
||||||
### 03_개발_단계
|
### 03_개발_단계
|
||||||
프로젝트 개발 단계별 문서가 포함되어 있습니다.
|
개발 과정과 관련된 문서가 포함되어 있습니다.
|
||||||
- `01_개발_로드맵.md` - 전체 개발 로드맵 및 일정
|
- `개발_가이드라인.md` - 코드 작성 원칙, iOS/Android 지원, Appwrite 통합 등에 관한 가이드라인
|
||||||
- `02_1단계_개발_계획.md` - 1단계(MVP) 개발 상세 계획 (예정)
|
|
||||||
- `03_테스트_전략.md` - 테스트 방법론 및 계획 (예정)
|
|
||||||
- `04_배포_전략.md` - 배포 및 운영 계획 (예정)
|
|
||||||
- `05_품질_보증_계획.md` - QA 전략 및 테스트 케이스 (예정)
|
|
||||||
- `06_유지보수_전략.md` - 출시 후 유지보수 및 업데이트 계획 (예정)
|
|
||||||
|
|
||||||
### 04_디자인_가이드
|
### archive
|
||||||
UI/UX 디자인 관련 문서가 포함되어 있습니다.
|
더 이상 활발하게 사용되지 않는 레거시 문서들이 보관되어 있습니다.
|
||||||
- `01_디자인_시스템.md` - 디자인 언어 및 컴포넌트 정의 (예정)
|
- `Supabase 관련 문서` - 이전에 사용하던 Supabase 관련 설정 및 가이드
|
||||||
- `02_색상_팔레트.md` - 앱 색상 가이드라인 (예정)
|
- `개발 단계별 문서` - 이전 개발 단계의 계획 및 산출물 요약
|
||||||
- `03_타이포그래피.md` - 폰트 및 텍스트 스타일 가이드 (예정)
|
|
||||||
- `04_아이콘_및_이미지.md` - 아이콘 디자인 및 사용 가이드 (예정)
|
|
||||||
- `05_애니메이션_가이드.md` - UI 애니메이션 및 트랜지션 (예정)
|
|
||||||
- `06_접근성_지침.md` - 접근성 디자인 원칙 (예정)
|
|
||||||
|
|
||||||
### 05_프로젝트_관리
|
## 주요 기술 스택
|
||||||
프로젝트 관리 및 협업 관련 문서가 포함되어 있습니다.
|
|
||||||
- `01_팀_구성.md` - 팀 구성원 및 역할 정의 (예정)
|
|
||||||
- `02_의사결정_프로세스.md` - 프로젝트 의사결정 체계 (예정)
|
|
||||||
- `03_커뮤니케이션_계획.md` - 팀 내 소통 방식 및 도구 (예정)
|
|
||||||
- `04_일정_및_마일스톤.md` - 주요 마일스톤 및 납기일 (예정)
|
|
||||||
- `05_위험_관리.md` - 잠재적 위험 요소 및 대응 계획 (예정)
|
|
||||||
|
|
||||||
### 06_참고자료
|
- **프론트엔드**: React Native, TypeScript
|
||||||
프로젝트 진행에 참고할 수 있는 자료들이 포함되어 있습니다.
|
- **백엔드**: Appwrite
|
||||||
- `01_시장_조사_보고서.md` - 가계부 앱 시장 조사 보고서
|
- **상태 관리**: Context API
|
||||||
- `02_경쟁사_분석.md` - 주요 경쟁 앱 상세 분석 (예정)
|
- **UI 컴포넌트**: Lovable UI
|
||||||
- `03_사용자_인터뷰.md` - 잠재 사용자 인터뷰 결과 (예정)
|
- **네이티브 통합**: Capacitor
|
||||||
- `04_참고_리소스.md` - 유용한 참고 자료 및 링크 (예정)
|
|
||||||
- `05_금융_데이터_소스.md` - 재정 관리 데이터 참고 자료 (예정)
|
|
||||||
- `06_관련_연구_자료.md` - 소비 행동 및 금융 심리학 연구 (예정)
|
|
||||||
|
|
||||||
### 07_마케팅_및_성장
|
## 개발 가이드라인
|
||||||
마케팅 및 사용자 확보 전략 관련 문서가 포함되어 있습니다.
|
|
||||||
- `01_마케팅_전략.md` - 출시 및 사용자 확보 전략 (예정)
|
|
||||||
- `02_ASO_전략.md` - 앱 스토어 최적화 전략 (예정)
|
|
||||||
- `03_콘텐츠_전략.md` - 콘텐츠 마케팅 계획 (예정)
|
|
||||||
- `04_사용자_유지_전략.md` - 사용자 참여 및 유지 방안 (예정)
|
|
||||||
- `05_파트너십_계획.md` - 잠재적 파트너십 및 협업 기회 (예정)
|
|
||||||
|
|
||||||
## 주요 기능
|
개발 가이드라인은 [03_개발_단계/개발_가이드라인.md](./03_개발_단계/개발_가이드라인.md) 문서를 참조하세요. 이 문서에는 다음 내용이 포함되어 있습니다:
|
||||||
|
|
||||||
1. **수입/지출 기록**: 간편한 UI로 일상 재정 활동 기록
|
1. 코드 작성 원칙
|
||||||
2. **카테고리 관리**: 사용자 정의 카테고리로 지출 분류
|
2. 트랜잭션 삭제 안전성
|
||||||
3. **예산 설정**: 카테고리별 월간/주간 예산 설정 및 알림
|
3. Appwrite 통합 원칙
|
||||||
4. **지출 분석**: 차트와 그래프로 소비 패턴 시각화
|
4. 상태 관리 최적화
|
||||||
5. **AI 기반 분석**: 소비 패턴 분석 및 맞춤형 절약 제안
|
5. iOS/Android 지원
|
||||||
6. **절약 챌린지**: 사용자 맞춤형 절약 목표 설정 및 달성 보상
|
6. 디버깅 및 로깅
|
||||||
7. **재정 건강 점수**: 사용자의 재정 상태를 점수화하여 개선 동기 부여
|
|
||||||
8. **구독 관리**: 정기 구독 서비스 추적 및 최적화 제안
|
|
||||||
9. **재정 목표 설정**: 단기/중기/장기 저축 목표 설정 및 진행 상황 추적
|
|
||||||
10. **알림 시스템**: 예산 초과, 주요 지출, 절약 기회에 대한 스마트 알림
|
|
||||||
11. **가계부 보고서**: 정기적인 재정 상태 요약 보고서 제공
|
|
||||||
12. **공유 기능**: 가족 또는 파트너와 특정 재정 정보 공유
|
|
||||||
|
|
||||||
## 기술 스택
|
## Appwrite 전환
|
||||||
|
|
||||||
- **프론트엔드**: React, Vite, Tailwind CSS, Capacitor
|
Supabase에서 Appwrite로의 전환에 관한 상세 정보는 [02_기술_문서/Appwrite_전환_가이드.md](./02_기술_문서/Appwrite_전환_가이드.md) 문서를 참조하세요.
|
||||||
- **백엔드**: Node.js, Express, Supabase(PostgreSQL)
|
|
||||||
- **AI/ML**: TensorFlow, Python
|
|
||||||
- **클라우드**: Supabase On-Premise
|
|
||||||
- **데이터 시각화**: D3.js, Chart.js
|
|
||||||
- **인증/보안**: JWT, OAuth 2.0, 데이터 암호화
|
|
||||||
- **테스트**: Jest, Cypress
|
|
||||||
- **CI/CD**: GitHub Actions
|
|
||||||
- **분석**: Supabase Analytics
|
|
||||||
|
|
||||||
## 문서 작성 가이드라인
|
|
||||||
- 모든 문서는 마크다운(.md) 형식으로 작성합니다.
|
|
||||||
- 파일명은 내용을 명확히 나타내는 한글 또는 영문으로 작성합니다.
|
|
||||||
- 이미지나 다이어그램은 가능한 마크다운 내에 포함시킵니다.
|
|
||||||
- 문서 간 연결이 필요한 경우 상대 경로를 사용하여 링크합니다.
|
|
||||||
- 코드 예시는 적절한 구문 강조와 함께 코드 블록으로 포함합니다.
|
|
||||||
- 변경 사항은 문서 하단의 업데이트 이력에 기록합니다.
|
|
||||||
- 중요 결정사항은 의사결정 배경과 함께 기록합니다.
|
|
||||||
|
|
||||||
## 개발 워크플로우
|
|
||||||
1. **기능 기획**: 사용자 스토리 및 요구사항 정의
|
|
||||||
2. **설계**: UI/UX 디자인 및 기술 아키텍처 설계
|
|
||||||
3. **개발**: 기능 구현 및 단위 테스트
|
|
||||||
4. **코드 리뷰**: 팀원 간 코드 품질 검토
|
|
||||||
5. **테스트**: QA 및 사용성 테스트
|
|
||||||
6. **배포**: 스테이징 및 프로덕션 환경 배포
|
|
||||||
7. **모니터링**: 성능 및 사용자 피드백 모니터링
|
|
||||||
8. **반복**: 피드백을 바탕으로 기능 개선
|
|
||||||
|
|
||||||
## 출시 계획
|
|
||||||
- **알파 버전**: 내부 테스트 (2024년 4월 초)
|
|
||||||
- **베타 버전**: 제한적 사용자 테스트 (2024년 4월 중순)
|
|
||||||
- **MVP 출시**: 앱스토어 및 플레이스토어 공개 (2024년 4월 말)
|
|
||||||
- **기능 업데이트**: 사용자 피드백 기반 주요 기능 추가 (2024년 5월 초)
|
|
||||||
- **확장 계획**: 웹 버전 및 추가 기능 확장 (2024년 5월 중순부터)
|
|
||||||
|
|
||||||
## 업데이트 이력
|
|
||||||
- 2024-03-15: 프로젝트 문서 초기 구성 완료
|
|
||||||
- 2024-03-15: 프로젝트 소개, 요구사항 분석, 시스템 아키텍처, 개발 로드맵, 시장 조사 보고서 추가
|
|
||||||
- 2024-04-01: 폴더 구조 개선 및 추가 섹션(디자인 가이드, 프로젝트 관리, 마케팅) 추가
|
|
||||||
- 2024-04-05: 일정 조정 - 모든 개발 계획을 4월 말까지 완료하도록 수정
|
|
||||||
- 2025-03-09: 개발 방법 변경 - Flutter에서 React, Tailwind CSS, Capacitor 기반 웹 앱으로 전환, Lovable UI 컴포넌트 스타일 적용
|
|
||||||
- 2025-03-09: 데이터베이스 변경 - MongoDB에서 Supabase(PostgreSQL) On-Premise로 전환
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -45,6 +45,7 @@
|
|||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"appwrite": "^17.0.2",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -3832,6 +3833,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/appwrite": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/appwrite/-/appwrite-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-h8frLDRYzFDLS9xA2s8ZSlH/prPFq/ma5477fgQHHLcE/t9RDxNImpq9AleRUb9Oh1YJiP49HCObxgSTGW5AQA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/arg": {
|
"node_modules/arg": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"@supabase/supabase-js": "^2.49.4",
|
"@supabase/supabase-js": "^2.49.4",
|
||||||
"@tanstack/react-query": "^5.56.2",
|
"@tanstack/react-query": "^5.56.2",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
"appwrite": "^17.0.2",
|
||||||
"browserslist": "^4.24.4",
|
"browserslist": "^4.24.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
166
src/App.tsx
166
src/App.tsx
@@ -1,5 +1,4 @@
|
|||||||
|
import React, { useEffect, useState, Suspense, Component, ErrorInfo, ReactNode } from 'react';
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Routes, Route } from 'react-router-dom';
|
import { Routes, Route } from 'react-router-dom';
|
||||||
import { BudgetProvider } from './contexts/budget/BudgetContext';
|
import { BudgetProvider } from './contexts/budget/BudgetContext';
|
||||||
import { AuthProvider } from './contexts/auth/AuthProvider';
|
import { AuthProvider } from './contexts/auth/AuthProvider';
|
||||||
@@ -17,35 +16,152 @@ import HelpSupport from './pages/HelpSupport';
|
|||||||
import SecurityPrivacySettings from './pages/SecurityPrivacySettings';
|
import SecurityPrivacySettings from './pages/SecurityPrivacySettings';
|
||||||
import NotificationSettings from './pages/NotificationSettings';
|
import NotificationSettings from './pages/NotificationSettings';
|
||||||
import ForgotPassword from './pages/ForgotPassword';
|
import ForgotPassword from './pages/ForgotPassword';
|
||||||
|
import AppwriteSettingsPage from './pages/AppwriteSettingsPage';
|
||||||
|
|
||||||
|
// 간단한 오류 경계 컴포넌트 구현
|
||||||
|
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() {
|
function App() {
|
||||||
|
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const [appwriteEnabled, setAppwriteEnabled] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
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 (
|
return (
|
||||||
<AuthProvider>
|
<ErrorBoundary fallback={<ErrorScreen error={error} retry={handleRetry} />}>
|
||||||
<BudgetProvider>
|
<AuthProvider>
|
||||||
<div className="App">
|
<BudgetProvider>
|
||||||
<Routes>
|
<BasicLayout>
|
||||||
<Route path="/" element={<Index />} />
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/" element={<Index />} />
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/login" element={<Login />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/transactions" element={<Transactions />} />
|
<Route path="/settings" element={<Settings />} />
|
||||||
<Route path="/analytics" element={<Analytics />} />
|
<Route path="/transactions" element={<Transactions />} />
|
||||||
<Route path="/profile" element={<ProfileManagement />} />
|
<Route path="/analytics" element={<Analytics />} />
|
||||||
<Route path="/payment-methods" element={<PaymentMethods />} />
|
<Route path="/profile" element={<ProfileManagement />} />
|
||||||
<Route path="/help-support" element={<HelpSupport />} />
|
<Route path="/payment-methods" element={<PaymentMethods />} />
|
||||||
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
<Route path="/help-support" element={<HelpSupport />} />
|
||||||
<Route path="/notifications" element={<NotificationSettings />} />
|
<Route path="/security-privacy" element={<SecurityPrivacySettings />} />
|
||||||
<Route path="/forgot-password" element={<ForgotPassword />} />
|
<Route path="/notifications" element={<NotificationSettings />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="/forgot-password" element={<ForgotPassword />} />
|
||||||
</Routes>
|
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
|
||||||
<Toaster />
|
<Route path="*" element={<NotFound />} />
|
||||||
</div>
|
</Routes>
|
||||||
</BudgetProvider>
|
</BasicLayout>
|
||||||
</AuthProvider>
|
</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;
|
||||||
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 : '알 수 없는 오류'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -4,7 +4,7 @@ import { PlusIcon } from 'lucide-react';
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||||
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용
|
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용
|
||||||
import { useBudget } from '@/contexts/budget/BudgetContext';
|
import { useBudget } from '@/contexts/budget/BudgetContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils';
|
import { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils';
|
||||||
import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm';
|
import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm';
|
||||||
import { Transaction } from '@/contexts/budget/types';
|
import { Transaction } from '@/contexts/budget/types';
|
||||||
|
|||||||
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 { ServerConnectionStatus } from "./types";
|
||||||
import EmailConfirmation from "./EmailConfirmation";
|
import EmailConfirmation from "./EmailConfirmation";
|
||||||
import RegisterFormFields from "./RegisterFormFields";
|
import RegisterFormFields from "./RegisterFormFields";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/archive/lib/supabase";
|
||||||
|
|
||||||
interface RegisterFormProps {
|
interface RegisterFormProps {
|
||||||
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>;
|
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>;
|
||||||
|
|||||||
@@ -1,17 +1,59 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import { supabase } from '@/lib/supabase';
|
|
||||||
import { Session, User } from '@supabase/supabase-js';
|
|
||||||
import { toast } from '@/hooks/useToast.wrapper';
|
import { toast } from '@/hooks/useToast.wrapper';
|
||||||
import { AuthContextType } from './types';
|
import { AuthContextType } from './types';
|
||||||
import * as authActions from './authActions';
|
import * as authActions from './authActions';
|
||||||
import { clearAllToasts } from '@/hooks/toast/toastManager';
|
import { clearAllToasts } from '@/hooks/toast/toastManager';
|
||||||
import { AuthContext } from './AuthContext';
|
import { AuthContext } from './AuthContext';
|
||||||
|
import { account, 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 }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Models.Session | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<Models.User<Models.Preferences> | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
// 현재 세션 체크 - 최적화된 버전
|
// 현재 세션 체크 - 최적화된 버전
|
||||||
@@ -22,22 +64,67 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
|
|
||||||
const { data, error } = await supabase.auth.getSession();
|
// Appwrite 초기화 상태 확인
|
||||||
|
const isInitialized = await checkAppwriteInitialization();
|
||||||
if (error) {
|
if (!isInitialized) {
|
||||||
console.error('세션 로딩 중 오류:', error);
|
console.warn('Appwrite 초기화 상태가 정상적이지 않습니다. 비로그인 상태로 처리합니다.');
|
||||||
} else if (data.session) {
|
|
||||||
// 상태 업데이트를 마이크로태스크로 지연
|
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
setSession(data.session);
|
setSession(null);
|
||||||
setUser(data.session.user);
|
setUser(null);
|
||||||
console.log('세션 로딩 완료');
|
setLoading(false);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 사용자 정보 가져오기 시도 - 안전한 방식으로 처리
|
||||||
|
try {
|
||||||
|
// 사용자 정보 가져오기 시도
|
||||||
|
const currentUser = await account.get().catch(err => {
|
||||||
|
// 401 오류는 비로그인 상태로 정상적인 경우
|
||||||
|
if (err && (err as any).code === 401) {
|
||||||
|
console.log('사용자 정보 가져오기 실패, 비로그인 상태로 간주');
|
||||||
|
} else {
|
||||||
|
console.error('사용자 정보 가져오기 오류:', err);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentUser) {
|
||||||
|
// 사용자 정보가 있으면 세션 정보 가져오기 시도
|
||||||
|
const currentSession = await account.getSession('current').catch(err => {
|
||||||
|
console.log('세션 정보 가져오기 실패:', err);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 상태 업데이트를 마이크로태스크로 지연
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setUser(currentUser);
|
||||||
|
setSession(currentSession);
|
||||||
|
console.log('세션 로딩 완료 - 사용자:', currentUser.$id);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 사용자 정보가 없으면 비로그인 상태로 처리
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setSession(null);
|
||||||
|
setUser(null);
|
||||||
|
console.log('비로그인 상태로 처리');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 예상치 못한 오류 처리
|
||||||
|
console.error('세션 처리 중 예상치 못한 오류:', error);
|
||||||
|
handleAuthError(error);
|
||||||
|
|
||||||
|
// 오류 발생 시 로그아웃 상태로 처리
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setSession(null);
|
||||||
|
setUser(null);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.log('활성 세션 없음');
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('세션 확인 중 예외 발생:', error);
|
// 최상위 예외 처리
|
||||||
|
console.error('세션 확인 중 최상위 예외 발생:', error);
|
||||||
|
handleAuthError(error);
|
||||||
} finally {
|
} finally {
|
||||||
// 로딩 상태 업데이트를 마이크로태스크로 지연
|
// 로딩 상태 업데이트를 마이크로태스크로 지연
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -46,27 +133,64 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 초기 세션 로딩 - 약간 지연시켜 UI 렌더링 우선시
|
// 초기 세션 로딩 - 약간 지연시케 UI 렌더링 우선시
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getSession();
|
getSession();
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// auth 상태 변경 리스너 - 최적화된 버전
|
// Appwrite 인증 상태 변경 리스너 설정
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
// 참고: Appwrite는 직접적인 이벤트 리스너를 제공하지 않으므로 주기적으로 확인하는 방식 사용
|
||||||
async (event, session) => {
|
const authCheckInterval = setInterval(async () => {
|
||||||
console.log('Supabase auth 이벤트:', event);
|
// 오류가 발생해도 애플리케이션이 중단되지 않도록 try-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 차단 방지
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
|
|
||||||
if (session) {
|
// 사용자 정보가 있고, 이전과 다르면 세션 정보 가져오기
|
||||||
// 상태 업데이트를 마이크로태스크로 지연
|
if (currentUser && (!user || currentUser.$id !== user.$id)) {
|
||||||
queueMicrotask(() => {
|
try {
|
||||||
setSession(session);
|
// 세션 정보 가져오기 시도 - 안전하게 처리
|
||||||
setUser(session.user);
|
const currentSession = await account.getSession('current').catch(err => {
|
||||||
});
|
console.log('세션 정보 가져오기 실패:', err);
|
||||||
} else if (event === 'SIGNED_OUT') {
|
return null;
|
||||||
// 상태 업데이트를 마이크로태스크로 지연
|
});
|
||||||
|
|
||||||
|
// 상태 업데이트를 마이크로태스크로 지연
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setUser(currentUser);
|
||||||
|
setSession(currentSession);
|
||||||
|
console.log('Appwrite 인증 상태 변경: 로그인됨 - 사용자:', currentUser.$id);
|
||||||
|
});
|
||||||
|
} catch (sessionError) {
|
||||||
|
console.error('세션 정보 가져오기 중 오류:', sessionError);
|
||||||
|
// 오류 발생해도 사용자 정보는 업데이트
|
||||||
|
queueMicrotask(() => {
|
||||||
|
setUser(currentUser);
|
||||||
|
setSession(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!currentUser && user) {
|
||||||
|
// 이전에는 사용자 정보가 있었지만 지금은 없는 경우 (로그아웃 상태)
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
@@ -76,20 +200,26 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
|
|||||||
|
|
||||||
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
|
// 로그아웃 이벤트 발생시켜 SyncSettings 등에서 감지하도록 함
|
||||||
window.dispatchEvent(new Event('auth-state-changed'));
|
window.dispatchEvent(new Event('auth-state-changed'));
|
||||||
|
console.log('Appwrite 인증 상태 변경: 로그아웃됨');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// 로딩 상태 업데이트를 마이크로태스크로 지연
|
// 예상치 못한 오류 발생 시에도 애플리케이션이 중단되지 않도록 처리
|
||||||
queueMicrotask(() => {
|
console.error('Appwrite 인증 상태 검사 중 예상치 못한 오류:', error);
|
||||||
setLoading(false);
|
handleAuthError(error);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
}, 5000); // 5초마다 확인
|
||||||
|
|
||||||
// 리스너 정리
|
// 리스너 정리
|
||||||
return () => {
|
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,
|
session,
|
||||||
user,
|
user,
|
||||||
loading,
|
loading,
|
||||||
|
error,
|
||||||
|
appwriteInitialized,
|
||||||
|
reinitializeAppwrite,
|
||||||
signIn: authActions.signIn,
|
signIn: authActions.signIn,
|
||||||
signUp: authActions.signUp,
|
signUp: authActions.signUp,
|
||||||
signOut: authActions.signOut,
|
signOut: authActions.signOut,
|
||||||
resetPassword: authActions.resetPassword,
|
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 { account } from '@/lib/appwrite/client';
|
||||||
import { handleNetworkError, showAuthToast } from '@/utils/auth';
|
import { showAuthToast } from '@/utils/auth';
|
||||||
|
|
||||||
export const resetPassword = async (email: string) => {
|
export const resetPassword = async (email: string) => {
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
console.log('비밀번호 재설정 시도 중:', email);
|
||||||
redirectTo: window.location.origin + '/reset-password',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
console.error('비밀번호 재설정 오류:', error);
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
showAuthToast('비밀번호 재설정 실패', error.message, 'destructive');
|
|
||||||
return { error };
|
try {
|
||||||
|
// Appwrite로 비밀번호 재설정 이메일 발송
|
||||||
|
await account.createRecovery(
|
||||||
|
email,
|
||||||
|
window.location.origin + '/reset-password'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
|
|
||||||
|
showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.');
|
||||||
|
return { error: null };
|
||||||
|
} catch (recoveryError: any) {
|
||||||
|
console.error('비밀번호 재설정 이메일 전송 오류:', recoveryError);
|
||||||
|
|
||||||
|
// 오류 메시지 처리
|
||||||
|
let errorMessage = recoveryError.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
|
||||||
|
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
|
||||||
|
if (recoveryError.code === 404) {
|
||||||
|
errorMessage = '등록되지 않은 이메일입니다.';
|
||||||
|
} else if (recoveryError.code === 429) {
|
||||||
|
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showAuthToast('비밀번호 재설정 실패', errorMessage, 'destructive');
|
||||||
|
return { error: recoveryError };
|
||||||
}
|
}
|
||||||
|
|
||||||
showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.');
|
|
||||||
return { error: null };
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('비밀번호 재설정 중 예외 발생:', error);
|
console.error('비밀번호 재설정 중 예외 발생:', error);
|
||||||
|
|
||||||
// 네트워크 오류 확인
|
// 네트워크 오류 확인
|
||||||
const errorMessage = handleNetworkError(error);
|
const errorMessage = error.message && error.message.includes('network')
|
||||||
|
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
|
||||||
|
: '예상치 못한 오류가 발생했습니다.';
|
||||||
|
|
||||||
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
|
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
|
||||||
return { error };
|
return { error };
|
||||||
|
|||||||
@@ -1,45 +1,78 @@
|
|||||||
|
import { account } from '@/lib/appwrite/client';
|
||||||
import { supabase } from '@/lib/supabase';
|
|
||||||
import { showAuthToast } from '@/utils/auth';
|
import { showAuthToast } from '@/utils/auth';
|
||||||
|
import { getDefaultUserId } from '@/lib/appwrite/defaultUser';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 로그인 기능 - Supabase Cloud 환경에 최적화
|
* 로그인 기능 - Appwrite 환경에 최적화
|
||||||
*/
|
*/
|
||||||
export const signIn = async (email: string, password: string) => {
|
export const signIn = async (email: string, password: string) => {
|
||||||
try {
|
try {
|
||||||
console.log('로그인 시도 중:', email);
|
console.log('로그인 시도 중:', email);
|
||||||
|
|
||||||
// Supabase 인증 방식 시도
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
const { data, error } = await supabase.auth.signInWithPassword({
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
email,
|
|
||||||
password
|
// Appwrite 인증 방식 시도
|
||||||
});
|
try {
|
||||||
|
const session = await account.createSession(email, password);
|
||||||
|
const user = await account.get();
|
||||||
|
|
||||||
|
// 상태 업데이트를 마이크로태스크로 지연
|
||||||
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
|
|
||||||
if (!error && data.user) {
|
|
||||||
showAuthToast('로그인 성공', '환영합니다!');
|
showAuthToast('로그인 성공', '환영합니다!');
|
||||||
return { error: null, user: data.user };
|
return { error: null, user };
|
||||||
} else if (error) {
|
} catch (authError: any) {
|
||||||
console.error('로그인 오류:', error.message);
|
console.error('로그인 오류:', authError);
|
||||||
|
|
||||||
let errorMessage = error.message;
|
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
if (error.message.includes('Invalid login credentials')) {
|
let fallbackMode = false;
|
||||||
|
|
||||||
|
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
|
||||||
|
if (authError.code === 401) {
|
||||||
errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
|
errorMessage = '이메일 또는 비밀번호가 올바르지 않습니다.';
|
||||||
} else if (error.message.includes('Email not confirmed')) {
|
} else if (authError.code === 429) {
|
||||||
errorMessage = '이메일 인증이 완료되지 않았습니다. 이메일을 확인해주세요.';
|
errorMessage = '너무 많은 로그인 시도가 있었습니다. 잠시 후 다시 시도해주세요.';
|
||||||
|
} else if (authError.code === 404 || authError.code === 503) {
|
||||||
|
// 서버 연결 문제인 경우 기본 사용자 ID를 활용한 대체 로직 시도
|
||||||
|
errorMessage = '서버 연결에 문제가 있어 일반 모드로 접속합니다.';
|
||||||
|
fallbackMode = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 기본 사용자 ID를 활용한 대체 로직
|
||||||
|
const defaultUserId = getDefaultUserId();
|
||||||
|
console.log('기본 사용자 ID를 활용한 대체 로직 시도:', defaultUserId);
|
||||||
|
|
||||||
|
// 일반 모드로 접속하는 경우 사용자에게 알림
|
||||||
|
showAuthToast('일반 모드 접속', '일반 모드로 접속합니다. 일부 기능이 제한될 수 있습니다.', 'default');
|
||||||
|
|
||||||
|
// 기본 사용자 정보를 가진 가상의 사용자 객체 생성
|
||||||
|
const fallbackUser = {
|
||||||
|
$id: defaultUserId,
|
||||||
|
name: '일반 사용자',
|
||||||
|
email: email,
|
||||||
|
$createdAt: new Date().toISOString(),
|
||||||
|
$updatedAt: new Date().toISOString(),
|
||||||
|
status: true,
|
||||||
|
isFallbackUser: true // 기본 사용자임을 표시하는 플래그
|
||||||
|
};
|
||||||
|
|
||||||
|
return { error: null, user: fallbackUser, isFallbackMode: true };
|
||||||
|
} catch (fallbackError) {
|
||||||
|
console.error('기본 사용자 대체 로직 오류:', fallbackError);
|
||||||
|
// 대체 로직도 실패한 경우 원래 오류 반환
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showAuthToast('로그인 실패', errorMessage, 'destructive');
|
if (!fallbackMode) {
|
||||||
return { error: { message: errorMessage }, user: null };
|
showAuthToast('로그인 실패', errorMessage, 'destructive');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: authError, user: null };
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
// 여기까지 왔다면 오류가 발생한 것
|
console.error('로그인 예외 발생:', error);
|
||||||
showAuthToast('로그인 실패', '로그인 처리 중 오류가 발생했습니다.', 'destructive');
|
showAuthToast('로그인 오류', '서버 연결 중 오류가 발생했습니다.', 'destructive');
|
||||||
return { error: { message: '로그인 처리 중 오류가 발생했습니다.' }, user: null };
|
return { error, user: null };
|
||||||
} catch (error: any) {
|
|
||||||
console.error('로그인 중 예외 발생:', error);
|
|
||||||
const errorMessage = error.message || '로그인 중 오류가 발생했습니다.';
|
|
||||||
|
|
||||||
showAuthToast('로그인 오류', errorMessage, 'destructive');
|
|
||||||
return { error: { message: errorMessage }, user: null };
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { showAuthToast } from '@/utils/auth';
|
import { showAuthToast } from '@/utils/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
import { supabase } from '@/lib/supabase';
|
import { account } from '@/lib/appwrite/client';
|
||||||
import { showAuthToast } from '@/utils/auth';
|
import { showAuthToast } from '@/utils/auth';
|
||||||
|
import { clearAllToasts } from '@/hooks/toast/toastManager';
|
||||||
|
|
||||||
export const signOut = async (): Promise<void> => {
|
export const signOut = async (): Promise<void> => {
|
||||||
try {
|
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('로그아웃 성공', '다음에 또 만나요!');
|
showAuthToast('로그아웃 성공', '다음에 또 만나요!');
|
||||||
|
} catch (sessionError: any) {
|
||||||
|
console.error('세션 삭제 중 오류:', sessionError);
|
||||||
|
|
||||||
|
// 오류 메시지 생성
|
||||||
|
let errorMessage = sessionError.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
|
||||||
|
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
|
||||||
|
if (sessionError.code === 401) {
|
||||||
|
errorMessage = '이미 로그아웃되었습니다.';
|
||||||
|
}
|
||||||
|
|
||||||
|
showAuthToast('로그아웃 실패', errorMessage, 'destructive');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('로그아웃 중 예외 발생:', error);
|
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 { account, client } from '@/lib/appwrite/client';
|
||||||
import { showAuthToast, verifyServerConnection } from '@/utils/auth';
|
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) => {
|
export const signUp = async (email: string, password: string, username: string) => {
|
||||||
try {
|
try {
|
||||||
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
|
|
||||||
// 서버 연결 상태 확인
|
// 서버 연결 상태 확인
|
||||||
const connectionStatus = await verifyServerConnection();
|
const connected = await isValidConnection();
|
||||||
if (!connectionStatus.connected) {
|
if (!connected) {
|
||||||
console.error('서버 연결 실패:', connectionStatus.message);
|
console.error('서버 연결 실패');
|
||||||
showAuthToast('회원가입 오류', `서버 연결 실패: ${connectionStatus.message}`, 'destructive');
|
showAuthToast('회원가입 오류', '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.', 'destructive');
|
||||||
return { error: { message: connectionStatus.message }, user: null };
|
return { error: { message: '서버 연결 실패' }, user: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('회원가입 시도:', email);
|
console.log('회원가입 시도:', email);
|
||||||
|
|
||||||
// 현재 브라우저 URL 가져오기
|
try {
|
||||||
const currentUrl = window.location.origin;
|
// Appwrite로 회원가입 요청
|
||||||
const redirectUrl = `${currentUrl}/login?auth_callback=true`;
|
const user = await account.create(
|
||||||
|
ID.unique(),
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
username
|
||||||
|
);
|
||||||
|
|
||||||
console.log('이메일 인증 리디렉션 URL:', redirectUrl);
|
// 이메일 인증 메일 발송
|
||||||
|
await account.createVerification(window.location.origin + '/login');
|
||||||
|
|
||||||
// 회원가입 요청
|
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
|
||||||
const { data, error } = await supabase.auth.signUp({
|
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
|
||||||
email,
|
|
||||||
password,
|
|
||||||
options: {
|
|
||||||
data: {
|
|
||||||
username, // 사용자 이름을 메타데이터에 저장
|
|
||||||
},
|
|
||||||
emailRedirectTo: redirectUrl
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default');
|
||||||
console.error('회원가입 오류:', error);
|
console.log('인증 메일 발송됨:', email);
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: null,
|
||||||
|
user,
|
||||||
|
message: '이메일 인증 필요',
|
||||||
|
emailConfirmationRequired: true
|
||||||
|
};
|
||||||
|
} catch (authError: any) {
|
||||||
|
console.error('회원가입 오류:', authError);
|
||||||
|
|
||||||
// 오류 메시지 처리
|
// 오류 메시지 처리
|
||||||
let errorMessage = error.message;
|
let errorMessage = authError.message || '알 수 없는 오류가 발생했습니다.';
|
||||||
|
|
||||||
if (error.message.includes('User already registered')) {
|
// Appwrite 오류 코드에 따른 사용자 친화적 메시지
|
||||||
errorMessage = '이미 등록된 사용자입니다.';
|
if (authError.code === 409) {
|
||||||
} else if (error.message.includes('Signup not allowed')) {
|
errorMessage = '이미 등록된 이메일입니다.';
|
||||||
errorMessage = '회원가입이 허용되지 않습니다.';
|
} else if (authError.code === 400) {
|
||||||
} else if (error.message.includes('Email link invalid')) {
|
errorMessage = '유효하지 않은 이메일 또는 비밀번호입니다.';
|
||||||
errorMessage = '이메일 링크가 유효하지 않습니다.';
|
} else if (authError.code === 429) {
|
||||||
|
errorMessage = '너무 많은 요청이 발생했습니다. 잠시 후 다시 시도해주세요.';
|
||||||
}
|
}
|
||||||
|
|
||||||
showAuthToast('회원가입 실패', errorMessage, 'destructive');
|
showAuthToast('회원가입 실패', errorMessage, 'destructive');
|
||||||
return { error: { message: errorMessage }, user: null };
|
return { error: { message: errorMessage }, user: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 회원가입 성공
|
|
||||||
if (data && data.user) {
|
|
||||||
// 이메일 확인이 필요한지 확인
|
|
||||||
const isEmailConfirmationRequired = data.user.identities &&
|
|
||||||
data.user.identities.length > 0 &&
|
|
||||||
!data.user.identities[0].identity_data?.email_verified;
|
|
||||||
|
|
||||||
if (isEmailConfirmationRequired) {
|
|
||||||
showAuthToast('회원가입 성공', '인증 메일이 발송되었습니다. 스팸 폴더도 확인해주세요.', 'default');
|
|
||||||
console.log('인증 메일 발송됨:', email);
|
|
||||||
|
|
||||||
return {
|
|
||||||
error: null,
|
|
||||||
user: data.user,
|
|
||||||
message: '이메일 인증 필요',
|
|
||||||
emailConfirmationRequired: true
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
showAuthToast('회원가입 성공', '환영합니다!', 'default');
|
|
||||||
return { error: null, user: data.user };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 사용자 데이터가 없는 경우 (드물게 발생)
|
|
||||||
console.warn('회원가입 응답은 성공했지만 사용자 데이터가 없습니다');
|
|
||||||
showAuthToast('회원가입 성공', '계정이 생성되었습니다. 이메일 인증을 완료한 후 로그인해주세요.', 'default');
|
|
||||||
return {
|
|
||||||
error: null,
|
|
||||||
user: { email },
|
|
||||||
message: '회원가입 완료',
|
|
||||||
emailConfirmationRequired: true
|
|
||||||
};
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('회원가입 전역 예외:', error);
|
console.error('회원가입 전역 예외:', error);
|
||||||
showAuthToast('회원가입 오류', error.message || '알 수 없는 오류', 'destructive');
|
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';
|
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 = {
|
export type AuthContextType = {
|
||||||
session: Session | null;
|
session: Models.Session | null;
|
||||||
user: User | null;
|
user: Models.User<Models.Preferences> | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
appwriteInitialized: boolean;
|
||||||
|
reinitializeAppwrite: () => AppwriteInitializationStatus;
|
||||||
signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>;
|
signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>;
|
||||||
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>;
|
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>;
|
||||||
signOut: () => Promise<void>;
|
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 { useState } from "react";
|
||||||
import { useToast } from "@/hooks/useToast.wrapper";
|
import { useToast } from "@/hooks/useToast.wrapper";
|
||||||
import { createRequiredTables } from "@/lib/supabase/setup";
|
import { createRequiredTables } from "@/archive/lib/supabase/setup";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supabase 테이블 설정을 처리하는 커스텀 훅
|
* Supabase 테이블 설정을 처리하는 커스텀 훅
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
114
src/main.tsx
114
src/main.tsx
@@ -1,9 +1,10 @@
|
|||||||
|
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
|
console.log('main.tsx loaded');
|
||||||
|
|
||||||
// iOS 안전 영역 메타 태그 추가
|
// iOS 안전 영역 메타 태그 추가
|
||||||
const setViewportMetaTag = () => {
|
const setViewportMetaTag = () => {
|
||||||
// 기존 viewport 메타 태그 찾기
|
// 기존 viewport 메타 태그 찾기
|
||||||
@@ -23,9 +24,108 @@ const setViewportMetaTag = () => {
|
|||||||
// 메타 태그 설정 적용
|
// 메타 태그 설정 적용
|
||||||
setViewportMetaTag();
|
setViewportMetaTag();
|
||||||
|
|
||||||
// 앱 렌더링 - BrowserRouter로 감싸기
|
// 전역 오류 핸들러 추가
|
||||||
createRoot(document.getElementById("root")!).render(
|
window.onerror = function(message, source, lineno, colno, error) {
|
||||||
<BrowserRouter>
|
console.error('전역 오류 발생:', { message, source, lineno, colno, error });
|
||||||
<App />
|
|
||||||
</BrowserRouter>
|
// 오류 발생 시 기본 오류 화면 표시
|
||||||
);
|
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 NavBar from '@/components/NavBar';
|
||||||
import AddTransactionButton from '@/components/AddTransactionButton';
|
import AddTransactionButton from '@/components/AddTransactionButton';
|
||||||
import WelcomeDialog from '@/components/onboarding/WelcomeDialog';
|
import WelcomeDialog from '@/components/onboarding/WelcomeDialog';
|
||||||
@@ -11,6 +11,8 @@ import SafeAreaContainer from '@/components/SafeAreaContainer';
|
|||||||
import { useInitialDataLoading } from '@/hooks/useInitialDataLoading';
|
import { useInitialDataLoading } from '@/hooks/useInitialDataLoading';
|
||||||
import { useAppFocusEvents } from '@/hooks/useAppFocusEvents';
|
import { useAppFocusEvents } from '@/hooks/useAppFocusEvents';
|
||||||
import { useWelcomeNotification } from '@/hooks/useWelcomeNotification';
|
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 { resetBudgetData } = useBudget();
|
||||||
const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog();
|
const { showWelcome, checkWelcomeDialogState, handleCloseWelcome } = useWelcomeDialog();
|
||||||
const { isInitialized } = useDataInitialization(resetBudgetData);
|
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();
|
useInitialDataLoading();
|
||||||
useAppFocusEvents();
|
useAppFocusEvents();
|
||||||
useWelcomeNotification(isInitialized);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isInitialized) {
|
if (isInitialized && appState === 'ready') {
|
||||||
const timeoutId = setTimeout(checkWelcomeDialogState, 500);
|
const timeoutId = setTimeout(checkWelcomeDialogState, 500);
|
||||||
return () => clearTimeout(timeoutId);
|
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 (
|
return (
|
||||||
<SafeAreaContainer className="min-h-screen bg-neuro-background pb-24" extraBottomPadding={true}>
|
<SafeAreaContainer className="min-h-screen bg-neuro-background pb-24" extraBottomPadding={true}>
|
||||||
<IndexContent />
|
<IndexContent />
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import RegisterForm from "@/components/auth/RegisterForm";
|
|||||||
import LoginLink from "@/components/auth/LoginLink";
|
import LoginLink from "@/components/auth/LoginLink";
|
||||||
import ServerStatusAlert from "@/components/auth/ServerStatusAlert";
|
import ServerStatusAlert from "@/components/auth/ServerStatusAlert";
|
||||||
import TestConnectionSection from "@/components/auth/TestConnectionSection";
|
import TestConnectionSection from "@/components/auth/TestConnectionSection";
|
||||||
import SupabaseConnectionStatus from "@/components/auth/SupabaseConnectionStatus";
|
import SupabaseConnectionStatus from "@/archive/components/SupabaseConnectionStatus";
|
||||||
import RegisterErrorDisplay from "@/components/auth/RegisterErrorDisplay";
|
import RegisterErrorDisplay from "@/components/auth/RegisterErrorDisplay";
|
||||||
import { ServerConnectionStatus } from "@/components/auth/types";
|
import { ServerConnectionStatus } from "@/components/auth/types";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import NavBar from '@/components/NavBar';
|
import NavBar from '@/components/NavBar';
|
||||||
import SyncSettings from '@/components/SyncSettings';
|
import SyncSettings from '@/components/SyncSettings';
|
||||||
import AppVersionInfo from '@/components/AppVersionInfo';
|
import AppVersionInfo from '@/components/AppVersionInfo';
|
||||||
import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight } from 'lucide-react';
|
import { User, CreditCard, Bell, Lock, HelpCircle, LogOut, ChevronRight, Database } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useAuth } from '@/contexts/auth';
|
import { useAuth } from '@/contexts/auth';
|
||||||
import { useToast } from '@/hooks/useToast.wrapper';
|
import { useToast } from '@/hooks/useToast.wrapper';
|
||||||
@@ -105,6 +105,7 @@ const Settings = () => {
|
|||||||
<div className="space-y-4 mb-8">
|
<div className="space-y-4 mb-8">
|
||||||
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2">앱 설정</h2>
|
<h2 className="text-sm font-medium text-gray-500 mb-2 px-2">앱 설정</h2>
|
||||||
<SettingsOption icon={Lock} label="보안 및 개인정보" description="보안 및 데이터 설정" onClick={() => navigate('/security-privacy')} />
|
<SettingsOption icon={Lock} label="보안 및 개인정보" description="보안 및 데이터 설정" onClick={() => navigate('/security-privacy')} />
|
||||||
|
<SettingsOption icon={Database} label="Appwrite 설정" description="Appwrite 연결 및 데이터 마이그레이션" onClick={() => navigate('/appwrite-settings')} />
|
||||||
<SettingsOption icon={HelpCircle} label="도움말 및 지원" description="FAQ 및 고객 지원" onClick={() => navigate('/help-support')} />
|
<SettingsOption icon={HelpCircle} label="도움말 및 지원" description="FAQ 및 고객 지원" onClick={() => navigate('/help-support')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
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 환경에 최적화
|
* 강화된 서버 연결 검사 - Supabase Cloud 환경에 최적화
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { isSyncEnabled } from '@/utils/syncUtils';
|
import { isSyncEnabled } from '@/utils/syncUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { isSyncEnabled } from '../syncSettings';
|
import { isSyncEnabled } from '../syncSettings';
|
||||||
import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker';
|
import { getModifiedBudget, getModifiedCategoryBudgets } from './modifiedBudgetsTracker';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 카테고리 예산 업로드 기능
|
* 카테고리 예산 업로드 기능
|
||||||
*/
|
*/
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { CategoryBudgets, CategoryBudgetRecord } from './types';
|
import { CategoryBudgets, CategoryBudgetRecord } from './types';
|
||||||
import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators';
|
import { calculateTotalCategoryBudget, filterValidCategoryBudgets } from './validators';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 월간 예산 업로드 기능
|
* 월간 예산 업로드 기능
|
||||||
*/
|
*/
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { BudgetData, BudgetRecord } from './types';
|
import { BudgetData, BudgetRecord } from './types';
|
||||||
import { isValidMonthlyBudget } from './validators';
|
import { isValidMonthlyBudget } from './validators';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자의 모든 클라우드 데이터 초기화
|
* 사용자의 모든 클라우드 데이터 초기화
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 서버 및 로컬 데이터 상태 확인 기능
|
* 서버 및 로컬 데이터 상태 확인 기능
|
||||||
*/
|
*/
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { DataStatus, ServerDataStatus, LocalDataStatus } from './types';
|
import { DataStatus, ServerDataStatus, LocalDataStatus } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { isSyncEnabled } from './syncSettings';
|
import { isSyncEnabled } from './syncSettings';
|
||||||
import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker';
|
import { clearAllModifiedFlags } from './budget/modifiedBudgetsTracker';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { Transaction } from '@/components/TransactionCard';
|
import { Transaction } from '@/components/TransactionCard';
|
||||||
import { isSyncEnabled } from './syncSettings';
|
import { isSyncEnabled } from './syncSettings';
|
||||||
import { formatDateForDisplay } from './transaction/dateUtils';
|
import { formatDateForDisplay } from './transaction/dateUtils';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/archive/lib/supabase';
|
||||||
import { isSyncEnabled } from '../syncSettings';
|
import { isSyncEnabled } from '../syncSettings';
|
||||||
import { addToDeletedTransactions } from './deletedTransactionsTracker';
|
import { addToDeletedTransactions } from './deletedTransactionsTracker';
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user