Compare commits

...

18 Commits

Author SHA1 Message Date
hansoo
61492549f6 Move Supabase files and scripts to archive, cleanup legacy code 2025-05-05 18:04:32 +09:00
hansoo
8efa986ed5 Remove unused files and clean up project structure 2025-05-05 17:12:09 +09:00
hansoo
5305c98970 Implement error handling and loading states for Appwrite integration 2025-05-05 15:41:19 +09:00
hansoo
f83bb384af Migrate from Supabase to Appwrite with core functionality and UI components 2025-05-05 08:58:27 +09:00
hansoo
fdfdf15166 Supabase on-prem 설정 및 마이그레이션 작업 2025-05-05 07:17:35 +09:00
hansoo
2bfe52fb7b Add JS migration script 2025-04-28 06:26:08 +09:00
hansoo
cdf2de5d9f Add migration script for Cloud to On-Prem 2025-04-28 06:19:57 +09:00
hansoo
c089195ea9 Update Java path config and upgrade Capacitor dependencies 2025-04-27 18:03:21 +09:00
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
129 changed files with 5685 additions and 1069 deletions

20
.env
View File

@@ -1 +1,21 @@
# Supabase 관련 설정 (이전 버전)
CLOUD_DATABASE_URL=postgresql://postgres:6vJj04eYUCKKozYE@db.qnerebtvwwfobfzdoftx.supabase.co:5432/postgres
ONPREM_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres # On-Prem Postgres 기본
VITE_SUPABASE_URL=http://localhost:9000
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
CLOUD_SUPABASE_URL=https://qnerebtvwwfobfzdoftx.supabase.co
CLOUD_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8
CLOUD_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MjA1MTQzOCwiZXhwIjoyMDU3NjI3NDM4fQ.3G9UksB-kE-ChGQrz6YrSZqQSqvzYsnhvZyCnE99Ifc
ONPREM_SUPABASE_URL=http://localhost:9000
ONPREM_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
ONPREM_SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q
# Appwrite 관련 설정
VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1
VITE_APPWRITE_PROJECT_ID=68182a300039f6d700a6
VITE_APPWRITE_DATABASE_ID=default
VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions
VITE_APPWRITE_API_KEY=standard_9672cc2d052d4fc56d9d28e75c6476ff1029d932b7d375dbb4bb0f705d741d8e6d9ae154929009e01c7168810884b6ee80e6bb564d3fe6439b8b142ed4a8d287546bb0bed2531c20188a7ecc36e6f9983abb1ab0022c1656cf2219d4c2799655c7baef00ae4861fe74186dbb421141d9e2332f2fad812975ae7b4b7f57527cea
VITE_DISABLE_LOVABLE_BANNER=true

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

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

View File

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

View File

@@ -1,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

View File

View File

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

View File

@@ -3,15 +3,62 @@
# 안드로이드 앱 빌드 스크립트 (디버그 및 릴리즈 버전)
# 사용법: ./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 폴더로 이동)
LOG_DIR="log"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/app_build.log"
ERROR_LOG_FILE="$LOG_DIR/app_error.log"
# 타임아웃 설정 (초 단위)
BUILD_TIMEOUT=600 # 10분
# 오류 처리 함수
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 +90,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 +210,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 +250,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 +317,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 +449,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

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

View File

@@ -0,0 +1,276 @@
# Appwrite 전환 가이드
## 개요
Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환합니다. 이 문서는 전환 과정과 새로운 코드 구조에 대한 가이드를 제공합니다.
## 전환 이유
1. **더 나은 성능**: Appwrite는 경량화된 서비스로 더 빠른 응답 시간 제공
2. **확장성**: 사용자 증가에 따른 확장성 개선
3. **기능 세트**: Appwrite의 실시간 데이터베이스와 인증 시스템 활용
4. **유지보수 용이성**: 단일 백엔드 서비스로 통합하여 유지보수 간소화
## 코드 구조
```
src/
├── lib/
│ ├── appwrite/ (Appwrite 서비스)
│ │ ├── index.ts (단일 진입점)
│ │ ├── client.ts (클라이언트 설정)
│ │ ├── config.ts (환경 설정)
│ │ └── setup.ts (데이터베이스 설정)
│ └── capacitor/ (네이티브 기능)
│ ├── index.ts (단일 진입점)
│ ├── buildInfo.ts (빌드 정보 관련)
│ ├── notification.ts (알림 관련)
│ └── permissions.ts (권한 관련)
├── hooks/
│ ├── auth/
│ │ └── useAppwriteAuth.ts (인증 관련 훅)
│ └── transactions/
│ └── useAppwriteTransactions.ts (트랜잭션 관련 훅)
├── components/
│ ├── auth/
│ │ └── AppwriteConnectionStatus.tsx (연결 상태 표시)
│ ├── migration/
│ │ └── SupabaseToAppwriteMigration.tsx (마이그레이션 도구)
│ └── native/
│ ├── PermissionRequest.tsx (권한 요청 UI)
│ └── NotificationSettings.tsx (알림 설정 UI)
└── utils/
└── appwriteTransactionUtils.ts (트랜잭션 유틸리티)
```
## 환경 설정
`.env` 파일에 다음 환경 변수를 설정합니다:
```
# Appwrite 설정
VITE_APPWRITE_ENDPOINT=https://a11.ism.kr/v1
VITE_APPWRITE_PROJECT_ID=zellyy-finance
VITE_APPWRITE_DATABASE_ID=zellyy-finance
VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID=transactions
# 네이티브 설정
VITE_ANDROID_MIN_API_LEVEL=21
VITE_ANDROID_TARGET_API_LEVEL=33
VITE_ANDROID_NOTIFICATION_CHANNEL_ID=zellyy_finance_notifications
```
## 마이그레이션 단계
1. **데이터베이스 설정**
- Appwrite 데이터베이스 및 컬렉션 생성
- 필요한 인덱스 및 권한 설정
2. **인증 시스템 전환**
- Appwrite 인증 시스템 설정
- 사용자 계정 마이그레이션
3. **데이터 마이그레이션**
- 트랜잭션 데이터 마이그레이션
- 데이터 무결성 검증
4. **Supabase 코드 제거**
- 마이그레이션 완료 후 Supabase 관련 코드 제거
- 환경 변수 정리
## 주요 컴포넌트 및 훅
### 1. Appwrite 클라이언트 설정
```typescript
// src/lib/appwrite/client.ts
import { Client, Account, Databases, Storage } from 'appwrite';
import { config } from './config';
// 클라이언트 초기화
export const client = new Client()
.setEndpoint(config.endpoint)
.setProject(config.projectId);
// 서비스 초기화
export const account = new Account(client);
export const databases = new Databases(client);
export const storage = new Storage(client);
```
### 2. 인증 훅
```typescript
// src/hooks/auth/useAppwriteAuth.ts
import { useState, useEffect } from 'react';
import { account } from '../../lib/appwrite';
export const useAppwriteAuth = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// 사용자 세션 확인
useEffect(() => {
const checkSession = async () => {
try {
const session = await account.getSession('current');
if (session) {
const currentUser = await account.get();
setUser(currentUser);
}
} catch (error) {
console.error('세션 확인 오류:', error);
} finally {
setLoading(false);
}
};
checkSession();
}, []);
// 로그인 함수
const login = async (email, password) => {
try {
await account.createEmailSession(email, password);
const currentUser = await account.get();
setUser(currentUser);
return { success: true };
} catch (error) {
console.error('로그인 오류:', error);
return { success: false, error };
}
};
// 로그아웃 함수
const logout = async () => {
try {
await account.deleteSession('current');
setUser(null);
return { success: true };
} catch (error) {
console.error('로그아웃 오류:', error);
return { success: false, error };
}
};
return { user, loading, login, logout };
};
```
### 3. 트랜잭션 훅
```typescript
// src/hooks/transactions/useAppwriteTransactions.ts
import { useState, useEffect, useCallback } from 'react';
import { databases } from '../../lib/appwrite';
import { config } from '../../lib/appwrite/config';
import { Query } from 'appwrite';
export const useAppwriteTransactions = (userId) => {
const [transactions, setTransactions] = useState([]);
const [loading, setLoading] = useState(true);
// 트랜잭션 불러오기
const fetchTransactions = useCallback(async () => {
if (!userId) return;
try {
setLoading(true);
const response = await databases.listDocuments(
config.databaseId,
config.transactionsCollectionId,
[Query.equal('userId', userId)]
);
setTransactions(response.documents);
} catch (error) {
console.error('트랜잭션 불러오기 오류:', error);
} finally {
setLoading(false);
}
}, [userId]);
// 초기 데이터 로드
useEffect(() => {
fetchTransactions();
}, [fetchTransactions]);
// 트랜잭션 추가
const addTransaction = async (transaction) => {
try {
const newTransaction = await databases.createDocument(
config.databaseId,
config.transactionsCollectionId,
'unique()',
{
...transaction,
userId,
createdAt: new Date().toISOString(),
}
);
setTransactions((prev) => [...prev, newTransaction]);
return { success: true, transaction: newTransaction };
} catch (error) {
console.error('트랜잭션 추가 오류:', error);
return { success: false, error };
}
};
// 트랜잭션 업데이트
const updateTransaction = async (id, data) => {
try {
const updatedTransaction = await databases.updateDocument(
config.databaseId,
config.transactionsCollectionId,
id,
data
);
setTransactions((prev) =>
prev.map((t) => (t.$id === id ? updatedTransaction : t))
);
return { success: true, transaction: updatedTransaction };
} catch (error) {
console.error('트랜잭션 업데이트 오류:', error);
return { success: false, error };
}
};
// 트랜잭션 삭제
const deleteTransaction = async (id) => {
try {
await databases.deleteDocument(
config.databaseId,
config.transactionsCollectionId,
id
);
setTransactions((prev) => prev.filter((t) => t.$id !== id));
return { success: true };
} catch (error) {
console.error('트랜잭션 삭제 오류:', error);
return { success: false, error };
}
};
return {
transactions,
loading,
fetchTransactions,
addTransaction,
updateTransaction,
deleteTransaction,
};
};
```
## 마이그레이션 도구 사용법
1. 설정 페이지에서 "Appwrite 설정" 메뉴 선택
2. "Supabase에서 Appwrite로 마이그레이션" 섹션에서 "마이그레이션 시작" 버튼 클릭
3. 마이그레이션 진행 상황 확인
4. 완료 후 데이터 검증
## 주의사항
1. 마이그레이션 중에는 데이터 변경을 최소화하세요.
2. 마이그레이션 전에 데이터 백업을 수행하세요.
3. 마이그레이션 후 모든 기능이 정상 작동하는지 테스트하세요.
4. 문제 발생 시 개발팀에 즉시 보고하세요.

