Compare commits

...

10 Commits

Author SHA1 Message Date
hansoo
4f1705c3b7 Update version to 1.1.8 and fix version code to 9 in Android build files 2025-04-05 20:52:14 +09:00
hansoo
e5caa27e91 버전 정보 파일 및 CI/CD 문서 업데이트 2025-04-05 15:52:14 +09:00
hansoo
d1e45edcdb 버전 정보 업데이트 2025-04-05 15:52:14 +09:00
gpt-engineer-app[bot]
980d8533d8 Visual edit in Lovable
Edited UI in Lovable
2025-04-05 06:31:12 +00:00
gpt-engineer-app[bot]
888d45683f Visual edit in Lovable
Edited UI in Lovable
2025-04-05 06:30:24 +00:00
gpt-engineer-app[bot]
0f2b29fe29 Refactor: Final build review and cleanup 2025-04-05 06:26:12 +00:00
gpt-engineer-app[bot]
5c7a24bc36 Fix: Resolve MONTHS_KR import error
Import MONTHS_KR from the correct file to resolve the "Cannot find name 'MONTHS_KR'" error in src/utils/dateUtils.ts.
2025-04-05 06:20:53 +00:00
gpt-engineer-app[bot]
1d3039c525 Fix: Import MONTHS_KR from dateUtils
The `MONTHS_KR` export was not being correctly imported in `useFilterApplication.ts` and `index.ts`. This commit fixes the import statements to correctly reference the `MONTHS_KR` export from `dateUtils.ts`.
2025-04-05 06:19:26 +00:00
gpt-engineer-app[bot]
4b7f422acd Refactor: Codebase review and cleanup
Review the entire codebase for potential issues and perform necessary cleanup.
2025-04-05 06:16:43 +00:00
gpt-engineer-app[bot]
6a55d33a6d Fix: Resolve 'useState' error in AuthProvider
The error "TypeError: Cannot read properties of null (reading 'useState')" indicates an issue with the `useState` hook within the `AuthProvider` component. This commit addresses the root cause of this error, ensuring the component functions correctly.
2025-04-05 06:05:51 +00:00
27 changed files with 1895 additions and 614 deletions

2
.gitignore vendored
View File

@@ -1,6 +1,6 @@
# Logs # Logs
logs logs
*.log #*.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*

View File

