Windows 11에서 Android 앱 개발부터 Google Play Store 배포까지의 완전 자동화 가이드

Windows 11에서 Android 앱 개발부터 Google Play Store 배포까지의 완전 자동화 가이드

opencode를 내pc(windows11) 에 설치하고 안드로이드 앱 시장수요조사 -> 앱개발 -> 빌드 및테스트 ->검수 및 구글스토어 필요 파일 생성 -> 구글스토어 업로드 까지 자동화 하기위한 방법을 상세히 하나한 알려줘
이제 정보가 충분하므로 상세한 가이드를 작성하겠습니다.


Windows 11에서 Android 앱 개발부터 Google Play Store 배포까지의 완전 자동화 가이드

📋 목차

  1. 환경 설정
  2. OpenCode 설치 및 구성
  3. Android 개발 환경 구축
  4. 시장수요조사 자동화
  5. Gradle 기반 빌드 자동화
  6. CI/CD 파이프라인 구성
  7. Google Play Store 배포 자동화

1. 환경 설정

1.1 필수 요구사항 설치

PowerShell 관리자 권한으로 실행:

# Chocolatey 설치 (Windows 패키지 관리자)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
# 필수 도구 설치
choco install jdk17 -y
choco install android-sdk -y
choco install android-studio -y
choco install git -y
choco install nodejs -y
choco install python -y

수동 설치 (권장):

  1. JDK 17 설치
  2. Android SDK 설치
  3. Git 설치

2. OpenCode 설치 및 구성

2.1 OpenCode CLI 설치

# Node.js를 통한 설치
npm install -g opencode
# 버전 확인
opencode --version
# Windows용 OpenCode TUI (Terminal User Interface) 설치
npm install -g opencode-tui

2.2 OpenCode 초기 설정

# OpenCode 프로젝트 디렉토리 생성
mkdir my-android-project
cd my-android-project
# Git 저장소 초기화
git init
# OpenCode 초기화
opencode init
# AGENTS.md 파일이 생성됨 (프로젝트 구조 분석)

2.3 OpenCode 에이전트 구성

AGENTS.md 파일 커스터마이징:

# Android App Development Pipeline
## Project Structure
- /app: Android 애플리케이션 소스 코드
- /app/build.gradle: 앱 빌드 설정
- /app/src/main: 메인 소스 코드
- /scripts: 자동화 스크립트
## Build & Deploy Agents
### 1. Market Research Agent
- 역할: 앱스토어 데이터 분석, 경쟁사 분석
- 스크립트 위치: /scripts/market_research.py
### 2. Build Agent
- 역할: Gradle 빌드 자동화
- 명령어: ./gradlew build bundleRelease
### 3. Test & QA Agent
- 역할: 단위 테스트, 통합 테스트 실행
- 명령어: ./gradlew test connectedAndroidTest
### 4. Store Upload Agent
- 역할: Google Play Console에 자동 업로드
- 스크립트 위치: /scripts/upload_gplay.py

3. Android 개발 환경 구축

3.1 새 Android 프로젝트 생성

# Android Studio에서 또는 CLI로 프로젝트 생성
# 프로젝트 구조
my-android-project/
├── app/
│   ├── build.gradle
│   ├── src/
│   │   └── main/
│   │       ├── AndroidManifest.xml
│   │       ├── java/
│   │       └── res/
│   └── proguard-rules.pro
├── build.gradle (Top-level)
├── gradle.properties
├── settings.gradle
└── gradlew.bat (Windows용 Gradle 래퍼)

3.2 build.gradle 설정 (Module: app)

plugins {
    id 'com.android.application'
}
android {
    compileSdk 34
    
    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0.0"
    }
    
    signingConfigs {
        release {
            storeFile file("keystore.jks")
            storePassword System.getenv("KEYSTORE_PASSWORD")
            keyAlias System.getenv("KEY_ALIAS")
            keyPassword System.getenv("KEY_PASSWORD")
        }
    }
    
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
        debug {
            debuggable true
        }
    }
}
dependencies {
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
}

4. 시장수요조사 자동화

4.1 Python 기반 시장분석 스크립트

scripts/market_research.py:

