diff --git a/app/Http/Controllers/Api/V1/AppVersionController.php b/app/Http/Controllers/Api/V1/AppVersionController.php new file mode 100644 index 0000000..1fe030f --- /dev/null +++ b/app/Http/Controllers/Api/V1/AppVersionController.php @@ -0,0 +1,38 @@ +input('platform', 'android'); + $currentVersionCode = (int) $request->input('current_version_code', 0); + + $result = AppVersionService::getLatestVersion($platform, $currentVersionCode); + + return response()->json([ + 'success' => true, + 'data' => $result, + ]); + } + + /** + * APK 다운로드 + * GET /api/v1/app/download/{id} + */ + public function download(int $id): StreamedResponse + { + return AppVersionService::downloadApk($id); + } +} diff --git a/app/Http/Middleware/ApiKeyMiddleware.php b/app/Http/Middleware/ApiKeyMiddleware.php index 3b4f110..5a6d769 100644 --- a/app/Http/Middleware/ApiKeyMiddleware.php +++ b/app/Http/Middleware/ApiKeyMiddleware.php @@ -123,6 +123,7 @@ public function handle(Request $request, Closure $next) 'api/v1/debug-apikey', 'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용) 'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근) + 'api/v1/app/*', // 앱 버전 확인/다운로드 (API Key만 필요) ]; // 현재 라우트 확인 (경로 또는 이름) diff --git a/app/Models/AppVersion.php b/app/Models/AppVersion.php new file mode 100644 index 0000000..4129a64 --- /dev/null +++ b/app/Models/AppVersion.php @@ -0,0 +1,36 @@ + 'integer', + 'apk_size' => 'integer', + 'force_update' => 'boolean', + 'is_active' => 'boolean', + 'download_count' => 'integer', + 'published_at' => 'datetime', + ]; +} diff --git a/app/Services/AppVersionService.php b/app/Services/AppVersionService.php new file mode 100644 index 0000000..465a4b5 --- /dev/null +++ b/app/Services/AppVersionService.php @@ -0,0 +1,62 @@ +where('is_active', true) + ->whereNotNull('published_at') + ->orderByDesc('version_code') + ->first(); + + if (! $latest || $latest->version_code <= $currentVersionCode) { + return [ + 'has_update' => false, + 'latest_version' => null, + ]; + } + + return [ + 'has_update' => true, + 'latest_version' => [ + 'id' => $latest->id, + 'version_code' => $latest->version_code, + 'version_name' => $latest->version_name, + 'release_notes' => $latest->release_notes, + 'force_update' => $latest->force_update, + 'apk_size' => $latest->apk_size, + 'download_url' => url("/api/v1/app/download/{$latest->id}"), + 'published_at' => $latest->published_at?->format('Y-m-d'), + ], + ]; + } + + /** + * APK 다운로드 + */ + public static function downloadApk(int $id): StreamedResponse + { + $version = AppVersion::where('is_active', true)->findOrFail($id); + + if (! $version->apk_path || ! Storage::disk('app_releases')->exists($version->apk_path)) { + abort(404, 'APK 파일을 찾을 수 없습니다.'); + } + + // 다운로드 수 증가 + $version->increment('download_count'); + + $fileName = $version->apk_original_name ?: "app-v{$version->version_name}.apk"; + + return Storage::disk('app_releases')->download($version->apk_path, $fileName); + } +} diff --git a/config/filesystems.php b/config/filesystems.php index cbdf6e2..199fd26 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -55,6 +55,14 @@ 'report' => false, ], + 'app_releases' => [ + 'driver' => 'local', + 'root' => storage_path('app/releases'), + 'visibility' => 'private', + 'throw' => false, + 'report' => false, + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), diff --git a/database/migrations/2026_01_30_200000_create_app_versions_table.php b/database/migrations/2026_01_30_200000_create_app_versions_table.php new file mode 100644 index 0000000..7c718cb --- /dev/null +++ b/database/migrations/2026_01_30_200000_create_app_versions_table.php @@ -0,0 +1,37 @@ +id(); + $table->unsignedInteger('version_code')->unique()->comment('정수 비교용 버전 코드'); + $table->string('version_name', 20)->comment('표시용 버전명 (예: 0.2)'); + $table->string('platform', 10)->default('android')->comment('android/ios'); + $table->text('release_notes')->nullable()->comment('변경사항'); + $table->string('apk_path', 500)->nullable()->comment('스토리지 경로'); + $table->unsignedBigInteger('apk_size')->nullable()->comment('파일 크기(bytes)'); + $table->string('apk_original_name', 255)->nullable()->comment('원본 파일명'); + $table->boolean('force_update')->default(false)->comment('강제 업데이트 여부'); + $table->boolean('is_active')->default(true)->comment('활성 여부'); + $table->unsignedInteger('download_count')->default(0)->comment('다운로드 수'); + $table->timestamp('published_at')->nullable()->comment('배포일'); + $table->unsignedBigInteger('created_by')->nullable()->comment('생성자'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('수정자'); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['platform', 'is_active']); + }); + } + + public function down(): void + { + Schema::dropIfExists('app_versions'); + } +}; diff --git a/routes/api.php b/routes/api.php index daf53ea..c7a3f3e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -38,6 +38,7 @@ require __DIR__.'/api/v1/documents.php'; require __DIR__.'/api/v1/common.php'; require __DIR__.'/api/v1/stats.php'; + require __DIR__.'/api/v1/app.php'; // 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖) Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download'); diff --git a/routes/api/v1/app.php b/routes/api/v1/app.php new file mode 100644 index 0000000..414cb8b --- /dev/null +++ b/routes/api/v1/app.php @@ -0,0 +1,16 @@ +group(function () { + Route::get('/version', [AppVersionController::class, 'latestVersion']); + Route::get('/download/{id}', [AppVersionController::class, 'download']); +});