[iOS] Xcode 프로젝트 Multi Scheme TestFlight 자동 업로드 스크립트 만들기



# [iOS] Xcode 프로젝트 Multi Scheme TestFlight 자동 업로드 스크립트 만들기

안녕하세요 __물먹고하자__ 입니다 :)
요즘에 AI(CLI)들로 재밌게 놀면서 귀찮았던 작업들을 정리하고 있습니다.
**더블클릭 한 번으로 TestFlight까지 자동 배포되는 스크립트**를 만든 내용을 공유합니다.

---
## 배경
> 일단 저희팀이 맡고있는 __프로젝트가 3개의 Scheme로 앱(A10, OmniEsol, CSAP) 여러개__로 나뉘어있는데,
메인급은 주기적은 TestFlight로 fastline을 통한 자동업로드가 설정되어있고,
최신 업데이트를 위한 2개의 앱을 한번에 업로드 하는 방향을 준비하고자 합니다.

__Xcode는 한 번에 1개 Scheme만 아카이브할 수 있다보니, 매번 반복 작업이 생겨 자동화를 고민하게 되었습니다.__


## 1. 스크립트 구성
```
프로젝트 루트/
ㄴ 프로젝트이름.xcworkspace
ㄴ deploy.sh                    // 실제 배포 로직
ㄴ Deploy_OmniEsol_CSAP.command // 더블클릭으로 실행하는 파일
```

### Deploy_OmniEsol_CSAP.command
팀원이 더블클릭하면 터미널이 열리며 자동으로 배포가 시작되는 파일입니다.  
`deploy.sh` 와 같은 폴더에 위치해야 합니다.

### 실행 권한 부여 (최초 1회)

```bash
chmod +x deploy.sh Deploy_OmniEsol_CSAP.command
```

Git에 올릴 때는 실행 권한도 같이 커밋하면 팀원이 따로 할 필요 없습니다.

```bash
git update-index --chmod=+x Deploy_OmniEsol_CSAP.command
git update-index --chmod=+x deploy.sh
```

### App Store Connect API Key 발급

`API_KEY_ID`, `API_ISSUER_ID`, `.p8` 파일은  
**App Store Connect → 사용자 및 액세스 → 통합 → App Store Connect API** 에서 발급받을 수 있습니다.

발급 후 `.p8` 파일은 아래 경로에 복사해야 `altool` 이 인식합니다.

```bash
mkdir -p ~/.appstoreconnect/private_keys
cp ./PushKey/AuthKey_XXXXXXXXXX.p8 ~/.appstoreconnect/private_keys/
```

### Deploy_OmniEsol_CSAP.command 파일
```
#!/bin/bash

# ================================================================
#  Deploy앱배포.command
#  이 파일을 더블클릭하면 터미널이 열리며 배포가 시작됩니다.
#  📌 deploy.sh 와 같은 폴더에 위치해야 합니다.
# ================================================================

# 이 파일이 있는 디렉토리 = 프로젝트 루트로 이동
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"

# deploy.sh 존재 확인
if [ ! -f "./deploy.sh" ]; then
  echo "❌ deploy.sh 파일을 찾을 수 없습니다."
  echo "   Deploy앱배포.command 와 deploy.sh 를 같은 폴더에 두세요."
  echo ""
  read -p "종료하려면 Enter..."
  exit 1
fi

# 실행 권한 자동 부여
chmod +x ./deploy.sh

# 배포 실행
./deploy.sh
EXIT_CODE=$?

# 완료 후 터미널 유지 (결과 확인용)
echo ""
if [ $EXIT_CODE -eq 0 ]; then
  echo "✅ 배포가 완료되었습니다. 창을 닫아도 됩니다."
else
  echo "⚠️  배포 중 오류가 발생했습니다. 위 로그를 확인해주세요."
fi

echo ""
read -p "종료하려면 Enter..."

```

### deploy.sh (실제 아카이브 > TestFlight 업로드)