#!/usr/bin/env python3
"""
안드로이드 앱 시장수요조사 자동화 스크립트
- Google Play Store API를 통한 경쟁사 분석
- 앱 카테고리별 트렌드 분석
- 사용자 리뷰 감정 분석
"""
import os
import json
from datetime import datetime
from google.play import googleplay
import pandas as pd
from textblob import TextBlob
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class MarketResearchAgent:
    def __init__(self, app_category, keywords):
        self.app_category = app_category
        self.keywords = keywords
        self.research_data = {}
        self.api = googleplay.GooglePlayAPI()
        
    def analyze_competitor_apps(self, max_results=10):
        """경쟁사 앱 분석"""
        logger.info(f"Analyzing top {max_results} competitors in '{self.app_category}'")
        
        competitors = []
        
        for keyword in self.keywords:
            results = self.api.search(keyword, category=self.app_category, limit=max_results)
            
            for app in results:
                app_data = {
                    'package_name': app['packageName'],
                    'title': app['title'],
                    'version': app['versionString'],
                    'rating': app['aggregateRating'],
                    'review_count': app.get('reviewCount', 0),
                    'installs': app.get('installs', 'N/A'),
                    'last_update': app.get('lastUpdateTime', 'N/A'),
                    'size': app.get('size', 'N/A'),
                    'content_rating': app.get('contentRating', 'N/A')
                }
                competitors.append(app_data)
        
        # 중복 제거 및 정렬
        unique_competitors = list({c['package_name']: c for c in competitors}.values())
        unique_competitors.sort(key=lambda x: x['rating'], reverse=True)
        
        self.research_data['competitors'] = unique_competitors
        return unique_competitors
    
    def analyze_sentiment_from_reviews(self, package_name, limit=100):
        """사용자 리뷰 감정분석"""
        logger.info(f"Analyzing reviews for {package_name}")
        
        reviews = self.api.get_reviews(package_name, limit=limit)
        
        sentiments = {
            'positive': 0,
            'neutral': 0,
            'negative': 0,
            'reviews': []
        }
        
        for review in reviews:
            blob = TextBlob(review['text'])
            polarity = blob.sentiment.polarity
            
            if polarity > 0.1:
                sentiments['positive'] += 1
            elif polarity < -0.1:
                sentiments['negative'] += 1
            else:
                sentiments['neutral'] += 1
            
            sentiments['reviews'].append({
                'rating': review['score'],
                'text': review['text'],
                'sentiment_score': polarity
            })
        
        return sentiments
    
    def market_trend_analysis(self):
        """시장 트렌드 분석"""
        logger.info("Analyzing market trends")
        
        competitors = self.research_data.get('competitors', [])
        
        trends = {
            'avg_rating': sum(c['rating'] for c in competitors) / len(competitors) if competitors else 0,
            'total_installs': sum(
                int(str(c['installs']).replace('+', '').replace('M', '000000').replace('K', '000'))
                for c in competitors if c['installs'] != 'N/A'
            ),
            'categories': list(set(c.get('category', 'Unknown') for c in competitors)),
            'recommendation': self._generate_recommendation(competitors)
        }
        
        return trends
    
    def _generate_recommendation(self, competitors):
        """앱 개발 권장사항 생성"""
        if not competitors:
            return "데이터 부족으로 권장사항을 제공할 수 없습니다."
        
        avg_rating = sum(c['rating'] for c in competitors) / len(competitors)
        
        recommendations = []
        
        if avg_rating > 4.0:
            recommendations.append("높은 평가 경쟁사들이 있습니다. 차별화된 기능 개발 필요")
        else:
            recommendations.append("낮은 평가 앱들이 많습니다. 품질 중심의 개발 가능")
        
        if any(c['review_count'] > 100000 for c in competitors):
            recommendations.append("인기 앱들이 많습니다. 강력한 마케팅 전략 필요")
        
        return "; ".join(recommendations)
    
    def generate_report(self, output_file="market_research_report.json"):
        """최종 보고서 생성"""
        logger.info("Generating market research report")
        
        report = {
            'timestamp': datetime.now().isoformat(),
            'category': self.app_category,
            'keywords': self.keywords,
            'competitors': self.research_data.get('competitors', []),
            'market_trends': self.market_trend_analysis(),
            'recommendations': self._generate_recommendation(self.research_data.get('competitors', []))
        }
        
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(report, f, ensure_ascii=False, indent=2)
        
        logger.info(f"Report saved to {output_file}")
        return report
