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
*.log
#*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

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

View File

@@ -1,5 +1,7 @@
<?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
android:allowBackup="true"

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
buildNumber=7
versionCode=7
versionName=1.1.1.3
# Zellyy Finance 앱 버전 정보
# 마지막 업데이트: 2025-04-05 19:45:54
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
# 스크립트 시작 시간 기록
START_TIME=$(date +%s)
# 색상 정의
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
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")"
# 릴리즈 디렉토리 생성
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}=============================${NC}"
@@ -43,76 +88,107 @@ case $CHOICE in
;;
esac
# 현재 버전 코드 가져오기
CURRENT_VERSION_CODE=$(grep -o 'versionCode [0-9]*' android/app/build.gradle | awk '{print $2}')
# 버전 코드가 비어있으면 기본값 1로 설정
if [ -z "$CURRENT_VERSION_CODE" ]; then
# 버전 정보 파일 확인
VERSION_PROPS_FILE="version.properties"
# 버전 정보 파일이 있는지 확인
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
echo -e "${YELLOW}버전 코드가 없어 기본값 1로 설정했습니다.${NC}"
fi
echo -e "${YELLOW}현재 버전 코드: ${CURRENT_VERSION_CODE}${NC}"
fi
# 현재 버전 이름 가져오기
CURRENT_VERSION_NAME=$(grep -o 'versionName "[^"]*"' android/app/build.gradle | sed 's/versionName "//' | sed 's/"//')
# 버전 이름이 비어있으면 기본값 1.0.0으로 설정
if [ -z "$CURRENT_VERSION_NAME" ]; then
# 버전 이름이 비어있으면 기본값 1.0.0으로 설정
if [ -z "$CURRENT_VERSION_NAME" ]; then
CURRENT_VERSION_NAME="1.0.0"
echo -e "${YELLOW}버전 이름이 없어 기본값 1.0.0으로 설정했습니다.${NC}"
fi
# 빌드 번호 기본값 설정
CURRENT_BUILD_NUMBER=$CURRENT_VERSION_CODE
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}"
read -r MODIFY_VERSION
# 기본값 설정
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
NEW_VERSION_NAME=$CURRENT_VERSION_NAME
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
if [ -n "$NEW_VERSION_CODE_INPUT" ]; then
NEW_VERSION_CODE=$NEW_VERSION_CODE_INPUT
else
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
fi
# 버전 이름 입력
echo -e "${YELLOW}새 버전 이름을 입력하세요 (현재: ${CURRENT_VERSION_NAME}):${NC}"
echo -e "${YELLOW}새 버전 이름을 입력하세요 (현재: ${BLUE}${CURRENT_VERSION_NAME}${YELLOW}, 엔터를 치면 기본값 사용):${NC}"
read -r NEW_VERSION_NAME_INPUT
if [ -n "$NEW_VERSION_NAME_INPUT" ]; then
VERSION_NAME=$NEW_VERSION_NAME_INPUT
else
VERSION_NAME=$CURRENT_VERSION_NAME
NEW_VERSION_NAME=$NEW_VERSION_NAME_INPUT
fi
echo -e "${GREEN}버전 정보가 업데이트되었습니다: 버전 코드=${NEW_VERSION_CODE}, 버전 이름=${VERSION_NAME}${NC}"
else
NEW_VERSION_CODE=$CURRENT_VERSION_CODE
VERSION_NAME=$CURRENT_VERSION_NAME
echo -e "${GREEN}버전 정보가 업데이트되었습니다: 버전 코드=${BLUE}${NEW_VERSION_CODE}${GREEN}, 버전 이름=${BLUE}${NEW_VERSION_NAME}${NC}"
fi
# 빌드 넘버 자동 설정
BUILD_NUMBER=$NEW_VERSION_CODE
echo -e "${GREEN}빌드 넘버가 자동으로 ${BUILD_NUMBER}(으)로 설정되었습니다.${NC}"
# 빌드 번호 기본값 설정
BUILD_NUMBER=$CURRENT_BUILD_NUMBER
# 빌드 넘버 수정 여부 확인
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
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
if [ -n "$NEW_BUILD_NUMBER" ]; then
BUILD_NUMBER=$NEW_BUILD_NUMBER
echo -e "${GREEN}빌드 넘버가 ${BUILD_NUMBER}(으)로 설정되었습니다.${NC}"
else
BUILD_NUMBER=$SUGGESTED_BUILD_NUMBER
fi
echo -e "${GREEN}빌드 번호가 ${BLUE}$BUILD_NUMBER${GREEN}(으)로 설정되었습니다.${NC}"
else
BUILD_NUMBER=$SUGGESTED_BUILD_NUMBER
echo -e "${GREEN}빌드 번호가 ${BLUE}$BUILD_NUMBER${GREEN}(으)로 자동 설정되었습니다.${NC}"
fi
# 릴리즈 빌드인 경우 버전 코드 증가 여부 확인
if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; 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
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}"
fi
fi
@@ -132,24 +208,39 @@ if [[ "$BUILD_TYPE" == "release-aab" || "$BUILD_TYPE" == "release-apk" ]]; then
echo -e "${GREEN}서명 설정이 업데이트되었습니다.${NC}"
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() {
# version.properties 파일 업데이트 - 루트 디렉토리와 app 디렉토리 모두에 저장
echo "buildNumber=$BUILD_NUMBER" > android/version.properties
echo "versionCode=$NEW_VERSION_CODE" >> android/version.properties
echo "versionName=$VERSION_NAME" >> android/version.properties
# 루트 디렉토리에 version.properties 파일 저장
cat > "$VERSION_PROPS_FILE" << EOF
# Zellyy Finance 앱 버전 정보
# 마지막 업데이트: $(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 디렉토리에도 동일한 파일 복사
cp android/version.properties android/app/version.properties
cp "android/version.properties" "android/app/version.properties"
# app_version.json 파일 업데이트
cat > android/app_version.json << EOF
# app_version.json 파일 업데이트 - 사용자 정보와 빌드 정보 포함
cat > "android/app_version.json" << EOF
{
"versionCode": $NEW_VERSION_CODE,
"versionName": "$VERSION_NAME",
"versionName": "$NEW_VERSION_NAME",
"buildNumber": $BUILD_NUMBER,
"buildDate": "$(date +"%Y-%m-%d %H:%M:%S")",
"buildType": "$BUILD_TYPE",
"notes": "사용자가 수정한 버전 정보입니다. 이 파일을 편집하여 앱 버전 정보를 변경할 수 있습니다."
}
EOF
@@ -157,17 +248,59 @@ EOF
# AppVersionInfo.tsx 파일의 하드코딩된 버전 정보 업데이트
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 파일에서 하드코딩된 버전 정보 업데이트
sed -i '' "s/versionName: '[^']*'/versionName: '$VERSION_NAME'/" src/components/AppVersionInfo.tsx
sed -i '' "/hardcodedVersionInfo/,/}/s/buildNumber: [0-9]*/buildNumber: $BUILD_NUMBER/" src/components/AppVersionInfo.tsx
sed -i '' "/hardcodedVersionInfo/,/}/s/versionCode: [0-9]*/versionCode: $NEW_VERSION_CODE/" 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/" "$APP_VERSION_FILE"
sed -i '' "/hardcodedVersionInfo/,/}/s/versionCode: [0-9]*/versionCode: $NEW_VERSION_CODE/" "$APP_VERSION_FILE"
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}"
# 1. 웹 앱 빌드
echo -e "${YELLOW}1. 웹 앱 빌드 중...${NC}"
npm run build
if [ $? -ne 0 ]; then
echo -e "${RED}웹 앱 빌드 실패. 빌드 프로세스를 중단합니다.${NC}"
exit 1
echo -e "${YELLOW}1. 웹 앱 빌드 중...${NC}" | tee -a "$LOG_FILE"
echo "실행 명령어: npm run build" >> "$LOG_FILE"
npm run build >> "$LOG_FILE" 2>&1
BUILD_RESULT=$?
if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "웹 앱 빌드 실패" "npm run build"
fi
echo -e "${GREEN}웹 앱 빌드 완료${NC}"
echo -e "${GREEN}웹 앱 빌드 완료${NC}" | tee -a "$LOG_FILE"
# 2. Capacitor에 웹 코드 복사 및 동기화
echo -e "${YELLOW}2. Capacitor에 웹 코드 동기화 중...${NC}"
npx cap sync android
if [ $? -ne 0 ]; then
echo -e "${RED}Capacitor 동기화 실패. 빌드 프로세스를 중단합니다.${NC}"
exit 1
echo -e "${YELLOW}2. Capacitor에 웹 코드 동기화 중...${NC}" | tee -a "$LOG_FILE"
echo "실행 명령어: npx cap sync android" >> "$LOG_FILE"
npx cap sync android >> "$LOG_FILE" 2>&1
BUILD_RESULT=$?
if [ $BUILD_RESULT -ne 0 ]; then
handle_error $BUILD_RESULT "Capacitor 동기화 실패" "npx cap sync android"
fi
echo -e "${GREEN}Capacitor 동기화 완료${NC}"
echo -e "${GREEN}Capacitor 동기화 완료${NC}" | tee -a "$LOG_FILE"
# 3. 안드로이드 APK/AAB 빌드
cd android
# Gradle 메모리 설정 최적화
export GRADLE_OPTS="-Xmx4g -Dorg.gradle.jvmargs='-Xmx4g -XX:+HeapDumpOnOutOfMemoryError'"
echo -e "${YELLOW}3. 안드로이드 빌드 시작 (${BUILD_TYPE})...${NC}"
# 빌드 타입에 따라 다른 명령어 실행
if [ "$BUILD_TYPE" = "debug" ]; then
# 디버그 빌드
./gradlew clean assembleDebug
if [ $? -ne 0 ]; then
echo -e "${RED}디버그 APK 빌드 실패. 오류를 확인하세요.${NC}"
exit 1
echo -e "${YELLOW}3. 안드로이드 빌드 시작 (debug)...${NC}" | tee -a "../$LOG_FILE"
echo "실행 명령어: ./gradlew assembleDebug" >> "../$LOG_FILE"
./gradlew assembleDebug >> "../$LOG_FILE" 2>&1
BUILD_RESULT=$?
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
APK_PATH="app/build/outputs/apk/debug/app-debug.apk"
DEST_PATH="$HOME/zellyy-finance-debug.apk"
elif [ "$BUILD_TYPE" = "release-aab" ]; then
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
echo -e "${GREEN}디버그 APK 빌드 성공!${NC}"
echo -e "APK 파일 위치: $(pwd)/$APK_PATH"
echo -e "${GREEN}서명된 APK 파일 생성 완료: ${BLUE}$APK_PATH${NC}" | tee -a "../$LOG_FILE"
# 홈 디렉토리로 APK 복사
cp "$APK_PATH" "$DEST_PATH"
echo -e "${GREEN}APK를 홈 디렉토리에 복사했습니다: $DEST_PATH${NC}"
# 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_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)
if [ $DEVICES -gt 0 ]; then
echo -e "${YELLOW}연결된 기기가 감지되었습니다. 설치하시겠습니까? (y/n)${NC}"
@@ -245,93 +447,34 @@ if [ "$BUILD_TYPE" = "debug" ]; then
echo -e "${YELLOW}연결된 기기가 없습니다. 다음 방법으로 APK를 설치할 수 있습니다:${NC}"
echo "1. USB 케이블로 폰을 연결하고 파일 전송"
echo "2. 이메일이나 메신저로 APK 파일 전송"
echo "3. adb 명령어 사용: adb install $DEST_PATH"
echo "3. adb 명령어 사용: adb install $APK_PATH"
fi
else
fi
# 빌드 실패 시 처리
if [ $? -ne 0 ]; then
echo -e "${RED}APK 빌드 실패. 오류를 확인하세요.${NC}"
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
# 빌드 시간 표시
show_build_time
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}"
# 기본적인 빌드 (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
echo -e "${RED}디버그 빌드 실패. 오류를 확인하세요.${NC}"
@@ -217,8 +217,17 @@ start_build_process() {
if [[ "$CREATE_DEBUG_IPA" == "y" || "$CREATE_DEBUG_IPA" == "Y" ]]; then
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"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@@ -226,55 +235,26 @@ start_build_process() {
<key>method</key>
<string>development</string>
<key>teamID</key>
<string>${TEAM_ID}</string>
<key>compileBitcode</key>
<false/>
<string>$TEAM_ID</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
EOF
# 아카이브 생성
xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug clean archive -archivePath "build/App-Debug.xcarchive" -allowProvisioningUpdates
EOL
xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath build/debug_ipa -exportOptionsPlist "$EXPORT_OPTIONS_PLIST_PATH" -allowProvisioningUpdates
if [ $? -ne 0 ]; then
echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}"
rm exportOptionsDebug.plist
exit 1
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에 업로드 필요)"
echo -e "${RED}디버그 IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
echo -e "${YELLOW}Apple 개발자 계정 정보(팀 ID: $TEAM_ID)가 정확한지, Xcode에서 서명 설정이 완료되었는지 확인하세요.${NC}"
echo -e "${GREEN}Xcode 빌드는 완료되었을 수 있습니다. Xcode에서 직접 확인하거나 실행해 보세요.${NC}"
else
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
echo -e "${GREEN}디버그 IPA 파일 생성 완료: build/debug_ipa/App.ipa${NC}"
fi
fi
else
echo -e "${YELLOW}개발자 등록이 필요하여 IPA 파일 생성 단계를 건너니다.${NC}"
echo -e "${YELLOW}시뮬레이터에서 앱을 테스트하거나 Apple 개발자 계정을 등록하세요.${NC}"
echo -e "${YELLOW}개발자 등록이 필요하여 IPA 파일 생성 단계를 건너<0xEB><0x9B><0x81>니다.${NC}"
echo -e "${YELLOW}앱을 기기에서 테스트하려면 Xcode에서 '${WORKSPACE_DIR}/ios/App/App.xcworkspace' 파일을 열고,${NC}"
echo -e "${YELLOW}USB로 연결된 기기를 선택한 후 직접 빌드 및 실행하세요.${NC}"
fi
elif [ "$BUILD_TYPE" = "release" ]; then
@@ -282,7 +262,7 @@ EOF
echo -e "${YELLOW}릴리즈 빌드 시작...${NC}"
# 기본 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
echo -e "${RED}릴리즈 빌드 실패. 오류를 확인하세요.${NC}"
@@ -304,18 +284,21 @@ EOF
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
echo -e "${RED}아카이브 생성 실패. 오류를 확인하세요.${NC}"
exit 1
fi
echo -e "${GREEN}아카이브 생성 성공!${NC}"
echo -e "아카이브 위치: $(pwd)/build/App.xcarchive"
# IPA Export
RELEASE_DIR="$WORKSPACE_DIR/release"
EXPORT_OPTIONS_PLIST_PATH="build/ReleaseExportOptions.plist"
# exportOptions.plist 파일 생성
cat > exportOptions.plist << EOF
# release 디렉토리 생성
mkdir -p "$RELEASE_DIR"
cat > "$EXPORT_OPTIONS_PLIST_PATH" <<EOL
<?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">
<plist version="1.0">
@@ -323,56 +306,49 @@ EOF
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>${TEAM_ID}</string>
<string>$TEAM_ID</string>
<key>signingStyle</key>
<string>automatic</string>
<key>uploadBitcode</key>
<false/>
<true/>
<key>uploadSymbols</key>
<true/>
<key>provisioningProfiles</key>
<dict>
<key>com.lovable.zellyfinance</key>
<string>Zellyy Finance App Store</string>
</dict>
</dict>
</plist>
EOF
# IPA 파일 생성
echo -e "${YELLOW}IPA 파일 생성 중...${NC}"
xcodebuild -exportArchive -archivePath "build/App.xcarchive" -exportOptionsPlist exportOptions.plist -exportPath "build/export" -allowProvisioningUpdates
EOL
# exportPath는 디렉토리만 지정
xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath "$RELEASE_DIR" -exportOptionsPlist "$EXPORT_OPTIONS_PLIST_PATH" -allowProvisioningUpdates
if [ $? -ne 0 ]; then
echo -e "${RED}IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
rm exportOptions.plist
exit 1
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시간 소요)"
echo -e "${RED}릴리즈 IPA 파일 생성 실패. 오류를 확인하세요.${NC}"
echo -e "${YELLOW}Apple 개발자 계정 정보(팀 ID: $TEAM_ID)가 정확한지, Xcode에서 서명 및 배포 설정이 완료되었는지 확인하세요.${NC}"
echo -e "${GREEN}Xcode 빌드는 완료되었을 수 있습니다. Xcode에서 직접 확인하거나 실행해 보세요.${NC}"
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
else
echo -e "${YELLOW}개발자 등록이 필요하여 아카이브 생성 단계를 건너니다.${NC}"
echo -e "${YELLOW}시뮬레이터에서 앱을 테스트하거나 Apple 개발자 계정을 등록하세요.${NC}"
echo -e "${YELLOW}개발자 등록이 필요하여 아카이브 생성 단계를 건너<0xEB><0x9B><0x81>니다.${NC}"
echo -e "${YELLOW}앱을 App Store에 제출하거나 Ad Hoc 배포를 하려면 유료 Apple 개발자 계정이 필요합니다.${NC}"
echo -e "${YELLOW}개발 테스트는 Xcode에서 '${WORKSPACE_DIR}/ios/App/App.xcworkspace' 파일을 열고 직접 진행할 수 있습니다.${NC}"
fi
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 = {
versionName: '1.1.1.3',
buildNumber: 7,
versionCode: 7,
versionName: '1.1.8',
buildNumber: 9,
versionCode: 9,
platform: Capacitor.getPlatform(),
defaultValuesUsed: false
};