<!-- 최종결과물 -->
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiluKu0UN43heJ8X_2fct9o_DvZJMDnG1zz5vKE6nyK3z_edRzskyniAU85J0LFbtxOYszSYgg53D75svqztq-m8RIoCvE8gD9cXRrjnG1LtAt63dbVrRHAGe_-S422jGh4xZBkly3cRV0Ghj_tkGvOEWjayXlYENR6HW4Jg1WYoY2B8xKV88TiHDg81EPn/s1600/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202026-03-19%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%201.16.27.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="1666" data-original-width="1340" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiluKu0UN43heJ8X_2fct9o_DvZJMDnG1zz5vKE6nyK3z_edRzskyniAU85J0LFbtxOYszSYgg53D75svqztq-m8RIoCvE8gD9cXRrjnG1LtAt63dbVrRHAGe_-S422jGh4xZBkly3cRV0Ghj_tkGvOEWjayXlYENR6HW4Jg1WYoY2B8xKV88TiHDg81EPn/s1600/%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA%202026-03-19%20%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE%201.16.27.png"/></a></div>

__결과물 먼저__
이렇게 실행해놓고 웹툰보면 됨!
올라가는 시간 로딩 효과도 넣어두고 생각보다 간단하네요.
Claude cowork 사용하면 __"앱 배포해줘"__ 라고 했을때 실행되게도 설정가능하다고 하네요. (아직못해봄)


---

## 2. 삽질 기록 (겪었던 이슈들)

### 이슈 1. 멀티 스킴에서 엉뚱한 앱이 빌드되는 문제

`xcodebuild -scheme ERP` 로 아카이브해도 `Amaranth10.app` (기본 스킴 앱) 이 나오는 현상이 있었습니다.

원인은 `ERP.xcscheme` 파일 내부를 보니 `BuildableName` 이 `Amaranth10.app` 으로 되어있었던 것.

```bash
cat KLAGO.xcodeproj/xcshareddata/xcschemes/ERP.xcscheme | grep "BuildableReference" -A5

# 결과
BuildableName = "Amaranth10.app"
BlueprintName = "KLAGO"
```

알고보니 ERP 스킴은 **타겟은 KLAGO(Amaranth10)를 바라보되, Configuration만 ERP** 로 설정된 구조였습니다.  
즉 `Amaranth10.app` 이 나오는 게 정상이었고, 내부 bundle ID 가 `com.XXXX.XXXXX.XXXX.erp` 이면 되는 구조였습니다.

따라서 ExportOptions.plist 에 bundle ID 를 명시해서 정확히 추출되도록 수정했습니다.

```xml
<key>provisioningProfiles</key>
<dict>
  <key>com.XXXX.XXXXX.XXXX.erp</key>
  <string>com.XXXX.XXXXX.XXXX.erp</string>
</dict>
```

### 이슈 2. Pod이 ERP/CSAP Configuration을 못 찾는 문제

`-configuration ERP` 로 빌드하면 아래 에러가 발생했습니다.

```
PhaseScriptExecution [CP] Embed Pods Frameworks ... FAILED
```

원인은 Podfile 에 `ERP`, `CSAP` Configuration 매핑이 없어서 Pods가 해당 Configuration을 찾지 못하는 것이었습니다.

처음엔 `PODS_CONFIGURATION_BUILD_DIR` 을 커스텀 경로로 지정해 해결하려 했지만,  
오히려 더 꼬이는 문제가 발생했고, 결국 **해당 옵션을 제거하는 것이 해결책** 이었습니다.

xcodebuild 가 이미 ERP Configuration 을 `ERP-iphoneos` 경로로 잘 찾고 있었던 것이었고,  
커스텀 경로 지정이 오히려 방해가 되고 있었습니다.

```bash
# 제거한 옵션
"PODS_CONFIGURATION_BUILD_DIR=$(pwd)/build/pods" \
"PODS_BUILD_DIR=$(pwd)/build/pods" \
```

만약 Pods가 진짜로 Configuration을 못 찾는다면 Podfile에 아래처럼 매핑을 추가하면 됩니다.