# 사용 예시
if __name__ == "__main__":
    agent = MarketResearchAgent(
        app_category="PRODUCTIVITY",
        keywords=["todo", "task manager", "productivity"]
    )
    
    # 1. 경쟁사 분석
    competitors = agent.analyze_competitor_apps(max_results=15)
    logger.info(f"Found {len(competitors)} competitor apps")
    
    # 2. 감정분석
    if competitors:
        sentiment = agent.analyze_sentiment_from_reviews(competitors[0]['package_name'])
        logger.info(f"Sentiment Analysis: Positive={sentiment['positive']}, Neutral={sentiment['neutral']}, Negative={sentiment['negative']}")
    
    # 3. 보고서 생성
    report = agent.generate_report()
    print(json.dumps(report, ensure_ascii=False, indent=2))

실행 방법:

pip install google-play-scraper textblob pandas
python scripts/market_research.py

5. Gradle 기반 빌드 자동화

5.1 커스텀 Gradle 태스크 생성

build.gradle (Module: app)에 추가:

// 커스텀 빌드 타스크
task generateVersionCode {
    doLast {
        def versionCode = (int)(System.currentTimeMillis() / 1000)
        println("Generated Version Code: $versionCode")
        
        // 파일에 저장
        file('version.properties').text = "versionCode=$versionCode\n"
    }
}
// 자동 버전 증가
task incrementVersionCode {
    doLast {
        def versionProps = file('version.properties')
        def properties = new Properties()
        properties.load(new FileInputStream(versionProps))
        
        def currentVersion = (properties.getProperty('versionCode', '1') as int) + 1
        properties.setProperty('versionCode', currentVersion.toString())
        
        properties.store(versionProps.newWriter(), null)
        println("Version incremented to: $currentVersion")
    }
}
// 릴리스 빌드 태스크
task buildReleaseBundle {
    dependsOn 'incrementVersionCode'
    dependsOn 'bundleRelease'
    
    doLast {
        println("Release bundle created successfully!")
        println("Output: ${buildDir}/outputs/bundle/release/app-release.aab")
    }
}
// 빌드 전 사전검사
task preBuildCheck {
    doLast {
        println("Checking build prerequisites...")
        
        // SDK 버전 확인
        println("✓ SDK Version: 34")
        
        // Keystore 확인
        if (file('keystore.jks').exists()) {
            println("✓ Keystore found")
        } else {
            println("✗ Keystore not found - generating new keystore...")
            exec {
                commandLine 'keytool', '-genkey', '-v', 
                    '-keystore', 'keystore.jks',
                    '-keyalg', 'RSA',
                    '-keysize', '2048',
                    '-validity', '10000'
            }
        }
    }
}
// 통합 빌드 파이프라인
task automate {
    dependsOn 'preBuildCheck'
    dependsOn 'test'
    dependsOn 'buildReleaseBundle'
    
    doLast {
        println("===== AUTOMATION COMPLETE =====")
        println("✓ Pre-build checks passed")
        println("✓ Tests completed")
        println("✓ Release bundle created")
    }
}

5.2 명령어 라인 빌드

# 디버그 APK 빌드
gradlew assembleDebug
# 릴리스 AAB(App Bundle) 빌드
gradlew bundleRelease
# 테스트 실행
gradlew test
# 커스텀 자동화 태스크 실행
gradlew automate
# 전체 빌드 및 정렬
gradlew clean build

6. CI/CD 파이프라인 구성

6.1 GitHub Actions 기반 자동화

.github/workflows/android-build-deploy.yml:

