#!/usr/bin/env bash # GoldenChart Android APK — 로컬 빌드 → exdev.co.kr 서버 업로드 # # 사용: # ./scripts/upload-android-apk.sh # 로컬 APK 업로드 # ./scripts/upload-android-apk.sh --build # 빌드+서명 후 업로드 # ./scripts/upload-android-apk.sh --version 1.2 # UI 표시 버전 (서버 .env 반영) # set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' # ── 배포 대상 설정 ─────────────────────────────────────────────────────── # shellcheck disable=SC1091 source "$ROOT/scripts/deploy-target.defaults" if [[ -f "$ROOT/scripts/deploy-target.local.env" ]]; then # shellcheck disable=SC1091 source "$ROOT/scripts/deploy-target.local.env" fi REMOTE_HOST="${REMOTE_HOST:-exdev.co.kr}" REMOTE_USER="${REMOTE_USER:-aidev}" REMOTE_PORT="${REMOTE_PORT:-22}" APK_REMOTE_SUBDIR="${APK_REMOTE_SUBDIR:-data/mobile-releases}" APK_REMOTE_NAME="${APK_REMOTE_NAME:-goldenchart-android.apk}" PUBLIC_URL="${PUBLIC_URL:-https://stock.exdev.co.kr}" LOCAL_APK="${LOCAL_APK:-$ROOT/data/mobile-releases/$APK_REMOTE_NAME}" DO_BUILD=false APK_VERSION="" SKIP_VERIFY=false SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}" SSH_CMD=(ssh -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -p "$REMOTE_PORT") SCP_CMD=(scp -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -P "$REMOTE_PORT") print_usage() { cat <&2 # deploy.sh WORK_TREE local scripts="${REMOTE_DEPLOY_SCRIPTS:-/Volumes/ADATA/git/deploy.sh /Users/aidev/git/deploy.sh}" local wt wt=$("${SSH_CMD[@]}" "$SSH_TARGET" "for f in $scripts; do [ -f \"\$f\" ] || continue grep -E '^WORK_TREE=' \"\$f\" | head -1 | sed 's/^WORK_TREE=//' | tr -d '\"' break done" 2>/dev/null || true) if [[ -n "$wt" ]] && remote_exec "test -f \"$wt/docker-compose.yml\"" >/dev/null 2>&1; then echo "$wt" return 0 fi # git remote origin → bare repo 인접 work-tree 추정 local origin_url origin_url=$(git -C "$ROOT" remote get-url origin 2>/dev/null || true) if [[ "$origin_url" == *goldenChart* ]]; then for candidate in \ /Users/aidev/apps/goldenChart \ /Volumes/ADATA/apps/goldenChart; do if remote_exec "test -f \"$candidate/docker-compose.yml\"" >/dev/null 2>&1; then echo "$candidate" return 0 fi done fi echo -e "${RED}서버 프로젝트 경로를 찾지 못했습니다.${NC}" >&2 echo " scripts/deploy-target.local.env 에 REMOTE_PROJECT_DIR=... 를 설정하거나" >&2 echo " $0 --project-dir /path/to/goldenChart 를 사용하세요." >&2 return 1 } sign_release_apk() { local unsigned="$ROOT/app/android/app/build/outputs/apk/release/app-release-unsigned.apk" local signed="$ROOT/app/android/app/build/outputs/apk/release/app-release.apk" local keystore="$ROOT/app/android/goldenchart-release.keystore" if [[ ! -f "$unsigned" ]]; then echo -e "${RED}unsigned APK 없음: $unsigned${NC}" echo " 먼저: cd app/android && ./gradlew assembleRelease" exit 1 fi if [[ ! -f "$keystore" ]]; then echo -e "${YELLOW}keystore 생성 중...${NC}" "$JAVA_HOME/bin/keytool" -genkeypair -v -keystore "$keystore" -alias goldenchart \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass goldenchart -keypass goldenchart \ -dname "CN=GoldenChart, OU=Mobile, O=GoldenChart, L=Seoul, ST=Seoul, C=KR" fi local apksigner apksigner="${ANDROID_HOME:-$HOME/Library/Android/sdk}/build-tools/35.0.0/apksigner" if [[ ! -x "$apksigner" ]]; then apksigner=$(command -v apksigner 2>/dev/null || true) fi if [[ -z "$apksigner" || ! -x "$apksigner" ]]; then echo -e "${RED}apksigner 없음. Android SDK build-tools 설치 필요.${NC}" exit 1 fi cp "$unsigned" "$signed" "$apksigner" sign --ks "$keystore" --ks-key-alias goldenchart \ --ks-pass pass:goldenchart --key-pass pass:goldenchart "$signed" cp "$signed" "$LOCAL_APK" echo -e "${GREEN}✓ 서명 완료: $LOCAL_APK${NC}" } build_apk_locally() { if [[ -z "${JAVA_HOME:-}" ]] && [[ -x "$HOME/.local/jdk-21.0.11+10/Contents/Home/bin/java" ]]; then export JAVA_HOME="$HOME/.local/jdk-21.0.11+10/Contents/Home" fi if [[ -z "${JAVA_HOME:-}" ]]; then echo -e "${RED}JAVA_HOME 미설정. JDK 21 설치 후 다시 시도하세요.${NC}" exit 1 fi echo -e "${CYAN}[빌드] google-services.json → android/app${NC}" "$ROOT/scripts/ensure-android-google-services.sh" if [[ ! -f "$ROOT/app/.env" ]] || ! grep -q 'VITE_API_BASE_URL=http' "$ROOT/app/.env" 2>/dev/null; then echo -e "${YELLOW}⚠ app/.env 에 VITE_API_BASE_URL=https://stock.exdev.co.kr/api 확인 (HTTPS 아님)${NC}" fi echo -e "${CYAN}[빌드] npm run cap:sync${NC}" npm run cap:sync echo -e "${CYAN}[빌드] ./gradlew assembleRelease${NC}" (cd "$ROOT/app/android" && ./gradlew assembleRelease --no-daemon) mkdir -p "$(dirname "$LOCAL_APK")" sign_release_apk } echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ GoldenChart APK → exdev.co.kr 업로드 ║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" echo -e " 서버: ${YELLOW}${SSH_TARGET}${NC}" echo "" if [[ "$DO_BUILD" == true ]]; then build_apk_locally fi if [[ ! -f "$LOCAL_APK" ]]; then echo -e "${RED}APK 없음: $LOCAL_APK${NC}" echo " ./scripts/copy-android-apk.sh 또는 $0 --build" exit 1 fi # 업로드 대상이 최신 Gradle 빌드보다 오래됐으면 구 UI APK일 수 있음 unsigned_apk="$ROOT/app/android/app/build/outputs/apk/release/app-release-unsigned.apk" signed_apk="$ROOT/app/android/app/build/outputs/apk/release/app-release.apk" if [[ -f "$unsigned_apk" ]] && [[ "$LOCAL_APK" -ot "$unsigned_apk" ]]; then echo -e "${RED}업로드 APK가 Gradle 빌드보다 오래되었습니다 (구 UI가 서버에 올라갈 수 있음).${NC}" echo -e " 로컬: $LOCAL_APK" echo -e " 최신: $unsigned_apk" echo -e " 해결: $0 --build 또는 ./scripts/copy-android-apk.sh && $0" exit 1 fi if [[ -f "$signed_apk" ]] && [[ "$LOCAL_APK" -ot "$signed_apk" ]]; then echo -e "${YELLOW}참고: 서명 APK가 더 최신입니다. copy-android-apk.sh 후 업로드하세요.${NC}" fi echo -e "${CYAN}[연결] SSH 테스트...${NC}" if ! "${SSH_CMD[@]}" "$SSH_TARGET" "exit" >/dev/null 2>&1; then echo -e "${RED}SSH 연결 실패: ${SSH_TARGET}${NC}" echo " ssh-copy-id -p ${REMOTE_PORT} ${SSH_TARGET}" exit 1 fi echo -e "${GREEN}✓ SSH 연결 성공${NC}" PROJECT_DIR=$(resolve_remote_project_dir) REMOTE_APK_DIR="${PROJECT_DIR}/${APK_REMOTE_SUBDIR}" REMOTE_APK_PATH="${REMOTE_APK_DIR}/${APK_REMOTE_NAME}" echo -e " 프로젝트: ${YELLOW}${PROJECT_DIR}${NC}" echo -e " APK 경로: ${YELLOW}${REMOTE_APK_PATH}${NC}" echo "" APK_SIZE=$(du -h "$LOCAL_APK" | cut -f1) echo -e "${CYAN}[업로드] ${LOCAL_APK} (${APK_SIZE})${NC}" remote_exec "mkdir -p \"$REMOTE_APK_DIR\"" "${SCP_CMD[@]}" "$LOCAL_APK" "${SSH_TARGET}:${REMOTE_APK_PATH}" echo -e "${GREEN}✓ 업로드 완료${NC}" if [[ -n "$APK_VERSION" ]]; then echo -e "${CYAN}[버전] GC_MOBILE_APP_VERSION=${APK_VERSION}${NC}" remote_exec "cd \"$PROJECT_DIR\" && \ { [ -f .env ] && grep -v '^GC_MOBILE_APP_VERSION=' .env || true; echo 'GC_MOBILE_APP_VERSION=${APK_VERSION}'; } > .env.tmp && \ mv .env.tmp .env" fi echo -e "${CYAN}[재시작] backend 컨테이너 (APK 볼륨 반영)${NC}" if remote_exec "cd \"$PROJECT_DIR\" && docker compose ps -q backend" | grep -q .; then remote_exec "cd \"$PROJECT_DIR\" && docker compose up -d --no-deps backend" echo -e "${GREEN}✓ backend 재시작 완료${NC}" else echo -e "${YELLOW}backend 컨테이너 없음 — docker compose up -d 가 필요할 수 있습니다.${NC}" fi if [[ "$SKIP_VERIFY" != true ]]; then echo -e "${CYAN}[확인] ${PUBLIC_URL}/api/mobile-app/info${NC}" sleep 2 INFO=$(curl -fsSL "${PUBLIC_URL}/api/mobile-app/info" 2>/dev/null || true) if echo "$INFO" | grep -q '"available":true'; then echo -e "${GREEN}✓ 서버에서 APK 제공 중${NC}" echo "$INFO" | python3 -m json.tool 2>/dev/null || echo "$INFO" else echo -e "${YELLOW}⚠ API에서 APK를 아직 찾지 못했습니다.${NC}" echo " docker-compose.yml 에 mobile-releases 볼륨이 있는지 확인 후 git push → Jenkins 재배포" echo " 응답: ${INFO:-(연결 실패)}" fi fi echo "" echo -e "${GREEN}완료${NC}" echo -e " 다운로드: ${PUBLIC_URL} → 상단 앱 다운로드 버튼" echo -e " QR/설치: ${PUBLIC_URL}/install.html" echo -e " 직접 URL: ${PUBLIC_URL}/api/mobile-app/download/android"