View File

@@ -0,0 +1,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를 통한 직접 설치는 개발자만 가능합니다.

View File

@@ -0,0 +1,59 @@
# Zellyy Finance 개발 가이드라인
## 1. 코드 작성 원칙
- 모든 컴포넌트는 함수형 컴포넌트로 작성할 것
- Hook 명명 규칙은 'use'로 시작하는 camelCase 사용할 것
- 비즈니스 로직은 훅으로 분리하여 재사용성 높일 것
- 주석은 한국어로 작성하여 가독성 높일 것
- prop 타입은 모두 TypeScript 인터페이스로 정의할 것
## 2. 트랜잭션 삭제 안전성
- 트랜잭션 삭제 작업은 UI 스레드를 차단하지 않도록 비동기로 처리할 것
- 상태 업데이트 전/후에 try-catch 블록으로 오류 처리할 것
- 가능한 requestAnimationFrame 또는 queueMicrotask를 사용하여 UI 업데이트 최적화할 것
- 컴포넌트 언마운트 상태를 추적하여 메모리 누수 방지할 것
- 이벤트 핸들러는 성능 병목 지점이 될 수 있으므로 디바운스/스로틀링 적용할 것
## 3. Appwrite 통합 원칙
- Appwrite 클라이언트는 앱 시작 시 한 번만 초기화
- 인증 및 데이터 동기화는 전용 훅 사용
- 오류 처리 및 사용자 피드백 제공
- 트랜잭션 작업은 비동기로 처리
- 네트워크 오류 시 적절한 재시도 메커니즘 구현
## 4. 상태 관리 최적화
- 컴포넌트 간 상태 공유는 Context API나 상태 관리 라이브러리 사용할 것
- 큰 상태 객체는 여러 작은 조각으로 분리하여 불필요한 리렌더링 방지할 것
- 불변성을 유지하여 React의 상태 업데이트 최적화 활용할 것
- useCallback, useMemo를 적극 활용하여 함수와 값 메모이제이션할 것
- 기본 데이터 로딩은 상위 컴포넌트에서 처리하고 하위 컴포넌트로 전달할 것
## 5. 디버깅 및 로깅
- 중요 작업(특히 트랜잭션 삭제와 같은 위험 작업)은 상세한 로그 남길 것
- 개발 모드에서는 상태 변화를 추적할 수 있는 로그 포함할 것
- 사용자에게 영향을 주는 오류는 UI 피드백(토스트 등)으로 표시할 것
- 백그라운드 작업 실패는 적절히 로깅하고 필요시 재시도 메커니즘 구현할 것
## 6. iOS 지원
- iOS 안전 영역(Safe Area) 고려한 UI 레이아웃 설계
- iOS 특유의 제스처와 상호작용 패턴 지원 (스와이프, 핀치 등)
- iOS 다크 모드 대응을 위한 동적 색상 시스템 활용
- iOS 기기별 화면 크기 및 노치(Notch) 대응
- iOS 앱 배포 시 필요한 인증서 및 프로비저닝 프로파일 관리
## 7. Android 지원
- BuildInfo와 같은 네이티브 플러그인은 반드시 MainActivity에 등록할 것
- 안드로이드 빌드 정보는 Capacitor 플러그인을 통해 JS로 전달할 것
- 플러그인 호출 시 항상 오류 처리 로직 포함할 것
- 네이티브 기능 실패 시 대체 방법(fallback) 제공할 것
- 안드로이드 버전별 호환성 고려 (API 레벨 차이)
- 다양한 화면 크기 및 해상도 대응 (태블릿 포함)
- 안드로이드 백 버튼 처리 및 생명주기 관리
- 권한 요청 및 처리 로직 구현
- 안드로이드 알림 채널 설정 및 관리
## 8. 버전 관리
- 모든 빌드는 자동으로 빌드 번호가 증가되도록 설정할 것
- 릴리즈 빌드는 versionCode와 buildNumber 모두 증가할 것
- 디버그 빌드는 buildNumber만 증가할 것
- 버전 정보는 항상 설정 페이지에 표시하여 사용자와 개발자가 확인 가능하게 할 것