name: Android Build & Deploy to Google Play
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
env:
  JAVA_VERSION: 17
  GRADLE_VERSION: 8.0
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    
    - name: Setup Android SDK
      uses: android-actions/setup-android@v2
    
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    
    - name: Market Research Analysis
      run: |
        pip install google-play-scraper textblob pandas
        python scripts/market_research.py > market_report.txt
        cat market_report.txt
    
    - name: Build Debug APK
      run: ./gradlew assembleDebug
    
    - name: Run unit tests
      run: ./gradlew test
    
    - name: Run connected tests (Android Emulator)
      uses: reactivecircus/android-emulator-runner@v2
      with:
        api-level: 30
        script: ./gradlew connectedAndroidTest
    
    - name: Build Release Bundle (AAB)
      run: ./gradlew bundleRelease
      env:
        KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
        KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
        KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
    
    - name: Sign Release Bundle
      run: |
        echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
        ./gradlew bundleRelease --build-file=app/build.gradle
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: release-bundle
        path: app/build/outputs/bundle/release/app-release.aab
    
    - name: Upload Debug APK
      uses: actions/upload-artifact@v3
      with:
        name: debug-apk
        path: app/build/outputs/apk/debug/app-debug.apk
  deploy-to-play-store:
    runs-on: ubuntu-latest
    needs: build-and-test
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Download release bundle
      uses: actions/download-artifact@v3
      with:
        name: release-bundle
    
    - name: Deploy to Google Play Store
      uses: r0adkll/upload-google-play@v1
      with:
        serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
        packageName: com.example.myapp
        releaseFiles: 'app-release.aab'
        track: internal
        status: inProgress
    
    - name: Slack Notification
      if: always()
      uses: 8398a7/action-slack@v3
      with:
        status: ${{ job.status }}
        text: 'App deployment to Play Store: ${{ job.status }}'
        webhook_url: ${{ secrets.SLACK_WEBHOOK }}

6.2 로컬 자동화 배치 스크립트 (Windows)

build-and-deploy.bat:

@echo off
setlocal enabledelayedexpansion
REM ===== Android App Build & Deploy Automation =====
REM Windows 11용 자동화 스크립트
echo [%date% %time%] Starting Android Build & Deploy Pipeline...
REM 1. 시장수요조사
echo.
echo ===== 1. Market Research Phase =====
python scripts/market_research.py > market_research_report.txt
if %errorlevel% neq 0 (
    echo Market research failed!
    exit /b 1
)
echo Market research completed successfully
REM 2. 사전검사
echo.
echo ===== 2. Pre-Build Checks =====
if not exist "keystore.jks" (
    echo Generating keystore...
    keytool -genkey -v -keystore keystore.jks ^
        -keyalg RSA -keysize 2048 -validity 10000 ^
        -alias release-key -dname "CN=MyApp"
)
echo Pre-build checks completed
REM 3. 빌드
echo.
echo ===== 3. Build Phase =====
set JAVA_HOME=C:\Program Files\Java\jdk-17.x.x
set ANDROID_HOME=C:\Users\%USERNAME%\AppData\Local\Android\Sdk
call gradlew.bat clean
if %errorlevel% neq 0 exit /b 1
REM 테스트 실행
echo Running unit tests...
call gradlew.bat test
if %errorlevel% neq 0 (
    echo Tests failed!
    exit /b 1
)
REM 디버그 빌드
echo Building debug APK...
call gradlew.bat assembleDebug
if %errorlevel% neq 0 exit /b 1
REM 릴리스 빌드
echo Building release AAB...
set KEYSTORE_PASSWORD=%1
set KEY_ALIAS=%2
set KEY_PASSWORD=%3
if [%KEYSTORE_PASSWORD%]==[] (
    echo Error: Keystore password required
    echo Usage: build-and-deploy.bat keystore_pwd key_alias key_pwd
    exit /b 1
)
call gradlew.bat bundleRelease ^
    -Pandroid.injected.signing.store.password=%KEYSTORE_PASSWORD% ^
    -Pandroid.injected.signing.key.alias=%KEY_ALIAS% ^
    -Pandroid.injected.signing.key.password=%KEY_PASSWORD%
if %errorlevel% neq 0 exit /b 1
REM 4. 검수
echo.
echo ===== 4. Quality Assurance Phase =====
echo Analyzing APK/Bundle...
REM APK 분석 도구 실행 (bundletool)
bundletool validate --bundle-path=app\build\outputs\bundle\release\app-release.aab
if %errorlevel% neq 0 (
    echo Bundle validation failed!
    exit /b 1
)
REM 5. Google Play Store 필수 파일 생성
echo.
echo ===== 5. Generating Required Files for Play Store =====
echo Creating play store listing files...
REM 메타데이터 파일 생성
(
    echo {
    echo   "title": "My Application",
    echo   "shortDescription": "Short app description",
    echo   "fullDescription": "Full app description with features and details",
    echo   "appCategory": "PRODUCTIVITY",
    echo   "contentRating": "4",
    echo   "appContentRatingRating": "EVERYONE"
    echo }
) > metadata.json
REM 스크린샷 및 그래픽 확인
if not exist "graphics" mkdir graphics
echo Check graphics folder for screenshots, icons, and feature graphics
REM 6. Google Play Store 업로드
echo.
echo ===== 6. Upload to Google Play Store =====
echo Preparing upload...
REM fastlane를 통한 자동 업로드
if exist "Fastfile" (
    echo Running fastlane...
    call fastlane android upload_to_play_store ^
        package: com.example.myapp ^
        track: internal
) else (
    echo Using manual upload method...
    echo Please upload manually:
    echo - File: app\build\outputs\bundle\release\app-release.aab
    echo - Metadata: metadata.json
    echo - Graphics: graphics folder
    pause
)
REM 7. 완료 및 보고
echo.
echo ===== 7. Completion Report =====
echo [%date% %time%] Build & Deploy completed successfully!
echo.
echo Generated artifacts:
echo - Debug APK: app\build\outputs\apk\debug\app-debug.apk
echo - Release AAB: app\build\outputs\bundle\release\app-release.aab
echo - Market Report: market_research_report.txt
echo.
pause