@@ -3,8 +3,10 @@ apply plugin: 'com.android.application'
// 버전 정보를 properties 파일에서 동적으로 로드 // 버전 정보를 properties 파일에서 동적으로 로드
android { android {
namespace "com.lovable.zellyfinance" namespace "com.lovable.zellyfinance"
compileSdk rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
versionCode = 9
versionName = "1.1.8"
applicationId "com.lovable.zellyfinance" applicationId "com.lovable.zellyfinance"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
@@ -50,7 +52,7 @@ android {
// 로드된 값이 유효한지 확인하고, 유효하지 않으면 기본값 사용 // 로드된 값이 유효한지 확인하고, 유효하지 않으면 기본값 사용
def versionName = defaultVersionName def versionName = defaultVersionName
def versionCode = defaultVersionCode def versionCode = 9
def buildNumber = defaultBuildNumber def buildNumber = defaultBuildNumber
if (versionProps['versionName'] && !versionProps['versionName'].toString().trim().isEmpty()) { if (versionProps['versionName'] && !versionProps['versionName'].toString().trim().isEmpty()) {
@@ -61,14 +63,17 @@ android {
if (versionProps['versionCode'] && !versionProps['versionCode'].toString().trim().isEmpty()) { if (versionProps['versionCode'] && !versionProps['versionCode'].toString().trim().isEmpty()) {
try { try {
// 버전 코드는 고정값 8을 사용하므로 properties에서 읽지 않음
/*
versionCode = versionProps['versionCode'].toString().toInteger() versionCode = versionProps['versionCode'].toString().toInteger()
if (versionCode <= 0) { if (versionCode <= 0) {
println "유효하지 않은 versionCode(0 이하), 기본값 사용: ${defaultVersionCode}" println "유효하지 않은 versionCode(0 이하), 기본값 사용: ${defaultVersionCode}"
versionCode = defaultVersionCode versionCode = defaultVersionCode
} }
*/
} catch (Exception e) { } catch (Exception e) {
println "versionCode 변환 오류, 기본값 사용: ${e.message}" println "versionCode 변환 오류, 기본값 사용: ${e.message}"
versionCode = defaultVersionCode // versionCode = defaultVersionCode
} }
} else { } else {
println "유효하지 않은 versionCode, 기본값 사용: ${defaultVersionCode}" println "유효하지 않은 versionCode, 기본값 사용: ${defaultVersionCode}"
@@ -92,9 +97,14 @@ android {
// 최종 로그 출력 // 최종 로그 출력
println "최종 버전 정보: versionName=${versionName}, versionCode=${versionCode}, buildNumber=${buildNumber}" println "최종 버전 정보: versionName=${versionName}, versionCode=${versionCode}, buildNumber=${buildNumber}"
// 앱 빌드 속성 설정 - 문법 오류 수정 versionCode = 8
versionName = versionName
versionCode = versionCode // 앱 빌드 속성 설정
// 버전 코드는 이미 고정값 8로 설정됨
// versionCode = versionCode.toInteger()
versionName = "${versionName}"
// 이 부분이 중요합니다 - 이 속성들이 안드로이드 매니페스트에 자동으로 병합됩니다
// BuildConfig 필드 설정 // BuildConfig 필드 설정
buildConfigField "String", "VERSION_NAME", "\"${versionName}\"" buildConfigField "String", "VERSION_NAME", "\"${versionName}\""

View File

@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="9"
android:versionName="1.1.8">
<application <application
android:allowBackup="true" android:allowBackup="true"

View File

@@ -1,3 +1,5 @@
buildNumber=7 # Zellyy Finance 앱 버전 정보
versionCode=7 # 마지막 업데이트: 2025-04-05 19:45:54
versionName=1.1.1.3 buildNumber=9
versionCode=9
versionName=1.1.8

View File

@@ -1,6 +1,8 @@
{ {
"versionCode": 7, "versionCode": 9,
"versionName": "1.1.1.3", "versionName": "1.1.8",
"buildNumber": 7, "buildNumber": 9,
"buildDate": "2025-04-05 19:45:54",
"buildType": "release-aab",
"notes": "사용자가 수정한 버전 정보입니다. 이 파일을 편집하여 앱 버전 정보를 변경할 수 있습니다." "notes": "사용자가 수정한 버전 정보입니다. 이 파일을 편집하여 앱 버전 정보를 변경할 수 있습니다."
} }

View File

@@ -1,3 +1,5 @@
buildNumber=7 # Zellyy Finance 앱 버전 정보
versionCode=7 # 마지막 업데이트: 2025-04-05 19:45:54
versionName=1.1.1.3 buildNumber=9
versionCode=9
versionName=1.1.8

284
app_build.log Normal file
View File

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

1
app_error.log Normal file
View File

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

View File

@@ -3,15 +3,60 @@
# 안드로이드 앱 빌드 스크립트 (디버그 및 릴리즈 버전) # 안드로이드 앱 빌드 스크립트 (디버그 및 릴리즈 버전)
# 사용법: ./build-apk-for-device.sh # 사용법: ./build-apk-for-device.sh
# 스크립트 시작 시간 기록
START_TIME=$(date +%s)
# 색상 정의 # 색상 정의
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
RED='\033[0;31m' RED='\033[0;31m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# 로그 파일 설정
LOG_FILE="app_build.log"
ERROR_LOG_FILE="app_error.log"
# 타임아웃 설정 (초 단위)
BUILD_TIMEOUT=600 # 10분
# 오류 처리 함수
handle_error() {
local exit_code=$1
local error_message=$2
local command=$3
echo -e "\n${RED}오류 발생: $error_message${NC}" | tee -a "$ERROR_LOG_FILE"
echo -e "${YELLOW}명령어: $command${NC}" | tee -a "$ERROR_LOG_FILE"
echo -e "${YELLOW}종료 코드: $exit_code${NC}" | tee -a "$ERROR_LOG_FILE"
echo -e "${YELLOW}전체 로그는 $LOG_FILE 파일을 확인하세요.${NC}"
exit $exit_code
}
# 빌드 시간 계산 함수
show_build_time() {
local end_time=$(date +%s)
local build_time=$((end_time - START_TIME))
local minutes=$((build_time / 60))
local seconds=$((build_time % 60))
echo -e "\n${GREEN}빌드 완료 시간: ${minutes}${seconds}${NC}"
}
# 로그 파일 초기화
echo "===== 빌드 시작: $(date) =====" > "$LOG_FILE"
echo "===== 오류 로그: $(date) =====" > "$ERROR_LOG_FILE"
# 프로젝트 디렉토리로 이동 # 프로젝트 디렉토리로 이동
cd "$(dirname "$0")" cd "$(dirname "$0")"
# 릴리즈 디렉토리 생성
RELEASE_DIR="$(pwd)/release"
if [ ! -d "$RELEASE_DIR" ]; then
mkdir -p "$RELEASE_DIR"
echo -e "${GREEN}릴리즈 디렉토리가 생성되었습니다: $RELEASE_DIR${NC}"
fi
# 빌드 타입 선택 메뉴 # 빌드 타입 선택 메뉴
echo -e "${YELLOW}Zellyy Finance 앱 빌드 스크립트${NC}" echo -e "${YELLOW}Zellyy Finance 앱 빌드 스크립트${NC}"
echo -e "${YELLOW}=============================${NC}" echo -e "${YELLOW}=============================${NC}"
@@ -43,76 +88,107 @@ case $CHOICE in
;; ;;
esac esac
# 현재 버전 코드 가져오기 # 버전 정보 파일 확인
CURRENT_VERSION_CODE=$(grep -o 'versionCode [0-9]*' android/app/build.gradle | awk '{print $2}') VERSION_PROPS_FILE="version.properties"
# 버전 코드가 비어있으면 기본값 1로 설정
if [ -z "$CURRENT_VERSION_CODE" ]; then # 버전 정보 파일이 있는지 확인
if [ -f "$VERSION_PROPS_FILE" ]; then
# 파일에서 버전 정보 로드
source "$VERSION_PROPS_FILE"
CURRENT_VERSION_CODE=$VERSION_CODE
CURRENT_VERSION_NAME=$VERSION_NAME
CURRENT_BUILD_NUMBER=$BUILD_NUMBER
echo -e "${GREEN}버전 정보 로드 완료: 버전 코드=${BLUE}$CURRENT_VERSION_CODE${GREEN}, 버전 이름=${BLUE}$CURRENT_VERSION_NAME${GREEN}, 빌드 번호=${BLUE}$CURRENT_BUILD_NUMBER${NC}"
else
# 파일이 없으면 build.gradle에서 버전 정보 가져오기
CURRENT_VERSION_CODE=$(grep -o 'versionCode [0-9]*' android/app/build.gradle | sed 's/versionCode //')
CURRENT_VERSION_NAME=$(grep -o 'versionName "[^"]*"' android/app/build.gradle | sed 's/versionName "//' | sed 's/"//')
# 버전 코드가 비어있으면 기본값 1로 설정
if [ -z "$CURRENT_VERSION_CODE" ]; then
CURRENT_VERSION_CODE=1 CURRENT_VERSION_CODE=1
echo -e "${YELLOW}버전 코드가 없어 기본값 1로 설정했습니다.${NC}" echo -e "${YELLOW}버전 코드가 없어 기본값 1로 설정했습니다.${NC}"
fi fi
echo -e "${YELLOW}현재 버전 코드: ${CURRENT_VERSION_CODE}${NC}"
# 현재 버전 이름 가져오기 # 버전 이름이 비어있으면 기본값 1.0.0으로 설정
CURRENT_VERSION_NAME=$(grep -o 'versionName "[^"]*"' android/app/build.gradle | sed 's/versionName "//' | sed 's/"//') if [ -z "$CURRENT_VERSION_NAME" ]; then
# 버전 이름이 비어있으면 기본값 1.0.0으로 설정
if [ -z "$CURRENT_VERSION_NAME" ]; then
CURRENT_VERSION_NAME="1.0.0" CURRENT_VERSION_NAME="1.0.0"
echo -e "${YELLOW}버전 이름이 없어 기본값 1.0.0으로 설정했습니다.${NC}" echo -e "${YELLOW}버전 이름이 없어 기본값 1.0.0으로 설정했습니다.${NC}"
fi
# 빌드 번호 기본값 설정
CURRENT_BUILD_NUMBER=$CURRENT_VERSION_CODE
fi fi
echo -e "${YELLOW}현재 버전 이름: ${CURRENT_VERSION_NAME}${NC}"
echo -e "${YELLOW}현재 버전 코드: ${BLUE}$CURRENT_VERSION_CODE${NC}"
echo -e "${YELLOW}현재 버전 이름: ${BLUE}$CURRENT_VERSION_NAME${NC}"
echo -e "${YELLOW}현재 빌드 번호: ${BLUE}$CURRENT_BUILD_NUMBER${NC}"
# 버전 정보 수정 여부 확인 # 버전 정보 수정 여부 확인
echo -e "${YELLOW}버전 정보를 수정하시겠습니까? (y/n)${NC}" echo -e "${YELLOW}버전 정보를 수정하시겠습니까? (y/n)${NC}"
read -r MODIFY_VERSION read -r MODIFY_VERSION
# 기본값 설정
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
NEW_VERSION_NAME=$CURRENT_VERSION_NAME
if [[ "$MODIFY_VERSION" == "y" || "$MODIFY_VERSION" == "Y" ]]; then if [[ "$MODIFY_VERSION" == "y" || "$MODIFY_VERSION" == "Y" ]]; then
# 버전 코드 입력 # 버전 코드 입력
echo -e "${YELLOW}새 버전 코드를 입력하세요 (현재: ${CURRENT_VERSION_CODE}):${NC}" echo -e "${YELLOW}새 버전 코드를 입력하세요 (현재: ${BLUE}${CURRENT_VERSION_CODE}${YELLOW}, 엔터를 치면 기본값 사용):${NC}"
read -r NEW_VERSION_CODE_INPUT read -r NEW_VERSION_CODE_INPUT
if [ -n "$NEW_VERSION_CODE_INPUT" ]; then if [ -n "$NEW_VERSION_CODE_INPUT" ]; then
NEW_VERSION_CODE=$NEW_VERSION_CODE_INPUT NEW_VERSION_CODE=$NEW_VERSION_CODE_INPUT
else
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
fi fi
# 버전 이름 입력 # 버전 이름 입력
echo -e "${YELLOW}새 버전 이름을 입력하세요 (현재: ${CURRENT_VERSION_NAME}):${NC}" echo -e "${YELLOW}새 버전 이름을 입력하세요 (현재: ${BLUE}${CURRENT_VERSION_NAME}${YELLOW}, 엔터를 치면 기본값 사용):${NC}"
read -r NEW_VERSION_NAME_INPUT read -r NEW_VERSION_NAME_INPUT
if [ -n "$NEW_VERSION_NAME_INPUT" ]; then if [ -n "$NEW_VERSION_NAME_INPUT" ]; then
VERSION_NAME=$NEW_VERSION_NAME_INPUT NEW_VERSION_NAME=$NEW_VERSION_NAME_INPUT
else
VERSION_NAME=$CURRENT_VERSION_NAME
fi fi
echo -e "${GREEN}버전 정보가 업데이트되었습니다: 버전 코드=${NEW_VERSION_CODE}, 버전 이름=${VERSION_NAME}${NC}" echo -e "${GREEN}버전 정보가 업데이트되었습니다: 버전 코드=${BLUE}${NEW_VERSION_CODE}${GREEN}, 버전 이름=${BLUE}${NEW_VERSION_NAME}${NC}"
else
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
VERSION_NAME=$CURRENT_VERSION_NAME
fi fi
# 빌드 넘버 자동 설정 # 빌드 번호 기본값 설정
BUILD_NUMBER=$NEW_VERSION_CODE BUILD_NUMBER=$CURRENT_BUILD_NUMBER
echo -e "${GREEN}빌드 넘버가 자동으로 ${BUILD_NUMBER}(으)로 설정되었습니다.${NC}"
# 빌드 넘버 수정 여부 확인 # 릴리즈 빌드일 경우 빌드 번호 자동 증가 제안
echo -e "${YELLOW}빌드 넘버를 수정하시겠습니까? (y/n)${NC}" if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; then
SUGGESTED_BUILD_NUMBER=$((CURRENT_BUILD_NUMBER + 1))
echo -e "${YELLOW}릴리즈 빌드에는 빌드 번호 증가가 권장됩니다 (${BLUE}$CURRENT_BUILD_NUMBER${YELLOW} -> ${BLUE}$SUGGESTED_BUILD_NUMBER${YELLOW})${NC}"
else
SUGGESTED_BUILD_NUMBER=$CURRENT_BUILD_NUMBER
fi
echo -e "${GREEN}현재 빌드 번호: ${BLUE}$CURRENT_BUILD_NUMBER${NC}"
# 빌드 번호 수정 여부 확인
echo -e "${YELLOW}빌드 번호를 수정하시겠습니까? (y/n)${NC}"
read -r MODIFY_BUILD read -r MODIFY_BUILD
if [[ "$MODIFY_BUILD" == "y" || "$MODIFY_BUILD" == "Y" ]]; then if [[ "$MODIFY_BUILD" == "y" || "$MODIFY_BUILD" == "Y" ]]; then
echo -e "${YELLOW}새 빌드 넘버를 입력하세요 (현재: ${BUILD_NUMBER}):${NC}" echo -e "${YELLOW}새 빌드 번호를 입력하세요 (추천: ${BLUE}$SUGGESTED_BUILD_NUMBER${YELLOW}, 엔터를 치면 추천값 사용):${NC}"
read -r NEW_BUILD_NUMBER read -r NEW_BUILD_NUMBER
if [ -n "$NEW_BUILD_NUMBER" ]; then if [ -n "$NEW_BUILD_NUMBER" ]; then
BUILD_NUMBER=$NEW_BUILD_NUMBER BUILD_NUMBER=$NEW_BUILD_NUMBER
echo -e "${GREEN}빌드 넘버가 ${BUILD_NUMBER}(으)로 설정되었습니다.${NC}" else
BUILD_NUMBER=$SUGGESTED_BUILD_NUMBER
fi fi
echo -e "${GREEN}빌드 번호가 ${BLUE}$BUILD_NUMBER${GREEN}(으)로 설정되었습니다.${NC}"
else
BUILD_NUMBER=$SUGGESTED_BUILD_NUMBER
echo -e "${GREEN}빌드 번호가 ${BLUE}$BUILD_NUMBER${GREEN}(으)로 자동 설정되었습니다.${NC}"
fi fi
# 릴리즈 빌드인 경우 버전 코드 증가 여부 확인 # 릴리즈 빌드인 경우 버전 코드 증가 여부 확인
if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; then if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; then
# 버전 정보를 이미 수정했다면 다시 묻지 않음 # 버전 정보를 이미 수정했다면 다시 묻지 않음
if [[ "$MODIFY_VERSION" != "y" && "$MODIFY_VERSION" != "Y" ]]; then if [[ "$MODIFY_VERSION" != "y" && "$MODIFY_VERSION" != "Y" ]]; then
echo -e "${YELLOW}버전 코드를 증가시키겠습니까? 현재 버전 코드: ${NEW_VERSION_CODE} (y/n)${NC}" SUGGESTED_VERSION_CODE=$((CURRENT_VERSION_CODE + 1))
echo -e "${YELLOW}릴리즈 빌드에는 버전 코드 증가가 권장됩니다 (${BLUE}$CURRENT_VERSION_CODE${YELLOW} -> ${BLUE}$SUGGESTED_VERSION_CODE${YELLOW})${NC}"
echo -e "${YELLOW}버전 코드를 증가시키겠습니까? (y/n)${NC}"
read -r INCREASE_VERSION read -r INCREASE_VERSION
if [[ "$INCREASE_VERSION" == "y" || "$INCREASE_VERSION" == "Y" ]]; then if [[ "$INCREASE_VERSION" == "y" || "$INCREASE_VERSION" == "Y" ]]; then
NEW_VERSION_CODE=$((NEW_VERSION_CODE + 1)) NEW_VERSION_CODE=$SUGGESTED_VERSION_CODE
echo -e "${GREEN}버전 코드가 ${NEW_VERSION_CODE}(으)로 증가됩니다.${NC}" echo -e "${GREEN}버전 코드가 ${NEW_VERSION_CODE}(으)로 증가됩니다.${NC}"
fi fi
fi fi
@@ -132,24 +208,39 @@ if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; then
echo -e "${GREEN}서명 설정이 업데이트되었습니다.${NC}" echo -e "${GREEN}서명 설정이 업데이트되었습니다.${NC}"
fi fi
echo -e "${YELLOW}Zellyy Finance 앱 빌드 시작 (${BUILD_TYPE}, 빌드 넘버: ${BUILD_NUMBER}, 버전 코드: ${NEW_VERSION_CODE}): $(date)${NC}" echo -e "${YELLOW}Zellyy Finance 앱 빌드 시작 (${BUILD_TYPE}, 빌드 번호: ${BUILD_NUMBER}, 버전 코드: ${NEW_VERSION_CODE}, 버전 이름: ${NEW_VERSION_NAME}): $(date)${NC}"
# 버전 정보를 파일에 저장하는 함수 # 버전 정보를 파일에 저장하는 함수
save_version_info() { save_version_info() {
# version.properties 파일 업데이트 - 루트 디렉토리와 app 디렉토리 모두에 저장 # 루트 디렉토리에 version.properties 파일 저장
echo "buildNumber=$BUILD_NUMBER" > android/version.properties cat > "$VERSION_PROPS_FILE" << EOF
echo "versionCode=$NEW_VERSION_CODE" >> android/version.properties # Zellyy Finance 앱 버전 정보
echo "versionName=$VERSION_NAME" >> android/version.properties # 마지막 업데이트: $(date +"%Y-%m-%d %H:%M:%S")
VERSION_CODE=$NEW_VERSION_CODE
VERSION_NAME=$NEW_VERSION_NAME
BUILD_NUMBER=$BUILD_NUMBER
EOF
# android 디렉토리에 version.properties 파일 저장
cat > "android/version.properties" << EOF
# Zellyy Finance 앱 버전 정보
# 마지막 업데이트: $(date +"%Y-%m-%d %H:%M:%S")
buildNumber=$BUILD_NUMBER
versionCode=$NEW_VERSION_CODE
versionName=$NEW_VERSION_NAME
EOF
# app 디렉토리에도 동일한 파일 복사 # app 디렉토리에도 동일한 파일 복사
cp android/version.properties android/app/version.properties cp "android/version.properties" "android/app/version.properties"
# app_version.json 파일 업데이트 # app_version.json 파일 업데이트 - 사용자 정보와 빌드 정보 포함
cat > android/app_version.json << EOF cat > "android/app_version.json" << EOF
{ {
"versionCode": $NEW_VERSION_CODE, "versionCode": $NEW_VERSION_CODE,
"versionName": "$VERSION_NAME", "versionName": "$NEW_VERSION_NAME",
"buildNumber": $BUILD_NUMBER, "buildNumber": $BUILD_NUMBER,
"buildDate": "$(date +"%Y-%m-%d %H:%M:%S")",
"buildType": "$BUILD_TYPE",
"notes": "사용자가 수정한 버전 정보입니다. 이 파일을 편집하여 앱 버전 정보를 변경할 수 있습니다." "notes": "사용자가 수정한 버전 정보입니다. 이 파일을 편집하여 앱 버전 정보를 변경할 수 있습니다."
} }
EOF EOF
@@ -157,17 +248,59 @@ EOF
# AppVersionInfo.tsx 파일의 하드코딩된 버전 정보 업데이트 # AppVersionInfo.tsx 파일의 하드코딩된 버전 정보 업데이트
echo -e "${YELLOW}AppVersionInfo.tsx 파일의 버전 정보 업데이트 중...${NC}" echo -e "${YELLOW}AppVersionInfo.tsx 파일의 버전 정보 업데이트 중...${NC}"
# AppVersionInfo.tsx 파일이 존재하는지 확인
APP_VERSION_FILE="src/components/AppVersionInfo.tsx"
if [ -f "$APP_VERSION_FILE" ]; then
# 백업 파일 생성 # 백업 파일 생성
cp src/components/AppVersionInfo.tsx src/components/AppVersionInfo.tsx.bak cp "$APP_VERSION_FILE" "${APP_VERSION_FILE}.bak"
# AppVersionInfo.tsx 파일에서 하드코딩된 버전 정보 업데이트 # AppVersionInfo.tsx 파일에서 하드코딩된 버전 정보 업데이트
sed -i '' "s/versionName: '[^']*'/versionName: '$VERSION_NAME'/" src/components/AppVersionInfo.tsx sed -i '' "s/versionName: '[^']*'/versionName: '$NEW_VERSION_NAME'/" "$APP_VERSION_FILE"
sed -i '' "/hardcodedVersionInfo/,/}/s/buildNumber: [0-9]*/buildNumber: $BUILD_NUMBER/" src/components/AppVersionInfo.tsx sed -i '' "/hardcodedVersionInfo/,/}/s/buildNumber: [0-9]*/buildNumber: $BUILD_NUMBER/" "$APP_VERSION_FILE"
sed -i '' "/hardcodedVersionInfo/,/}/s/versionCode: [0-9]*/versionCode: $NEW_VERSION_CODE/" src/components/AppVersionInfo.tsx sed -i '' "/hardcodedVersionInfo/,/}/s/versionCode: [0-9]*/versionCode: $NEW_VERSION_CODE/" "$APP_VERSION_FILE"
echo -e "${GREEN}AppVersionInfo.tsx 파일의 버전 정보가 업데이트되었습니다.${NC}" echo -e "${GREEN}AppVersionInfo.tsx 파일의 버전 정보가 업데이트되었습니다.${NC}"
else
echo -e "${YELLOW}경고: AppVersionInfo.tsx 파일을 찾을 수 없습니다.${NC}"
fi
echo -e "${GREEN}버전 정보가 모든 파일에 저장되었습니다: 버전 코드=${NEW_VERSION_CODE}, 버전 이름=${VERSION_NAME}, 빌드 넘버=${BUILD_NUMBER}${NC}" # AndroidManifest.xml 파일의 버전 정보 업데이트
MANIFEST_FILE="android/app/src/main/AndroidManifest.xml"
if [ -f "$MANIFEST_FILE" ]; then
# 기존 버전 정보가 있는지 확인
if grep -q 'android:versionCode' "$MANIFEST_FILE"; then
# 기존 버전 정보 업데이트
sed -i '' "s/android:versionCode=\"[0-9]*\"/android:versionCode=\"$NEW_VERSION_CODE\"/" "$MANIFEST_FILE"
sed -i '' "s/android:versionName=\"[^\"]*\"/android:versionName=\"$NEW_VERSION_NAME\"/" "$MANIFEST_FILE"
else
# 버전 정보가 없으면 추가
sed -i '' "s/<manifest xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\">/<manifest xmlns:android=\"http:\/\/schemas.android.com\/apk\/res\/android\"\n android:versionCode=\"$NEW_VERSION_CODE\"\n android:versionName=\"$NEW_VERSION_NAME\">/" "$MANIFEST_FILE"
fi
echo -e "${GREEN}AndroidManifest.xml 파일의 버전 정보가 업데이트되었습니다.${NC}"
else
echo -e "${YELLOW}경고: AndroidManifest.xml 파일을 찾을 수 없습니다.${NC}"
fi
# build.gradle 파일의 버전 정보 업데이트
GRADLE_FILE="android/app/build.gradle"
if [ -f "$GRADLE_FILE" ]; then
# 백업 파일 생성
cp "$GRADLE_FILE" "${GRADLE_FILE}.bak"
# 버전 코드와 버전 이름 업데이트 (더 안정적인 방식으로 수정)
# Update versionCode within defaultConfig block (ensure correct indentation)
sed -i '' "/defaultConfig {/,/}/s/^[[:space:]]*versionCode[[:space:]]*=.*$/ versionCode = $NEW_VERSION_CODE/" "$GRADLE_FILE"
# Update 'def versionCode = ...' (ensure correct indentation)
sed -i '' "s/^[[:space:]]*def versionCode[[:space:]]*=.*$/ def versionCode = $NEW_VERSION_CODE/" "$GRADLE_FILE"
# Update versionName within defaultConfig block (ensure correct indentation and quotes)
sed -i '' "/defaultConfig {/,/}/s/^[[:space:]]*versionName[[:space:]]*=.*$/ versionName = \\\"$NEW_VERSION_NAME\\\"/" "$GRADLE_FILE"
echo -e "${GREEN}build.gradle 파일의 버전 정보가 업데이트되었습니다.${NC}"
else
echo -e "${YELLOW}경고: build.gradle 파일을 찾을 수 없습니다.${NC}"
fi
echo -e "${GREEN}버전 정보가 모든 파일에 저장되었습니다: 버전 코드=${BLUE}$NEW_VERSION_CODE${GREEN}, 버전 이름=${BLUE}$NEW_VERSION_NAME${GREEN}, 빌드 번호=${BLUE}$BUILD_NUMBER${NC}"
} }
# 빌드 시작 전에 버전 정보 저장 # 빌드 시작 전에 버전 정보 저장
@@ -182,48 +315,117 @@ rm -rf dist
echo -e "${GREEN}빌드 캐시가 삭제되었습니다.${NC}" echo -e "${GREEN}빌드 캐시가 삭제되었습니다.${NC}"
# 1. 웹 앱 빌드 # 1. 웹 앱 빌드
echo -e "${YELLOW}1. 웹 앱 빌드 중...${NC}" echo -e "${YELLOW}1. 웹 앱 빌드 중...${NC}" | tee -a "$LOG_FILE"
npm run build echo "실행 명령어: npm run build" >> "$LOG_FILE"
if [ $? -ne 0 ]; then npm run build >> "$LOG_FILE" 2>&1
echo -e "${RED}웹 앱 빌드 실패. 빌드 프로세스를 중단합니다.${NC}" BUILD_RESULT=$?
exit 1 if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "웹 앱 빌드 실패" "npm run build"
fi fi
echo -e "${GREEN}웹 앱 빌드 완료${NC}" echo -e "${GREEN}웹 앱 빌드 완료${NC}" | tee -a "$LOG_FILE"
# 2. Capacitor에 웹 코드 복사 및 동기화 # 2. Capacitor에 웹 코드 복사 및 동기화
echo -e "${YELLOW}2. Capacitor에 웹 코드 동기화 중...${NC}" echo -e "${YELLOW}2. Capacitor에 웹 코드 동기화 중...${NC}" | tee -a "$LOG_FILE"
npx cap sync android echo "실행 명령어: npx cap sync android" >> "$LOG_FILE"
if [ $? -ne 0 ]; then npx cap sync android >> "$LOG_FILE" 2>&1
echo -e "${RED}Capacitor 동기화 실패. 빌드 프로세스를 중단합니다.${NC}" BUILD_RESULT=$?
exit 1 if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "Capacitor 동기화 실패" "npx cap sync android"
fi fi
echo -e "${GREEN}Capacitor 동기화 완료${NC}" echo -e "${GREEN}Capacitor 동기화 완료${NC}" | tee -a "$LOG_FILE"
# 3. 안드로이드 APK/AAB 빌드 # 3. 안드로이드 APK/AAB 빌드
cd android cd android
# Gradle 메모리 설정 최적화
export GRADLE_OPTS="-Xmx4g -Dorg.gradle.jvmargs='-Xmx4g -XX:+HeapDumpOnOutOfMemoryError'"
echo -e "${YELLOW}3. 안드로이드 빌드 시작 (${BUILD_TYPE})...${NC}" echo -e "${YELLOW}3. 안드로이드 빌드 시작 (${BUILD_TYPE})...${NC}"
# 빌드 타입에 따라 다른 명령어 실행 # 빌드 타입에 따라 다른 명령어 실행
if [ "$BUILD_TYPE" = "debug" ]; then if [ "$BUILD_TYPE" = "debug" ]; then
# 디버그 빌드 echo -e "${YELLOW}3. 안드로이드 빌드 시작 (debug)...${NC}" | tee -a "../$LOG_FILE"
./gradlew clean assembleDebug echo "실행 명령어: ./gradlew assembleDebug" >> "../$LOG_FILE"
if [ $? -ne 0 ]; then ./gradlew assembleDebug >> "../$LOG_FILE" 2>&1
echo -e "${RED}디버그 APK 빌드 실패. 오류를 확인하세요.${NC}" BUILD_RESULT=$?
exit 1 if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "디버그 APK 빌드 실패" "./gradlew assembleDebug"
fi
echo -e "${GREEN}디버그 APK 빌드 완료${NC}" | tee -a "../$LOG_FILE"
# 빌드된 APK 파일 경로 출력
APK_PATH="app/build/outputs/apk/debug/app-debug.apk"
if [ -f "$APK_PATH" ]; then
echo -e "${GREEN}APK 파일 생성 완료: ${BLUE}$APK_PATH${NC}" | tee -a "../$LOG_FILE"
# APK 파일 크기 출력
APK_SIZE=$(du -h "$APK_PATH" | cut -f1)
echo -e "${GREEN}APK 파일 크기: ${BLUE}$APK_SIZE${NC}" | tee -a "../$LOG_FILE"
# 릴리즈 디렉토리로 복사
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
RELEASE_FILENAME="zellyy_debug_v${VERSION_NAME}_${TIMESTAMP}.apk"
cp "$APK_PATH" "../release/$RELEASE_FILENAME"
echo -e "${GREEN}APK 파일이 릴리즈 디렉토리에 복사되었습니다: ${BLUE}release/$RELEASE_FILENAME${NC}" | tee -a "../$LOG_FILE"
else
echo -e "${RED}APK 파일을 찾을 수 없습니다: $APK_PATH${NC}" | tee -a "../$ERROR_LOG_FILE"
fi fi
APK_PATH="app/build/outputs/apk/debug/app-debug.apk" elif [ "$BUILD_TYPE" = "release-aab" ]; then
DEST_PATH="$HOME/zellyy-finance-debug.apk" echo -e "${YELLOW}3. 안드로이드 빌드 시작 (release-aab)...${NC}" | tee -a "../$LOG_FILE"
echo "실행 명령어: ./gradlew bundleRelease" >> "../$LOG_FILE"
./gradlew bundleRelease >> "../$LOG_FILE" 2>&1
BUILD_RESULT=$?
if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "릴리즈 AAB 빌드 실패" "./gradlew bundleRelease"
fi
echo -e "${GREEN}릴리즈 AAB 빌드 완료${NC}" | tee -a "../$LOG_FILE"
# 빌드된 AAB 파일 경로 출력
AAB_PATH="app/build/outputs/bundle/release/app-release.aab"
if [ -f "$AAB_PATH" ]; then
echo -e "${GREEN}AAB 파일 생성 완료: ${BLUE}$AAB_PATH${NC}" | tee -a "../$LOG_FILE"
# AAB 파일 크기 출력
AAB_SIZE=$(du -h "$AAB_PATH" | cut -f1)
echo -e "${GREEN}AAB 파일 크기: ${BLUE}$AAB_SIZE${NC}" | tee -a "../$LOG_FILE"
# 릴리즈 디렉토리로 복사
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
RELEASE_FILENAME="zellyy_release_v${VERSION_NAME}_${TIMESTAMP}.aab"
cp "$AAB_PATH" "../release/$RELEASE_FILENAME"
echo -e "${GREEN}AAB 파일이 릴리즈 디렉토리에 복사되었습니다: ${BLUE}release/$RELEASE_FILENAME${NC}" | tee -a "../$LOG_FILE"
else
echo -e "${RED}AAB 파일을 찾을 수 없습니다: $AAB_PATH${NC}" | tee -a "../$ERROR_LOG_FILE"
fi
elif [ "$BUILD_TYPE" = "release-apk" ]; then
echo -e "${YELLOW}3. 안드로이드 빌드 시작 (release-apk)...${NC}" | tee -a "../$LOG_FILE"
echo "실행 명령어: ./gradlew assembleRelease" >> "../$LOG_FILE"
./gradlew assembleRelease >> "../$LOG_FILE" 2>&1
BUILD_RESULT=$?
if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "릴리즈 APK 빌드 실패" "./gradlew assembleRelease"
fi
echo -e "${GREEN}릴리즈 APK 빌드 완료${NC}" | tee -a "../$LOG_FILE"
# 빌드된 APK 파일 경로 출력
APK_PATH="app/build/outputs/apk/release/app-release.apk"
if [ -f "$APK_PATH" ]; then if [ -f "$APK_PATH" ]; then
echo -e "${GREEN}디버그 APK 빌드 성공!${NC}" echo -e "${GREEN}서명된 APK 파일 생성 완료: ${BLUE}$APK_PATH${NC}" | tee -a "../$LOG_FILE"
echo -e "APK 파일 위치: $(pwd)/$APK_PATH"
# 홈 디렉토리로 APK 복사 # APK 파일 크기 출력
cp "$APK_PATH" "$DEST_PATH" APK_SIZE=$(du -h "$APK_PATH" | cut -f1)
echo -e "${GREEN}APK를 홈 디렉토리에 복사했습니다: $DEST_PATH${NC}" echo -e "${GREEN}APK 파일 크기: ${BLUE}$APK_SIZE${NC}" | tee -a "../$LOG_FILE"
# 릴리즈 디렉토리로 복사
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
RELEASE_FILENAME="zellyy_release_signed_v${VERSION_NAME}_${TIMESTAMP}.apk"
cp "$APK_PATH" "../release/$RELEASE_FILENAME"
echo -e "${GREEN}서명된 APK 파일이 릴리즈 디렉토리에 복사되었습니다: ${BLUE}release/$RELEASE_FILENAME${NC}" | tee -a "../$LOG_FILE"
else
echo -e "${RED}APK 파일을 찾을 수 없습니다: $APK_PATH${NC}" | tee -a "../$ERROR_LOG_FILE"
fi
# 연결된 기기 확인
DEVICES=$(adb devices | grep -v "List" | grep "device" | wc -l) DEVICES=$(adb devices | grep -v "List" | grep "device" | wc -l)
if [ $DEVICES -gt 0 ]; then if [ $DEVICES -gt 0 ]; then
echo -e "${YELLOW}연결된 기기가 감지되었습니다. 설치하시겠습니까? (y/n)${NC}" echo -e "${YELLOW}연결된 기기가 감지되었습니다. 설치하시겠습니까? (y/n)${NC}"
@@ -245,93 +447,34 @@ if [ "$BUILD_TYPE" = "debug" ]; then
echo -e "${YELLOW}연결된 기기가 없습니다. 다음 방법으로 APK를 설치할 수 있습니다:${NC}" echo -e "${YELLOW}연결된 기기가 없습니다. 다음 방법으로 APK를 설치할 수 있습니다:${NC}"
echo "1. USB 케이블로 폰을 연결하고 파일 전송" echo "1. USB 케이블로 폰을 연결하고 파일 전송"
echo "2. 이메일이나 메신저로 APK 파일 전송" echo "2. 이메일이나 메신저로 APK 파일 전송"
echo "3. adb 명령어 사용: adb install $DEST_PATH" echo "3. adb 명령어 사용: adb install $APK_PATH"
fi fi
else fi
# 빌드 실패 시 처리
if [ $? -ne 0 ]; then
echo -e "${RED}APK 빌드 실패. 오류를 확인하세요.${NC}" echo -e "${RED}APK 빌드 실패. 오류를 확인하세요.${NC}"
exit 1 exit 1
fi
elif [ "$BUILD_TYPE" = "release-aab" ]; then
# AAB 릴리즈 빌드
./gradlew clean bundleRelease
if [ $? -ne 0 ]; then
echo -e "${RED}릴리즈 AAB 빌드 실패. 오류를 확인하세요.${NC}"
exit 1
fi
AAB_PATH="app/build/outputs/bundle/release/app-release.aab"
DEST_PATH="$HOME/zellyy-finance-release.aab"
if [ -f "$AAB_PATH" ]; then
echo -e "${GREEN}릴리즈 AAB 빌드 성공!${NC}"
echo -e "AAB 파일 위치: $(pwd)/$AAB_PATH"
# 홈 디렉토리로 AAB 복사
cp "$AAB_PATH" "$DEST_PATH"
echo -e "${GREEN}AAB를 홈 디렉토리에 복사했습니다: $DEST_PATH${NC}"
echo -e "${YELLOW}다음 단계:${NC}"
echo "1. Google Play Console에 AAB 파일 업로드: $DEST_PATH"
echo "2. 내부 테스트 트랙을 선택하여 업로드"
echo "3. 검토 과정이 완료될 때까지 기다리기 (보통 몇 시간에서 24시간 소요)"
else
echo -e "${RED}AAB 빌드 실패. 오류를 확인하세요.${NC}"
exit 1
fi
elif [ "$BUILD_TYPE" = "release-apk" ]; then
# 서명된 APK 릴리즈 빌드
./gradlew clean assembleRelease
if [ $? -ne 0 ]; then
echo -e "${RED}서명된 릴리즈 APK 빌드 실패. 오류를 확인하세요.${NC}"
exit 1
fi
SIGNED_APK_PATH="app/build/outputs/apk/release/app-release.apk"
DEST_PATH="$HOME/zellyy-finance-release.apk"
if [ -f "$SIGNED_APK_PATH" ]; then
echo -e "${GREEN}서명된 릴리즈 APK 빌드 성공!${NC}"
echo -e "APK 파일 위치: $(pwd)/$SIGNED_APK_PATH"
# 홈 디렉토리로 APK 복사
cp "$SIGNED_APK_PATH" "$DEST_PATH"
echo -e "${GREEN}서명된 APK를 홈 디렉토리에 복사했습니다: $DEST_PATH${NC}"
# 연결된 기기 확인
DEVICES=$(adb devices | grep -v "List" | grep "device" | wc -l)
if [ $DEVICES -gt 0 ]; then
echo -e "${YELLOW}연결된 기기가 감지되었습니다. 설치하시겠습니까? (y/n)${NC}"
read -r INSTALL
if [ "$INSTALL" = "y" ] || [ "$INSTALL" = "Y" ]; then
# 기기가 여러 개인 경우
if [ $(adb devices | grep -v "List" | grep "device" | wc -l) -gt 1 ]; then
echo -e "${YELLOW}여러 기기가 연결되어 있습니다. 특정 기기를 선택하세요:${NC}"
adb devices | grep -v "List" | grep "device"
echo -e "${YELLOW}기기 ID를 입력하세요:${NC}"
read -r DEVICE_ID
adb -s "$DEVICE_ID" install -r "$SIGNED_APK_PATH"
else
adb install -r "$SIGNED_APK_PATH"
fi
echo -e "${GREEN}설치 완료!${NC}"
fi
else
echo -e "${YELLOW}연결된 기기가 없습니다. 다음 방법으로 APK를 설치할 수 있습니다:${NC}"
echo "1. USB 케이블로 폰을 연결하고 파일 전송"
echo "2. 이메일이나 메신저로 APK 파일 전송"
echo "3. adb 명령어 사용: adb install $DEST_PATH"
fi
else
echo -e "${RED}APK 빌드 실패. 오류를 확인하세요.${NC}"
exit 1
fi
else
echo -e "${RED}지원되지 않는 빌드 타입입니다: $BUILD_TYPE${NC}"
echo -e "${YELLOW}사용법: ./build-apk-for-device.sh${NC}"
exit 1
fi fi
# 빌드 시간 표시
show_build_time
echo -e "${GREEN}빌드 프로세스 완료: $(date)${NC}" echo -e "${GREEN}빌드 프로세스 완료: $(date)${NC}"
echo -e "${BLUE}빌드 로그: $LOG_FILE${NC}"
echo -e "${YELLOW}오류 로그: $ERROR_LOG_FILE${NC}"
# 최종 결과 요약
echo -e "\n${PURPLE}===== 빌드 결과 요약 =====${NC}"
echo -e "${CYAN}빌드 타입: $BUILD_TYPE${NC}"
echo -e "${CYAN}버전 코드: $NEW_VERSION_CODE${NC}"
echo -e "${CYAN}버전 이름: $VERSION_NAME${NC}"
echo -e "${CYAN}빌드 넘버: $BUILD_NUMBER${NC}"
if [ "$BUILD_TYPE" = "debug" ] && [ -f "android/$APK_PATH" ]; then
echo -e "${GREEN}디버그 APK: android/$APK_PATH${NC}"
elif [ "$BUILD_TYPE" = "release-aab" ] && [ -f "android/$AAB_PATH" ]; then
echo -e "${GREEN}릴리즈 AAB: android/$AAB_PATH${NC}"
elif [ "$BUILD_TYPE" = "release-apk" ] && [ -f "android/$APK_PATH" ]; then
echo -e "${GREEN}릴리즈 APK: android/$APK_PATH${NC}"
fi

View File

@@ -196,7 +196,7 @@ start_build_process() {
echo -e "${YELLOW}디버그 빌드 시작...${NC}" echo -e "${YELLOW}디버그 빌드 시작...${NC}"
# 기본적인 빌드 (IPA 없이) # 기본적인 빌드 (IPA 없이)
xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug -derivedDataPath build xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug -derivedDataPath build -allowProvisioningUpdates
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${RED}디버그 빌드 실패. 오류를 확인하세요.${NC}" echo -e "${RED}디버그 빌드 실패. 오류를 확인하세요.${NC}"
@@ -217,8 +217,17 @@ start_build_process() {
if [[ "$CREATE_DEBUG_IPA" == "y" || "$CREATE_DEBUG_IPA" == "Y" ]]; then if [[ "$CREATE_DEBUG_IPA" == "y" || "$CREATE_DEBUG_IPA" == "Y" ]]; then
echo -e "${YELLOW}디버그용 IPA 파일 생성 중...${NC}" echo -e "${YELLOW}디버그용 IPA 파일 생성 중...${NC}"
# exportOptions.plist 파일 생성 # 아카이브 생성
cat > exportOptionsDebug.plist << EOF xcodebuild archive -workspace App.xcworkspace -scheme App -configuration Debug -archivePath build/App.xcarchive -allowProvisioningUpdates
if [ $? -ne 0 ]; then
echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}"
exit 1
fi
# IPA Export (기본 경로 사용)
EXPORT_OPTIONS_PLIST_PATH="build/DebugExportOptions.plist"
cat > "$EXPORT_OPTIONS_PLIST_PATH" <<EOL
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -226,55 +235,26 @@ start_build_process() {
<key>method</key> <key>method</key>
<string>development</string> <string>development</string>
<key>teamID</key> <key>teamID</key>
<string>${TEAM_ID}</string> <string>$TEAM_ID</string>
<key>compileBitcode</key> <key>signingStyle</key>
<false/> <string>automatic</string>
</dict> </dict>
</plist> </plist>
EOF EOL
xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath build/debug_ipa -exportOptionsPlist "$EXPORT_OPTIONS_PLIST_PATH" -allowProvisioningUpdates
# 아카이브 생성
xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug clean archive -archivePath "build/App-Debug.xcarchive" -allowProvisioningUpdates
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}" echo -e "${RED}디버그 IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
rm exportOptionsDebug.plist echo -e "${YELLOW}Apple 개발자 계정 정보(팀 ID: $TEAM_ID)가 정확한지, Xcode에서 서명 설정이 완료되었는지 확인하세요.${NC}"
exit 1 echo -e "${GREEN}Xcode 빌드는 완료되었을 수 있습니다. Xcode에서 직접 확인하거나 실행해 보세요.${NC}"
fi
# IPA 파일 생성
xcodebuild -exportArchive -archivePath "build/App-Debug.xcarchive" -exportOptionsPlist exportOptionsDebug.plist -exportPath "build/export-debug" -allowProvisioningUpdates
if [ $? -ne 0 ]; then
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
rm exportOptionsDebug.plist
exit 1
fi
rm exportOptionsDebug.plist
DEBUG_IPA_PATH="build/export-debug/App.ipa"
DEBUG_DEST_PATH="$HOME/Dev/zellyy-finance-ios-debug.ipa"
if [ -f "$DEBUG_IPA_PATH" ]; then
echo -e "${GREEN}디버그용 IPA 파일 생성 성공!${NC}"
echo -e "IPA 파일 위치: $(pwd)/$DEBUG_IPA_PATH"
# 홈 디렉토리로 IPA 복사
cp "$DEBUG_IPA_PATH" "$DEBUG_DEST_PATH"
echo -e "${GREEN}IPA를 홈 디렉토리에 복사했습니다: $DEBUG_DEST_PATH${NC}"
echo -e "${YELLOW}다음 방법으로 다른 기기에 설치할 수 있습니다:${NC}"
echo "1. Apple Configurator 2 앱 사용"
echo "2. 기기 등록 및 프로비저닝 프로파일이 있는 경우 iTunes로 설치"
echo "3. TestFlight를 통한 배포 (App Store Connect에 업로드 필요)"
else else
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}" echo -e "${GREEN}디버그 IPA 파일 생성 완료: build/debug_ipa/App.ipa${NC}"
fi fi
fi fi
else else
echo -e "${YELLOW}개발자 등록이 필요하여 IPA 파일 생성 단계를 건너니다.${NC}" echo -e "${YELLOW}개발자 등록이 필요하여 IPA 파일 생성 단계를 건너<0xEB><0x9B><0x81>니다.${NC}"
echo -e "${YELLOW}시뮬레이터에서 앱을 테스트하거나 Apple 개발자 계정을 등록하세요.${NC}" echo -e "${YELLOW}앱을 기기에서 테스트하려면 Xcode에서 '${WORKSPACE_DIR}/ios/App/App.xcworkspace' 파일을 열고,${NC}"
echo -e "${YELLOW}USB로 연결된 기기를 선택한 후 직접 빌드 및 실행하세요.${NC}"
fi fi
elif [ "$BUILD_TYPE" = "release" ]; then elif [ "$BUILD_TYPE" = "release" ]; then
@@ -282,7 +262,7 @@ EOF
echo -e "${YELLOW}릴리즈 빌드 시작...${NC}" echo -e "${YELLOW}릴리즈 빌드 시작...${NC}"
# 기본 Xcode 빌드 # 기본 Xcode 빌드
xcodebuild -workspace App.xcworkspace -scheme App -configuration Release -derivedDataPath build xcodebuild -workspace App.xcworkspace -scheme App -configuration Release -derivedDataPath build -allowProvisioningUpdates
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${RED}릴리즈 빌드 실패. 오류를 확인하세요.${NC}" echo -e "${RED}릴리즈 빌드 실패. 오류를 확인하세요.${NC}"
@@ -304,18 +284,21 @@ EOF
echo -e "${YELLOW}App Store 배포용 아카이브 생성 중...${NC}" echo -e "${YELLOW}App Store 배포용 아카이브 생성 중...${NC}"
# 아카이브 생성 # 아카이브 생성
xcodebuild -workspace App.xcworkspace -scheme App -configuration Release clean archive -archivePath "build/App.xcarchive" -allowProvisioningUpdates xcodebuild archive -workspace App.xcworkspace -scheme App -configuration Release -archivePath build/App.xcarchive -allowProvisioningUpdates
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}" echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}"
exit 1 exit 1
fi fi
echo -e "${GREEN}아카이브 생성 성공!${NC}" # IPA Export
echo -e "아카이브 위치: $(pwd)/build/App.xcarchive" RELEASE_DIR="$WORKSPACE_DIR/release"
EXPORT_OPTIONS_PLIST_PATH="build/ReleaseExportOptions.plist"
# exportOptions.plist 파일 생성 # release 디렉토리 생성
cat > exportOptions.plist << EOF mkdir -p "$RELEASE_DIR"
cat > "$EXPORT_OPTIONS_PLIST_PATH" <<EOL
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -323,56 +306,49 @@ EOF
<key>method</key> <key>method</key>
<string>app-store</string> <string>app-store</string>
<key>teamID</key> <key>teamID</key>
<string>${TEAM_ID}</string> <string>$TEAM_ID</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadBitcode</key> <key>uploadBitcode</key>
<false/> <true/>
<key>uploadSymbols</key> <key>uploadSymbols</key>
<true/> <true/>
<key>provisioningProfiles</key>
<dict>
<key>com.lovable.zellyfinance</key>
<string>Zellyy Finance App Store</string>
</dict>
</dict> </dict>
</plist> </plist>
EOF EOL
# exportPath는 디렉토리만 지정
# IPA 파일 생성 xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath "$RELEASE_DIR" -exportOptionsPlist "$EXPORT_OPTIONS_PLIST_PATH" -allowProvisioningUpdates
echo -e "${YELLOW}IPA 파일 생성 중...${NC}"
xcodebuild -exportArchive -archivePath "build/App.xcarchive" -exportOptionsPlist exportOptions.plist -exportPath "build/export" -allowProvisioningUpdates
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}" echo -e "${RED}릴리즈 IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
rm exportOptions.plist echo -e "${YELLOW}Apple 개발자 계정 정보(팀 ID: $TEAM_ID)가 정확한지, Xcode에서 서명 및 배포 설정이 완료되었는지 확인하세요.${NC}"
exit 1 echo -e "${GREEN}Xcode 빌드는 완료되었을 수 있습니다. Xcode에서 직접 확인하거나 실행해 보세요.${NC}"
fi
rm exportOptions.plist
IPA_PATH="build/export/App.ipa"
DEST_PATH="$HOME/Dev/zellyy-finance-ios.ipa"
if [ -f "$IPA_PATH" ]; then
echo -e "${GREEN}IPA 파일 생성 성공!${NC}"
echo -e "IPA 파일 위치: $(pwd)/$IPA_PATH"
# 홈 디렉토리로 IPA 복사
cp "$IPA_PATH" "$DEST_PATH"
echo -e "${GREEN}IPA를 홈 디렉토리에 복사했습니다: $DEST_PATH${NC}"
echo -e "${YELLOW}다음 단계:${NC}"
echo "1. App Store Connect에 로그인: https://appstoreconnect.apple.com"
echo "2. 앱 > 젤리의 적자탈출 > iOS 앱 > 빌드 섹션으로 이동"
echo "3. Transporter 앱을 사용하여 IPA 파일 업로드: $DEST_PATH"
echo "4. 검토 과정이 완료될 때까지 기다리기 (보통 몇 시간에서 24시간 소요)"
else else
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}" # 버전 정보 읽기 (스크립트 상단에서 이미 읽었지만 명확성을 위해 다시 확인)
exit 1 CURRENT_VERSION_NAME=$(grep "VERSION_NAME" "$PROPERTIES_FILE" | cut -d'=' -f2)
CURRENT_BUILD_NUMBER=$(grep "BUILD_NUMBER" "$PROPERTIES_FILE" | cut -d'=' -f2)
# IPA 파일 이름 변경 (기본 이름은 App.ipa 로 가정)
DEFAULT_IPA_NAME="App.ipa"
TARGET_IPA_NAME="App-v${CURRENT_VERSION_NAME}-b${CURRENT_BUILD_NUMBER}.ipa"
if [ -f "$RELEASE_DIR/$DEFAULT_IPA_NAME" ]; then
mv "$RELEASE_DIR/$DEFAULT_IPA_NAME" "$RELEASE_DIR/$TARGET_IPA_NAME"
if [ $? -eq 0 ]; then
echo -e "${GREEN}릴리즈 IPA 파일 생성 완료:${NC} ${BOLD}${RELEASE_DIR}/${TARGET_IPA_NAME}${NC}"
else
echo -e "${RED}IPA 파일 이름 변경 실패: ${RELEASE_DIR}/${DEFAULT_IPA_NAME} -> ${TARGET_IPA_NAME}${NC}"
echo -e "${YELLOW}생성된 IPA 파일은 ${RELEASE_DIR}/${DEFAULT_IPA_NAME} 에 있습니다.${NC}"
fi
else
echo -e "${RED}예상된 IPA 파일(${DEFAULT_IPA_NAME})을 찾을 수 없습니다. Export 과정을 확인하세요.${NC}"
fi
fi fi
fi fi
else else
echo -e "${YELLOW}개발자 등록이 필요하여 아카이브 생성 단계를 건너니다.${NC}" echo -e "${YELLOW}개발자 등록이 필요하여 아카이브 생성 단계를 건너<0xEB><0x9B><0x81>니다.${NC}"
echo -e "${YELLOW}시뮬레이터에서 앱을 테스트하거나 Apple 개발자 계정을 등록하세요.${NC}" echo -e "${YELLOW}앱을 App Store에 제출하거나 Ad Hoc 배포를 하려면 유료 Apple 개발자 계정이 필요합니다.${NC}"
echo -e "${YELLOW}개발 테스트는 Xcode에서 '${WORKSPACE_DIR}/ios/App/App.xcworkspace' 파일을 열고 직접 진행할 수 있습니다.${NC}"
fi fi
else else

View File

@@ -0,0 +1,561 @@
# Zellyy Finance CI/CD 파이프라인 가이드
이 문서는 Zellyy Finance 앱의 CI/CD(지속적 통합/지속적 배포) 파이프라인 구축 및 사용 방법에 대해 설명합니다.
## 목차
1. [개요](#개요)
2. [CI/CD 파이프라인 구성](#cicd-파이프라인-구성)
3. [GitHub Actions 설정](#github-actions-설정)
4. [버전 관리 자동화](#버전-관리-자동화)
5. [테스트 빌드 배포 및 설치](#테스트-빌드-배포-및-설치)
6. [앱스토어 자동 배포](#앱스토어-자동-배포)
7. [비용 분석](#비용-분석)
8. [문제 해결 및 FAQ](#문제-해결-및-faq)
## 개요
CI/CD 파이프라인은 코드 변경사항을 자동으로 빌드, 테스트 및 배포하는 자동화된 프로세스입니다. Zellyy Finance 앱에서는 GitHub Actions를 사용하여 이 프로세스를 구현합니다.
### 주요 이점
- **개발 효율성 향상**: 반복적인 빌드 및 배포 작업 자동화
- **일관된 빌드 품질**: 동일한 환경에서 항상 일관된 방식으로 빌드
- **버전 관리 자동화**: 버전 코드 및 빌드 번호 자동 증가
- **배포 프로세스 간소화**: 앱스토어 제출 과정 자동화
- **팀 협업 개선**: 빌드 상태 및 결과 공유 용이
## CI/CD 파이프라인 구성
Zellyy Finance의 CI/CD 파이프라인은 다음과 같은 단계로 구성됩니다:
1. **코드 검증**: 린트 검사 및 단위 테스트 실행
2. **웹 앱 빌드**: React 앱 빌드
3. **네이티브 앱 빌드**: Capacitor를 통한 Android/iOS 앱 빌드
4. **테스트 배포**: Firebase App Distribution을 통한 테스터 배포
5. **앱스토어 배포**: Google Play Store 및 Apple App Store 배포
### 워크플로우 다이어그램
```
코드 푸시/PR → 코드 검증 → 웹 앱 빌드 → 네이티브 앱 빌드 → 테스트 배포 → 앱스토어 배포
```
## GitHub Actions 설정
### 워크플로우 파일 생성
`.github/workflows/ci-cd.yml` 파일을 생성하여 CI/CD 파이프라인을 구성합니다.
```yaml
name: Zellyy Finance CI/CD
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:
inputs:
buildType:
description: '빌드 타입 (debug, release-apk, release-aab, ios-release)'
required: true
default: 'debug'
versionCode:
description: '버전 코드 (자동 증가는 1, 수동 지정은 원하는 숫자 입력)'
required: false
versionName:
description: '버전 이름 (예: 1.0.0)'
required: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 노드 설정
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
- name: 종속성 설치
run: npm ci
- name: 린트 검사
run: npm run lint
- name: 버전 정보 설정
id: version
run: |
# 현재 버전 코드 가져오기
CURRENT_VERSION_CODE=$(grep -o 'versionCode [0-9]*' android/app/build.gradle | awk '{print $2}')
if [ -z "$CURRENT_VERSION_CODE" ]; then
CURRENT_VERSION_CODE=1
fi
# 현재 버전 이름 가져오기
CURRENT_VERSION_NAME=$(grep -o 'versionName "[^"]*"' android/app/build.gradle | sed 's/versionName "//' | sed 's/"//')
if [ -z "$CURRENT_VERSION_NAME" ]; then
CURRENT_VERSION_NAME="1.0.0"
fi
# 입력된 버전 정보 또는 기본값 사용
if [ "${{ github.event.inputs.versionCode }}" = "1" ] || [ -z "${{ github.event.inputs.versionCode }}" ]; then
NEW_VERSION_CODE=$((CURRENT_VERSION_CODE + 1))
else
NEW_VERSION_CODE=${{ github.event.inputs.versionCode }}
fi
VERSION_NAME="${{ github.event.inputs.versionName }}"
if [ -z "$VERSION_NAME" ]; then
VERSION_NAME=$CURRENT_VERSION_NAME
fi
BUILD_NUMBER=$NEW_VERSION_CODE
# 버전 정보 파일 업데이트
echo "buildNumber=$BUILD_NUMBER" > android/version.properties
echo "versionCode=$NEW_VERSION_CODE" >> android/version.properties
echo "versionName=$VERSION_NAME" >> android/version.properties
cp android/version.properties android/app/version.properties
# app_version.json 파일 업데이트
cat > android/app_version.json << EOF
{
"versionCode": $NEW_VERSION_CODE,
"versionName": "$VERSION_NAME",
"buildNumber": $BUILD_NUMBER,
"notes": "자동 빌드에 의해 생성된 버전 정보입니다."
}
EOF
echo "VERSION_CODE=$NEW_VERSION_CODE" >> $GITHUB_OUTPUT
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_OUTPUT
echo "BUILD_NUMBER=$BUILD_NUMBER" >> $GITHUB_OUTPUT
- name: 웹 앱 빌드
run: npm run build
- name: Capacitor 동기화
run: npx cap sync android
- name: JDK 설정
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: 안드로이드 앱 빌드
working-directory: android
run: |
if [ "${{ github.event.inputs.buildType }}" = "release-aab" ]; then
./gradlew clean bundleRelease
elif [ "${{ github.event.inputs.buildType }}" = "release-apk" ]; then
./gradlew clean assembleRelease
else
./gradlew clean assembleDebug
fi
- name: Firebase App Distribution 배포
if: github.event.inputs.buildType == 'debug'
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{ secrets.FIREBASE_APP_ID }}
serviceCredentialsFileContent: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }}
groups: testers
file: android/app/build/outputs/apk/debug/app-debug.apk
releaseNotes: |
버전: ${{ steps.version.outputs.VERSION_NAME }} (${{ steps.version.outputs.BUILD_NUMBER }})
빌드 날짜: $(date +'%Y-%m-%d %H:%M:%S')
커밋: ${{ github.sha }}
- name: Google Play 배포
if: github.event.inputs.buildType == 'release-aab'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
packageName: com.zellyy.finance
releaseFiles: android/app/build/outputs/bundle/release/app-release.aab
track: internal
status: completed
releaseName: ${{ steps.version.outputs.VERSION_NAME }}
releaseNotes: |
ko-KR: 버전 ${{ steps.version.outputs.VERSION_NAME }} 업데이트
- 버그 수정 및 성능 개선
- name: 빌드 결과물 저장
uses: actions/upload-artifact@v3
with:
name: app-${{ steps.version.outputs.VERSION_NAME }}-${{ steps.version.outputs.BUILD_NUMBER }}
path: |
android/app/build/outputs/apk/debug/*.apk
android/app/build/outputs/apk/release/*.apk
android/app/build/outputs/bundle/release/*.aab
- name: 슬랙 알림
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: always()
```
### GitHub Secrets 설정
GitHub 저장소의 Settings > Secrets and variables > Actions에서 다음 시크릿을 설정해야 합니다:
- `FIREBASE_APP_ID`: Firebase 프로젝트의 앱 ID
- `FIREBASE_SERVICE_ACCOUNT`: Firebase 서비스 계정 JSON 파일 내용
- `PLAY_STORE_SERVICE_ACCOUNT_JSON`: Google Play 서비스 계정 JSON 파일 내용
- `SLACK_WEBHOOK_URL`: 슬랙 알림을 위한 웹훅 URL
## 버전 관리 자동화
### 버전 관리 스크립트
`scripts/version-manager.sh` 파일을 생성하여 버전 관리를 자동화합니다.
```bash
#!/bin/bash
# version-manager.sh
# 현재 버전 정보 가져오기
get_current_version() {
if [ -f "android/app_version.json" ]; then
VERSION_CODE=$(grep -o '"versionCode": [0-9]*' android/app_version.json | awk '{print $2}')
VERSION_NAME=$(grep -o '"versionName": "[^"]*"' android/app_version.json | sed 's/"versionName": "//' | sed 's/"//')
BUILD_NUMBER=$(grep -o '"buildNumber": [0-9]*' android/app_version.json | awk '{print $2}')
# 값이 비어있는지 확인
if [ -z "$VERSION_CODE" ]; then VERSION_CODE=1; fi
if [ -z "$VERSION_NAME" ]; then VERSION_NAME="1.0.0"; fi
if [ -z "$BUILD_NUMBER" ]; then BUILD_NUMBER=1; fi
else
VERSION_CODE=1
VERSION_NAME="1.0.0"
BUILD_NUMBER=1
fi
echo "현재 버전 정보:"
echo "버전 코드: $VERSION_CODE"
echo "버전 이름: $VERSION_NAME"
echo "빌드 넘버: $BUILD_NUMBER"
}
# 버전 정보 업데이트
update_version_files() {
# version.properties 파일 업데이트
echo "buildNumber=$BUILD_NUMBER" > android/version.properties
echo "versionCode=$VERSION_CODE" >> android/version.properties
echo "versionName=$VERSION_NAME" >> android/version.properties
# app 디렉토리에도 동일한 파일 복사
cp android/version.properties android/app/version.properties
# app_version.json 파일 업데이트
cat > android/app_version.json << EOF
{
"versionCode": $VERSION_CODE,
"versionName": "$VERSION_NAME",
"buildNumber": $BUILD_NUMBER,
"notes": "자동 업데이트된 버전 정보입니다."
}
EOF
echo "버전 정보가 모든 파일에 저장되었습니다."
}
# 메인 함수
main() {
get_current_version
# 빌드 타입에 따라 버전 증가
case "$1" in
"debug")
BUILD_NUMBER=$((BUILD_NUMBER + 1))
;;
"release")
VERSION_CODE=$((VERSION_CODE + 1))
BUILD_NUMBER=$VERSION_CODE
;;
*)
echo "사용법: $0 [debug|release]"
exit 1
;;
esac
update_version_files
echo "업데이트된 버전 정보:"
echo "버전 코드: $VERSION_CODE"
echo "버전 이름: $VERSION_NAME"
echo "빌드 넘버: $BUILD_NUMBER"
}
main "$@"
```
### 버전 관리 규칙
Zellyy Finance 앱의 버전 관리는 다음 규칙을 따릅니다:
- **versionCode**: 앱스토어에 제출할 때마다 증가하는 정수 값
- **versionName**: 사용자에게 표시되는 버전 (예: "1.0.0")
- **buildNumber**: 내부 빌드 추적을 위한 번호
## 테스트 빌드 배포 및 설치
테스트 빌드가 완료된 후 개발자와 테스터가 앱을 설치하고 테스트할 수 있는 방법을 설명합니다.
### Firebase App Distribution을 통한 배포
Firebase App Distribution은 테스트 버전 앱을 테스터에게 쉽게 배포할 수 있는 서비스입니다.
#### 설정 방법
1. **Firebase 프로젝트 설정**:
- [Firebase 콘솔](https://console.firebase.google.com/)에서 프로젝트 생성
- App Distribution 서비스 활성화
- 테스터 그룹 생성 및 테스터 이메일 추가
2. **GitHub Actions 설정**:
- `FIREBASE_APP_ID``FIREBASE_SERVICE_ACCOUNT` 시크릿 설정
- 워크플로우 파일에 Firebase 배포 단계 추가
#### 테스터 초대 및 앱 설치 방법
##### 안드로이드 테스터
1. **테스터 초대**:
- Firebase 콘솔에서 테스터 이메일 추가
- 테스터에게 초대 이메일 발송
2. **앱 설치 방법**:
- 테스터는 초대 이메일의 링크를 클릭
- Firebase App Tester 앱 설치 (처음 사용 시)
- 테스트 앱 다운로드 및 설치
- 설치 시 "알 수 없는 출처" 앱 설치 허용 필요
```bash
# 안드로이드 테스터를 위한 안내 메시지 예시
안드로이드 테스트 앱 설치 방법:
1. 초대 이메일의 링크를 클릭하세요
2. Firebase App Tester 앱을 설치하세요 (처음 사용 시)
3. 테스트 앱을 다운로드하고 설치하세요
4. 설정 > 보안 > 알 수 없는 출처에서 설치 허용을 활성화하세요
```
##### iOS 테스터
1. **TestFlight 설정**:
- Apple Developer 계정에서 앱 등록
- 내부 테스터 그룹 생성
2. **테스터 초대**:
- App Store Connect에서 테스터 이메일 추가
- 테스터에게 초대 이메일 발송
3. **앱 설치 방법**:
- 테스터는 초대 이메일의 링크를 클릭
- TestFlight 앱 설치 (App Store에서 다운로드)
- TestFlight를 통해 테스트 앱 설치
```bash
# iOS 테스터를 위한 안내 메시지 예시
iOS 테스트 앱 설치 방법:
1. 초대 이메일의 링크를 클릭하세요
2. App Store에서 TestFlight 앱을 설치하세요
3. TestFlight 앱을 열고 테스트 앱을 설치하세요
```
### GitHub Actions Artifacts를 통한 직접 다운로드
Firebase App Distribution 외에도 GitHub Actions의 Artifacts 기능을 사용하여 빌드 결과물을 직접 다운로드할 수 있습니다.
1. **빌드 결과물 확인**:
- GitHub 저장소의 Actions 탭으로 이동
- 해당 워크플로우 실행 결과 페이지 열기
- Artifacts 섹션에서 APK 또는 IPA 파일 다운로드
2. **안드로이드 APK 설치**:
- 다운로드한 APK 파일을 안드로이드 기기로 전송
- 파일 관리자에서 APK 파일 실행
- 알 수 없는 출처 앱 설치 허용
3. **iOS IPA 설치** (개발자만 가능):
- Apple Developer 계정이 있는 개발자만 가능
- Xcode 또는 Apple Configurator를 사용하여 설치
### QR 코드를 통한 간편 설치
테스터의 편의를 위해 QR 코드를 생성하여 배포할 수도 있습니다:
```yaml
- name: QR 코드 생성
if: github.event.inputs.buildType == 'debug'
run: |
# Firebase 배포 URL에 대한 QR 코드 생성
FIREBASE_URL="https://appdistribution.firebase.dev/i/..."
curl -s "https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=$FIREBASE_URL" > qrcode.png
echo "QR 코드가 생성되었습니다. 테스터는 이 코드를 스캔하여 앱을 설치할 수 있습니다."
- name: QR 코드 저장
if: github.event.inputs.buildType == 'debug'
uses: actions/upload-artifact@v3
with:
name: install-qrcode
path: qrcode.png
```
## 앱스토어 자동 배포
### Google Play Store 배포
Google Play Store에 자동으로 앱을 배포하려면 다음 단계가 필요합니다:
1. **Google Play Console 설정**:
- API 액세스 활성화
- 서비스 계정 생성 및 권한 부여
- JSON 키 다운로드
2. **GitHub Actions 설정**:
- `PLAY_STORE_SERVICE_ACCOUNT_JSON` 시크릿 설정
- 워크플로우 파일에 배포 단계 추가
### Apple App Store 배포
Apple App Store에 자동으로 앱을 배포하려면 Fastlane을 사용합니다:
1. **Fastlane 설정**:
- iOS 디렉토리에 Fastfile 생성
```ruby
default_platform(:ios)
platform :ios do
desc "배포용 iOS 앱 빌드 및 TestFlight 업로드"
lane :release do
setup_ci
# 인증서 및 프로비저닝 프로파일 동기화
match(
type: "appstore",
readonly: true
)
# 버전 업데이트
increment_build_number(
build_number: ENV["BUILD_NUMBER"]
)
# 앱 빌드
build_app(
scheme: "App",
workspace: "App.xcworkspace",
export_method: "app-store"
)
# TestFlight 업로드
upload_to_testflight(
skip_waiting_for_build_processing: true
)
end
end
```
2. **GitHub Actions 설정**:
- App Store Connect API 키 생성 및 GitHub Secrets에 저장
- 워크플로우 파일에 iOS 빌드 및 배포 단계 추가
## 비용 분석
### GitHub Actions 비용
GitHub Actions는 다음과 같은 무료 할당량을 제공합니다:
- **퍼블릭 저장소**: 무제한 무료 사용 가능
- **프라이빗 저장소**:
- GitHub Free: 매월 2,000분의 무료 빌드 시간
- GitHub Pro: 매월 3,000분의 무료 빌드 시간
- GitHub Team: 매월 계정당 3,000분의 무료 빌드 시간
- GitHub Enterprise: 매월 계정당 50,000분의 무료 빌드 시간
### Zellyy Finance 앱의 예상 비용
- **빌드 시간 예상**: 한 번의 워크플로우당 약 7-20분
- **월간 빌드 횟수 시나리오**:
- 하루 1회 빌드: 월 30회 × 15분 = 450분
- 하루 3회 빌드: 월 90회 × 15분 = 1,350분
- 하루 5회 빌드: 월 150회 × 15분 = 2,250분
- **비용 계산**:
- GitHub Free 계정 사용 시: 월 2,000분 무료 (하루 4회 빌드까지 무료)
- 초과 사용 시: 분당 $0.008 (Linux 머신 기준)
### 비용 최적화 전략
1. **캐싱 활용**:
```yaml
- uses: actions/cache@v3
with:
path: |
~/.npm
node_modules
android/.gradle
key: ${{ runner.os }}-build-${{ hashFiles('**/package-lock.json') }}
```
2. **빌드 트리거 최적화**:
```yaml
on:
push:
branches: [ main, develop ]
paths-ignore:
- '**.md'
- 'docs/**'
```
3. **조건부 작업 설정**:
```yaml
- name: Google Play 배포
if: github.ref == 'refs/heads/main' && github.event.inputs.buildType == 'release-aab'
# ...
```
## 문제 해결 및 FAQ
### 자주 발생하는 문제
1. **빌드 실패**:
- 종속성 문제: `npm ci` 대신 `npm install`을 사용해 보세요.
- 캐시 문제: 캐시를 삭제하고 다시 시도하세요.
2. **버전 관리 문제**:
- 버전 파일이 비어 있는 경우: 스크립트가 기본값을 설정하도록 합니다.
- 버전 코드 충돌: 수동으로 버전 코드를 설정하여 해결합니다.
3. **배포 실패**:
- 인증 문제: 서비스 계정 권한을 확인하세요.
- 앱 서명 문제: 키스토어 설정을 확인하세요.
### FAQ
**Q: 워크플로우를 수동으로 실행하려면 어떻게 해야 하나요?**
A: GitHub 저장소의 Actions 탭에서 "Zellyy Finance CI/CD" 워크플로우를 선택하고 "Run workflow" 버튼을 클릭합니다.
**Q: 특정 커밋에서 빌드를 실행하려면 어떻게 해야 하나요?**
A: 해당 커밋으로 체크아웃한 후 수동으로 워크플로우를 실행하거나, GitHub API를 사용하여 특정 커밋에서 워크플로우를 트리거할 수 있습니다.
**Q: 빌드 결과물은 어디에서 찾을 수 있나요?**
A: 워크플로우 실행 페이지의 "Artifacts" 섹션에서 다운로드할 수 있습니다.
**Q: 테스터가 테스트 앱을 설치하는 가장 쉬운 방법은 무엇인가요?**
A: Firebase App Distribution을 사용하는 것이 가장 쉽습니다. 테스터는 이메일로 초대를 받고 링크를 통해 앱을 설치할 수 있습니다. QR 코드를 제공하면 더욱 편리하게 설치할 수 있습니다.
**Q: iOS 테스트 앱 설치 시 개발자 계정이 필요한가요?**
A: TestFlight를 통한 배포의 경우 테스터는 개발자 계정이 필요하지 않습니다. 하지만 개발자는 Apple Developer 계정이 필요합니다. GitHub Actions Artifacts를 통한 직접 설치는 개발자만 가능합니다.

62
refresh-web.sh Executable file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
# 색상 정의
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo -e "${YELLOW}웹앱 새로고침 스크립트${NC}"
echo -e "${YELLOW}====================${NC}"
# 1. 캐시 완전 삭제
echo -e "${YELLOW}1. 캐시 완전 삭제 중...${NC}"
rm -rf node_modules/.vite
rm -rf node_modules/.cache
rm -rf android/app/build
rm -rf android/.gradle
rm -rf dist
rm -rf android/app/src/main/assets/public
echo -e "${GREEN}캐시가 삭제되었습니다.${NC}"
# 2. 웹 앱 다시 빌드
echo -e "${YELLOW}2. 웹 앱 다시 빌드 중...${NC}"
npm run build
if [ $? -ne 0 ]; then
echo -e "${RED}웹 앱 빌드 실패${NC}"
exit 1
fi
echo -e "${GREEN}웹 앱 빌드 완료${NC}"
# 3. Capacitor 강제 동기화
echo -e "${YELLOW}3. Capacitor 강제 동기화 중...${NC}"
npx cap sync android --inline
if [ $? -ne 0 ]; then
echo -e "${RED}Capacitor 동기화 실패${NC}"
exit 1
fi
echo -e "${GREEN}Capacitor 동기화 완료${NC}"
# 4. 빌드된 웹앱 확인
echo -e "${YELLOW}4. 빌드된 웹앱 확인${NC}"
if [ -d "dist" ]; then
echo -e "${GREEN}dist 디렉토리가 존재합니다.${NC}"
ls -la dist
else
echo -e "${RED}dist 디렉토리가 없습니다!${NC}"
fi
# 5. 안드로이드 assets 확인
echo -e "${YELLOW}5. 안드로이드 assets 확인${NC}"
ASSETS_DIR="android/app/src/main/assets/public"
if [ -d "$ASSETS_DIR" ]; then
echo -e "${GREEN}안드로이드 assets 디렉토리가 존재합니다.${NC}"
ls -la "$ASSETS_DIR"
else
echo -e "${RED}안드로이드 assets 디렉토리가 없습니다!${NC}"
fi
echo -e "${GREEN}웹앱 새로고침 완료!${NC}"
echo -e "${YELLOW}이제 build-apk.sh 스크립트를 실행하여 앱을 빌드하세요.${NC}"

View File

@@ -0,0 +1,242 @@
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 20cd809..fe19eb4 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -1,5 +1,19 @@
apply plugin: 'com.android.application'
+// 버전 정보를 properties 파일에서 동적으로 로드
+def versionPropsFile = rootProject.file('version.properties')
+def versionProps = new Properties()
+if (versionPropsFile.exists()) {
+ versionPropsFile.withInputStream { stream -> versionProps.load(stream) }
+}
+
+def versionName = versionProps['versionName'] ?: "1.1.1.2"
+def versionCode = (versionProps['versionCode'] ?: "6").toInteger()
+def buildNumber = (versionProps['buildNumber'] ?: "6").toInteger()
+
+// 버전 정보 로깅
+println "버전 정보 로드: versionName=${versionName}, versionCode=${versionCode}, buildNumber=${buildNumber}"
+
// 버전 정보를 직접 설정
android {
namespace "com.lovable.zellyfinance"
@@ -8,10 +22,10 @@ android {
applicationId "com.lovable.zellyfinance"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 6
- versionName "1.1.1.2"
+ versionCode versionCode
+ versionName "${versionName}"
// 빌드 번호 추가 - BuildConfig 필드로 정의
- buildConfigField "int", "BUILD_NUMBER", "6"
+ buildConfigField "int", "BUILD_NUMBER", "${buildNumber}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/android/app/src/main/java/com/lovable/zellyfinance/BuildInfoPlugin.java b/android/app/src/main/java/com/lovable/zellyfinance/BuildInfoPlugin.java
index 3520f5d..0957128 100644
--- a/android/app/src/main/java/com/lovable/zellyfinance/BuildInfoPlugin.java
+++ b/android/app/src/main/java/com/lovable/zellyfinance/BuildInfoPlugin.java
@@ -1,4 +1,3 @@
-
package com.lovable.zellyfinance;
import android.os.Build;
@@ -27,16 +26,27 @@ public class BuildInfoPlugin extends Plugin {
JSObject ret = new JSObject();
- // 빌드 정보 수집
+ // BuildConfig에서 동적으로 버전 정보 가져오기
String versionName = BuildConfig.VERSION_NAME;
int versionCode = BuildConfig.VERSION_CODE;
- int buildNumber = BuildConfig.BUILD_NUMBER;
- String packageName = getContext().getPackageName();
+ int buildNumber;
+
+ // 빌드 넘버는 커스텀 필드이므로 try-catch로 확인
+ try {
+ buildNumber = BuildConfig.BUILD_NUMBER;
+ Log.d(TAG, "BuildConfig.BUILD_NUMBER: " + buildNumber);
+ } catch (Exception e) {
+ Log.e(TAG, "BUILD_NUMBER 필드 접근 오류, 기본값 사용", e);
+ buildNumber = versionCode; // 빌드 넘버가 없으면 버전 코드와 동일하게 설정
+ }
// 디버깅을 위한 로그 출력
+ Log.d(TAG, "BuildConfig 클래스: " + BuildConfig.class.getName());
Log.d(TAG, "버전명: " + versionName);
Log.d(TAG, "버전 코드: " + versionCode);
Log.d(TAG, "빌드 번호: " + buildNumber);
+
+ String packageName = getContext().getPackageName();
Log.d(TAG, "패키지명: " + packageName);
// 결과 객체에 값 설정
@@ -49,19 +59,17 @@ public class BuildInfoPlugin extends Plugin {
ret.put("platform", "android");
// 현재 날짜를 디버깅 정보로 추가
- ret.put("buildDate", new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()));
+ ret.put("timestamp", System.currentTimeMillis());
- Log.d(TAG, "빌드 정보 요청 성공 처리: " + ret.toString());
+ // 성공 응답
call.resolve(ret);
} catch (Exception e) {
- Log.e(TAG, "빌드 정보 가져오기 실패", e);
- JSObject errorResult = new JSObject();
- errorResult.put("versionName", "1.0.0");
- errorResult.put("versionCode", 1);
- errorResult.put("buildNumber", 1);
- errorResult.put("error", e.getMessage());
- errorResult.put("platform", "android-error");
- call.resolve(errorResult); // 에러가 발생해도 앱이 중단되지 않도록 resolve 호출
+ // 오류 로깅 강화
+ Log.e(TAG, "빌드 정보 가져오기 오류", e);
+ JSObject errorObj = new JSObject();
+ errorObj.put("message", e.getMessage());
+ errorObj.put("stack", Log.getStackTraceString(e));
+ call.reject("빌드 정보 가져오기 실패", errorObj);
}
}
}
diff --git a/src/utils/platform.ts b/src/utils/platform.ts
index c9a89de..1a5af86 100644
--- a/src/utils/platform.ts
+++ b/src/utils/platform.ts
@@ -1,4 +1,3 @@
-
/**
* 플랫폼 관련 유틸리티 함수들
*/
@@ -30,6 +29,15 @@ export const isNativePlatform = (): boolean => {
* 앱 버전 정보 가져오기
*/
export const getAppVersionInfo = async () => {
+ // 기본값 설정 - 플러그인 실패 시 사용
+ const defaultVersionInfo = {
+ versionName: '1.0.0',
+ versionCode: 1,
+ buildNumber: 1,
+ platform: Capacitor.getPlatform(),
+ defaultValues: true
+ };
+
try {
// 디버깅을 위한 플랫폼 체크 로그
console.log('현재 플랫폼:', Capacitor.getPlatform());
@@ -39,65 +47,63 @@ export const getAppVersionInfo = async () => {
if (Capacitor.isPluginAvailable('BuildInfo')) {
try {
// 플러그인 호출 시도
- // @ts-ignore - 플러그인이 런타임에 등록되므로 타입 체크를 무시
+ // @ts-expect-error - 플러그인이 런타임에 등록되므로 타입 체크를 무시
const buildInfo = await Capacitor.Plugins.BuildInfo.getBuildInfo();
- console.log('네이티브에서 받은 빌드 정보:', buildInfo);
+ console.log('네이티브에서 받은 빌드 정보(raw):', JSON.stringify(buildInfo));
- // 받은 정보가 유효한지 확인
- if (buildInfo && typeof buildInfo === 'object') {
- // iOS에서는 buildNumber가 문자열로 올 수 있으므로 숫자로 변환
- let buildNumberValue = buildInfo.buildNumber;
- if (typeof buildNumberValue === 'string') {
- buildNumberValue = parseInt(buildNumberValue, 10);
- }
-
- return {
- versionName: buildInfo.versionName || '1.0.1',
- buildNumber: !isNaN(buildNumberValue) ? buildNumberValue : 2,
- versionCode: buildInfo.versionCode,
- platform: Capacitor.getPlatform(),
+ // 수신한 데이터가 null 또는 undefined인 경우 체크
+ if (!buildInfo) {
+ console.warn('네이티브 빌드 정보가 없음');
+ throw new Error('빌드 정보를 받지 못했습니다');
+ }
+
+ // 받은 정보가 유효한지 확인 및 타입 변환
+ if (typeof buildInfo === 'object') {
+ // 타입 변환 및 기본값 설정 - 명시적으로 문자열/숫자 타입 변환
+ const result = {
+ versionName: typeof buildInfo.versionName === 'string' ? buildInfo.versionName :
+ (buildInfo.versionName ? String(buildInfo.versionName) : defaultVersionInfo.versionName),
+ versionCode: typeof buildInfo.versionCode === 'number' ? buildInfo.versionCode :
+ (buildInfo.versionCode ? Number(buildInfo.versionCode) : defaultVersionInfo.versionCode),
+ buildNumber: typeof buildInfo.buildNumber === 'number' ? buildInfo.buildNumber :
+ (buildInfo.buildNumber ? Number(buildInfo.buildNumber) : defaultVersionInfo.buildNumber),
+ platform: typeof buildInfo.platform === 'string' ? buildInfo.platform :
+ (buildInfo.platform ? String(buildInfo.platform) : defaultVersionInfo.platform),
+ timestamp: buildInfo.timestamp || Date.now(),
pluginResponse: JSON.stringify(buildInfo)
};
+
+ // 값이 기본값인지 확인 (BuildConfig에서 기본값을 반환하는 경우)
+ if (result.versionName === '1.0' && result.versionCode === 1 && isAndroidPlatform()) {
+ console.warn('플러그인이 기본값을 반환함, 빌드 설정을 확인해야 함');
+ }
+
+ console.log('변환된 빌드 정보:', result);
+ return result;
}
+ throw new Error('유효하지 않은 빌드 정보 응답');
} catch (pluginError) {
console.error('BuildInfo 플러그인 호출 오류:', pluginError);
+ // 오류 발생시 기본값 사용
+ return {
+ ...defaultVersionInfo,
+ error: true,
+ errorMessage: String(pluginError)
+ };
}
+ } else {
+ console.warn('BuildInfo 플러그인을 사용할 수 없음');
+ // 플러그인이 없는 경우 기본값 반환
+ return defaultVersionInfo;
}
-
- // 안드로이드인 경우 기본값을 하드코딩된 값으로 설정
- if (isAndroidPlatform()) {
- // 안드로이드 앱 빌드 정보를 하드코딩된 값으로 제공
- // 실제 앱에서는 빌드 과정에서 이 값들이 업데이트되어야 함
- return {
- versionName: '1.0.1',
- buildNumber: 3, // 업데이트된 빌드 번호
- versionCode: 1,
- platform: 'android'
- };
- }
-
- // iOS인 경우 기본값
- if (isIOSPlatform()) {
- return {
- versionName: '1.0.1',
- buildNumber: 3, // 업데이트된 빌드 번호
- platform: 'ios'
- };
- }
-
- // 플러그인이 없는 경우 기본값 반환
- return {
- versionName: '1.0.1',
- buildNumber: 3, // 업데이트된 빌드 번호
- platform: Capacitor.getPlatform()
- };
} catch (error) {
- console.error('앱 버전 정보를 가져오는 중 오류 발생:', error);
+ console.error('앱 버전 정보 가져오기 오류:', error);
+ // 오류 발생 시에도 앱 실행은 계속되도록 기본값 반환
return {
- versionName: '1.0.1',
- buildNumber: 3, // 업데이트된 빌드 번호
- error: true
+ ...defaultVersionInfo,
+ error: true,
+ errorMessage: String(error)
};
}
};

View File

@@ -30,9 +30,9 @@ const AppVersionInfo: React.FC<AppVersionInfoProps> = ({
}) => { }) => {
// 하드코딩된 버전 정보 - 빌드 스크립트에서 설정한 값과 일치시켜야 함 // 하드코딩된 버전 정보 - 빌드 스크립트에서 설정한 값과 일치시켜야 함
const hardcodedVersionInfo: VersionInfo = { const hardcodedVersionInfo: VersionInfo = {
versionName: '1.1.1.3', versionName: '1.1.8',
buildNumber: 7, buildNumber: 9,
versionCode: 7, versionCode: 9,
platform: Capacitor.getPlatform(), platform: Capacitor.getPlatform(),
defaultValuesUsed: false defaultValuesUsed: false
}; };

View File

@@ -1,80 +1,31 @@
import React, { useEffect, useState, ReactNode } from 'react'; import React from 'react';
import { isIOSPlatform } from '@/utils/platform'; import { cn } from '@/lib/utils';
interface SafeAreaContainerProps { interface SafeAreaContainerProps {
children: ReactNode; children: React.ReactNode;
className?: string; className?: string;
topOnly?: boolean; extraBottomPadding?: boolean;
bottomOnly?: boolean;
extraBottomPadding?: boolean; // 추가 하단 여백 옵션
} }
/** /**
* 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트 * iOS의 안전 영역(notch, home indicator 등)을 고려한 컨테이너
* iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용 * 모든 페이지 최상위 컴포넌트로 사용해야 함
*/ */
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({ const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
children, children,
className = '', className = '',
topOnly = false, extraBottomPadding = false
bottomOnly = false,
extraBottomPadding = false // 기본값은 false
}) => { }) => {
const [isIOS, setIsIOS] = useState(false);
// 마운트 시 플랫폼 확인
useEffect(() => {
const checkPlatform = async () => {
const isiOS = isIOSPlatform();
console.log('SafeAreaContainer: 플랫폼 확인 - iOS:', isiOS);
setIsIOS(isiOS);
};
checkPlatform();
}, []);
// 플랫폼에 따른 클래스 결정
let safeAreaClass = 'safe-area-container';
if (isIOS) {
if (!bottomOnly) safeAreaClass += ' has-safe-area-top'; // iOS 상단 안전 영역
if (!topOnly) safeAreaClass += ' has-safe-area-bottom'; // iOS 하단 안전 영역
safeAreaClass += ' ios-safe-area'; // iOS 전용 클래스 추가
} else {
if (!bottomOnly) safeAreaClass += ' pt-4'; // 안드로이드 상단 여백
if (!topOnly) safeAreaClass += ' pb-4'; // 안드로이드 하단 여백
}
// 추가 하단 여백 적용
const extraBottomClass = extraBottomPadding ? 'pb-[80px]' : '';
// 디버그용 로그 추가
useEffect(() => {
if (isIOS) {
console.log('SafeAreaContainer: iOS 안전 영역 적용됨', {
topOnly,
bottomOnly,
extraBottomPadding
});
// 안전 영역 값 확인 (CSS 변수)
try {
const computedStyle = getComputedStyle(document.documentElement);
console.log('Safe area 변수 값:', {
top: computedStyle.getPropertyValue('--safe-area-top'),
bottom: computedStyle.getPropertyValue('--safe-area-bottom'),
left: computedStyle.getPropertyValue('--safe-area-left'),
right: computedStyle.getPropertyValue('--safe-area-right')
});
} catch (error) {
console.error('CSS 변수 확인 중 오류:', error);
}
}
}, [isIOS, topOnly, bottomOnly]);
return ( return (
<div className={`${safeAreaClass} ${extraBottomClass} ${className}`}> <div
className={cn(
'min-h-screen bg-neuro-background',
'pt-safe pb-safe pl-safe pr-safe', // iOS 안전 영역 적용
extraBottomPadding ? 'pb-24' : '',
className
)}
>
{children} {children}
</div> </div>
); );

View File

@@ -1,19 +1,15 @@
import React from 'react'; import React from 'react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts'; import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, Cell } from 'recharts';
import { formatCurrency } from '@/utils/formatters'; import { formatCurrency } from '@/utils/formatters';
interface MonthlyData { interface MonthlyData {
name: string; name: string;
budget: number; budget: number;
expense: number; expense: number;
} }
interface MonthlyComparisonChartProps { interface MonthlyComparisonChartProps {
monthlyData: MonthlyData[]; monthlyData: MonthlyData[];
isEmpty?: boolean; isEmpty?: boolean;
} }
const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
monthlyData, monthlyData,
isEmpty = false isEmpty = false
@@ -35,24 +31,19 @@ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
console.log('MonthlyComparisonChart 데이터:', monthlyData); console.log('MonthlyComparisonChart 데이터:', monthlyData);
// EmptyGraphState 컴포넌트: 데이터가 없을 때 표시 // EmptyGraphState 컴포넌트: 데이터가 없을 때 표시
const EmptyGraphState = () => ( const EmptyGraphState = () => <div className="flex flex-col items-center justify-center h-48 text-gray-400">
<div className="flex flex-col items-center justify-center h-48 text-gray-400">
<p> </p> <p> </p>
<p className="text-sm mt-2"> </p> <p className="text-sm mt-2"> </p>
</div> </div>;
);
// 데이터 여부 확인 로직 개선 - 데이터가 비어있거나 모든 값이 0인 경우도 고려 // 데이터 여부 확인 로직 개선 - 데이터가 비어있거나 모든 값이 0인 경우도 고려
const hasValidData = monthlyData && const hasValidData = monthlyData && monthlyData.length > 0 && monthlyData.some(item => item.budget > 0 || item.expense > 0);
monthlyData.length > 0 &&
monthlyData.some(item => (item.budget > 0 || item.expense > 0));
// 지출 색상 결정 함수 추가 // 지출 색상 결정 함수 추가
const getExpenseColor = (budget: number, expense: number) => { const getExpenseColor = (budget: number, expense: number) => {
if (budget === 0) return "#81c784"; // 예산이 0이면 기본 색상 if (budget === 0) return "#81c784"; // 예산이 0이면 기본 색상
const ratio = expense / budget; const ratio = expense / budget;
if (ratio > 1) return "#f44336"; // 빨간색 (예산 초과) if (ratio > 1) return "#f44336"; // 빨간색 (예산 초과)
if (ratio >= 0.9) return "#ffeb3b"; // 노란색 (예산의 90% 이상) if (ratio >= 0.9) return "#ffeb3b"; // 노란색 (예산의 90% 이상)
return "#81c784"; // 기본 초록색 return "#81c784"; // 기본 초록색
@@ -64,10 +55,8 @@ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
// 예산 색상을 좀 더 짙은 회색으로 변경 // 예산 색상을 좀 더 짙은 회색으로 변경
const darkGrayColor = "#9F9EA1"; // 이전 색상 #C8C8C9에서 더 짙은 회색으로 변경 const darkGrayColor = "#9F9EA1"; // 이전 색상 #C8C8C9에서 더 짙은 회색으로 변경
return ( return <div className="neuro-card h-72 w-full">
<div className="neuro-card h-72 w-full"> {hasValidData ? <ResponsiveContainer width="100%" height="100%">
{hasValidData ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={monthlyData} margin={{ <BarChart data={monthlyData} margin={{
top: 20, top: 20,
right: 10, right: 10,
@@ -78,28 +67,24 @@ const MonthlyComparisonChart: React.FC<MonthlyComparisonChartProps> = ({
}}> }}>
<XAxis dataKey="name" /> <XAxis dataKey="name" />
<YAxis tickFormatter={formatYAxisTick} /> <YAxis tickFormatter={formatYAxisTick} />
<Tooltip <Tooltip formatter={formatTooltip} contentStyle={{
formatter={formatTooltip} backgroundColor: 'white',
contentStyle={{ backgroundColor: 'white', border: 'none' }} border: 'none'
cursor={{ fill: 'transparent' }} }} cursor={{
/> fill: 'transparent'
<Legend }} />
formatter={(value) => { <Legend formatter={value => {
// 범례 텍스트 색상 설정 // 범례 텍스트 색상 설정
return <span style={{ color: value === '지출' ? mainGreenColor : undefined }}>{value}</span>; return <span style={{
}} color: value === '지출' ? mainGreenColor : undefined
/> }} className="text-sm">{value}</span>;
}} />
<Bar dataKey="budget" name="예산" fill={darkGrayColor} radius={[4, 4, 0, 0]} /> <Bar dataKey="budget" name="예산" fill={darkGrayColor} radius={[4, 4, 0, 0]} />
<Bar dataKey="expense" name="지출" fill={mainGreenColor} radius={[4, 4, 0, 0]}> <Bar dataKey="expense" name="지출" fill={mainGreenColor} radius={[4, 4, 0, 0]}>
{/* 개별 셀 색상 설정은 제거하고 통일된 메인 그린 색상 사용 */} {/* 개별 셀 색상 설정은 제거하고 통일된 메인 그린 색상 사용 */}
</Bar> </Bar>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer> : <EmptyGraphState />}
) : ( </div>;
<EmptyGraphState />
)}
</div>
);
}; };
export default MonthlyComparisonChart; export default MonthlyComparisonChart;

View File

@@ -1,15 +1,12 @@
import React from 'react'; import React from 'react';
import { Wallet, CreditCard, Coins } from 'lucide-react'; import { Wallet, CreditCard, Coins } from 'lucide-react';
import { formatCurrency } from '@/utils/formatters'; import { formatCurrency } from '@/utils/formatters';
import { useIsMobile } from '@/hooks/use-mobile'; import { useIsMobile } from '@/hooks/use-mobile';
interface SummaryCardsProps { interface SummaryCardsProps {
totalBudget: number; totalBudget: number;
totalExpense: number; totalExpense: number;
savingsPercentage: number; savingsPercentage: number;
} }
const SummaryCards: React.FC<SummaryCardsProps> = ({ const SummaryCards: React.FC<SummaryCardsProps> = ({
totalBudget, totalBudget,
totalExpense, totalExpense,
@@ -20,11 +17,9 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
// 남은 예산 계산 // 남은 예산 계산
const remainingBudget = totalBudget - totalExpense; const remainingBudget = totalBudget - totalExpense;
const isOverBudget = remainingBudget < 0; const isOverBudget = remainingBudget < 0;
return <div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-3'} gap-3 mb-8 w-full desktop-card`}>
return (
<div className={`grid ${isMobile ? 'grid-cols-1' : 'grid-cols-3'} gap-3 mb-8 w-full desktop-card`}>
<div className="neuro-card w-full"> <div className="neuro-card w-full">
{isMobile ? ( {isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치) // 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]"> <div className="flex items-center justify-between px-3 py-[5px]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -34,22 +29,20 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
<p className="text-sm font-bold text-neuro-income"> <p className="text-sm font-bold text-neuro-income">
{formatCurrency(totalBudget)} {formatCurrency(totalBudget)}
</p> </p>
</div> </div> :
) : (
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄) // 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<> <>
<div className="flex items-center justify-center gap-2 py-[5px]"> <div className="flex items-center justify-center gap-2 py-[5px]">
<Wallet size={24} className="text-gray-500" /> <Wallet size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p> <p className="text-gray-500 text-base"></p>
</div> </div>
<p className="text-sm font-bold text-neuro-income text-center mt-3"> <p className="font-bold text-neuro-income text-center mt-3 text-xs">
{formatCurrency(totalBudget)} {formatCurrency(totalBudget)}
</p> </p>
</> </>}
)}
</div> </div>
<div className="neuro-card w-full"> <div className="neuro-card w-full">
{isMobile ? ( {isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치) // 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]"> <div className="flex items-center justify-between px-3 py-[5px]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -59,59 +52,45 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
<p className="text-sm font-bold text-neuro-income"> <p className="text-sm font-bold text-neuro-income">
{formatCurrency(totalExpense)} {formatCurrency(totalExpense)}
</p> </p>
</div> </div> :
) : (
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄) // 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<> <>
<div className="flex items-center justify-center gap-2 py-[5px]"> <div className="flex items-center justify-center gap-2 py-[5px]">
<CreditCard size={24} className="text-gray-500" /> <CreditCard size={24} className="text-gray-500" />
<p className="text-gray-500 font-medium text-base"></p> <p className="text-gray-500 font-medium text-base"></p>
</div> </div>
<p className="text-sm font-bold text-neuro-income text-center mt-3"> <p className="font-bold text-neuro-income text-center mt-3 text-xs">
{formatCurrency(totalExpense)} {formatCurrency(totalExpense)}
</p> </p>
</> </>}
)}
</div> </div>
<div className="neuro-card w-full"> <div className="neuro-card w-full">
{isMobile ? ( {isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치) // 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]"> <div className="flex items-center justify-between px-3 py-[5px]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Coins size={24} className="text-gray-500" /> <Coins size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p> <p className="text-gray-500 text-base"></p>
</div> </div>
{isOverBudget ? ( {isOverBudget ? <p className="text-sm font-bold text-red-500">
<p className="text-sm font-bold text-red-500">
: {formatCurrency(Math.abs(remainingBudget))} : {formatCurrency(Math.abs(remainingBudget))}
</p> </p> : <p className="text-sm font-bold text-neuro-income">
) : (
<p className="text-sm font-bold text-neuro-income">
{formatCurrency(remainingBudget)} {formatCurrency(remainingBudget)}
</p> </p>}
)} </div> :
</div>
) : (
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄) // 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<> <>
<div className="flex items-center justify-center gap-2 py-[5px]"> <div className="flex items-center justify-center gap-2 py-[5px]">
<Coins size={24} className="text-gray-500" /> <Coins size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p> <p className="text-gray-500 text-base"></p>
</div> </div>
{isOverBudget ? ( {isOverBudget ? <p className="text-sm font-bold text-red-500 text-center mt-3">
<p className="text-sm font-bold text-red-500 text-center mt-3">
: {formatCurrency(Math.abs(remainingBudget))} : {formatCurrency(Math.abs(remainingBudget))}
</p> </p> : <p className="font-bold text-neuro-income text-center mt-3 text-xs">
) : (
<p className="text-sm font-bold text-neuro-income text-center mt-3">
{formatCurrency(remainingBudget)} {formatCurrency(remainingBudget)}
</p> </p>}
)} </>}
</>
)}
</div> </div>
</div> </div>;
);
}; };
export default SummaryCards; export default SummaryCards;

View File

@@ -1,7 +1,8 @@
import React from 'react'; import React, { useMemo } from 'react';
import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react'; import { Calendar, Search, ChevronLeft, ChevronRight } from 'lucide-react';
import { formatCurrency } from '@/utils/formatters'; import { formatCurrency } from '@/utils/formatters';
import { formatMonthForDisplay } from '@/hooks/transactions/dateUtils';
interface TransactionsHeaderProps { interface TransactionsHeaderProps {
selectedMonth: string; selectedMonth: string;
@@ -24,14 +25,25 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
totalExpenses, totalExpenses,
isDisabled isDisabled
}) => { }) => {
console.log('TransactionsHeader 렌더링:', { // 월 표시 형식 변환 (2024-04 -> 2024년 04월)
selectedMonth, const displayMonth = useMemo(() =>
totalExpenses formatMonthForDisplay(selectedMonth),
}); [selectedMonth]
);
// 예산 정보가 없는 경우 기본값 사용 // 예산 정보가 없는 경우 기본값 사용
const targetAmount = budgetData?.monthly?.targetAmount || 0; const targetAmount = budgetData?.monthly?.targetAmount || 0;
// 디버깅을 위한 로그
React.useEffect(() => {
console.log('TransactionsHeader 렌더링:', {
selectedMonth,
displayMonth,
totalExpenses,
targetAmount
});
}, [selectedMonth, displayMonth, totalExpenses, targetAmount]);
return ( return (
<header className="py-4"> <header className="py-4">
<h1 className="font-bold neuro-text mb-3 text-xl"> </h1> <h1 className="font-bold neuro-text mb-3 text-xl"> </h1>
@@ -61,7 +73,7 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar size={18} className="text-neuro-income" /> <Calendar size={18} className="text-neuro-income" />
<span className="font-medium text-lg">{selectedMonth}</span> <span className="font-medium text-lg">{displayMonth}</span>
</div> </div>
<button <button

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { AuthProvider } from './auth/AuthProvider'; import { AuthProvider } from './auth/AuthProvider';
import { useAuth } from './auth/useAuth';
export { AuthProvider, useAuth }; export { AuthProvider } from './auth/AuthProvider';
export { useAuth } from './auth/useAuth';
export default function AuthContextWrapper({ children }: { children: React.ReactNode }) { export default function AuthContextWrapper({ children }: { children: React.ReactNode }) {
return <AuthProvider>{children}</AuthProvider>; return <AuthProvider>{children}</AuthProvider>;

View File

@@ -0,0 +1,6 @@
import React, { createContext } from 'react';
import { AuthContextType } from './types';
// AuthContext 생성
export const AuthContext = createContext<AuthContextType | undefined>(undefined);

View File

@@ -6,7 +6,7 @@ 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 './useAuth'; import { AuthContext } from './AuthContext';
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<Session | null>(null);
@@ -105,5 +105,3 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}; };
// useAuth 후크는 useAuth.ts 파일로 이동했습니다

View File

@@ -1,8 +1,7 @@
import { useContext, createContext } from 'react';
import { AuthContextType } from './types';
// AuthContext 생성 import { useContext } from 'react';
export const AuthContext = createContext<AuthContextType | undefined>(undefined); import { AuthContext } from './AuthContext';
import { AuthContextType } from './types';
/** /**
* 인증 컨텍스트에 접근하기 위한 커스텀 훅 * 인증 컨텍스트에 접근하기 위한 커스텀 훅

View File

@@ -1,6 +1,9 @@
import { format, parse, addMonths, subMonths } from 'date-fns';
import { ko } from 'date-fns/locale';
/** /**
* 한글 월 이름 배열 * 월 이름 배열 (한국어)
*/ */
export const MONTHS_KR = [ export const MONTHS_KR = [
'1월', '2월', '3월', '4월', '5월', '6월', '1월', '2월', '3월', '4월', '5월', '6월',
@@ -8,40 +11,83 @@ export const MONTHS_KR = [
]; ];
/** /**
* 현재 월 가져오기 * 월 형식 검증 함수 (YYYY-MM 형식)
*/
export const isValidMonth = (month: string): boolean => {
const regex = /^\d{4}-(0[1-9]|1[0-2])$/;
return regex.test(month);
};
/**
* 현재 년월 가져오기
*/ */
export const getCurrentMonth = (): string => { export const getCurrentMonth = (): string => {
const now = new Date(); return format(new Date(), 'yyyy-MM');
const month = now.getMonth(); // 0-indexed
return `${MONTHS_KR[month]}`;
}; };
/** /**
* 이전 월 가져오기 * 이전 월 가져오기
*/ */
export const getPrevMonth = (currentMonth: string): string => { export const getPrevMonth = (month: string): string => {
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth); // 입력값 검증
if (!isValidMonth(month)) {
console.warn('유효하지 않은 월 형식:', month);
return getCurrentMonth();
}
if (currentMonthIdx === 0) { try {
// 1월인 경우 12월로 변경 // 월 문자열을 날짜로 파싱
return `${MONTHS_KR[11]}`; const date = parse(month, 'yyyy-MM', new Date());
} else { // 한 달 이전
const prevMonthIdx = currentMonthIdx - 1; const prevMonth = subMonths(date, 1);
return `${MONTHS_KR[prevMonthIdx]}`; // yyyy-MM 형식으로 반환
return format(prevMonth, 'yyyy-MM');
} catch (error) {
console.error('이전 월 계산 중 오류:', error);
return getCurrentMonth();
} }
}; };
/** /**
* 다음 월 가져오기 * 다음 월 가져오기
*/ */
export const getNextMonth = (currentMonth: string): string => { export const getNextMonth = (month: string): string => {
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth); // 입력값 검증
if (!isValidMonth(month)) {
console.warn('유효하지 않은 월 형식:', month);
return getCurrentMonth();
}
if (currentMonthIdx === 11) { try {
// 12월인 경우 1월로 변경 // 월 문자열을 날짜로 파싱
return `${MONTHS_KR[0]}`; const date = parse(month, 'yyyy-MM', new Date());
} else { // 한 달 이후
const nextMonthIdx = currentMonthIdx + 1; const nextMonth = addMonths(date, 1);
return `${MONTHS_KR[nextMonthIdx]}`; // yyyy-MM 형식으로 반환
return format(nextMonth, 'yyyy-MM');
} catch (error) {
console.error('다음 월 계산 중 오류:', error);
return getCurrentMonth();
}
};
/**
* 표시 형식으로 변환 (yyyy년 MM월)
*/
export const formatMonthForDisplay = (month: string): string => {
try {
// 입력값 검증
if (!isValidMonth(month)) {
console.warn('유효하지 않은 월 형식:', month);
return format(new Date(), 'yyyy년 MM월', { locale: ko });
}
// 월 문자열을 날짜로 파싱
const date = parse(month, 'yyyy-MM', new Date());
// yyyy년 MM월 형식으로 반환 (한국어 로케일)
return format(date, 'yyyy년 MM월', { locale: ko });
} catch (error) {
console.error('월 형식 변환 중 오류:', error);
return month;
} }
}; };

View File

@@ -2,126 +2,115 @@
import { Transaction } from '@/contexts/budget/types'; import { Transaction } from '@/contexts/budget/types';
import { parseTransactionDate } from '@/utils/dateParser'; import { parseTransactionDate } from '@/utils/dateParser';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
/** /**
* 월별로 트랜잭션 필터링 - 개선된 버전 * 트랜잭션을 월별로 필터링
*/ */
export const filterTransactionsByMonth = ( export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => {
transactions: Transaction[], if (!transactions || transactions.length === 0) {
selectedMonth: string
): Transaction[] => {
console.log(`월별 트랜잭션 필터링: ${selectedMonth}, 총 데이터 수: ${transactions.length}`);
// 필터링 전 샘플 데이터 로그
if (transactions.length > 0) {
console.log('샘플 트랜잭션 날짜:', transactions.slice(0, 3).map(t => t.date));
}
// 선택된 월의 숫자 추출 (ex: "4월" -> 4)
const selectedMonthNumber = parseInt(selectedMonth.replace('월', ''));
if (isNaN(selectedMonthNumber) || selectedMonthNumber < 1 || selectedMonthNumber > 12) {
console.error('잘못된 월 형식:', selectedMonth);
return []; return [];
} }
const filtered = transactions.filter(transaction => { console.log(`월별 필터링 시작: ${selectedMonth}, 트랜잭션 수: ${transactions.length}`);
// 트랜잭션 타입 확인 - 지출 항목만 포함
if (transaction.type !== 'expense') {
return false;
}
try { try {
// 날짜가 없는 경우 필터링 제외 const [year, month] = selectedMonth.split('-').map(Number);
if (!transaction.date) {
console.warn('날짜 없는 트랜잭션:', transaction); const filtered = transactions.filter(transaction => {
const date = parseTransactionDate(transaction.date);
if (!date) {
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
return false; return false;
} }
// 날짜 파싱 const transactionYear = date.getFullYear();
const parsedDate = parseTransactionDate(transaction.date); const transactionMonth = date.getMonth() + 1; // JavaScript 월은 0부터 시작하므로 +1
if (!parsedDate) {
console.warn('날짜 파싱 실패:', transaction.date); const match = transactionYear === year && transactionMonth === month;
return false;
if (match) {
console.log(`트랜잭션 매칭: ${transaction.id}, 제목: ${transaction.title}, 날짜: ${transaction.date}`);
} }
// 월 비교 return match;
const transactionMonth = parsedDate.getMonth() + 1; // 0-based -> 1-based
const isMatchingMonth = transactionMonth === selectedMonthNumber;
if (isMatchingMonth) {
console.log(`트랜잭션 매칭: ${transaction.title}, 날짜: ${transaction.date}, 월: ${transactionMonth}`);
}
return isMatchingMonth;
} catch (e) {
console.error('트랜잭션 필터링 중 오류:', e, transaction);
return false;
}
}); });
console.log(`월별 필터링 결과: ${filtered.length}항목 (${selectedMonth})`); console.log(`월별 필터링 결과: ${filtered.length}트랜잭션`);
return filtered; return filtered;
} catch (error) {
console.error('월별 필터링 중 오류:', error);
return [];
}
}; };
/** /**
* 검색어로 트랜잭션 필터링 * 트랜잭션을 검색어로 필터링
*/ */
export const filterTransactionsByQuery = ( export const filterTransactionsByQuery = (transactions: Transaction[], searchQuery: string): Transaction[] => {
transactions: Transaction[], if (!searchQuery || searchQuery.trim() === '') {
searchQuery: string return transactions;
): Transaction[] => {
if (!searchQuery.trim()) return transactions;
const query = searchQuery.toLowerCase().trim();
console.log(`검색어 필터링: "${query}"`);
const filtered = transactions.filter(transaction => {
try {
return (
(transaction.title?.toLowerCase().includes(query)) ||
(transaction.category?.toLowerCase().includes(query)) ||
(transaction.paymentMethod?.toLowerCase().includes(query))
);
} catch (e) {
console.error('검색어 필터링 중 오류:', e, transaction);
return false;
} }
});
console.log(`검색어 필터링 결과: ${filtered.length}개 항목`); const normalizedQuery = searchQuery.toLowerCase().trim();
return filtered;
return transactions.filter(transaction => {
const titleMatch = transaction.title.toLowerCase().includes(normalizedQuery);
const categoryMatch = transaction.category.toLowerCase().includes(normalizedQuery);
const amountMatch = transaction.amount.toString().includes(normalizedQuery);
return titleMatch || categoryMatch || amountMatch;
});
}; };
/** /**
* 총 지출 금액 계산 - 개선된 버전 * 총 지출 계산 (지출 타입만 포함)
*/ */
export const calculateTotalExpenses = (transactions: Transaction[]): number => { export const calculateTotalExpenses = (transactions: Transaction[]): number => {
try { if (!transactions || transactions.length === 0) {
// 유효한 트랜잭션만 필터링 (undefined, null 제외) console.log('계산할 트랜잭션이 없습니다.');
const validTransactions = transactions.filter(t => t && typeof t.amount !== 'undefined'); return 0;
console.log(`유효한 트랜잭션 수: ${validTransactions.length}/${transactions.length}`);
// 디버깅용 로그
if (validTransactions.length > 0) {
console.log('첫 번째 트랜잭션 정보:', {
title: validTransactions[0].title,
amount: validTransactions[0].amount,
type: validTransactions[0].type
});
} }
const total = validTransactions.reduce((sum, t) => { console.log(`총 지출 계산 시작: 트랜잭션 ${transactions.length}`);
// 유효한 숫자인지 확인하고 기본값 처리
const amount = typeof t.amount === 'number' ? t.amount : // 지출 타입만 필터링하고 합산
parseInt(t.amount as any) || 0; const expenses = transactions
.filter(t => t.type === 'expense')
.reduce((sum, transaction) => {
const amount = Number(transaction.amount);
if (isNaN(amount)) {
console.warn(`유효하지 않은 금액: ${transaction.amount}, 트랜잭션 ID: ${transaction.id}`);
return sum;
}
return sum + amount; return sum + amount;
}, 0); }, 0);
console.log(`총 지출 계산: ${total}원 (${validTransactions.length}개 항목)`); console.log(`총 지출 계산 결과: ${expenses}`);
return total; return expenses;
} catch (e) { };
console.error('총 지출 계산 중 오류:', e);
return 0; /**
} * 트랜잭션을 날짜별로 그룹화
*/
export const groupTransactionsByDate = (transactions: Transaction[]): Record<string, Transaction[]> => {
const groups: Record<string, Transaction[]> = {};
transactions.forEach(transaction => {
const date = parseTransactionDate(transaction.date);
if (!date) {
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
return;
}
const formattedDate = format(date, 'yyyy-MM-dd');
if (!groups[formattedDate]) {
groups[formattedDate] = [];
}
groups[formattedDate].push(transaction);
});
return groups;
}; };

View File

@@ -2,35 +2,47 @@
import { format, parse, isValid, parseISO } from 'date-fns'; import { format, parse, isValid, parseISO } from 'date-fns';
import { ko } from 'date-fns/locale'; import { ko } from 'date-fns/locale';
// 날짜 파싱 결과를 캐싱하기 위한 Map
const dateParseCache = new Map<string, Date | null>();
/** /**
* 다양한 형식의 날짜 문자열을 Date 객체로 변환하는 유틸리티 * 다양한 형식의 날짜 문자열을 Date 객체로 변환하는 유틸리티
* 성능 최적화를 위해 결과 캐싱 기능 추가
*/ */
export const parseTransactionDate = (dateStr: string): Date | null => { export const parseTransactionDate = (dateStr: string): Date | null => {
// 빈 문자열 체크 // 빈 문자열 체크
if (!dateStr || dateStr === '') { if (!dateStr || dateStr === '') {
console.log('빈 날짜 문자열');
return null; return null;
} }
// 캐시된 결과가 있으면 반환
if (dateParseCache.has(dateStr)) {
return dateParseCache.get(dateStr) || null;
}
try { try {
let result: Date | null = null;
// 특수 키워드 처리 // 특수 키워드 처리
if (dateStr.toLowerCase().includes('오늘')) { if (dateStr.toLowerCase().includes('오늘')) {
console.log('오늘 날짜로 변환'); result = new Date();
return new Date(); dateParseCache.set(dateStr, result);
return result;
} }
if (dateStr.toLowerCase().includes('어제')) { if (dateStr.toLowerCase().includes('어제')) {
console.log('어제 날짜로 변환');
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
return yesterday; result = yesterday;
dateParseCache.set(dateStr, result);
return result;
} }
// ISO 형식 (yyyy-MM-dd) 시도 // ISO 형식 (yyyy-MM-dd) 시도
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) { if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
console.log('ISO 형식 날짜 감지');
const date = parseISO(dateStr); const date = parseISO(dateStr);
if (isValid(date)) { if (isValid(date)) {
dateParseCache.set(dateStr, date);
return date; return date;
} }
} }
@@ -40,27 +52,30 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
const koreanMatch = dateStr.match(koreanDatePattern); const koreanMatch = dateStr.match(koreanDatePattern);
if (koreanMatch) { if (koreanMatch) {
console.log('한국어 날짜 형식 감지', koreanMatch);
const month = parseInt(koreanMatch[1]) - 1; // 0-based month const month = parseInt(koreanMatch[1]) - 1; // 0-based month
const day = parseInt(koreanMatch[2]); const day = parseInt(koreanMatch[2]);
const date = new Date(); const date = new Date();
date.setMonth(month); date.setMonth(month);
date.setDate(day); date.setDate(day);
dateParseCache.set(dateStr, date);
return date; return date;
} }
// 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM") // 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM")
const commaSplit = dateStr.split(','); const commaSplit = dateStr.split(',');
if (commaSplit.length > 1) { if (commaSplit.length > 1) {
console.log('쉼표 구분 날짜 감지');
// 첫 부분이 오늘/어제 등의 키워드인 경우 처리 // 첫 부분이 오늘/어제 등의 키워드인 경우 처리
const firstPart = commaSplit[0].trim().toLowerCase(); const firstPart = commaSplit[0].trim().toLowerCase();
if (firstPart === '오늘') { if (firstPart === '오늘') {
return new Date(); result = new Date();
dateParseCache.set(dateStr, result);
return result;
} else if (firstPart === '어제') { } else if (firstPart === '어제') {
const yesterday = new Date(); const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
return yesterday; result = yesterday;
dateParseCache.set(dateStr, result);
return result;
} }
} }
@@ -78,13 +93,12 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
for (const formatStr of formats) { for (const formatStr of formats) {
try { try {
console.log(`날짜 형식 시도: ${formatStr}`);
const date = parse(dateStr, formatStr, new Date(), { locale: ko }); const date = parse(dateStr, formatStr, new Date(), { locale: ko });
if (isValid(date)) { if (isValid(date)) {
console.log('날짜 파싱 성공:', formatStr); dateParseCache.set(dateStr, date);
return date; return date;
} }
} catch (e) { } catch {
// 이 형식이 실패하면 다음 형식 시도 // 이 형식이 실패하면 다음 형식 시도
continue; continue;
} }
@@ -93,19 +107,27 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
// 위 모든 형식이 실패하면 마지막으로 Date 생성자 시도 // 위 모든 형식이 실패하면 마지막으로 Date 생성자 시도
const dateFromConstructor = new Date(dateStr); const dateFromConstructor = new Date(dateStr);
if (isValid(dateFromConstructor)) { if (isValid(dateFromConstructor)) {
console.log('Date 생성자로 파싱 성공'); dateParseCache.set(dateStr, dateFromConstructor);
return dateFromConstructor; return dateFromConstructor;
} }
// 모든 방법이 실패하면 null 반환 // 모든 방법이 실패하면 null 반환
console.warn(`날짜 파싱 실패: ${dateStr}`); dateParseCache.set(dateStr, null);
return null; return null;
} catch (error) { } catch (error) {
console.error(`날짜 파싱 중 오류 발생: ${dateStr}`, error); console.error(`날짜 파싱 중 오류 발생: ${dateStr}`, error);
dateParseCache.set(dateStr, null);
return null; return null;
} }
}; };
/**
* 캐시를 정리하는 함수 (필요시 호출)
*/
export const clearDateParseCache = () => {
dateParseCache.clear();
};
/** /**
* Date 객체를 yyyy-MM-dd 형식의 문자열로 변환 * Date 객체를 yyyy-MM-dd 형식의 문자열로 변환
*/ */

View File

@@ -1,10 +1,12 @@
// 월 이름 정의 // 이 파일은 레거시 코드로 사용되지만 `MONTHS_KR`가 중복 정의되어 있으므로
export const MONTHS_KR = [ // 새로운 구현에서는 src/hooks/transactions/dateUtils.ts에서 가져와 사용합니다.
'1월', '2월', '3월', '4월', '5월', '6월', import { MONTHS_KR } from '@/hooks/transactions/dateUtils';
'7월', '8월', '9월', '10월', '11월', '12월'
];
// 다른 파일에서 사용할 수 있도록 재내보내기
export { MONTHS_KR };
// 아래 함수들은 하위 호환성을 위해 유지합니다
// 현재 월 가져오기 // 현재 월 가져오기
export const getCurrentMonth = () => { export const getCurrentMonth = () => {
const now = new Date(); const now = new Date();

5
version.properties Normal file
View File

@@ -0,0 +1,5 @@
# Zellyy Finance 앱 버전 정보
# 마지막 업데이트: 2025-04-05 19:45:54
VERSION_CODE=9
VERSION_NAME=1.1.8
BUILD_NUMBER=9