View File

@@ -1,141 +1,59 @@
# 적자 탈출 가계부 프로젝트 문서
# Zellyy Finance 프로젝트 문서
이 디렉토리는 적자 탈출 가계부 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱 개발 프로젝트입니다.
이 디렉토리는 Zellyy Finance 프로젝트의 모든 문서를 체계적으로 정리한 곳입니다. 사용자들이 개인 재정을 효과적으로 관리하고 적자 상태에서 벗어날 수 있도록 도와주는 모바일 앱입니다.
## 프로젝트 개요
'적자 탈출 가계부'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. AI 기술을 활용한 개인화된 재정 관리 경험을 제공하고, 궁극적으로는 사용자들의 재정적 웰빙을 향상시키는 것을 목표로 합니다.
'Zellyy Finance'는 단순한 수입/지출 기록을 넘어, 사용자의 소비 패턴을 분석하고 맞춤형 절약 전략을 제안하여 재정 건전성을 개선하는 데 중점을 둔 모바일 앱입니다. Appwrite 백엔드를 활용하여 안정적인 데이터 관리와 인증 시스템을 제공합니다.
## 폴더 구조
### 00_프로젝트_개요
프로젝트의 기본 개요와 목표, 사용자 정의에 관한 문서가 포함되어 있습니다.
- `01_프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
- `02_핵심_문제_정의.md` - 해결하고자 하는 문제 정의 (예정)
- `03_사용자_페르소나.md` - 타겟 사용자 프로필 (예정)
- `04_사용자_스토리.md` - 사용자 관점의 요구사항 (예정)
- `05_비즈니스_모델.md` - 수익 모델 및 사업화 전략 (예정)
- `06_법률_규제_검토.md` - 금융 앱 관련 법규 및 규제 검토 (예정)
- `프로젝트_소개.md` - 프로젝트 개요 및 주요 기능 설명
- `핵심_문제_정의.md` - 해결하고자 하는 문제 정의
- `사용자_페르소나.md` - 타겟 사용자 프로필
### 01_기획_및_설계
프로젝트의 기획 및 UI/UX 설계에 관한 문서가 포함되어 있습니다.
- `01_요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
- `02_MVP_기능_목록.md` - 최소 기능 제품(MVP)의 기능 목록 (예정)
- `03_주요_사용_시나리오.md` - 주요 사용 사례 시나리오 (예정)
- `04_UI_와이어프레임.md` - 핵심 화면 와이어프레임 (예정)
- `05_사용자_여정_맵.md` - 사용자 경험 흐름도 (예정)
- `06_정보_아키텍처.md` - 앱 구조 및 화면 흐름도 (예정)
- `요구사항_분석.md` - 사용자 요구사항 및 기능적/비기능적 요구사항 분석
- `UI_와이어프레임.md` - 핵심 화면 와이어프레임
- `사용자_경험_전략.md` - 사용자 경험 설계 전략
### 02_기술_문서
프로젝트의 기술적 구현에 관한 문서가 포함되어 있습니다.
- `01_시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
- `02_데이터_모델_설계.md` - 데이터 모델 설계 문서 (예정)
- `03_API_명세서.md` - API 엔드포인트 명세 (예정)
- `04_보안_설계.md` - 보안 및 개인정보 보호 설계 (예정)
- `05_성능_최적화_전략.md` - 앱 성능 최적화 전략 (예정)
- `06_CI_CD_파이프라인.md` - 지속적 통합/배포 전략 (예정)
- `07_AI_ML_구현_전략.md` - AI 기반 소비 패턴 분석 구현 방법 (예정)
- `시스템_아키텍처.md` - 시스템 아키텍처 설계 문서
- `데이터_모델_설계.md` - 데이터베이스 스키마 및 모델 설계
- `Appwrite_전환_가이드.md` - Supabase에서 Appwrite로의 전환 가이드
### 03_개발_단계
프로젝트 개발 단계별 문서가 포함되어 있습니다.
- `01_개발_로드맵.md` - 전체 개발 로드맵 및 일정
- `02_1단계_개발_계획.md` - 1단계(MVP) 개발 상세 계획 (예정)
- `03_테스트_전략.md` - 테스트 방법론 및 계획 (예정)
- `04_배포_전략.md` - 배포 및 운영 계획 (예정)
- `05_품질_보증_계획.md` - QA 전략 및 테스트 케이스 (예정)
- `06_유지보수_전략.md` - 출시 후 유지보수 및 업데이트 계획 (예정)
개발 과정과 관련된 문서가 포함되어 있습니다.
- `개발_가이드라인.md` - 코드 작성 원칙, iOS/Android 지원, Appwrite 통합 등에 관한 가이드라인
### 04_디자인_가이드
UI/UX 디자인 관련 문서가 포함되어 있습니다.
- `01_디자인_시스템.md` - 디자인 언어 및 컴포넌트 정의 (예정)
- `02_색상_팔레트.md` - 앱 색상 가이드라인 (예정)
- `03_타이포그래피.md` - 폰트 및 텍스트 스타일 가이드 (예정)
- `04_아이콘_및_이미지.md` - 아이콘 디자인 및 사용 가이드 (예정)
- `05_애니메이션_가이드.md` - UI 애니메이션 및 트랜지션 (예정)
- `06_접근성_지침.md` - 접근성 디자인 원칙 (예정)
### archive
더 이상 활발하게 사용되지 않는 레거시 문서들이 보관되어 있습니다.
- `Supabase 관련 문서` - 이전에 사용하던 Supabase 관련 설정 및 가이드
- `개발 단계별 문서` - 이전 개발 단계의 계획 및 산출물 요약
### 05_프로젝트_관리
프로젝트 관리 및 협업 관련 문서가 포함되어 있습니다.
- `01_팀_구성.md` - 팀 구성원 및 역할 정의 (예정)
- `02_의사결정_프로세스.md` - 프로젝트 의사결정 체계 (예정)
- `03_커뮤니케이션_계획.md` - 팀 내 소통 방식 및 도구 (예정)
- `04_일정_및_마일스톤.md` - 주요 마일스톤 및 납기일 (예정)
- `05_위험_관리.md` - 잠재적 위험 요소 및 대응 계획 (예정)
## 주요 기술 스택
### 06_참고자료
프로젝트 진행에 참고할 수 있는 자료들이 포함되어 있습니다.
- `01_시장_조사_보고서.md` - 가계부 앱 시장 조사 보고서
- `02_경쟁사_분석.md` - 주요 경쟁 앱 상세 분석 (예정)
- `03_사용자_인터뷰.md` - 잠재 사용자 인터뷰 결과 (예정)
- `04_참고_리소스.md` - 유용한 참고 자료 및 링크 (예정)
- `05_금융_데이터_소스.md` - 재정 관리 데이터 참고 자료 (예정)
- `06_관련_연구_자료.md` - 소비 행동 및 금융 심리학 연구 (예정)
- **프론트엔드**: React Native, TypeScript
- **백엔드**: Appwrite
- **상태 관리**: Context API
- **UI 컴포넌트**: Lovable UI
- **네이티브 통합**: Capacitor
### 07_마케팅_및_성장
마케팅 및 사용자 확보 전략 관련 문서가 포함되어 있습니다.
- `01_마케팅_전략.md` - 출시 및 사용자 확보 전략 (예정)
- `02_ASO_전략.md` - 앱 스토어 최적화 전략 (예정)
- `03_콘텐츠_전략.md` - 콘텐츠 마케팅 계획 (예정)
- `04_사용자_유지_전략.md` - 사용자 참여 및 유지 방안 (예정)
- `05_파트너십_계획.md` - 잠재적 파트너십 및 협업 기회 (예정)
## 개발 가이드라인
## 주요 기능
개발 가이드라인은 [03_개발_단계/개발_가이드라인.md](./03_개발_단계/개발_가이드라인.md) 문서를 참조하세요. 이 문서에는 다음 내용이 포함되어 있습니다:
1. **수입/지출 기록**: 간편한 UI로 일상 재정 활동 기록
2. **카테고리 관리**: 사용자 정의 카테고리로 지출 분류
3. **예산 설정**: 카테고리별 월간/주간 예산 설정 및 알림
4. **지출 분석**: 차트와 그래프로 소비 패턴 시각
5. **AI 기반 분석**: 소비 패턴 분석 및 맞춤형 절약 제안
6. **절약 챌린지**: 사용자 맞춤형 절약 목표 설정 및 달성 보상
7. **재정 건강 점수**: 사용자의 재정 상태를 점수화하여 개선 동기 부여
8. **구독 관리**: 정기 구독 서비스 추적 및 최적화 제안
9. **재정 목표 설정**: 단기/중기/장기 저축 목표 설정 및 진행 상황 추적
10. **알림 시스템**: 예산 초과, 주요 지출, 절약 기회에 대한 스마트 알림
11. **가계부 보고서**: 정기적인 재정 상태 요약 보고서 제공
12. **공유 기능**: 가족 또는 파트너와 특정 재정 정보 공유
1. 코드 작성 원칙
2. 트랜잭션 삭제 안전성
3. Appwrite 통합 원칙
4. 상태 관리 최적
5. iOS/Android 지원
6. 디버깅 및 로깅
## 기술 스택
## Appwrite 전환
- **프론트엔드**: React, Vite, Tailwind CSS, Capacitor
- **백엔드**: Node.js, Express, Supabase(PostgreSQL)
- **AI/ML**: TensorFlow, Python
- **클라우드**: Supabase On-Premise
- **데이터 시각화**: D3.js, Chart.js
- **인증/보안**: JWT, OAuth 2.0, 데이터 암호화
- **테스트**: Jest, Cypress
- **CI/CD**: GitHub Actions
- **분석**: Supabase Analytics
## 문서 작성 가이드라인
- 모든 문서는 마크다운(.md) 형식으로 작성합니다.
- 파일명은 내용을 명확히 나타내는 한글 또는 영문으로 작성합니다.
- 이미지나 다이어그램은 가능한 마크다운 내에 포함시킵니다.
- 문서 간 연결이 필요한 경우 상대 경로를 사용하여 링크합니다.
- 코드 예시는 적절한 구문 강조와 함께 코드 블록으로 포함합니다.
- 변경 사항은 문서 하단의 업데이트 이력에 기록합니다.
- 중요 결정사항은 의사결정 배경과 함께 기록합니다.
## 개발 워크플로우
1. **기능 기획**: 사용자 스토리 및 요구사항 정의
2. **설계**: UI/UX 디자인 및 기술 아키텍처 설계
3. **개발**: 기능 구현 및 단위 테스트
4. **코드 리뷰**: 팀원 간 코드 품질 검토
5. **테스트**: QA 및 사용성 테스트
6. **배포**: 스테이징 및 프로덕션 환경 배포
7. **모니터링**: 성능 및 사용자 피드백 모니터링
8. **반복**: 피드백을 바탕으로 기능 개선
## 출시 계획
- **알파 버전**: 내부 테스트 (2024년 4월 초)
- **베타 버전**: 제한적 사용자 테스트 (2024년 4월 중순)
- **MVP 출시**: 앱스토어 및 플레이스토어 공개 (2024년 4월 말)
- **기능 업데이트**: 사용자 피드백 기반 주요 기능 추가 (2024년 5월 초)
- **확장 계획**: 웹 버전 및 추가 기능 확장 (2024년 5월 중순부터)
## 업데이트 이력
- 2024-03-15: 프로젝트 문서 초기 구성 완료
- 2024-03-15: 프로젝트 소개, 요구사항 분석, 시스템 아키텍처, 개발 로드맵, 시장 조사 보고서 추가
- 2024-04-01: 폴더 구조 개선 및 추가 섹션(디자인 가이드, 프로젝트 관리, 마케팅) 추가
- 2024-04-05: 일정 조정 - 모든 개발 계획을 4월 말까지 완료하도록 수정
- 2025-03-09: 개발 방법 변경 - Flutter에서 React, Tailwind CSS, Capacitor 기반 웹 앱으로 전환, Lovable UI 컴포넌트 스타일 적용
- 2025-03-09: 데이터베이스 변경 - MongoDB에서 Supabase(PostgreSQL) On-Premise로 전환
Supabase에서 Appwrite로의 전환에 관한 상세 정보는 [02_기술_문서/Appwrite_전환_가이드.md](./02_기술_문서/Appwrite_전환_가이드.md) 문서를 참조하세요.

116
docs/WEB_SERVER_SETUP.md Normal file
View File

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

View File

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

View File

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

View File

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

152
package-lock.json generated
View File

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

View File

@@ -45,14 +45,16 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@supabase/supabase-js": "^2.49.1",
"@supabase/supabase-js": "^2.49.4",
"@tanstack/react-query": "^5.56.2",
"@types/uuid": "^10.0.0",
"appwrite": "^17.0.2",
"browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4",
"lucide-react": "^0.462.0",

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

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

21
src/archive/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Archive 폴더
이 폴더는 Zellyy Finance 프로젝트에서 더 이상 활발하게 사용되지 않는 레거시 코드를 보관하는 곳입니다.
## 개요
Zellyy Finance는 백엔드 서비스를 Supabase에서 Appwrite로 전환했습니다. 이 폴더에는 Supabase 관련 코드가 보관되어 있으며, 참조용으로만 유지됩니다.
## 폴더 구조
- `components/`: Supabase 관련 UI 컴포넌트
- `hooks/`: Supabase 관련 훅
- `integrations/`: Supabase 통합 코드
- `lib/`: Supabase 클라이언트 및 유틸리티
- `utils/`: Supabase 트랜잭션 유틸리티
## 주의사항
이 폴더의 코드는 더 이상 유지보수되지 않으며, 새로운 기능 개발에 사용해서는 안 됩니다. 모든 새로운 개발은 Appwrite 기반으로 진행해야 합니다.
마이그레이션이 완전히 완료되면 이 폴더는 삭제될 예정입니다.

View File

@@ -0,0 +1,300 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { Loader2, CheckCircle, AlertCircle } from 'lucide-react';
import {
migrateTransactionsFromSupabase,
checkMigrationStatus
} from '@/lib/appwrite/migrateFromSupabase';
import { useAppwriteAuth } from '@/hooks/auth/useAppwriteAuth';
import { toast } from '@/hooks/useToast.wrapper';
/**
* Supabase에서 Appwrite로 마이그레이션 컴포넌트
* 데이터 마이그레이션 상태 확인 및 마이그레이션 실행
*/
const SupabaseToAppwriteMigration: React.FC = () => {
// 인증 상태
const { user } = useAppwriteAuth();
// 마이그레이션 상태
const [migrationStatus, setMigrationStatus] = useState<{
supabaseCount: number;
appwriteCount: number;
isComplete: boolean;
error?: string;
} | null>(null);
// 마이그레이션 진행 상태
const [migrationProgress, setMigrationProgress] = useState<{
isRunning: boolean;
current: number;
total: number;
percentage: number;
}>({
isRunning: false,
current: 0,
total: 0,
percentage: 0
});
// 마이그레이션 결과
const [migrationResult, setMigrationResult] = useState<{
success: boolean;
migrated: number;
total: number;
error?: string;
} | null>(null);
// 컴포넌트 마운트 상태 추적
const [isMounted, setIsMounted] = useState(true);
// 마이그레이션 상태 확인
const checkStatus = useCallback(async () => {
if (!user || !isMounted) return;
try {
const status = await checkMigrationStatus(user);
if (isMounted) {
setMigrationStatus(status);
}
} catch (error) {
console.error('마이그레이션 상태 확인 오류:', error);
if (isMounted) {
toast({
title: '상태 확인 실패',
description: '마이그레이션 상태를 확인하는 중 오류가 발생했습니다.',
variant: 'destructive'
});
}
}
}, [user, isMounted]);
// 마이그레이션 실행
const runMigration = useCallback(async () => {
if (!user || !isMounted) return;
// 진행 상태 초기화
setMigrationProgress({
isRunning: true,
current: 0,
total: migrationStatus?.supabaseCount || 0,
percentage: 0
});
// 결과 초기화
setMigrationResult(null);
try {
// 진행 상황 콜백
const progressCallback = (current: number, total: number) => {
if (!isMounted) return;
// requestAnimationFrame을 사용하여 UI 업데이트 최적화
requestAnimationFrame(() => {
if (!isMounted) return;
setMigrationProgress({
isRunning: true,
current,
total,
percentage: Math.round((current / total) * 100)
});
});
};
// 마이그레이션 실행
const result = await migrateTransactionsFromSupabase(user, progressCallback);
if (!isMounted) return;
// 결과 설정
setMigrationResult(result);
// 성공 메시지
if (result.success) {
toast({
title: '마이그레이션 완료',
description: `${result.migrated}개의 트랜잭션이 성공적으로 마이그레이션되었습니다.`,
variant: 'default'
});
} else {
toast({
title: '마이그레이션 실패',
description: result.error || '알 수 없는 오류가 발생했습니다.',
variant: 'destructive'
});
}
// 상태 다시 확인
checkStatus();
} catch (error) {
console.error('마이그레이션 오류:', error);
if (!isMounted) return;
// 오류 메시지
toast({
title: '마이그레이션 실패',
description: error instanceof Error ? error.message : '알 수 없는 오류가 발생했습니다.',
variant: 'destructive'
});
// 결과 설정
setMigrationResult({
success: false,
migrated: 0,
total: migrationStatus?.supabaseCount || 0,
error: error instanceof Error ? error.message : '알 수 없는 오류'
});
} finally {
// 진행 상태 종료
if (isMounted) {
setMigrationProgress(prev => ({
...prev,
isRunning: false
}));
}
}
}, [user, migrationStatus, checkStatus, isMounted]);
// 컴포넌트 마운트 시 상태 확인
useEffect(() => {
setIsMounted(true);
if (user) {
checkStatus();
}
return () => {
setIsMounted(false);
};
}, [user, checkStatus]);
// 사용자가 로그인하지 않은 경우
if (!user) {
return (
<div className="p-4 bg-yellow-50 rounded-md">
<p className="text-yellow-800">
.
</p>
</div>
);
}
return (
<div className="space-y-6 p-4 border rounded-md">
<div>
<h3 className="text-lg font-medium">Supabase에서 Appwrite로 </h3>
<p className="text-sm text-gray-500 mt-1">
Supabase의 Appwrite로 .
</p>
</div>
{/* 마이그레이션 상태 */}
{migrationStatus && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<Button
variant="ghost"
size="sm"
onClick={checkStatus}
disabled={migrationProgress.isRunning}
>
</Button>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="p-3 bg-gray-50 rounded-md">
<div className="text-xs text-gray-500">Supabase </div>
<div className="text-lg font-semibold">{migrationStatus.supabaseCount}</div>
</div>
<div className="p-3 bg-gray-50 rounded-md">
<div className="text-xs text-gray-500">Appwrite </div>
<div className="text-lg font-semibold">{migrationStatus.appwriteCount}</div>
</div>
</div>
<div className="flex items-center mt-2">
{migrationStatus.isComplete ? (
<div className="flex items-center text-green-600 text-sm">
<CheckCircle className="h-4 w-4 mr-1" />
</div>
) : (
<div className="flex items-center text-amber-600 text-sm">
<AlertCircle className="h-4 w-4 mr-1" />
</div>
)}
</div>
{migrationStatus.error && (
<div className="text-sm text-red-600 mt-2">
: {migrationStatus.error}
</div>
)}
</div>
)}
{/* 마이그레이션 진행 상태 */}
{migrationProgress.isRunning && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium"> </span>
<span className="text-xs text-gray-500">
{migrationProgress.current} / {migrationProgress.total} ({migrationProgress.percentage}%)
</span>
</div>
<Progress value={migrationProgress.percentage} className="h-2" />
</div>
)}
{/* 마이그레이션 결과 */}
{migrationResult && !migrationProgress.isRunning && (
<div className={`p-3 rounded-md ${migrationResult.success ? 'bg-green-50' : 'bg-red-50'}`}>
<div className="flex items-start">
{migrationResult.success ? (
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
) : (
<AlertCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-medium ${migrationResult.success ? 'text-green-800' : 'text-red-800'}`}>
{migrationResult.success ? '마이그레이션 성공' : '마이그레이션 실패'}
</p>
<p className={migrationResult.success ? 'text-green-700' : 'text-red-700'}>
{migrationResult.success
? `${migrationResult.migrated}개의 트랜잭션이 마이그레이션되었습니다.`
: migrationResult.error || '알 수 없는 오류가 발생했습니다.'
}
</p>
</div>
</div>
</div>
)}
{/* 마이그레이션 버튼 */}
<div className="flex justify-end">
<Button
onClick={runMigration}
disabled={migrationProgress.isRunning || migrationStatus?.isComplete}
>
{migrationProgress.isRunning && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{migrationStatus?.isComplete
? '이미 마이그레이션 완료됨'
: migrationProgress.isRunning
? '마이그레이션 중...'
: '마이그레이션 시작'
}
</Button>
</div>
</div>
);
};
export default SupabaseToAppwriteMigration;