7. Google Play Store 배포 자동화

7.1 Fastlane 설치 및 설정

# Fastlane 설치
gem install fastlane
# 프로젝트에서 fastlane 초기화
fastlane init android

7.2 Fastfile 설정

android/fastlane/Fastfile:

default_platform(:android)
platform :android do
  desc "Build and upload to Google Play Store"
  lane :deploy do |options|
    # 1. 빌드
    gradle(
      task: "clean bundle",
      project_dir: "android/",
      properties: {
        "android.injected.signing.store.file" => "keystore.jks",
        "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
        "android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
        "android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
      }
    )
    
    # 2. 메타데이터 준비
    upload_to_play_store(
      package_name: "com.example.myapp",
      json_key_data: ENV["PLAY_STORE_CREDENTIALS"],
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
      track: options[:track] || "internal",
      skip_upload_screenshots: false,
      skip_upload_images: false,
      metadata_path: "android/fastlane/metadata/"
    )
    
    # 3. 완료 알림
    slack(
      message: "✅ App successfully deployed to #{options[:track] || 'internal'} track",
      success: true
    )
  end
  
  desc "Internal testing build"
  lane :internal do
    deploy(track: "internal")
  end
  
  desc "Beta testing build"
  lane :beta do
    deploy(track: "beta")
  end
  
  desc "Production release"
  lane :production do
    deploy(track: "production")
  end
end

7.3 Google Play Service Account 설정

Google Cloud Console에서:

# 1. 서비스 계정 생성
# https://console.cloud.google.com/ → 서비스 계정 → 새 서비스 계정
# 2. JSON 키 다운로드
# 다운로드된 파일을 GitHub Secrets에 추가
# GitHub Secrets 설정:
PLAY_STORE_SERVICE_ACCOUNT = [전체 JSON 내용]
KEYSTORE_PASSWORD = [keystore 비밀번호]
KEY_ALIAS = release-key
KEY_PASSWORD = [키 비밀번호]

7.4 필수 Play Store 메타데이터 준비

android/fastlane/metadata/android/ 디렉토리 구조:

metadata/
├── ko/
│   ├── short_description.txt (80자 이내)
│   ├── full_description.txt (4000자 이내)
│   ├── title.txt (50자 이내)
│   ├── video.txt (유튜브 링크)
│   └── images/
│       ├── icon.png (512x512)
│       ├── feature_graphic.png (1024x500)
│       ├── screenshots/
│       │   ├── phone_1.png (최소 540x720)
│       │   └── phone_2.png
│       └── tv_screenshots/
├── en/
│   └── (영문 메타데이터)
└── changelogs/
    ├── default.txt
    ├── ko.txt
    └── en.txt

short_description.txt:

효율적인 작업 관리를 위한 간편한 앱입니다.

full_description.txt:

주요 기능:
- 직관적인 UI
- 실시간 동기화
- 오프라인 지원
- 멀티 디바이스 동기화
릴리스 노트:
- 버전 1.0.0: 초기 출시

8. 통합 자동화 스크립트 (Python)

automation_pipeline.py - 전체 파이프라인 통합 스크립트:

