feat: 인앱 업데이트 체크 API 구현
- 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 <noreply@anthropic.com>
This commit is contained in:
38
app/Http/Controllers/Api/V1/AppVersionController.php
Normal file
38
app/Http/Controllers/Api/V1/AppVersionController.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\AppVersionService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class AppVersionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 최신 버전 확인
|
||||||
|
* GET /api/v1/app/version?platform=android¤t_version_code=1
|
||||||
|
*/
|
||||||
|
public function latestVersion(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$platform = $request->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -123,6 +123,7 @@ public function handle(Request $request, Closure $next)
|
|||||||
'api/v1/debug-apikey',
|
'api/v1/debug-apikey',
|
||||||
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
'api/v1/internal/exchange-token', // 내부 서버간 토큰 교환 (HMAC 인증 사용)
|
||||||
'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근)
|
'api/v1/admin/fcm/*', // Admin FCM API (MNG에서 API Key만으로 접근)
|
||||||
|
'api/v1/app/*', // 앱 버전 확인/다운로드 (API Key만 필요)
|
||||||
];
|
];
|
||||||
|
|
||||||
// 현재 라우트 확인 (경로 또는 이름)
|
// 현재 라우트 확인 (경로 또는 이름)
|
||||||
|
|||||||
36
app/Models/AppVersion.php
Normal file
36
app/Models/AppVersion.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class AppVersion extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'version_code',
|
||||||
|
'version_name',
|
||||||
|
'platform',
|
||||||
|
'release_notes',
|
||||||
|
'apk_path',
|
||||||
|
'apk_size',
|
||||||
|
'apk_original_name',
|
||||||
|
'force_update',
|
||||||
|
'is_active',
|
||||||
|
'download_count',
|
||||||
|
'published_at',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'version_code' => 'integer',
|
||||||
|
'apk_size' => 'integer',
|
||||||
|
'force_update' => 'boolean',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'download_count' => 'integer',
|
||||||
|
'published_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
62
app/Services/AppVersionService.php
Normal file
62
app/Services/AppVersionService.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\AppVersion;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class AppVersionService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 최신 버전 확인
|
||||||
|
*/
|
||||||
|
public static function getLatestVersion(string $platform, int $currentVersionCode): array
|
||||||
|
{
|
||||||
|
$latest = AppVersion::where('platform', $platform)
|
||||||
|
->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,14 @@
|
|||||||
'report' => false,
|
'report' => false,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'app_releases' => [
|
||||||
|
'driver' => 'local',
|
||||||
|
'root' => storage_path('app/releases'),
|
||||||
|
'visibility' => 'private',
|
||||||
|
'throw' => false,
|
||||||
|
'report' => false,
|
||||||
|
],
|
||||||
|
|
||||||
's3' => [
|
's3' => [
|
||||||
'driver' => 's3',
|
'driver' => 's3',
|
||||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('app_versions', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
require __DIR__.'/api/v1/documents.php';
|
require __DIR__.'/api/v1/documents.php';
|
||||||
require __DIR__.'/api/v1/common.php';
|
require __DIR__.'/api/v1/common.php';
|
||||||
require __DIR__.'/api/v1/stats.php';
|
require __DIR__.'/api/v1/stats.php';
|
||||||
|
require __DIR__.'/api/v1/app.php';
|
||||||
|
|
||||||
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
// 공유 링크 다운로드 (인증 불필요 - auth.apikey 그룹 밖)
|
||||||
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
Route::get('/files/share/{token}', [FileStorageController::class, 'downloadShared'])->name('v1.files.share.download');
|
||||||
|
|||||||
16
routes/api/v1/app.php
Normal file
16
routes/api/v1/app.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V1\AppVersionController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| App Version Routes (앱 버전 관리)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 인앱 업데이트용 API (Bearer 토큰 불필요, API Key만 필요)
|
||||||
|
*/
|
||||||
|
|
||||||
|
Route::prefix('app')->group(function () {
|
||||||
|
Route::get('/version', [AppVersionController::class, 'latestVersion']);
|
||||||
|
Route::get('/download/{id}', [AppVersionController::class, 'download']);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user