View File

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

View File

@@ -0,0 +1,186 @@
import { ID, Query } from 'appwrite';
import { supabase } from '@/archive/lib/supabase';
import { databases, account } from './client';
import { config } from './config';
import { setupAppwriteDatabase } from './setup';
/**
* Supabase에서 Appwrite로 트랜잭션 데이터 마이그레이션
* 1. Appwrite 데이터베이스 설정
* 2. Supabase에서 트랜잭션 데이터 가져오기
* 3. Appwrite에 트랜잭션 데이터 저장
*/
export const migrateTransactionsFromSupabase = async (
user: any,
progressCallback?: (progress: number, total: number) => void
): Promise<{
success: boolean;
migrated: number;
total: number;
error?: string;
}> => {
try {
// 1. Appwrite 데이터베이스 설정
const setupSuccess = await setupAppwriteDatabase();
if (!setupSuccess) {
return {
success: false,
migrated: 0,
total: 0,
error: 'Appwrite 데이터베이스 설정에 실패했습니다.'
};
}
// 2. Supabase에서 트랜잭션 데이터 가져오기
const { data: supabaseTransactions, error } = await supabase
.from('transactions')
.select('*')
.eq('user_id', user.id);
if (error) {
console.error('Supabase 데이터 조회 오류:', error);
return {
success: false,
migrated: 0,
total: 0,
error: `Supabase 데이터 조회 실패: ${error.message}`
};
}
if (!supabaseTransactions || supabaseTransactions.length === 0) {
return {
success: true,
migrated: 0,
total: 0
};
}
// 3. Appwrite에 트랜잭션 데이터 저장
const databaseId = config.databaseId;
const collectionId = config.transactionsCollectionId;
// 현재 Appwrite에 있는 트랜잭션 확인 (중복 방지)
const { documents: existingTransactions } = await databases.listDocuments(
databaseId,
collectionId,
[Query.equal('user_id', user.$id)]
);
// 이미 마이그레이션된 트랜잭션 ID 목록
const existingTransactionIds = existingTransactions.map(
doc => doc.transaction_id
);
let migratedCount = 0;
const totalCount = supabaseTransactions.length;
// 트랜잭션 데이터 마이그레이션
for (let i = 0; i < supabaseTransactions.length; i++) {
const transaction = supabaseTransactions[i];
// 이미 마이그레이션된 트랜잭션은 건너뛰기
if (existingTransactionIds.includes(transaction.transaction_id)) {
// 진행 상황 콜백
if (progressCallback) {
progressCallback(i + 1, totalCount);
}
continue;
}
try {
// 트랜잭션 데이터 Appwrite에 저장
await databases.createDocument(
databaseId,
collectionId,
ID.unique(),
{
user_id: user.$id,
transaction_id: transaction.transaction_id,
title: transaction.title,
amount: transaction.amount,
date: transaction.date,
category: transaction.category,
type: transaction.type
}
);
migratedCount++;
} catch (docError) {
console.error('트랜잭션 마이그레이션 오류:', docError);
}
// 진행 상황 콜백
if (progressCallback) {
progressCallback(i + 1, totalCount);
}
}
return {
success: true,
migrated: migratedCount,
total: totalCount
};
} catch (error) {
console.error('마이그레이션 오류:', error);
return {
success: false,
migrated: 0,
total: 0,
error: error instanceof Error ? error.message : '알 수 없는 오류'
};
}
};
/**
* 마이그레이션 상태 확인
* Supabase와 Appwrite의 트랜잭션 수를 비교
*/
export const checkMigrationStatus = async (
user: any
): Promise<{
supabaseCount: number;
appwriteCount: number;
isComplete: boolean;
error?: string;
}> => {
try {
// Supabase 트랜잭션 수 확인
const { count: supabaseCount, error: supabaseError } = await supabase
.from('transactions')
.select('*', { count: 'exact', head: true })
.eq('user_id', user.id);
if (supabaseError) {
return {
supabaseCount: 0,
appwriteCount: 0,
isComplete: false,
error: `Supabase 데이터 조회 실패: ${supabaseError.message}`
};
}
// Appwrite 트랜잭션 수 확인
const databaseId = config.databaseId;
const collectionId = config.transactionsCollectionId;
const { total: appwriteCount } = await databases.listDocuments(
databaseId,
collectionId,
[Query.equal('user_id', user.$id)]
);
return {
supabaseCount: supabaseCount || 0,
appwriteCount,
isComplete: (supabaseCount || 0) <= appwriteCount
};
} catch (error) {
console.error('마이그레이션 상태 확인 오류:', error);
return {
supabaseCount: 0,
appwriteCount: 0,
isComplete: false,
error: error instanceof Error ? error.message : '알 수 없는 오류'
};
}
};

