Files
goldenChart/scripts/upload-android-apk.sh
2026-06-17 10:00:34 +09:00

271 lines
10 KiB
Bash
Executable File

#!/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 <<EOF
사용법: $0 [옵션]
(기본) 로컬 data/mobile-releases/$APK_REMOTE_NAME → 서버 업로드
--build cap:sync + assembleRelease + 서명 후 업로드
--apk PATH 업로드할 APK 경로
--version VER 서버 GC_MOBILE_APP_VERSION (.env) 설정
--project-dir PATH 서버 프로젝트 경로 (자동 탐색 생략)
--host HOST 서버 호스트 (기본: $REMOTE_HOST)
--user USER SSH 사용자 (기본: $REMOTE_USER)
--no-verify 업로드 후 API 확인 생략
-h, --help 도움말
서버 경로 자동 탐색 순서:
1) --project-dir / REMOTE_PROJECT_DIR
2) SSH → deploy.sh 의 WORK_TREE
3) 후보: /Users/aidev/apps/goldenChart
설정 파일: scripts/deploy-target.defaults
scripts/deploy-target.local.env (선택, gitignore)
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--build) DO_BUILD=true; shift ;;
--apk) LOCAL_APK="$2"; shift 2 ;;
--version) APK_VERSION="$2"; shift 2 ;;
--project-dir) REMOTE_PROJECT_DIR="$2"; shift 2 ;;
--host) REMOTE_HOST="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
--user) REMOTE_USER="$2"; SSH_TARGET="${REMOTE_USER}@${REMOTE_HOST}"; shift 2 ;;
--no-verify) SKIP_VERIFY=true; shift ;;
-h|--help) print_usage; exit 0 ;;
*) echo -e "${RED}알 수 없는 옵션: $1${NC}"; print_usage; exit 1 ;;
esac
done
remote_exec() {
"${SSH_CMD[@]}" "$SSH_TARGET" "bash -lc $(printf '%q' "$*")"
}
resolve_remote_project_dir() {
if [[ -n "${REMOTE_PROJECT_DIR:-}" ]]; then
echo "$REMOTE_PROJECT_DIR"
return 0
fi
echo -e "${CYAN}서버 프로젝트 경로 탐색 중...${NC}" >&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"