✨ 주요 개선사항: - any 타입 83개에서 62개로 21개 수정 (25% 감소) - 모든 ESLint 에러 11개 → 0개 완전 해결 - 타입 안전성 대폭 향상으로 런타임 오류 가능성 감소 🔧 수정된 파일들: • PWADebug.tsx - 사용하지 않는 import들에 _ prefix 추가 • categoryUtils.ts - 불필요한 any 캐스트 제거 • TransactionsHeader.tsx - BudgetData 인터페이스 정의 • storageUtils.ts - generic 타입과 unknown 타입 적용 • 각종 error handler들 - Error | {message?: string} 타입 적용 • test 파일들 - 적절한 mock 인터페이스 정의 • 유틸리티 파일들 - any → unknown 또는 적절한 타입으로 교체 🏆 성과: - 코드 품질 크게 향상 (280 → 80 문제로 71% 감소) - TypeScript 컴파일러의 타입 체크 효과성 증대 - 개발자 경험 개선 (IDE 자동완성, 타입 추론 등) 🧹 추가 정리: - ESLint no-console/no-alert 경고 해결 - Prettier 포맷팅 적용으로 코드 스타일 통일 🎯 다음 단계: 남은 62개 any 타입 계속 개선 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
427 lines
12 KiB
JavaScript
427 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const { execSync } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
/**
|
|
* 앱 스토어 배포 자동화 스크립트
|
|
* Google Play Store 및 Apple App Store 배포 프로세스 관리
|
|
*/
|
|
|
|
// 설정
|
|
const config = {
|
|
// 프로젝트 정보
|
|
packageName: "com.zellyy.finance",
|
|
appName: "Zellyy Finance",
|
|
|
|
// 배포 트랙
|
|
googlePlayTrack: process.env.GOOGLE_PLAY_TRACK || "internal",
|
|
|
|
// 파일 경로
|
|
androidBundlePath: "android/app/build/outputs/bundle/release/app-release.aab",
|
|
iosIpaPath: "ios/App/build/App.ipa",
|
|
|
|
// API 설정
|
|
googlePlayServiceAccount: process.env.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON,
|
|
appStoreIssuerId: process.env.APPSTORE_ISSUER_ID,
|
|
appStoreKeyId: process.env.APPSTORE_KEY_ID,
|
|
appStorePrivateKey: process.env.APPSTORE_PRIVATE_KEY,
|
|
|
|
// CI 환경
|
|
isCI: process.env.CI === "true",
|
|
githubRepository: process.env.GITHUB_REPOSITORY,
|
|
githubRunId: process.env.GITHUB_RUN_ID,
|
|
};
|
|
|
|
// 명령행 인수 파싱
|
|
const args = process.argv.slice(2);
|
|
const platform = args[0]; // 'android', 'ios', 'both'
|
|
const dryRun = args.includes("--dry-run");
|
|
|
|
/**
|
|
* 메인 배포 함수
|
|
*/
|
|
async function deployToStores() {
|
|
if (!platform || !["android", "ios", "both"].includes(platform)) {
|
|
console.error("사용법: node store-deploy.js <platform> [--dry-run]");
|
|
console.error("플랫폼: android, ios, both");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`📱 ${config.appName} 스토어 배포 시작 (${platform})`);
|
|
|
|
if (dryRun) {
|
|
console.log("🔍 DRY RUN 모드 - 실제 배포 없이 검증만 수행");
|
|
}
|
|
|
|
// 배포 전 검증
|
|
await validateEnvironment();
|
|
await validateBuildFiles();
|
|
|
|
// 릴리즈 노트 생성
|
|
const releaseNotes = await generateReleaseNotes();
|
|
|
|
// 플랫폼별 배포
|
|
const results = {};
|
|
|
|
if (platform === "android" || platform === "both") {
|
|
results.android = await deployToGooglePlay(releaseNotes);
|
|
}
|
|
|
|
if (platform === "ios" || platform === "both") {
|
|
results.ios = await deployToAppStore(releaseNotes);
|
|
}
|
|
|
|
// 결과 리포트
|
|
await generateDeploymentReport(results);
|
|
|
|
console.log("✅ 배포 프로세스 완료");
|
|
}
|
|
|
|
/**
|
|
* 환경 검증
|
|
*/
|
|
async function validateEnvironment() {
|
|
console.log("🔍 환경 설정 검증 중...");
|
|
|
|
const requiredVars = [];
|
|
|
|
if (platform === "android" || platform === "both") {
|
|
if (!config.googlePlayServiceAccount) {
|
|
requiredVars.push("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON");
|
|
}
|
|
}
|
|
|
|
if (platform === "ios" || platform === "both") {
|
|
if (!config.appStoreIssuerId) requiredVars.push("APPSTORE_ISSUER_ID");
|
|
if (!config.appStoreKeyId) requiredVars.push("APPSTORE_KEY_ID");
|
|
if (!config.appStorePrivateKey) requiredVars.push("APPSTORE_PRIVATE_KEY");
|
|
}
|
|
|
|
if (requiredVars.length > 0) {
|
|
if (dryRun) {
|
|
console.warn(
|
|
"⚠️ DRY RUN 모드: 필수 환경 변수 누락 무시:",
|
|
requiredVars.join(", ")
|
|
);
|
|
} else {
|
|
console.error(
|
|
"❌ 필수 환경 변수가 설정되지 않음:",
|
|
requiredVars.join(", ")
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log("✅ 환경 설정 검증 완료");
|
|
}
|
|
|
|
/**
|
|
* 빌드 파일 검증
|
|
*/
|
|
async function validateBuildFiles() {
|
|
console.log("📦 빌드 파일 검증 중...");
|
|
|
|
if (platform === "android" || platform === "both") {
|
|
if (!fs.existsSync(config.androidBundlePath)) {
|
|
if (dryRun) {
|
|
console.warn(
|
|
`⚠️ DRY RUN 모드: Android AAB 파일 누락 무시: ${config.androidBundlePath}`
|
|
);
|
|
} else {
|
|
console.error(
|
|
`❌ Android AAB 파일을 찾을 수 없음: ${config.androidBundlePath}`
|
|
);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
const stats = fs.statSync(config.androidBundlePath);
|
|
console.log(
|
|
`✅ Android AAB 파일 확인: ${(stats.size / 1024 / 1024).toFixed(1)}MB`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (platform === "ios" || platform === "both") {
|
|
if (!fs.existsSync(config.iosIpaPath)) {
|
|
if (dryRun) {
|
|
console.warn(
|
|
`⚠️ DRY RUN 모드: iOS IPA 파일 누락 무시: ${config.iosIpaPath}`
|
|
);
|
|
} else {
|
|
console.error(`❌ iOS IPA 파일을 찾을 수 없음: ${config.iosIpaPath}`);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
const stats = fs.statSync(config.iosIpaPath);
|
|
console.log(
|
|
`✅ iOS IPA 파일 확인: ${(stats.size / 1024 / 1024).toFixed(1)}MB`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 릴리즈 노트 생성
|
|
*/
|
|
async function generateReleaseNotes() {
|
|
console.log("📝 릴리즈 노트 생성 중...");
|
|
|
|
try {
|
|
// package.json에서 현재 버전 읽기
|
|
const packageJson = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
|
const version = packageJson.version;
|
|
|
|
// Git 태그에서 릴리즈 노트 찾기
|
|
let releaseNotes = "";
|
|
|
|
try {
|
|
// GitHub Releases에서 최신 릴리즈 정보 가져오기
|
|
const latestTag = execSync("git describe --tags --abbrev=0", {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
|
|
if (latestTag === `v${version}`) {
|
|
// CHANGELOG.md에서 해당 버전 섹션 찾기
|
|
if (fs.existsSync("CHANGELOG.md")) {
|
|
const changelog = fs.readFileSync("CHANGELOG.md", "utf8");
|
|
const versionRegex = new RegExp(
|
|
`## \\[?${version}\\]?.*?\n(.*?)(?=\n## |$)`,
|
|
"s"
|
|
);
|
|
const match = changelog.match(versionRegex);
|
|
|
|
if (match) {
|
|
releaseNotes = match[1].trim();
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn(
|
|
"⚠️ Git 태그에서 릴리즈 노트를 가져올 수 없음:",
|
|
error.message
|
|
);
|
|
}
|
|
|
|
// 기본 릴리즈 노트 생성
|
|
if (!releaseNotes) {
|
|
releaseNotes = `Zellyy Finance v${version}\n\n새로운 기능과 개선사항이 포함된 업데이트입니다.`;
|
|
}
|
|
|
|
console.log(`✅ 릴리즈 노트 생성 완료 (${releaseNotes.length}자)`);
|
|
return releaseNotes;
|
|
} catch (error) {
|
|
console.error("❌ 릴리즈 노트 생성 실패:", error);
|
|
return `Zellyy Finance 업데이트\n\n새로운 기능과 개선사항이 포함되어 있습니다.`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Google Play Store 배포
|
|
*/
|
|
async function deployToGooglePlay(releaseNotes) {
|
|
console.log("🟢 Google Play Store 배포 시작...");
|
|
|
|
const result = {
|
|
platform: "android",
|
|
status: "pending",
|
|
track: config.googlePlayTrack,
|
|
startTime: new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
if (dryRun) {
|
|
console.log("🔍 DRY RUN: Google Play Store 배포 시뮬레이션");
|
|
result.status = "dry-run-success";
|
|
return result;
|
|
}
|
|
|
|
// Google Play Console API 호출 (r0adkll/upload-google-play 액션 시뮬레이션)
|
|
console.log(`📦 AAB 파일 업로드: ${config.androidBundlePath}`);
|
|
console.log(`🎯 배포 트랙: ${config.googlePlayTrack}`);
|
|
console.log(`📝 릴리즈 노트 (${releaseNotes.length}자):`);
|
|
console.log(
|
|
releaseNotes.substring(0, 200) + (releaseNotes.length > 200 ? "..." : "")
|
|
);
|
|
|
|
// 실제 배포는 GitHub Actions에서 수행
|
|
if (config.isCI) {
|
|
console.log(
|
|
"🔄 CI 환경에서 실제 배포는 GitHub Actions r0adkll/upload-google-play@v1에서 처리됩니다"
|
|
);
|
|
}
|
|
|
|
result.status = "success";
|
|
result.endTime = new Date().toISOString();
|
|
|
|
console.log("✅ Google Play Store 배포 성공");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("❌ Google Play Store 배포 실패:", error);
|
|
result.status = "failed";
|
|
result.error = error.message;
|
|
result.endTime = new Date().toISOString();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apple App Store 배포
|
|
*/
|
|
async function deployToAppStore(releaseNotes) {
|
|
console.log("🍎 Apple App Store (TestFlight) 배포 시작...");
|
|
|
|
const result = {
|
|
platform: "ios",
|
|
status: "pending",
|
|
track: "testflight",
|
|
startTime: new Date().toISOString(),
|
|
};
|
|
|
|
try {
|
|
if (dryRun) {
|
|
console.log("🔍 DRY RUN: TestFlight 배포 시뮬레이션");
|
|
result.status = "dry-run-success";
|
|
return result;
|
|
}
|
|
|
|
// App Store Connect API 호출 (Apple-Actions/upload-testflight-build 액션 시뮬레이션)
|
|
console.log(`📦 IPA 파일 업로드: ${config.iosIpaPath}`);
|
|
console.log(`🎯 배포 대상: TestFlight`);
|
|
console.log(`📝 빌드 노트 (${releaseNotes.length}자):`);
|
|
console.log(
|
|
releaseNotes.substring(0, 200) + (releaseNotes.length > 200 ? "..." : "")
|
|
);
|
|
|
|
// 실제 배포는 GitHub Actions에서 수행
|
|
if (config.isCI) {
|
|
console.log(
|
|
"🔄 CI 환경에서 실제 배포는 GitHub Actions Apple-Actions/upload-testflight-build@v1에서 처리됩니다"
|
|
);
|
|
}
|
|
|
|
result.status = "success";
|
|
result.endTime = new Date().toISOString();
|
|
|
|
console.log("✅ TestFlight 배포 성공");
|
|
return result;
|
|
} catch (error) {
|
|
console.error("❌ TestFlight 배포 실패:", error);
|
|
result.status = "failed";
|
|
result.error = error.message;
|
|
result.endTime = new Date().toISOString();
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 배포 결과 리포트 생성
|
|
*/
|
|
async function generateDeploymentReport(results) {
|
|
console.log("\n📊 배포 결과 리포트");
|
|
console.log("".padEnd(50, "="));
|
|
|
|
const reportData = {
|
|
timestamp: new Date().toISOString(),
|
|
project: config.appName,
|
|
repository: config.githubRepository,
|
|
runId: config.githubRunId,
|
|
results: results,
|
|
};
|
|
|
|
// 콘솔 리포트
|
|
Object.values(results).forEach((result) => {
|
|
const emoji =
|
|
result.status === "success"
|
|
? "✅"
|
|
: result.status === "failed"
|
|
? "❌"
|
|
: result.status === "dry-run-success"
|
|
? "🔍"
|
|
: "⏳";
|
|
|
|
console.log(`${emoji} ${result.platform.toUpperCase()}: ${result.status}`);
|
|
|
|
if (result.track) {
|
|
console.log(` 트랙: ${result.track}`);
|
|
}
|
|
|
|
if (result.startTime && result.endTime) {
|
|
const duration =
|
|
(new Date(result.endTime) - new Date(result.startTime)) / 1000;
|
|
console.log(` 소요시간: ${duration}초`);
|
|
}
|
|
|
|
if (result.error) {
|
|
console.log(` 오류: ${result.error}`);
|
|
}
|
|
|
|
console.log("");
|
|
});
|
|
|
|
// JSON 리포트 저장
|
|
const reportPath = path.join(__dirname, "..", "deployment-report.json");
|
|
fs.writeFileSync(reportPath, JSON.stringify(reportData, null, 2));
|
|
console.log(`📄 상세 리포트 저장: ${reportPath}`);
|
|
|
|
// 전체 성공 여부 확인
|
|
const allSuccess = Object.values(results).every(
|
|
(r) => r.status === "success" || r.status === "dry-run-success"
|
|
);
|
|
|
|
if (!allSuccess) {
|
|
console.log(
|
|
"⚠️ 일부 배포에서 문제가 발생했습니다. 상세 내용을 확인하세요."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용 예시 출력
|
|
*/
|
|
function printUsage() {
|
|
console.log(`
|
|
📱 Store Deployment Usage:
|
|
|
|
# Android만 배포
|
|
node scripts/store-deploy.cjs android
|
|
|
|
# iOS만 배포
|
|
node scripts/store-deploy.cjs ios
|
|
|
|
# 두 플랫폼 모두 배포
|
|
node scripts/store-deploy.cjs both
|
|
|
|
# 검증 모드 (실제 배포 없음)
|
|
node scripts/store-deploy.cjs both --dry-run
|
|
|
|
Environment Variables:
|
|
- GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: Google Play API 서비스 계정 JSON
|
|
- GOOGLE_PLAY_TRACK: 배포 트랙 (기본: internal)
|
|
- APPSTORE_ISSUER_ID: App Store Connect API Issuer ID
|
|
- APPSTORE_KEY_ID: App Store Connect API Key ID
|
|
- APPSTORE_PRIVATE_KEY: App Store Connect API Private Key
|
|
|
|
Build Files Required:
|
|
- Android: android/app/build/outputs/bundle/release/app-release.aab
|
|
- iOS: ios/App/build/App.ipa
|
|
`);
|
|
}
|
|
|
|
// 메인 실행
|
|
if (require.main === module) {
|
|
if (args.includes("--help") || args.includes("-h")) {
|
|
printUsage();
|
|
process.exit(0);
|
|
}
|
|
|
|
deployToStores().catch((error) => {
|
|
console.error("배포 실패:", error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { deployToStores, generateReleaseNotes };
|