View File

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

View File

@@ -4,7 +4,7 @@ import { PlusIcon } from 'lucide-react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
import { toast } from '@/hooks/useToast.wrapper'; // 래퍼 사용
import { useBudget } from '@/contexts/budget/BudgetContext';
import { supabase } from '@/lib/supabase';
import { supabase } from '@/archive/lib/supabase';
import { isSyncEnabled, setLastSyncTime, trySyncAllData } from '@/utils/syncUtils';
import ExpenseForm, { ExpenseFormValues } from './expenses/ExpenseForm';
import { Transaction } from '@/contexts/budget/types';

View File

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

@@ -0,0 +1,41 @@
import React from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
interface AppwriteConnectionStatusProps {
testResults: {
connected: boolean;
message: string;
details?: string;
} | null;
}
const AppwriteConnectionStatus = ({ testResults }: AppwriteConnectionStatusProps) => {
if (!testResults) return null;
return (
<div className={`mt-2 p-3 rounded-md text-sm ${testResults.connected ? 'bg-green-50' : 'bg-red-50'}`}>
<div className="flex items-start">
{testResults.connected ? (
<CheckCircle className="h-5 w-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" />
) : (
<XCircle className="h-5 w-5 text-red-500 mr-2 flex-shrink-0 mt-0.5" />
)}
<div>
<p className={`font-medium ${testResults.connected ? 'text-green-800' : 'text-red-800'}`}>
{testResults.connected ? '연결됨' : '연결 실패'}
</p>
<p className={testResults.connected ? 'text-green-700' : 'text-red-700'}>
{testResults.message}
</p>
{testResults.details && (
<p className="text-gray-500 mt-1 text-xs">
{testResults.details}
</p>
)}
</div>
</div>
</div>
);
};
export default AppwriteConnectionStatus;

