feat: Phase 1 mobile tablet — Jenkins APK, PC tab, in-app update

Capacitor 확장으로 git push 시 Mobile Jenkins 빌드, PC 탭 Android/iPad 다운로드,
앱 내 APK 업데이트, iOS TestFlight API 및 Capacitor ios 프로젝트를 추가한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Macbook
2026-06-14 16:31:55 +09:00
parent 36c06b55ea
commit f34104b721
38 changed files with 1468 additions and 15 deletions
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# GoldenChart Android APK — 로컬(CI) 빌드 → data/mobile-releases/
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
APK_REMOTE_NAME="${APK_REMOTE_NAME:-goldenchart-android.apk}"
LOCAL_APK="${LOCAL_APK:-$ROOT/data/mobile-releases/$APK_REMOTE_NAME}"
log() { echo "[build-mobile-apk $(date '+%H:%M:%S')] $*"; }
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 "unsigned APK 없음: $unsigned" >&2
exit 1
fi
if [[ ! -f "$keystore" ]]; then
log "keystore 생성..."
"${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 "apksigner 없음. Android SDK build-tools 설치 필요." >&2
exit 1
fi
cp "$unsigned" "$signed"
"$apksigner" sign --ks "$keystore" --ks-key-alias goldenchart \
--ks-pass pass:goldenchart --key-pass pass:goldenchart "$signed"
mkdir -p "$(dirname "$LOCAL_APK")"
cp "$signed" "$LOCAL_APK"
log "서명 완료: $LOCAL_APK"
}
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 "JAVA_HOME 미설정" >&2
exit 1
fi
log "google-services.json 확인"
"$ROOT/scripts/ensure-android-google-services.sh"
log "cap:sync"
npm run cap:sync
log "assembleRelease"
(cd "$ROOT/app/android" && ./gradlew assembleRelease --no-daemon)
sign_release_apk
log "완료 $(du -h "$LOCAL_APK" | cut -f1)"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Capacitor iOS 플랫폼 추가·동기화 (Mac + CocoaPods 권장)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT/app"
npm run build
if command -v pod >/dev/null 2>&1; then
npx cap add ios 2>/dev/null || true
npx cap sync ios
else
echo "CocoaPods 없음 — iOS 템플릿만 추출합니다 (pod install은 나중에)."
node --input-type=module <<'NODE'
import { loadConfig } from '@capacitor/cli/dist/config.js';
import { addIOS } from '@capacitor/cli/dist/ios/add.js';
import { editProjectSettingsIOS } from '@capacitor/cli/dist/ios/common.js';
const config = await loadConfig({ cwd: process.cwd() });
const fs = await import('fs');
if (!fs.existsSync('ios')) {
await addIOS(config);
await editProjectSettingsIOS(config);
}
NODE
echo "완료. brew install cocoapods 후: cd ios/App && pod install"
fi
echo "Xcode: npm run cap:open:ios"
echo "TestFlight: docs/ios-testflight-setup.md"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Jenkins goldenChart-Mobile-Pipeline — Android APK 빌드·배포
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
for prefix in "$HOME/homebrew" /opt/homebrew; do
[[ -x "$prefix/bin/brew" ]] && eval "$("$prefix/bin/brew" shellenv)"
done
log() { echo "[mobile-pipeline $(date '+%H:%M:%S')] $*"; }
log "=== goldenChart Mobile Pipeline #${BUILD_NUMBER:-local} ==="
log "WORK_TREE=$WORK_TREE"
cd "$WORK_TREE"
if [[ -d "${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" ]]; then
git --git-dir="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}" \
--work-tree="$WORK_TREE" checkout -f main 2>/dev/null || true
fi
chmod +x "$ROOT/scripts/"*.sh 2>/dev/null || true
"$ROOT/scripts/build-mobile-apk.sh"
VERSION_NAME="$(grep -E 'versionName\s+"' "$ROOT/app/android/app/build.gradle" | head -1 | sed -E 's/.*versionName\s+"([^"]+)".*/\1/' || true)"
if [[ -n "$VERSION_NAME" ]]; then
log "APK version → $VERSION_NAME"
ENV_FILE="$WORK_TREE/.env"
if [[ -f "$ENV_FILE" ]]; then
{ grep -v '^GC_MOBILE_APP_VERSION=' "$ENV_FILE" || true; echo "GC_MOBILE_APP_VERSION=${VERSION_NAME}"; } > "${ENV_FILE}.tmp"
mv "${ENV_FILE}.tmp" "$ENV_FILE"
else
echo "GC_MOBILE_APP_VERSION=${VERSION_NAME}" >> "$ENV_FILE"
fi
fi
if [[ -x "$WORK_TREE/scripts/restart-backend-if-running.sh" ]]; then
WORK_TREE="$WORK_TREE" "$WORK_TREE/scripts/restart-backend-if-running.sh" || true
elif docker compose -f "$WORK_TREE/docker-compose.yml" ps -q backend 2>/dev/null | grep -q .; then
log "backend 재시작 (APK 볼륨 반영)"
(cd "$WORK_TREE" && docker compose up -d --no-deps backend) || true
fi
log "Artifacts: $WORK_TREE/data/mobile-releases/"
log "Pipeline complete."
+13
View File
@@ -62,6 +62,19 @@ while read -r OLDREV NEWREV REFNAME; do
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] trigger-desktop-jenkins-build.sh 없음: $TRIGGER"
fi
# Mobile(Android APK) Jenkins 빌드 — git push 성공 후 자동
TRIGGER_MOBILE="${WORK_TREE}/scripts/trigger-mobile-jenkins-build.sh"
if [ -x "$TRIGGER_MOBILE" ]; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Mobile Jenkins 빌드 트리거"
if "$TRIGGER_MOBILE" >> "$DEPLOY_LOG" 2>&1; then
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG Mobile 빌드 큐 등록 완료"
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] Mobile Jenkins 트리거 실패 (웹 배포는 성공)"
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG [WARN] trigger-mobile-jenkins-build.sh 없음: $TRIGGER_MOBILE"
fi
else
echo "$(date '+%Y-%m-%d %H:%M:%S') $HOOK_TAG '$BRANCH_NAME' — 배포 스킵 (main만)"
fi
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# docker compose backend가 실행 중이면 재시작 (APK·env 반영)
set -euo pipefail
WORK_TREE="${WORK_TREE:-$(cd "$(dirname "$0")/.." && pwd)}"
cd "$WORK_TREE"
if docker compose ps -q backend 2>/dev/null | grep -q .; then
echo "[restart-backend] docker compose up -d --no-deps backend"
docker compose up -d --no-deps backend
fi
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# exdev Jenkins — goldenChart-Mobile-Pipeline Job 생성
set -euo pipefail
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:${PATH:-/usr/bin:/bin}"
JENKINS_HOME="${JENKINS_HOME:-$HOME/.jenkins}"
JENKINS_JOB="goldenChart-Mobile-Pipeline"
JENKINS_TRIGGER_TOKEN="${JENKINS_MOBILE_TRIGGER_TOKEN:-${GC_MOBILE_JENKINS_TRIGGER_TOKEN:-goldenchart-mobile-build}}"
GIT_DIR="${GIT_DIR:-/Volumes/ADATA/git/goldenChart.git}"
WORK_TREE="${WORK_TREE:-/Users/aidev/apps/goldenChart}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
log() { echo "[install-mobile-jenkins] $*"; }
log "Jenkins job 생성: $JENKINS_JOB"
mkdir -p "${JENKINS_HOME}/jobs/${JENKINS_JOB}"
cat > "${JENKINS_HOME}/jobs/${JENKINS_JOB}/config.xml" << JENKINS_XML
<?xml version='1.1' encoding='UTF-8'?>
<project>
<description>GoldenChart Mobile — Capacitor Android APK</description>
<keepDependencies>false</keepDependencies>
<properties/>
<scm class="hudson.plugins.git.GitSCM" plugin="git@5.0.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>file://${GIT_DIR}</url>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/main</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="empty-list"/>
<extensions/>
</scm>
<canRoam>true</canRoam>
<disabled>false</disabled>
<blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
<blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
<authToken>${JENKINS_TRIGGER_TOKEN}</authToken>
<triggers/>
<concurrentBuild>false</concurrentBuild>
<builders>
<hudson.tasks.Shell>
<command>#!/bin/bash
set -euo pipefail
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:\$PATH"
export WORK_TREE="${WORK_TREE}"
export GIT_DIR="${GIT_DIR}"
export BUILD_NUMBER="\${BUILD_NUMBER}"
export ANDROID_HOME="\${ANDROID_HOME:-\$HOME/Library/Android/sdk}"
export JAVA_HOME="\${JAVA_HOME:-\$HOME/.local/jdk-21.0.11+10/Contents/Home}"
"${WORK_TREE}/scripts/jenkins-mobile-pipeline.sh"
</command>
</hudson.tasks.Shell>
</builders>
<publishers>
<hudson.tasks.ArtifactArchiver>
<artifacts>data/mobile-releases/**</artifacts>
<allowEmptyArchive>true</allowEmptyArchive>
<onlyIfSuccessful>false</onlyIfSuccessful>
<fingerprint>false</fingerprint>
<defaultExcludes>true</defaultExcludes>
</hudson.tasks.ArtifactArchiver>
</publishers>
<buildWrappers/>
</project>
JENKINS_XML
if curl -sf -o /dev/null "http://127.0.0.1:8090/login" 2>/dev/null; then
curl -sf -X POST "http://127.0.0.1:8090/reload" 2>/dev/null || true
fi
log "완료: http://127.0.0.1:8090/job/${JENKINS_JOB}/"
log "원격 트리거 토큰: $JENKINS_TRIGGER_TOKEN"
log "post-receive: trigger-mobile-jenkins-build.sh (git push main)"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Git push(main) 후 Jenkins goldenChart-Mobile-Pipeline 트리거
set -euo pipefail
JENKINS_URL="${JENKINS_URL:-${GC_MOBILE_JENKINS_URL:-http://127.0.0.1:8090}}"
JENKINS_JOB="${JENKINS_JOB:-${GC_MOBILE_JENKINS_JOB:-goldenChart-Mobile-Pipeline}}"
JENKINS_USER="${JENKINS_USER:-${GC_MOBILE_JENKINS_USER:-}}"
JENKINS_TOKEN="${JENKINS_TOKEN:-${GC_MOBILE_JENKINS_TOKEN:-}}"
JENKINS_TRIGGER_TOKEN="${JENKINS_TRIGGER_TOKEN:-${GC_MOBILE_JENKINS_TRIGGER_TOKEN:-goldenchart-mobile-build}}"
LOG_TAG="[trigger-mobile]"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_TAG $*"; }
JENKINS_URL="${JENKINS_URL%/}"
JOB_API="${JENKINS_URL}/job/${JENKINS_JOB// /%20}"
curl_get() {
if [[ -n "$JENKINS_USER" && -n "$JENKINS_TOKEN" ]]; then
curl -sf -u "${JENKINS_USER}:${JENKINS_TOKEN}" "$@"
else
curl -sf "$@"
fi
}
curl_post() {
if [[ -n "$JENKINS_USER" && -n "$JENKINS_TOKEN" ]]; then
curl -sf -X POST -u "${JENKINS_USER}:${JENKINS_TOKEN}" "$@"
else
curl -sf -X POST "$@"
fi
}
log "Jenkins mobile build — $JENKINS_JOB @ $JENKINS_URL"
if ! curl -sf -o /dev/null "${JENKINS_URL}/login" 2>/dev/null; then
log "[ERROR] Jenkins unreachable: $JENKINS_URL"
exit 1
fi
if curl_get "${JOB_API}/lastBuild/api/json?tree=building" 2>/dev/null | grep -q '"building":true'; then
log "[SKIP] 이미 빌드 진행 중"
exit 0
fi
CRUMB_HDR=()
CRUMB_JSON="$(curl_get "${JENKINS_URL}/crumbIssuer/api/json" 2>/dev/null || true)"
if [[ -n "$CRUMB_JSON" ]]; then
FIELD="$(node -e "try{console.log(JSON.parse(process.argv[1]).crumbRequestField||'')}catch{}" "$CRUMB_JSON" 2>/dev/null || true)"
VAL="$(node -e "try{console.log(JSON.parse(process.argv[1]).crumb||'')}catch{}" "$CRUMB_JSON" 2>/dev/null || true)"
if [[ -n "$FIELD" && -n "$VAL" ]]; then
CRUMB_HDR=(-H "${FIELD}: ${VAL}")
fi
fi
if ((${#CRUMB_HDR[@]})); then
if curl_post "${CRUMB_HDR[@]}" -H "Content-Type: application/x-www-form-urlencoded" \
"${JOB_API}/build?token=${JENKINS_TRIGGER_TOKEN}"; then
log "빌드 큐 등록 완료 (token)"
exit 0
fi
if curl_post "${CRUMB_HDR[@]}" -H "Content-Type: application/x-www-form-urlencoded" "${JOB_API}/build"; then
log "빌드 큐 등록 완료"
exit 0
fi
else
if curl_post -H "Content-Type: application/x-www-form-urlencoded" \
"${JOB_API}/build?token=${JENKINS_TRIGGER_TOKEN}"; then
log "빌드 큐 등록 완료 (token)"
exit 0
fi
if curl_post -H "Content-Type: application/x-www-form-urlencoded" "${JOB_API}/build"; then
log "빌드 큐 등록 완료"
exit 0
fi
fi
log "[ERROR] Jenkins build trigger failed"
exit 1