#!/usr/bin/env python3
"""
완전 자동화 Android 배포 파이프라인
모든 단계를 자동으로 관리합니다
"""
import os
import sys
import subprocess
import json
import time
from datetime import datetime
from pathlib import Path
import logging
# 로깅 설정
logging.basicConfig(
    level=logging.INFO,
    format='[%(asctime)s] %(levelname)s: %(message)s',
    handlers=[
        logging.FileHandler('build_log.txt'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)
class AndroidAutomationPipeline:
    def __init__(self, config_file='pipeline_config.json'):
        self.config = self.load_config(config_file)
        self.start_time = time.time()
        self.results = {}
        
    def load_config(self, config_file):
        """설정 파일 로드"""
        if not os.path.exists(config_file):
            logger.warning(f"Config file not found: {config_file}")
            return self.get_default_config()
        
        with open(config_file, 'r') as f:
            return json.load(f)
    
    def get_default_config(self):
        """기본 설정"""
        return {
            'package_name': 'com.example.myapp',
            'app_name': 'My Application',
            'keystore_path': 'keystore.jks',
            'gradle_wrapper': './gradlew' if sys.platform != 'win32' else '.\\gradlew.bat',
            'play_store_track': 'internal',
            'enable_market_research': True,
            'enable_tests': True,
            'enable_upload': False,
            'slack_webhook': os.getenv('SLACK_WEBHOOK_URL', '')
        }
    
    def run_step(self, step_name, command, env=None):
        """단계별 명령어 실행"""
        logger.info(f"Running: {step_name}")
        
        try:
            result = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                text=True,
                env={**os.environ, **(env or {})}
            )
            
            if result.returncode == 0:
                logger.info(f"✓ {step_name} completed successfully")
                self.results[step_name] = {'status': 'success', 'output': result.stdout}
                return True
            else:
                logger.error(f"✗ {step_name} failed: {result.stderr}")
                self.results[step_name] = {'status': 'failed', 'error': result.stderr}
                return False
        except Exception as e:
            logger.error(f"✗ {step_name} exception: {str(e)}")
            self.results[step_name] = {'status': 'error', 'error': str(e)}
            return False
    
    def phase_1_market_research(self):
        """Phase 1: 시장수요조사"""
        logger.info("\n========== PHASE 1: MARKET RESEARCH ==========")
        
        if not self.config['enable_market_research']:
            logger.info("Market research disabled")
            return True
        
        return self.run_step(
            "Market Research",
            f"python scripts/market_research.py"
        )
    
    def phase_2_prebuild_checks(self):
        """Phase 2: 사전검사"""
        logger.info("\n========== PHASE 2: PRE-BUILD CHECKS ==========")
        
        # 필수 파일 확인
        required_files = ['build.gradle', 'AndroidManifest.xml']
        
        for file in required_files:
            if not os.path.exists(file):
                logger.error(f"Missing required file: {file}")
                return False
        
        logger.info("✓ All required files present")
        
        # Keystore 확인/생성
        if not os.path.exists(self.config['keystore_path']):
            logger.warning("Keystore not found, generating new keystore...")
            return self.run_step(
                "Generate Keystore",
                f"keytool -genkey -v -keystore {self.config['keystore_path']} "
                f"-keyalg RSA -keysize 2048 -validity 10000 -alias release-key"
            )
        
        logger.info(f"✓ Keystore found: {self.config['keystore_path']}")
        return True
    
    def phase_3_build(self):
        """Phase 3: 빌드"""
        logger.info("\n========== PHASE 3: BUILD PHASE ==========")
        
        gradle = self.config['gradle_wrapper']
        
        # 정리
        if not self.run_step("Clean", f"{gradle} clean"):
            return False
        
        # 테스트
        if self.config['enable_tests']:
            if not self.run_step("Unit Tests", f"{gradle} test"):
                logger.warning("Tests failed, continuing anyway...")
        
        # 디버그 빌드
        if not self.run_step("Debug Build", f"{gradle} assembleDebug"):
            return False
        
        # 릴리스 빌드
        env = {
            'KEYSTORE_PASSWORD': os.getenv('KEYSTORE_PASSWORD', ''),
            'KEY_ALIAS': os.getenv('KEY_ALIAS', 'release-key'),
            'KEY_PASSWORD': os.getenv('KEY_PASSWORD', '')
        }
        
        return self.run_step("Release Build", f"{gradle} bundleRelease", env=env)
    
    def phase_4_qa(self):
        """Phase 4: 검수"""
        logger.info("\n========== PHASE 4: QUALITY ASSURANCE ==========")
        
        # Bundle 검증
        bundle_path = 'app/build/outputs/bundle/release/app-release.aab'
        
        if not os.path.exists(bundle_path):
            logger.error(f"Bundle not found: {bundle_path}")
            return False
        
        logger.info(f"✓ Bundle found: {bundle_path}")
        
        # 파일 크기 확인
        bundle_size = os.path.getsize(bundle_path) / (1024 * 1024)
        logger.info(f"Bundle size: {bundle_size:.2f} MB")
        
        if bundle_size > 150:  # Google Play 최대 크기
            logger.error("Bundle size exceeds 150MB limit")
            return False
        
        # 메타데이터 검증
        metadata_required = {
            'short_description.txt': 80,
            'full_description.txt': 4000,
            'title.txt': 50
        }
        
        for meta_file, max_size in metadata_required.items():
            path = f'metadata/{meta_file}'
            if os.path.exists(path):
                with open(path, 'r', encoding='utf-8') as f:
                    content = f.read()
                    if len(content) <= max_size:
                        logger.info(f"✓ {meta_file}: {len(content)}/{max_size} chars")
                    else:
                        logger.warning(f"⚠ {meta_file} exceeds recommended size")
        
        logger.info("✓ QA checks completed")
        return True
    
    def phase_5_generate_store_files(self):
        """Phase 5: Google Play Store 파일 생성"""
        logger.info("\n========== PHASE 5: GENERATE STORE FILES ==========")
        
        os.makedirs('generated_store_files', exist_ok=True)
        
        # Store Listing JSON
        store_listing = {
            'title': self.config['app_name'],
            'shortDescription': 'Short description',
            'fullDescription': 'Full description with features',
            'category': 'PRODUCTIVITY',
            'contentRating': '4',
            'targetAudience': 'EVERYONE',
            'releaseNotes': f'Release {datetime.now().strftime("%Y.%m.%d")}'
        }
        
        store_file = 'generated_store_files/store_listing.json'
        with open(store_file, 'w', encoding='utf-8') as f:
            json.dump(store_listing, f, ensure_ascii=False, indent=2)
        
        logger.info(f"✓ Generated: {store_file}")
        
        # 메타데이터 목록
        files_info = {
            'bundle': 'app/build/outputs/bundle/release/app-release.aab',
            'store_listing': store_file,
            'generated_at': datetime.now().isoformat()
        }
        
        info_file = 'generated_store_files/files_info.json'
        with open(info_file, 'w', encoding='utf-8') as f:
            json.dump(files_info, f, indent=2)
        
        logger.info(f"✓ Generated: {info_file}")
        return True
    
    def phase_6_upload(self):
        """Phase 6: Play Store 업로드"""
        logger.info("\n========== PHASE 6: UPLOAD TO PLAY STORE ==========")
        
        if not self.config['enable_upload']:
            logger.info("Upload disabled in configuration")
            return True
        
        # Fastlane을 통한 업로드
        track = self.config['play_store_track']
        
        return self.run_step(
            f"Upload to {track} track",
            f"fastlane android upload_to_play_store "
            f"track:{track} "
            f"package:{self.config['package_name']}"
        )
    
    def send_notification(self, success):
        """Slack 알림 발송"""
        if not self.config['slack_webhook']:
            return
        
        status = "✅ SUCCESS" if success else "❌ FAILED"
        elapsed = time.time() - self.start_time
        
        message = {
            'text': f"{status} - Android Deployment Pipeline",
            'blocks': [
                {
                    'type': 'section',
                    'text': {
                        'type': 'mrkdwn',
                        'text': f"*{status}* - Android App Deployment\n"
                                f"*App:* {self.config['app_name']}\n"
                                f"*Time:* {elapsed:.0f}s\n"
                                f"*Timestamp:* {datetime.now().isoformat()}"
                    }
                }
            ]
        }
        
        try:
            import requests
            requests.post(self.config['slack_webhook'], json=message)
        except:
            logger.warning("Failed to send Slack notification")
    
    def generate_report(self):
        """최종 보고서 생성"""
        report = {
            'timestamp': datetime.now().isoformat(),
            'duration_seconds': time.time() - self.start_time,
            'app_name': self.config['app_name'],
            'package_name': self.config['package_name'],
            'results': self.results,
            'overall_status': 'SUCCESS' if all(
                r['status'] == 'success' for r in self.results.values()
            ) else 'FAILED'
        }
        
        report_file = f"deployment_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        with open(report_file, 'w', encoding='utf-8') as f:
            json.dump(report, f, ensure_ascii=False, indent=2)
        
        logger.info(f"\n✓ Report saved: {report_file}")
        return report
    
    def run_full_pipeline(self):
        """전체 파이프라인 실행"""
        logger.info(f"\n{'='*50}")
        logger.info("ANDROID AUTOMATION PIPELINE - STARTED")
        logger.info(f"{'='*50}\n")
        
        phases = [
            self.phase_1_market_research,
            self.phase_2_prebuild_checks,
            self.phase_3_build,
            self.phase_4_qa,
            self.phase_5_generate_store_files,
            self.phase_6_upload,
        ]
        
        for phase in phases:
            try:
                if not phase():
                    logger.error(f"\n❌ Pipeline stopped at: {phase.__name__}")
                    self.send_notification(False)
                    return False
            except Exception as e:
                logger.error(f"\n❌ Exception in {phase.__name__}: {str(e)}")
                self.send_notification(False)
                return False
        
        logger.info(f"\n{'='*50}")
        logger.info("✅ PIPELINE COMPLETED SUCCESSFULLY")
        logger.info(f"{'='*50}\n")
        
        report = self.generate_report()
        self.send_notification(True)
        
        return True
if __name__ == "__main__":
    pipeline = AndroidAutomationPipeline()
    success = pipeline.run_full_pipeline()
    sys.exit(0 if success else 1)

9. 설정 파일 샘플

pipeline_config.json:

{
  "package_name": "com.example.myapp",
  "app_name": "My Application",
  "keystore_path": "keystore.jks",
  "gradle_wrapper": ".\\gradlew.bat",
  "play_store_track": "internal",
  "enable_market_research": true,
  "enable_tests": true,
  "enable_upload": false,
  "slack_webhook": "${SLACK_WEBHOOK_URL}",
  "market_research": {
    "categories": ["PRODUCTIVITY"],
    "keywords": ["todo", "task", "productivity"],
    "max_competitors": 20
  }
}

10. 실행 방법

10.1 첫 번째 실행 (수동)

# 1. 저장소 클론 또는 프로젝트 생성
mkdir my-android-app
cd my-android-app
# 2. 필수 파일 준비
pip install google-play-scraper textblob pandas requests
# 3. Gradle 시스템 준비
chmod +x gradlew  # Linux/Mac
# 또는 Windows에서는 gradlew.bat 사용
# 4. 전체 파이프라인 실행
python automation_pipeline.py

10.2 CI/CD를 통한 자동 실행

# GitHub에 푸시하면 자동으로 실행됨
git add .
git commit -m "Add app features"
git push origin main
# 또는 수동 트리거
gh workflow run android-build-deploy.yml

10.3 로컬에서 스케줄링

Windows 작업 스케줄러:

# PowerShell 관리자 권한으로 실행
$action = New-ScheduledTaskAction -Execute "python" -Argument "automation_pipeline.py"
$trigger = New-ScheduledTaskTrigger -Daily -At 02:00AM
Register-ScheduledTask -TaskName "AndroidBuildDeploy" -Action $action -Trigger $trigger

11. 환경변수 설정 (필수)

Windows 환경변수:

KEYSTORE_PASSWORD=your_keystore_password
KEY_ALIAS=release-key
KEY_PASSWORD=your_key_password
JAVA_HOME=C:\Program Files\Java\jdk-17.x.x
ANDROID_HOME=C:\Users\%USERNAME%\AppData\Local\Android\Sdk
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL

12. 트러블슈팅

문제 해결방법
gradlew 명령어 없음 gradle wrapper 또는 Android Studio에서 생성
Keystore 비밀번호 오류 환경변수 정확히 설정, 또는 keytool 재생성
번들 크기 초과 불필요한 의존성 제거, ProGuard 활성화
Play Store 업로드 실패 서비스 계정 권한 확인, JSON 키 재발급
테스트 실패 에뮬레이터 실행, connectedAndroidTest 실행

이 가이드로 Windows 11에서 완전 자동화된 Android 앱 개발 및 배포 파이프라인을 구성할 수 있습니다. 각 단계별로 질문이 있으시면 알려주세요!

개인정보보호링크