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 VITE_DISABLE_LOVABLE_BANNER=true

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { 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' classpath 'com.google.gms:google-services:4.4.2'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@@ -22,7 +22,8 @@ org.gradle.jvmargs=-Xmx1536m
android.useAndroidX=true android.useAndroidX=true
# Java 및 Kotlin 버전 설정 # 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.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false android.nonTransitiveRClass=false
android.nonFinalResIds=false android.nonFinalResIds=false

View File

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

View File

View File

View File

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

View File

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

View File

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

View File

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

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> <body>
<div id="root"></div> <div id="root"></div>
<!-- IMPORTANT: DO NOT REMOVE THIS SCRIPT TAG OR THIS VERY COMMENT! -->
<script src="https://cdn.gpteng.co/gptengineer.js" type="module"></script>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

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

View File

@@ -45,14 +45,16 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4", "@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", "@tanstack/react-query": "^5.56.2",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"appwrite": "^17.0.2",
"browserslist": "^4.24.4", "browserslist": "^4.24.4",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^4.1.0", "date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"embla-carousel-react": "^8.3.0", "embla-carousel-react": "^8.3.0",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"lucide-react": "^0.462.0", "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, useState, Suspense, Component, ErrorInfo, ReactNode } from 'react';
import React, { useEffect } from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { BudgetProvider } from './contexts/budget/BudgetContext'; import { BudgetProvider } from './contexts/budget/BudgetContext';
import { AuthProvider } from './contexts/auth/AuthProvider'; import { AuthProvider } from './contexts/auth/AuthProvider';
@@ -17,16 +16,132 @@ import HelpSupport from './pages/HelpSupport';
import SecurityPrivacySettings from './pages/SecurityPrivacySettings'; import SecurityPrivacySettings from './pages/SecurityPrivacySettings';
import NotificationSettings from './pages/NotificationSettings'; import NotificationSettings from './pages/NotificationSettings';
import ForgotPassword from './pages/ForgotPassword'; import ForgotPassword from './pages/ForgotPassword';
import AppwriteSettingsPage from './pages/AppwriteSettingsPage';
// 간단한 오류 경계 컴포넌트 구현
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
console.error('애플리케이션 오류:', error, errorInfo);
}
render(): ReactNode {
if (this.state.hasError) {
// 오류 발생 시 대체 UI 표시
return this.props.fallback || (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center">
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="mb-4"> .</p>
<button
onClick={() => window.location.reload()}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
}
return this.props.children;
}
}
// 로딩 상태 표시 컴포넌트
const LoadingScreen: React.FC = () => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500 mb-4"></div>
<h2 className="text-xl font-bold mb-2">Zellyy Finance</h2>
<p className="text-gray-600"> ...</p>
</div>
);
// 오류 화면 컴포넌트
const ErrorScreen: React.FC<{ error: Error | null; retry: () => void }> = ({ error, retry }) => (
<div className="flex flex-col items-center justify-center min-h-screen p-4 text-center bg-neuro-background">
<div className="text-red-500 text-5xl mb-4"></div>
<h2 className="text-xl font-bold mb-4"> </h2>
<p className="text-center mb-6">{error?.message || '애플리케이션 로딩 중 오류가 발생했습니다.'}</p>
<button
onClick={retry}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
>
</button>
</div>
);
// 기본 레이아웃 컴포넌트 - 인증 없이도 표시 가능
const BasicLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="App">
{children}
<Toaster />
</div>
);
function App() { function App() {
const [appState, setAppState] = useState<'loading' | 'error' | 'ready'>('loading');
const [error, setError] = useState<Error | null>(null);
const [appwriteEnabled, setAppwriteEnabled] = useState(true);
useEffect(() => { useEffect(() => {
document.title = "적자 탈출 가계부"; document.title = "Zellyy Finance";
// 애플리케이션 초기화 시간 지연 설정
const timer = setTimeout(() => {
setAppState('ready');
}, 1500); // 1.5초 후 로딩 상태 해제
return () => clearTimeout(timer);
}, []); }, []);
// 재시도 기능
const handleRetry = () => {
setAppState('loading');
setError(null);
// 재시도 시 지연 후 상태 변경
setTimeout(() => {
setAppState('ready');
}, 1500);
};
// 로딩 상태 표시
if (appState === 'loading') {
return ( 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> <AuthProvider>
<BudgetProvider> <BudgetProvider>
<div className="App"> <BasicLayout>
<Routes> <Routes>
<Route path="/" element={<Index />} /> <Route path="/" element={<Index />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
@@ -40,12 +155,13 @@ function App() {
<Route path="/security-privacy" element={<SecurityPrivacySettings />} /> <Route path="/security-privacy" element={<SecurityPrivacySettings />} />
<Route path="/notifications" element={<NotificationSettings />} /> <Route path="/notifications" element={<NotificationSettings />} />
<Route path="/forgot-password" element={<ForgotPassword />} /> <Route path="/forgot-password" element={<ForgotPassword />} />
<Route path="/appwrite-settings" element={<AppwriteSettingsPage />} />
<Route path="*" element={<NotFound />} /> <Route path="*" element={<NotFound />} />
</Routes> </Routes>
<Toaster /> </BasicLayout>
</div>
</BudgetProvider> </BudgetProvider>
</AuthProvider> </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. // This file is automatically generated. Do not edit it directly.
import { createClient } from '@supabase/supabase-js'; import { createClient } from '@supabase/supabase-js';
import type { Database } from './types'; import type { Database } from './types';
const SUPABASE_URL = "https://qnerebtvwwfobfzdoftx.supabase.co"; const SUPABASE_URL = (() => {
const SUPABASE_PUBLISHABLE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFuZXJlYnR2d3dmb2JmemRvZnR4Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIwNTE0MzgsImV4cCI6MjA1NzYyNzQzOH0.Wm7h2DUhoQbeANuEM3wm2tz22ITrVEW8FizyLgIVmv8"; 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 the supabase client like this:
// import { supabase } from "@/integrations/supabase/client"; // 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 설정 // Supabase Cloud URL과 anon key 설정
export const getSupabaseUrl = () => { 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 = () => { 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 환경에서는 항상 유효함 // Supabase 키 유효성 검사 - Cloud 환경에서는 항상 유효함
export const isValidSupabaseKey = () => { export const isValidSupabaseKey = () => {
return true; return Boolean(import.meta.env.VITE_SUPABASE_ANON_KEY);
}; };
// 다음 함수들은 Cloud 환경에서는 필요 없지만 호환성을 위해 유지 // 다음 함수들은 Cloud 환경에서는 필요 없지만 호환성을 위해 유지

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,48 @@
import { supabase } from '@/lib/supabase'; import { account } from '@/lib/appwrite/client';
import { handleNetworkError, showAuthToast } from '@/utils/auth'; import { showAuthToast } from '@/utils/auth';
export const resetPassword = async (email: string) => { export const resetPassword = async (email: string) => {
try { try {
const { error } = await supabase.auth.resetPasswordForEmail(email, { console.log('비밀번호 재설정 시도 중:', email);
redirectTo: window.location.origin + '/reset-password',
});
if (error) { // 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
console.error('비밀번호 재설정 오류:', error); await new Promise<void>(resolve => queueMicrotask(() => resolve()));
showAuthToast('비밀번호 재설정 실패', error.message, 'destructive');
return { error }; try {
} // Appwrite로 비밀번호 재설정 이메일 발송
await account.createRecovery(
email,
window.location.origin + '/reset-password'
);
// 비동기 작업을 마이크로태스크로 지연하여 UI 차단 방지
await new Promise<void>(resolve => queueMicrotask(() => resolve()));
showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.'); showAuthToast('비밀번호 재설정 이메일 전송됨', '이메일을 확인하여 비밀번호를 재설정해주세요.');
return { error: null }; 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) { } catch (error: any) {
console.error('비밀번호 재설정 중 예외 발생:', error); console.error('비밀번호 재설정 중 예외 발생:', error);
// 네트워크 오류 확인 // 네트워크 오류 확인
const errorMessage = handleNetworkError(error); const errorMessage = error.message && error.message.includes('network')
? '서버 연결에 실패했습니다. 네트워크 연결을 확인해주세요.'
: '예상치 못한 오류가 발생했습니다.';
showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive'); showAuthToast('비밀번호 재설정 오류', errorMessage, 'destructive');
return { error }; return { error };

View File

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

View File

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

View File

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

View File

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

View File

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

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 = { export type AuthContextType = {
session: Session | null; session: Models.Session | null;
user: User | null; user: Models.User<Models.Preferences> | null;
loading: boolean; loading: boolean;
error: Error | null;
appwriteInitialized: boolean;
reinitializeAppwrite: () => AppwriteInitializationStatus;
signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>; signIn: (email: string, password: string) => Promise<{ error: any; user?: any }>;
signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>; signUp: (email: string, password: string, username: string) => Promise<{ error: any, user: any }>;
signOut: () => Promise<void>; signOut: () => Promise<void>;

View File

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

View File

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

View File

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

View File

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

153
src/lib/appwrite/client.ts Normal file
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