From 49d163ae0cb7e37334467592a6d3da489fa3b7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Fri, 30 Jan 2026 19:53:09 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9D=B8=EC=95=B1=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=B2=B4=ED=81=AC=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app_versions 테이블 마이그레이션 (시스템 레벨, tenant_id 없음) - AppVersion 모델 (SoftDeletes) - AppVersionService: getLatestVersion, downloadApk - AppVersionController: GET /api/v1/app/version, GET /api/v1/app/download/{id} - ApiKeyMiddleware 화이트리스트에 api/v1/app/* 추가 - app_releases 스토리지 디스크 추가 Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/AppVersionController.php | 38 ++++++++++++ app/Http/Middleware/ApiKeyMiddleware.php | 1 + app/Models/AppVersion.php | 36 +++++++++++ app/Services/AppVersionService.php | 62 +++++++++++++++++++ config/filesystems.php | 8 +++ ...01_30_200000_create_app_versions_table.php | 37 +++++++++++ routes/api.php | 1 + routes/api/v1/app.php | 16 +++++ 8 files changed, 199 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/AppVersionController.php create mode 100644 app/Models/AppVersion.php create mode 100644 app/Services/AppVersionService.php create mode 100644 database/migrations/2026_01_30_200000_create_app_versions_table.php create mode 100644 routes/api/v1/app.php 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']); +});