```ruby
project 'KLAGO', {
  'Debug'   => :debug,
  'Release' => :release,
  'ERP'     => :release,
  'CSAP'    => :release
}
```

### 이슈 3. 빌드 번호가 프로젝트 설정과 다르게 빌드되는 문제

Xcode에서 빌드 번호를 `1621` 로 올렸는데, 실제 아카이브된 앱은 `1614` 로 빌드되는 현상이 있었습니다.

원인을 추적해보니 두 가지였습니다.

**원인 1.** `project.pbxproj` 에 Configuration별로 `CURRENT_PROJECT_VERSION` 이 따로 저장되어 있었고,  
ERP/CSAP Configuration은 여전히 `1614` 로 남아있었습니다.

```bash
grep "CURRENT_PROJECT_VERSION" KLAGO.xcodeproj/project.pbxproj | sort -u
# CURRENT_PROJECT_VERSION = 1614;  // ERP/CSAP Configuration
# CURRENT_PROJECT_VERSION = 1621;  // 메인 설정
```

Xcode UI 에서 빌드 번호를 변경할 때 `All` 탭이 아닌 특정 Configuration에서만 변경하면 이런 현상이 발생합니다.

전체 일괄 수정 방법:

```bash
sed -i '' 's/CURRENT_PROJECT_VERSION = 1614/CURRENT_PROJECT_VERSION = 1621/g' KLAGO.xcodeproj/project.pbxproj
```

**원인 2.** fastlane의 `increment_build_number` 액션이 `$(CURRENT_PROJECT_VERSION)` 변수 참조를  
실제 숫자값으로 하드코딩해버리는 동작 때문이었습니다.

`Info_ERP.plist`, `Info_CSAP.plist` 의 `CFBundleVersion` 이 변수 참조 대신 숫자로 고정된 경우 아래처럼 복구합니다.

```xml
<!-- 수정 전 -->
<key>CFBundleVersion</key>
<string>1614</string>

<!-- 수정 후 -->
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
```

---
### deploy.sh 파일부분