View File

@@ -0,0 +1,146 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import AppwriteConnectionStatus from './AppwriteConnectionStatus';
import { client, account, isValidAppwriteConfig, getAppwriteEndpoint } from '@/lib/appwrite';
import { setupAppwriteDatabase } from '@/lib/appwrite/setup';
/**
* Appwrite 연결 테스트 컴포넌트
* 서버 연결 상태를 확인하고 데이터베이스 설정을 진행합니다.
*/
const AppwriteConnectionTest = () => {
// 연결 테스트 결과 상태
const [testResults, setTestResults] = useState<{
connected: boolean;
message: string;
details?: string;
} | null>(null);
// 로딩 상태
const [loading, setLoading] = useState<boolean>(false);
// 데이터베이스 설정 상태
const [dbSetupDone, setDbSetupDone] = useState<boolean>(false);
// 연결 테스트 함수
const testConnection = useCallback(async () => {
setLoading(true);
setTestResults(null);
try {
// 설정 유효성 검사
if (!isValidAppwriteConfig()) {
setTestResults({
connected: false,
message: 'Appwrite 설정이 완료되지 않았습니다.',
details: '환경 변수 VITE_APPWRITE_ENDPOINT 및 VITE_APPWRITE_PROJECT_ID를 확인하세요.'
});
return;
}
// 서버 연결 테스트
try {
await account.get();
setTestResults({
connected: true,
message: 'Appwrite 서버에 성공적으로 연결되었습니다.',
details: `서버: ${getAppwriteEndpoint()}`
});
} catch (error: any) {
// 인증 오류는 연결 성공으로 간주 (로그인 필요)
if (error.code === 401) {
setTestResults({
connected: true,
message: 'Appwrite 서버에 연결되었지만 로그인이 필요합니다.',
details: `서버: ${getAppwriteEndpoint()}`
});
} else {
setTestResults({
connected: false,
message: '서버 연결에 실패했습니다.',
details: error.message
});
}
}
} catch (error: any) {
setTestResults({
connected: false,
message: '연결 테스트 중 오류가 발생했습니다.',
details: error.message
});
} finally {
setLoading(false);
}
}, []);
// 데이터베이스 설정 함수
const setupDatabase = useCallback(async () => {
setLoading(true);
try {
const success = await setupAppwriteDatabase();
if (success) {
setDbSetupDone(true);
setTestResults({
connected: true,
message: '데이터베이스 설정이 완료되었습니다.',
details: '트랜잭션 컬렉션이 준비되었습니다.'
});
} else {
setTestResults({
connected: false,
message: '데이터베이스 설정에 실패했습니다.',
details: '로그를 확인하세요.'
});
}
} catch (error: any) {
setTestResults({
connected: false,
message: '데이터베이스 설정 중 오류가 발생했습니다.',
details: error.message
});
} finally {
setLoading(false);
}
}, []);
// 컴포넌트 마운트 시 자동 테스트
useEffect(() => {
testConnection();
}, [testConnection]);
return (
<div className="space-y-4">
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={testConnection}
disabled={loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
{testResults?.connected && !dbSetupDone && (
<Button
variant="outline"
size="sm"
onClick={setupDatabase}
disabled={loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
</Button>
)}
</div>
<AppwriteConnectionStatus testResults={testResults} />
</div>
);
};
export default AppwriteConnectionTest;

View File

@@ -7,7 +7,7 @@ import { verifyServerConnection } from "@/contexts/auth/auth.utils";
import { ServerConnectionStatus } from "./types";
import EmailConfirmation from "./EmailConfirmation";
import RegisterFormFields from "./RegisterFormFields";
import { supabase } from "@/lib/supabase";
import { supabase } from "@/archive/lib/supabase";
interface RegisterFormProps {
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any, redirectToSettings?: boolean, emailConfirmationRequired?: boolean }>;

View File

@@ -1,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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -0,0 +1,182 @@
import { useState, useEffect, useCallback } from 'react';
import { account } from '@/lib/appwrite';
import { ID } from 'appwrite';
// 인증 상태 인터페이스
interface AuthState {
user: any | null;
loading: boolean;
error: Error | null;
}
// 로그인 입력값 인터페이스
interface LoginCredentials {
email: string;
password: string;
}
// 회원가입 입력값 인터페이스
interface SignupCredentials extends LoginCredentials {
name?: string;
}
/**
* Appwrite 인증 관련 훅
* 로그인, 로그아웃, 회원가입 및 사용자 상태 관리
*/
export const useAppwriteAuth = () => {
// 인증 상태 관리
const [authState, setAuthState] = useState<AuthState>({
user: null,
loading: true,
error: null
});
// 컴포넌트 마운트 상태 추적
const [isMounted, setIsMounted] = useState(true);
// 사용자 정보 가져오기
const getCurrentUser = useCallback(async () => {
try {
const user = await account.get();
if (isMounted) {
setAuthState({
user,
loading: false,
error: null
});
}
return user;
} catch (error) {
if (isMounted) {
setAuthState({
user: null,
loading: false,
error: error as Error
});
}
return null;
}
}, [isMounted]);
// 이메일/비밀번호로 로그인
const login = useCallback(async ({ email, password }: LoginCredentials) => {
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
// 비동기 작업 시작 전 UI 스레드 차단 방지
await new Promise(resolve => setTimeout(resolve, 0));
const session = await account.createEmailPasswordSession(email, password);
const user = await account.get();
if (isMounted) {
setAuthState({
user,
loading: false,
error: null
});
}
return { user, session };
} catch (error) {
if (isMounted) {
setAuthState(prev => ({
...prev,
loading: false,
error: error as Error
}));
}
throw error;
}
}, [isMounted]);
// 회원가입
const signup = useCallback(async ({ email, password, name }: SignupCredentials) => {
try {
setAuthState(prev => ({ ...prev, loading: true, error: null }));
// 비동기 작업 시작 전 UI 스레드 차단 방지
await new Promise(resolve => setTimeout(resolve, 0));
const user = await account.create(
ID.unique(),
email,
password,
name
);
// 회원가입 후 자동 로그인
await account.createEmailPasswordSession(email, password);
if (isMounted) {
setAuthState({
user,
loading: false,
error: null
});
}
return user;
} catch (error) {
if (isMounted) {
setAuthState(prev => ({
...prev,
loading: false,
error: error as Error
}));
}
throw error;
}
}, [isMounted]);
// 로그아웃
const logout = useCallback(async () => {
try {
setAuthState(prev => ({ ...prev, loading: true }));
// 현재 세션 삭제
await account.deleteSession('current');
if (isMounted) {
setAuthState({
user: null,
loading: false,
error: null
});
}
} catch (error) {
if (isMounted) {
setAuthState(prev => ({
...prev,
loading: false,
error: error as Error
}));
}
throw error;
}
}, [isMounted]);
// 초기 사용자 정보 로드
useEffect(() => {
setIsMounted(true);
getCurrentUser();
// 정리 함수
return () => {
setIsMounted(false);
};
}, [getCurrentUser]);
return {
user: authState.user,
loading: authState.loading,
error: authState.error,
login,
signup,
logout,
getCurrentUser
};
};
export default useAppwriteAuth;

View File

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

@@ -0,0 +1,162 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { Transaction } from '@/components/TransactionCard';
import {
syncTransactionsWithAppwrite,
updateTransactionInAppwrite,
deleteTransactionFromAppwrite,
debouncedDeleteTransaction
} from '@/utils/appwriteTransactionUtils';
import { toast } from '@/hooks/useToast.wrapper';
import { isSyncEnabled } from '@/utils/syncUtils';
/**
* Appwrite 트랜잭션 관리 훅
* 트랜잭션 동기화, 추가, 수정, 삭제 기능 제공
*/
export const useAppwriteTransactions = (user: any, localTransactions: Transaction[]) => {
// 트랜잭션 상태 관리
const [transactions, setTransactions] = useState<Transaction[]>(localTransactions);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
// 컴포넌트 마운트 상태 추적
const isMountedRef = useRef<boolean>(true);
// 진행 중인 작업 추적
const pendingOperations = useRef<Set<string>>(new Set());
// 트랜잭션 동기화
const syncTransactions = useCallback(async () => {
if (!user || !isSyncEnabled()) return localTransactions;
try {
setLoading(true);
setError(null);
// UI 스레드 차단 방지
await new Promise(resolve => setTimeout(resolve, 0));
const syncedTransactions = await syncTransactionsWithAppwrite(user, localTransactions);
if (isMountedRef.current) {
setTransactions(syncedTransactions);
setLoading(false);
}
return syncedTransactions;
} catch (err) {
console.error('트랜잭션 동기화 오류:', err);
if (isMountedRef.current) {
setError(err as Error);
setLoading(false);
}
return localTransactions;
}
}, [user, localTransactions]);
// 트랜잭션 추가/수정
const saveTransaction = useCallback(async (transaction: Transaction) => {
if (!user || !isSyncEnabled()) return;
try {
// 작업 추적 시작
pendingOperations.current.add(transaction.id);
// UI 스레드 차단 방지
await new Promise(resolve => requestAnimationFrame(resolve));
await updateTransactionInAppwrite(user, transaction);
if (!isMountedRef.current) return;
// 로컬 상태 업데이트
setTransactions(prev => {
const index = prev.findIndex(t => t.id === transaction.id);
if (index >= 0) {
const updated = [...prev];
updated[index] = transaction;
return updated;
} else {
return [...prev, transaction];
}
});
} catch (err) {
console.error('트랜잭션 저장 오류:', err);
if (isMountedRef.current) {
toast({
title: '저장 실패',
description: '트랜잭션을 저장하는 중 오류가 발생했습니다.',
variant: 'destructive'
});
}
} finally {
// 작업 추적 종료
pendingOperations.current.delete(transaction.id);
}
}, [user]);
// 트랜잭션 삭제
const removeTransaction = useCallback(async (transactionId: string) => {
if (!user || !isSyncEnabled()) return;
try {
// 작업 추적 시작
pendingOperations.current.add(transactionId);
// 로컬 상태 먼저 업데이트 (낙관적 UI 업데이트)
setTransactions(prev => prev.filter(t => t.id !== transactionId));
// 디바운스된 삭제 작업 실행 (여러 번 연속 호출 방지)
await debouncedDeleteTransaction(user, transactionId);
} catch (err) {
console.error('트랜잭션 삭제 오류:', err);
if (isMountedRef.current) {
toast({
title: '삭제 실패',
description: '트랜잭션을 삭제하는 중 오류가 발생했습니다.',
variant: 'destructive'
});
// 실패 시 트랜잭션 복원 (서버에서 가져오기)
syncTransactions();
}
} finally {
// 작업 추적 종료
pendingOperations.current.delete(transactionId);
}
}, [user, syncTransactions]);
// 초기 동기화
useEffect(() => {
if (user && isSyncEnabled()) {
syncTransactions();
} else {
setTransactions(localTransactions);
}
}, [user, localTransactions, syncTransactions]);
// 컴포넌트 언마운트 시 정리
useEffect(() => {
return () => {
isMountedRef.current = false;
};
}, []);
return {
transactions,
loading,
error,
syncTransactions,
saveTransaction,
removeTransaction,
hasPendingOperations: pendingOperations.current.size > 0
};
};
export default useAppwriteTransactions;

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { useToast } from "@/hooks/useToast.wrapper";
import { createRequiredTables } from "@/lib/supabase/setup";
import { createRequiredTables } from "@/archive/lib/supabase/setup";
/**
* Supabase 테이블 설정을 처리하는 커스텀 훅

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

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

View File

@@ -0,0 +1,63 @@
/**
* Appwrite 설정
*
* 이 파일은 Appwrite 서비스에 필요한 모든 설정 값을 정의합니다.
* 환경 변수에서 값을 가져오며, 기본값을 제공합니다.
*/
// Appwrite 설정 타입 정의
export interface AppwriteConfig {
endpoint: string;
projectId: string;
databaseId: string;
transactionsCollectionId: string;
apiKey: string;
}
// 환경 변수에서 설정 값 가져오기
const endpoint = import.meta.env.VITE_APPWRITE_ENDPOINT || 'https://a11.ism.kr/v1';
const projectId = import.meta.env.VITE_APPWRITE_PROJECT_ID || '68182a300039f6d700a6';
const databaseId = import.meta.env.VITE_APPWRITE_DATABASE_ID || 'default';
const transactionsCollectionId = import.meta.env.VITE_APPWRITE_TRANSACTIONS_COLLECTION_ID || 'transactions';
const apiKey = import.meta.env.VITE_APPWRITE_API_KEY || '';
// 개발 모드에서 설정 값 로깅
console.log('현재 Appwrite 설정:', {
endpoint,
projectId,
databaseId,
transactionsCollectionId,
apiKey: apiKey ? '설정됨' : '설정되지 않음' // API 키는 안전을 위해 완전한 값을 로깅하지 않음
});
// 설정 객체 생성
export const config: AppwriteConfig = {
endpoint,
projectId,
databaseId,
transactionsCollectionId,
apiKey,
};
// Getter functions for config values
export const getAppwriteEndpoint = (): string => endpoint;
export const getAppwriteProjectId = (): string => projectId;
export const getAppwriteDatabaseId = (): string => databaseId;
export const getAppwriteTransactionsCollectionId = (): string => transactionsCollectionId;
/**
* 서버 연결 유효성 검사
* @returns 유효한 설정인지 여부
*/
export const isValidAppwriteConfig = (): boolean => {
return Boolean(endpoint && projectId);
};
/**
* 설정 값 검증 및 오류 발생
* @throws 필수 설정이 없는 경우 오류 발생
*/
export const validateConfig = (): void => {
if (!endpoint) throw new Error("VITE_APPWRITE_ENDPOINT is not set");
if (!projectId) throw new Error("VITE_APPWRITE_PROJECT_ID is not set");
};

View File

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

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

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

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