feat: 대시보드 API 및 FCM 푸시 알림 API 구현

Dashboard API:
- DashboardController, DashboardService 추가
- /dashboard/summary, /charts, /approvals 엔드포인트

Push Notification API:
- FCM 토큰 관리 (등록/해제/목록)
- 알림 설정 관리 (유형별 on/off, 알림음 설정)
- 알림 유형: deposit, withdrawal, order, approval, attendance, notice, system
- 알림음: default, deposit, withdrawal, order, approval, urgent
- PushDeviceToken, PushNotificationSetting 모델
- Swagger 문서 추가
This commit is contained in:
2025-12-18 11:16:24 +09:00
parent 7089dd1e46
commit 6477cf2c83
15 changed files with 1697 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\V1\Dashboard\DashboardApprovalsRequest;
use App\Http\Requests\V1\Dashboard\DashboardChartsRequest;
use App\Http\Responses\ApiResponse;
use App\Services\DashboardService;
use Illuminate\Http\JsonResponse;
class DashboardController extends Controller
{
public function __construct(
private readonly DashboardService $dashboardService
) {}
/**
* 대시보드 요약 데이터 조회
*/
public function summary(): JsonResponse
{
$data = $this->dashboardService->summary();
return ApiResponse::handle(['data' => $data], __('message.fetched'));
}
/**
* 대시보드 차트 데이터 조회
*/
public function charts(DashboardChartsRequest $request): JsonResponse
{
$data = $this->dashboardService->charts($request->validated());
return ApiResponse::handle(['data' => $data], __('message.fetched'));
}
/**
* 결재 현황 조회
*/
public function approvals(DashboardApprovalsRequest $request): JsonResponse
{
$data = $this->dashboardService->approvals($request->validated());
return ApiResponse::handle(['data' => $data], __('message.fetched'));
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\Push\RegisterTokenRequest;
use App\Http\Requests\Push\UpdateSettingsRequest;
use App\Services\PushNotificationService;
use App\Utils\ApiResponse;
use Illuminate\Http\Request;
class PushNotificationController extends Controller
{
/**
* FCM 토큰 등록
*/
public function registerToken(RegisterTokenRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$service = new PushNotificationService;
return $service->registerToken($request->validated());
}, __('message.push.token_registered'));
}
/**
* FCM 토큰 해제
*/
public function unregisterToken(Request $request)
{
return ApiResponse::handle(function () use ($request) {
$token = $request->input('token');
if (! $token) {
throw new \InvalidArgumentException(__('error.push.token_required'));
}
$service = new PushNotificationService;
return ['unregistered' => $service->unregisterToken($token)];
}, __('message.push.token_unregistered'));
}
/**
* 사용자의 등록된 디바이스 토큰 목록
*/
public function getTokens()
{
return ApiResponse::handle(function () {
$service = new PushNotificationService;
return $service->getUserTokens();
});
}
/**
* 알림 설정 조회
*/
public function getSettings()
{
return ApiResponse::handle(function () {
$service = new PushNotificationService;
return $service->getSettings();
});
}
/**
* 알림 설정 업데이트
*/
public function updateSettings(UpdateSettingsRequest $request)
{
return ApiResponse::handle(function () use ($request) {
$service = new PushNotificationService;
return $service->updateSettings($request->validated()['settings']);
}, __('message.push.settings_updated'));
}
/**
* 알림 유형 목록 조회
*/
public function getNotificationTypes()
{
return ApiResponse::handle(function () {
return [
'types' => \App\Models\PushNotificationSetting::getAllTypes(),
'sounds' => \App\Models\PushNotificationSetting::getAllSounds(),
];
});
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\Push;
use App\Models\PushDeviceToken;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RegisterTokenRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'token' => ['required', 'string', 'min:10'],
'platform' => [
'required',
'string',
Rule::in([
PushDeviceToken::PLATFORM_IOS,
PushDeviceToken::PLATFORM_ANDROID,
PushDeviceToken::PLATFORM_WEB,
]),
],
'device_name' => ['nullable', 'string', 'max:255'],
'app_version' => ['nullable', 'string', 'max:50'],
];
}
public function messages(): array
{
return [
'token.required' => __('error.push.token_required'),
'token.min' => __('error.push.token_invalid'),
'platform.required' => __('error.push.platform_required'),
'platform.in' => __('error.push.platform_invalid'),
];
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Requests\Push;
use App\Models\PushNotificationSetting;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateSettingsRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'settings' => ['required', 'array'],
'settings.*.notification_type' => [
'required',
'string',
Rule::in(PushNotificationSetting::getAllTypes()),
],
'settings.*.is_enabled' => ['required', 'boolean'],
'settings.*.sound' => [
'nullable',
'string',
Rule::in(PushNotificationSetting::getAllSounds()),
],
'settings.*.vibrate' => ['nullable', 'boolean'],
'settings.*.show_preview' => ['nullable', 'boolean'],
];
}
public function messages(): array
{
return [
'settings.required' => __('error.push.settings_required'),
'settings.*.notification_type.required' => __('error.push.type_required'),
'settings.*.notification_type.in' => __('error.push.type_invalid'),
'settings.*.is_enabled.required' => __('error.push.enabled_required'),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Requests\V1\Dashboard;
use Illuminate\Foundation\Http\FormRequest;
class DashboardApprovalsRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'limit' => ['nullable', 'integer', 'min:1', 'max:50'],
];
}
public function messages(): array
{
return [
'limit.min' => __('error.validation.min', ['min' => 1]),
'limit.max' => __('error.validation.max', ['max' => 50]),
];
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Requests\V1\Dashboard;
use Illuminate\Foundation\Http\FormRequest;
class DashboardChartsRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'period' => ['nullable', 'string', 'in:week,month,quarter'],
];
}
public function messages(): array
{
return [
'period.in' => __('error.dashboard.invalid_period'),
];
}
}