```
#!/bin/bash

# ================================================================
#  A10 Project - OmniEsol & CSAP TestFlight 배포 스크립트
#  사용법: 직접 실행하거나 Deploy앱배포.command 를 더블클릭
# ================================================================

# 스크립트가 있는 폴더로 자동 이동 (어디서 실행해도 경로 문제 없음)
cd "$(dirname "$0")"

# ----------------------------------------------------------------
# &#9881;&#65039;  설정값 (프로젝트에 맞게 수정하세요)
# ----------------------------------------------------------------
WORKSPACE="프로젝트이름.xcworkspace"
SCHEMES=("ERP" "CSAP")
BUNDLE_IDS=("1번항목_bundle_id_넣기" "2번항목_bundle_id_넣기")
CONFIGURATIONS=("ERP" "CSAP")   # 스킴명과 동일한 Configuration
BUILD_DIR="$HOME/Library/Developer/Xcode/Archives/$(date +%Y-%m-%d)"
API_KEY_ID="XXXXXXXXX"        # App Store Connect API Key ID
API_ISSUER_ID="XXXXXXXXX" # App Store Connect Issuer ID
API_KEY_PATH="./PushKey/XXXXXXXXX.p8"      # .p8 키 파일 경로
# ----------------------------------------------------------------

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'

TOTAL=${#SCHEMES[@]}
TOTAL_STEPS=3

declare -A STEP_TIMES
declare -A APP_VERSIONS

print_header() {
  clear
  echo ""
  echo -e "${BOLD}${BLUE}&#9556;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9559;${RESET}"
  echo -e "${BOLD}${BLUE}&#9553;       A10 TestFlight 배포 도구         &#9553;${RESET}"
  echo -e "${BOLD}${BLUE}&#9553;     OmniEsol  &#183;  CSAP                  &#9553;${RESET}"
  echo -e "${BOLD}${BLUE}&#9562;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9552;&#9565;${RESET}"
  echo ""
}

format_time() {
  local secs=$1
  local m=$((secs / 60))
  local s=$((secs % 60))
  if [ $m -gt 0 ]; then
    echo "${m}분 ${s}초"
  else
    echo "${s}초"
  fi
}

print_step() {
  local app_idx=$1
  local step_idx=$2
  local scheme=$3
  local msg=$4
  echo -e "${CYAN}[${scheme}]${RESET} ${DIM}(앱 ${app_idx}/${TOTAL} &#183; 단계 ${step_idx}/${TOTAL_STEPS})${RESET}  ${msg}"
}

print_success() {
  echo -e "${GREEN}  &#9989;  $1${RESET}"
}

print_error() {
  echo -e "${RED}  &#10060;  오류: $1${RESET}"
}

print_divider() {
  echo -e "${DIM}&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;${RESET}"
}

spinner() {
  local pid=$1
  local msg=$2
  local start=$3
  local frames=("&#10251;" "&#10265;" "&#10297;" "&#10296;" "&#10300;" "&#10292;" "&#10278;" "&#10279;" "&#10247;" "&#10255;")
  local i=0
  while kill -0 "$pid" 2>/dev/null; do
    local now=$(date +%s)
    local elapsed=$((now - start))
    local m=$((elapsed / 60))
    local s=$((elapsed % 60))
    local timer=$(printf "%02d:%02d" $m $s)
    printf "\r  ${YELLOW}${frames[$i]}${RESET}  ${DIM}%s...${RESET}  ${DIM}[%s]${RESET}" "$msg" "$timer"
    i=$(( (i+1) % ${#frames[@]} ))
    sleep 0.1
  done
  printf "\r\033[K"
}

get_app_version() {
  local archive_path="$1"
  local info_plist="${archive_path}/Info.plist"
  if [ -f "$info_plist" ]; then
    local version=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$info_plist" 2>/dev/null)
    local build=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$info_plist" 2>/dev/null)
    echo "${version} (${build})"
  else
    echo "버전 정보 없음"
  fi
}

# ----------------------------------------------------------------
# 사전 체크
# ----------------------------------------------------------------
preflight_check() {
  echo -e "${BOLD}🔍 사전 체크 중...${RESET}"
  echo ""

  if ! command -v xcodebuild &>/dev/null; then
    print_error "Xcode가 설치되어 있지 않습니다."
    exit 1
  fi
  print_success "Xcode: $(xcodebuild -version | head -1)"

  if [ ! -d "$WORKSPACE" ]; then
    print_error "$WORKSPACE 파일을 찾을 수 없습니다. 프로젝트 루트에서 실행해주세요."
    exit 1
  fi
  print_success "Workspace: $WORKSPACE"

  if [ ! -f "$API_KEY_PATH" ]; then
    print_error "API Key 파일을 찾을 수 없습니다: $API_KEY_PATH"
    exit 1
  fi
  print_success "API Key: $API_KEY_PATH"

  echo ""
  print_divider
}
# ----------------------------------------------------------------
# 배포 함수
# ----------------------------------------------------------------
deploy_scheme() {
  local scheme=$1
  local app_idx=$2
  local bundle_id=$3
  local configuration=$4
 
  local archive_name="$scheme $(LC_TIME=en_US date +'%-m-%-d-%y, %I.%M %p')"
  local archive_path="$BUILD_DIR/${archive_name}.xcarchive"
  local export_path="/tmp/deploy_export/$scheme"
  local export_plist="/tmp/${scheme}_ExportOptions.plist"
  local log_file="/tmp/${scheme}_build.log"
  local scheme_start=$(date +%s)
 
  echo ""
  echo -e "${BOLD}🚀 [$scheme] 배포 시작${RESET}"
 
  # 아카이브 전 프로젝트 설정에서 버전 읽기
  local pre_version=$(xcodebuild -workspace "$WORKSPACE" -scheme "$scheme" -configuration "$configuration" -showBuildSettings 2>/dev/null | grep -E "^\s+MARKETING_VERSION" | head -1 | awk '{print $3}')
  local pre_build=$(xcodebuild -workspace "$WORKSPACE" -scheme "$scheme" -configuration "$configuration" -showBuildSettings 2>/dev/null | grep -E "^\s+CURRENT_PROJECT_VERSION" | head -1 | awk '{print $3}')
  echo -e "${DIM}   앱 버전: ${pre_version} (${pre_build})${RESET}"
  print_divider
 
  # &#9472;&#9472; Step 1: 아카이브 &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  print_step $app_idx 1 $scheme "아카이브 중..."
  local step1_start=$(date +%s)
 
  xcodebuild archive \
    -workspace "$WORKSPACE" \
    -scheme "$scheme" \
    -configuration "$configuration" \
    -destination "generic/platform=iOS" \
    -archivePath "$archive_path" \
    -allowProvisioningUpdates \
    > "$log_file" 2>&1 &
 
  spinner $! "아카이브 빌드 중" $step1_start
  wait $!
  local step1_result=$?
  local step1_elapsed=$(($(date +%s) - step1_start))
  STEP_TIMES["${scheme}_1"]=$step1_elapsed
 
  if [ $step1_result -ne 0 ]; then
    print_error "아카이브 실패. 로그: $log_file"
    echo ""
    echo -e "${DIM}--- 마지막 20줄 ---${RESET}"
    tail -20 "$log_file"
    return 1
  fi
  print_success "아카이브 완료 &#8594; $(format_time $step1_elapsed)"
 
  # 아카이브 완료 후 실제 생성된 xcarchive 에서 버전 읽기
  local actual_archive=$(find "$BUILD_DIR" -maxdepth 1 -name "${scheme}*.xcarchive" -type d | sort | tail -1)
  APP_VERSIONS["$scheme"]="${pre_version} (${pre_build})"
  echo -e "  ${DIM}📦 $scheme ${pre_version} (${pre_build})${RESET}"
 
  # &#9472;&#9472; Step 2: IPA Export &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  print_step $app_idx 2 $scheme "IPA 추출 중..."
  local step2_start=$(date +%s)
 
  mkdir -p "$export_path"
 
  cat > "$export_plist" << EOF
<!--xml version="1.0" encoding="UTF-8"?-->
<!--DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd"-->
<plist version="1.0">
<dict>
  <key>method</key>
  <string>app-store</string>
  <key>signingStyle</key>
  <string>automatic</string>
  <key>provisioningProfiles</key>
  <dict>
    <key>${bundle_id}</key>
    <string>${bundle_id}</string>
  </dict>
  <key>uploadBitcode</key>
  <false/>
  <key>uploadSymbols</key>
  <true/>
</dict>
</plist>
EOF
 
  xcodebuild -exportArchive \
    -archivePath "$archive_path" \
    -exportPath "$export_path" \
    -exportOptionsPlist "$export_plist" \
    -allowProvisioningUpdates \
    > "$log_file" 2>&1 &
 
  spinner $! "IPA 추출 중" $step2_start
  wait $!
  local step2_result=$?
  local step2_elapsed=$(($(date +%s) - step2_start))
  STEP_TIMES["${scheme}_2"]=$step2_elapsed
 
  if [ $step2_result -ne 0 ]; then
    print_error "IPA Export 실패. 로그: $log_file"
    tail -20 "$log_file"
    return 1
  fi
  print_success "IPA 추출 완료 &#8594; $(format_time $step2_elapsed)"
 
  # &#9472;&#9472; Step 3: TestFlight 업로드 &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  print_step $app_idx 3 $scheme "TestFlight 업로드 중..."
  local step3_start=$(date +%s)
 
  local IPA_FILE="$(find "$export_path" -name "*.ipa" | head -1)"
  local UPLOAD_SUCCESS=false
 
  for attempt in 1 2 3; do
    xcrun altool --upload-app \
      --type ios \
      --file "$IPA_FILE" \
      --apiKey "$API_KEY_ID" \
      --apiIssuer "$API_ISSUER_ID" \
      > "$log_file" 2>&1 &
 
    spinner $! "TestFlight 업로드 중" $step3_start
    wait $!
    if [ $? -eq 0 ]; then
      UPLOAD_SUCCESS=true
      break
    fi
    if [ $attempt -lt 3 ]; then
      echo -e "  ${YELLOW}&#9888;&#65039;  업로드 재시도 중... ($attempt/3)${RESET}"
      sleep 5
    fi
  done
 
  local step3_elapsed=$(($(date +%s) - step3_start))
  STEP_TIMES["${scheme}_3"]=$step3_elapsed
  STEP_TIMES["${scheme}_total"]=$(($(date +%s) - scheme_start))
 
  if [ "$UPLOAD_SUCCESS" = false ]; then
    print_error "TestFlight 업로드 실패 (3회 시도). 로그: $log_file"
    tail -20 "$log_file"
    return 1
  fi
  print_success "TestFlight 업로드 완료 &#8594; $(format_time $step3_elapsed) 🎉"
 
  return 0
}
 
# ----------------------------------------------------------------
# 메인 실행
# ----------------------------------------------------------------
main() {
  print_header
  preflight_check
 
  mkdir -p "$BUILD_DIR"
 
  local TOTAL_START=$(date +%s)
  FAILED=()
 
  echo -e "${BOLD}📦 배포 대상: ${SCHEMES[*]}${RESET}"
  echo ""
 
  for i in "${!SCHEMES[@]}"; do
    deploy_scheme "${SCHEMES[$i]}" "$((i+1))" "${BUNDLE_IDS[$i]}" "${CONFIGURATIONS[$i]}"
    if [ $? -ne 0 ]; then
      FAILED+=("${SCHEMES[$i]}")
    fi
    print_divider
  done
 
  # &#9472;&#9472; 최종 결과 &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;
  local TOTAL_ELAPSED=$(($(date +%s) - TOTAL_START))
 
  echo ""
  echo -e "${BOLD}📊 배포 결과 요약${RESET}"
  echo ""
 
  for scheme in "${SCHEMES[@]}"; do
    if [[ " ${FAILED[*]} " == *" $scheme "* ]]; then
      echo -e "  ${RED}&#10007;  $scheme  &#8212;  실패${RESET}"
      echo ""
    else
      local ver="${APP_VERSIONS["$scheme"]:-버전 정보 없음}"
      local t1=$(format_time "${STEP_TIMES["${scheme}_1"]:-0}")
      local t2=$(format_time "${STEP_TIMES["${scheme}_2"]:-0}")
      local t3=$(format_time "${STEP_TIMES["${scheme}_3"]:-0}")
      local tt=$(format_time "${STEP_TIMES["${scheme}_total"]:-0}")
      echo -e "  ${GREEN}&#10003;  $scheme  $ver${RESET}"
      echo -e "     ${DIM}아카이브 $t1  &#183;  IPA추출 $t2  &#183;  업로드 $t3  &#8594;  총 $tt${RESET}"
      echo ""
    fi
  done
 
  echo -e "${DIM}&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;${RESET}"
  echo -e "  ${BOLD}총 소요시간: $(format_time $TOTAL_ELAPSED)${RESET}"
  echo ""
 
  if [ ${#FAILED[@]} -eq 0 ]; then
    echo -e "${BOLD}${GREEN}🎉 모든 앱 배포 완료!${RESET}"
  else
    echo -e "${BOLD}${RED}&#9888;&#65039;  일부 앱 배포 실패: ${FAILED[*]}${RESET}"
    exit 1
  fi
 
  echo ""
}
 
main

```

---
## 마무리
팀내에서 자동화 부분에 대해 관심이 많아지고 있고, 귀찮은 일은 하나씩 줄이려고 노력하고 있습니다.
스크립트쪽은 저도 정확히 몰라서 AI 도움을 많이 받았고,

팀내에서는 FastLine으로만 사용할지, 스크립트부분을 사용할지 정하진 못해서
백업 / 블로그 글 우선 써둡니다.

오늘은 이만

즐거운 코딩되세요~

끝.



댓글