View File

@@ -1,80 +1,31 @@
import React, { useEffect, useState, ReactNode } from 'react';
import { isIOSPlatform } from '@/utils/platform';
import React from 'react';
import { cn } from '@/lib/utils';
interface SafeAreaContainerProps {
children: ReactNode;
children: React.ReactNode;
className?: string;
topOnly?: boolean;
bottomOnly?: boolean;
extraBottomPadding?: boolean; // 추가 하단 여백 옵션
extraBottomPadding?: boolean;
}
/**
* 플랫폼별 안전 영역(Safe Area)을 고려한 컨테이너 컴포넌트
* iOS에서는 노치/다이나믹 아일랜드를 고려한 여백 적용
* iOS의 안전 영역(notch, home indicator 등)을 고려한 컨테이너
* 모든 페이지 최상위 컴포넌트로 사용해야 함
*/
const SafeAreaContainer: React.FC<SafeAreaContainerProps> = ({
children,
className = '',
topOnly = false,
bottomOnly = false,
extraBottomPadding = false // 기본값은 false
extraBottomPadding = 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 (
<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}
</div>
);

View File

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

View File

@@ -1,15 +1,12 @@
import React from 'react';
import { Wallet, CreditCard, Coins } from 'lucide-react';
import { formatCurrency } from '@/utils/formatters';
import { useIsMobile } from '@/hooks/use-mobile';
interface SummaryCardsProps {
totalBudget: number;
totalExpense: number;
savingsPercentage: number;
}
const SummaryCards: React.FC<SummaryCardsProps> = ({
totalBudget,
totalExpense,
@@ -20,11 +17,9 @@ const SummaryCards: React.FC<SummaryCardsProps> = ({
// 남은 예산 계산
const remainingBudget = totalBudget - totalExpense;
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">
{isMobile ? (
{isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]">
<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">
{formatCurrency(totalBudget)}
</p>
</div>
) : (
</div> :
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<>
<div className="flex items-center justify-center gap-2 py-[5px]">
<Wallet size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p>
</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)}
</p>
</>
)}
</>}
</div>
<div className="neuro-card w-full">
{isMobile ? (
{isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]">
<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">
{formatCurrency(totalExpense)}
</p>
</div>
) : (
</div> :
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<>
<div className="flex items-center justify-center gap-2 py-[5px]">
<CreditCard size={24} className="text-gray-500" />
<p className="text-gray-500 font-medium text-base"></p>
</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)}
</p>
</>
)}
</>}
</div>
<div className="neuro-card w-full">
{isMobile ? (
{isMobile ?
// 모바일 레이아웃 (1줄: 아이콘, 제목, 금액 가로배치)
<div className="flex items-center justify-between px-3 py-[5px]">
<div className="flex items-center gap-2">
<Coins size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p>
</div>
{isOverBudget ? (
<p className="text-sm font-bold text-red-500">
{isOverBudget ? <p className="text-sm font-bold text-red-500">
: {formatCurrency(Math.abs(remainingBudget))}
</p>
) : (
<p className="text-sm font-bold text-neuro-income">
</p> : <p className="text-sm font-bold text-neuro-income">
{formatCurrency(remainingBudget)}
</p>
)}
</div>
) : (
</p>}
</div> :
// 데스크탑 레이아웃 (2줄: 아이콘과 제목이 첫째 줄, 금액이 둘째 줄)
<>
<div className="flex items-center justify-center gap-2 py-[5px]">
<Coins size={24} className="text-gray-500" />
<p className="text-gray-500 text-base"></p>
</div>
{isOverBudget ? (
<p className="text-sm font-bold text-red-500 text-center mt-3">
{isOverBudget ? <p className="text-sm font-bold text-red-500 text-center mt-3">
: {formatCurrency(Math.abs(remainingBudget))}
</p>
) : (
<p className="text-sm font-bold text-neuro-income text-center mt-3">
</p> : <p className="font-bold text-neuro-income text-center mt-3 text-xs">
{formatCurrency(remainingBudget)}
</p>
)}
</>
)}
</p>}
</>}
</div>
</div>
);
</div>;
};
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 { formatCurrency } from '@/utils/formatters';
import { formatMonthForDisplay } from '@/hooks/transactions/dateUtils';
interface TransactionsHeaderProps {
selectedMonth: string;
@@ -24,14 +25,25 @@ const TransactionsHeader: React.FC<TransactionsHeaderProps> = ({
totalExpenses,
isDisabled
}) => {
console.log('TransactionsHeader 렌더링:', {
selectedMonth,
totalExpenses
});
// 월 표시 형식 변환 (2024-04 -> 2024년 04월)
const displayMonth = useMemo(() =>
formatMonthForDisplay(selectedMonth),
[selectedMonth]
);
// 예산 정보가 없는 경우 기본값 사용
const targetAmount = budgetData?.monthly?.targetAmount || 0;
// 디버깅을 위한 로그
React.useEffect(() => {
console.log('TransactionsHeader 렌더링:', {
selectedMonth,
displayMonth,
totalExpenses,
targetAmount
});
}, [selectedMonth, displayMonth, totalExpenses, targetAmount]);
return (
<header className="py-4">
<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">
<Calendar size={18} className="text-neuro-income" />
<span className="font-medium text-lg">{selectedMonth}</span>
<span className="font-medium text-lg">{displayMonth}</span>
</div>
<button

View File

@@ -1,9 +1,9 @@
import React from 'react';
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 }) {
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 * as authActions from './authActions';
import { clearAllToasts } from '@/hooks/toast/toastManager';
import { AuthContext } from './useAuth';
import { AuthContext } from './AuthContext';
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
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>;
};
// useAuth 후크는 useAuth.ts 파일로 이동했습니다

View File

@@ -1,8 +1,7 @@
import { useContext, createContext } from 'react';
import { AuthContextType } from './types';
// AuthContext 생성
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
import { useContext } from 'react';
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 = [
'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 => {
const now = new Date();
const month = now.getMonth(); // 0-indexed
return `${MONTHS_KR[month]}`;
return format(new Date(), 'yyyy-MM');
};
/**
* 이전 월 가져오기
*/
export const getPrevMonth = (currentMonth: string): string => {
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth);
export const getPrevMonth = (month: string): string => {
// 입력값 검증
if (!isValidMonth(month)) {
console.warn('유효하지 않은 월 형식:', month);
return getCurrentMonth();
}
if (currentMonthIdx === 0) {
// 1월인 경우 12월로 변경
return `${MONTHS_KR[11]}`;
} else {
const prevMonthIdx = currentMonthIdx - 1;
return `${MONTHS_KR[prevMonthIdx]}`;
try {
// 월 문자열을 날짜로 파싱
const date = parse(month, 'yyyy-MM', new Date());
// 한 달 이전
const prevMonth = subMonths(date, 1);
// yyyy-MM 형식으로 반환
return format(prevMonth, 'yyyy-MM');
} catch (error) {
console.error('이전 월 계산 중 오류:', error);
return getCurrentMonth();
}
};
/**
* 다음 월 가져오기
*/
export const getNextMonth = (currentMonth: string): string => {
const currentMonthIdx = MONTHS_KR.findIndex(m => m === currentMonth);
export const getNextMonth = (month: string): string => {
// 입력값 검증
if (!isValidMonth(month)) {
console.warn('유효하지 않은 월 형식:', month);
return getCurrentMonth();
}
if (currentMonthIdx === 11) {
// 12월인 경우 1월로 변경
return `${MONTHS_KR[0]}`;
} else {
const nextMonthIdx = currentMonthIdx + 1;
return `${MONTHS_KR[nextMonthIdx]}`;
try {
// 월 문자열을 날짜로 파싱
const date = parse(month, 'yyyy-MM', new Date());
// 한 달 이후
const nextMonth = addMonths(date, 1);
// 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 { parseTransactionDate } from '@/utils/dateParser';
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
/**
* 월별로 트랜잭션 필터링 - 개선된 버전
* 트랜잭션을 월별로 필터링
*/
export const filterTransactionsByMonth = (
transactions: Transaction[],
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);
export const filterTransactionsByMonth = (transactions: Transaction[], selectedMonth: string): Transaction[] => {
if (!transactions || transactions.length === 0) {
return [];
}
const filtered = transactions.filter(transaction => {
// 트랜잭션 타입 확인 - 지출 항목만 포함
if (transaction.type !== 'expense') {
return false;
}
console.log(`월별 필터링 시작: ${selectedMonth}, 트랜잭션 수: ${transactions.length}`);
try {
// 날짜가 없는 경우 필터링 제외
if (!transaction.date) {
console.warn('날짜 없는 트랜잭션:', transaction);
const [year, month] = selectedMonth.split('-').map(Number);
const filtered = transactions.filter(transaction => {
const date = parseTransactionDate(transaction.date);
if (!date) {
console.warn(`날짜를 파싱할 수 없음: ${transaction.date}, 트랜잭션 ID: ${transaction.id}`);
return false;
}
// 날짜 파싱
const parsedDate = parseTransactionDate(transaction.date);
if (!parsedDate) {
console.warn('날짜 파싱 실패:', transaction.date);
return false;
const transactionYear = date.getFullYear();
const transactionMonth = date.getMonth() + 1; // JavaScript 월은 0부터 시작하므로 +1
const match = transactionYear === year && transactionMonth === month;
if (match) {
console.log(`트랜잭션 매칭: ${transaction.id}, 제목: ${transaction.title}, 날짜: ${transaction.date}`);
}
// 월 비교
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;
}
return match;
});
console.log(`월별 필터링 결과: ${filtered.length}항목 (${selectedMonth})`);
console.log(`월별 필터링 결과: ${filtered.length}트랜잭션`);
return filtered;
} catch (error) {
console.error('월별 필터링 중 오류:', error);
return [];
}
};
/**
* 검색어로 트랜잭션 필터링
* 트랜잭션을 검색어로 필터링
*/
export const filterTransactionsByQuery = (
transactions: Transaction[],
searchQuery: string
): 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;
export const filterTransactionsByQuery = (transactions: Transaction[], searchQuery: string): Transaction[] => {
if (!searchQuery || searchQuery.trim() === '') {
return transactions;
}
});
console.log(`검색어 필터링 결과: ${filtered.length}개 항목`);
return filtered;
const normalizedQuery = searchQuery.toLowerCase().trim();
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 => {
try {
// 유효한 트랜잭션만 필터링 (undefined, null 제외)
const validTransactions = transactions.filter(t => t && typeof t.amount !== 'undefined');
console.log(`유효한 트랜잭션 수: ${validTransactions.length}/${transactions.length}`);
// 디버깅용 로그
if (validTransactions.length > 0) {
console.log('첫 번째 트랜잭션 정보:', {
title: validTransactions[0].title,
amount: validTransactions[0].amount,
type: validTransactions[0].type
});
if (!transactions || transactions.length === 0) {
console.log('계산할 트랜잭션이 없습니다.');
return 0;
}
const total = validTransactions.reduce((sum, t) => {
// 유효한 숫자인지 확인하고 기본값 처리
const amount = typeof t.amount === 'number' ? t.amount :
parseInt(t.amount as any) || 0;
console.log(`총 지출 계산 시작: 트랜잭션 ${transactions.length}`);
// 지출 타입만 필터링하고 합산
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;
}, 0);
console.log(`총 지출 계산: ${total}원 (${validTransactions.length}개 항목)`);
return total;
} catch (e) {
console.error('총 지출 계산 중 오류:', e);
return 0;
}
console.log(`총 지출 계산 결과: ${expenses}`);
return expenses;
};
/**
* 트랜잭션을 날짜별로 그룹화
*/
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 { ko } from 'date-fns/locale';
// 날짜 파싱 결과를 캐싱하기 위한 Map
const dateParseCache = new Map<string, Date | null>();
/**
* 다양한 형식의 날짜 문자열을 Date 객체로 변환하는 유틸리티
* 성능 최적화를 위해 결과 캐싱 기능 추가
*/
export const parseTransactionDate = (dateStr: string): Date | null => {
// 빈 문자열 체크
if (!dateStr || dateStr === '') {
console.log('빈 날짜 문자열');
return null;
}
// 캐시된 결과가 있으면 반환
if (dateParseCache.has(dateStr)) {
return dateParseCache.get(dateStr) || null;
}
try {
let result: Date | null = null;
// 특수 키워드 처리
if (dateStr.toLowerCase().includes('오늘')) {
console.log('오늘 날짜로 변환');
return new Date();
result = new Date();
dateParseCache.set(dateStr, result);
return result;
}
if (dateStr.toLowerCase().includes('어제')) {
console.log('어제 날짜로 변환');
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return yesterday;
result = yesterday;
dateParseCache.set(dateStr, result);
return result;
}
// ISO 형식 (yyyy-MM-dd) 시도
if (/^\d{4}-\d{2}-\d{2}/.test(dateStr)) {
console.log('ISO 형식 날짜 감지');
const date = parseISO(dateStr);
if (isValid(date)) {
dateParseCache.set(dateStr, date);
return date;
}
}
@@ -40,27 +52,30 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
const koreanMatch = dateStr.match(koreanDatePattern);
if (koreanMatch) {
console.log('한국어 날짜 형식 감지', koreanMatch);
const month = parseInt(koreanMatch[1]) - 1; // 0-based month
const day = parseInt(koreanMatch[2]);
const date = new Date();
date.setMonth(month);
date.setDate(day);
dateParseCache.set(dateStr, date);
return date;
}
// 쉼표 구분 형식 처리 (예: "오늘, 14:52 PM")
const commaSplit = dateStr.split(',');
if (commaSplit.length > 1) {
console.log('쉼표 구분 날짜 감지');
// 첫 부분이 오늘/어제 등의 키워드인 경우 처리
const firstPart = commaSplit[0].trim().toLowerCase();
if (firstPart === '오늘') {
return new Date();
result = new Date();
dateParseCache.set(dateStr, result);
return result;
} else if (firstPart === '어제') {
const yesterday = new Date();
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) {
try {
console.log(`날짜 형식 시도: ${formatStr}`);
const date = parse(dateStr, formatStr, new Date(), { locale: ko });
if (isValid(date)) {
console.log('날짜 파싱 성공:', formatStr);
dateParseCache.set(dateStr, date);
return date;
}
} catch (e) {
} catch {
// 이 형식이 실패하면 다음 형식 시도
continue;
}
@@ -93,19 +107,27 @@ export const parseTransactionDate = (dateStr: string): Date | null => {
// 위 모든 형식이 실패하면 마지막으로 Date 생성자 시도
const dateFromConstructor = new Date(dateStr);
if (isValid(dateFromConstructor)) {
console.log('Date 생성자로 파싱 성공');
dateParseCache.set(dateStr, dateFromConstructor);
return dateFromConstructor;
}
// 모든 방법이 실패하면 null 반환
console.warn(`날짜 파싱 실패: ${dateStr}`);
dateParseCache.set(dateStr, null);
return null;
} catch (error) {
console.error(`날짜 파싱 중 오류 발생: ${dateStr}`, error);
dateParseCache.set(dateStr, null);
return null;
}
};
/**
* 캐시를 정리하는 함수 (필요시 호출)
*/
export const clearDateParseCache = () => {
dateParseCache.clear();
};
/**
* Date 객체를 yyyy-MM-dd 형식의 문자열로 변환
*/

View File

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