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,184 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(name="Dashboard", description="대시보드")
*
* @OA\Schema(
* schema="DashboardSummary",
* type="object",
* description="대시보드 요약 데이터",
*
* @OA\Property(property="today", type="object", description="오늘 요약",
* @OA\Property(property="date", type="string", format="date", example="2025-01-15", description="오늘 날짜"),
* @OA\Property(property="attendances_count", type="integer", example=25, description="오늘 출근자 수"),
* @OA\Property(property="leaves_count", type="integer", example=3, description="오늘 휴가자 수"),
* @OA\Property(property="approvals_pending", type="integer", example=5, description="결재 대기 문서 수")
* ),
* @OA\Property(property="finance", type="object", description="재무 요약",
* @OA\Property(property="monthly_deposit", type="number", format="float", example=50000000, description="월간 입금액"),
* @OA\Property(property="monthly_withdrawal", type="number", format="float", example=30000000, description="월간 출금액"),
* @OA\Property(property="balance", type="number", format="float", example=150000000, description="현재 잔액")
* ),
* @OA\Property(property="sales", type="object", description="매출/매입 요약",
* @OA\Property(property="monthly_sales", type="number", format="float", example=80000000, description="월간 매출"),
* @OA\Property(property="monthly_purchases", type="number", format="float", example=45000000, description="월간 매입")
* ),
* @OA\Property(property="tasks", type="object", description="할 일 요약",
* @OA\Property(property="pending_approvals", type="integer", example=3, description="내가 결재할 문서 수"),
* @OA\Property(property="pending_leaves", type="integer", example=2, description="승인 대기 휴가 신청 수")
* )
* )
*
* @OA\Schema(
* schema="DashboardCharts",
* type="object",
* description="대시보드 차트 데이터",
*
* @OA\Property(property="period", type="string", enum={"week","month","quarter"}, example="month", description="조회 기간"),
* @OA\Property(property="start_date", type="string", format="date", example="2024-12-17", description="시작일"),
* @OA\Property(property="end_date", type="string", format="date", example="2025-01-15", description="종료일"),
* @OA\Property(property="deposit_trend", type="array", description="입금 추이",
* @OA\Items(type="object",
* @OA\Property(property="date", type="string", format="date", example="2025-01-15"),
* @OA\Property(property="amount", type="number", format="float", example=5000000)
* )
* ),
* @OA\Property(property="withdrawal_trend", type="array", description="출금 추이",
* @OA\Items(type="object",
* @OA\Property(property="date", type="string", format="date", example="2025-01-15"),
* @OA\Property(property="amount", type="number", format="float", example=3000000)
* )
* ),
* @OA\Property(property="sales_by_client", type="array", description="거래처별 매출 (상위 10개)",
* @OA\Items(type="object",
* @OA\Property(property="client_id", type="integer", example=1),
* @OA\Property(property="client_name", type="string", example="(주)테스트"),
* @OA\Property(property="amount", type="number", format="float", example=15000000)
* )
* )
* )
*
* @OA\Schema(
* schema="DashboardApprovals",
* type="object",
* description="대시보드 결재 현황",
*
* @OA\Property(property="pending_approvals", type="array", description="결재 대기 문서 (내가 결재할 문서)",
* @OA\Items(type="object",
* @OA\Property(property="id", type="integer", example=1, description="결재문서 ID"),
* @OA\Property(property="title", type="string", example="출장 신청서", description="제목"),
* @OA\Property(property="drafter_name", type="string", example="홍길동", description="기안자명"),
* @OA\Property(property="status", type="string", example="pending", description="상태"),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-01-15 10:30:00", description="생성일시")
* )
* ),
* @OA\Property(property="my_drafts", type="array", description="내가 기안한 진행중인 문서",
* @OA\Items(type="object",
* @OA\Property(property="id", type="integer", example=2, description="결재문서 ID"),
* @OA\Property(property="title", type="string", example="휴가 신청서", description="제목"),
* @OA\Property(property="status", type="string", example="pending", description="상태"),
* @OA\Property(property="current_step", type="integer", example=2, description="현재 결재 단계"),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-01-14 09:00:00", description="생성일시")
* )
* )
* )
*/
class DashboardApi
{
/**
* @OA\Get(
* path="/api/v1/dashboard/summary",
* tags={"Dashboard"},
* summary="대시보드 요약 데이터 조회",
* description="오늘 현황, 재무 요약, 매출/매입 요약, 할 일 요약을 반환합니다.",
* operationId="getDashboardSummary",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Response(
* response=200,
* description="성공",
*
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/DashboardSummary")
* )
* ),
*
* @OA\Response(response=401, description="인증 실패")
* )
*/
public function summary() {}
/**
* @OA\Get(
* path="/api/v1/dashboard/charts",
* tags={"Dashboard"},
* summary="대시보드 차트 데이터 조회",
* description="입금/출금 추이, 거래처별 매출 차트 데이터를 반환합니다.",
* operationId="getDashboardCharts",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(
* name="period",
* in="query",
* description="조회 기간 (week: 7일, month: 30일, quarter: 3개월)",
* required=false,
*
* @OA\Schema(type="string", enum={"week","month","quarter"}, default="month")
* ),
*
* @OA\Response(
* response=200,
* description="성공",
*
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/DashboardCharts")
* )
* ),
*
* @OA\Response(response=401, description="인증 실패"),
* @OA\Response(response=422, description="유효성 검사 실패")
* )
*/
public function charts() {}
/**
* @OA\Get(
* path="/api/v1/dashboard/approvals",
* tags={"Dashboard"},
* summary="결재 현황 조회",
* description="결재 대기 문서(결재함)와 내가 기안한 진행중인 문서 목록을 반환합니다.",
* operationId="getDashboardApprovals",
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
*
* @OA\Parameter(
* name="limit",
* in="query",
* description="각 목록의 최대 항목 수 (1~50)",
* required=false,
*
* @OA\Schema(type="integer", minimum=1, maximum=50, default=10)
* ),
*
* @OA\Response(
* response=200,
* description="성공",
*
* @OA\JsonContent(
* @OA\Property(property="success", type="boolean", example=true),
* @OA\Property(property="message", type="string", example="데이터를 조회했습니다."),
* @OA\Property(property="data", ref="#/components/schemas/DashboardApprovals")
* )
* ),
*
* @OA\Response(response=401, description="인증 실패"),
* @OA\Response(response=422, description="유효성 검사 실패")
* )
*/
public function approvals() {}
}

303
app/Swagger/v1/PushApi.php Normal file
View File

@@ -0,0 +1,303 @@
<?php
namespace App\Swagger\v1;
/**
* @OA\Tag(name="Push", description="푸시 알림 관리")
*/
/**
* Push 관련 스키마 정의
* -----------------------------------------------------------------------------
*/
/**
* @OA\Schema(
* schema="PushDeviceToken",
* type="object",
* description="푸시 디바이스 토큰",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="user_id", type="integer", example=5),
* @OA\Property(property="token", type="string", example="dGhpcyBpcyBhIHNhbXBsZSBGQ00gdG9rZW4..."),
* @OA\Property(property="platform", type="string", enum={"ios", "android", "web"}, example="android"),
* @OA\Property(property="device_name", type="string", nullable=true, example="Samsung Galaxy S24"),
* @OA\Property(property="app_version", type="string", nullable=true, example="1.0.0"),
* @OA\Property(property="is_active", type="boolean", example=true),
* @OA\Property(property="last_used_at", type="string", format="date-time", example="2025-12-18 10:30:00"),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-12-18 10:00:00"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-12-18 10:30:00")
* )
*
* @OA\Schema(
* schema="PushNotificationSetting",
* type="object",
* description="푸시 알림 설정",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="tenant_id", type="integer", example=1),
* @OA\Property(property="user_id", type="integer", example=5),
* @OA\Property(property="notification_type", type="string", enum={"deposit", "withdrawal", "order", "approval", "attendance", "notice", "system"}, example="deposit"),
* @OA\Property(property="is_enabled", type="boolean", example=true),
* @OA\Property(property="sound", type="string", enum={"default.wav", "deposit.wav", "withdrawal.wav", "order.wav", "approval.wav", "urgent.wav"}, example="deposit.wav"),
* @OA\Property(property="vibrate", type="boolean", example=true),
* @OA\Property(property="show_preview", type="boolean", example=true),
* @OA\Property(property="created_at", type="string", format="date-time", example="2025-12-18 10:00:00"),
* @OA\Property(property="updated_at", type="string", format="date-time", example="2025-12-18 10:30:00")
* )
*
* @OA\Schema(
* schema="RegisterTokenRequest",
* type="object",
* required={"token", "platform"},
* @OA\Property(property="token", type="string", minLength=10, example="dGhpcyBpcyBhIHNhbXBsZSBGQ00gdG9rZW4...", description="FCM 토큰"),
* @OA\Property(property="platform", type="string", enum={"ios", "android", "web"}, example="android", description="디바이스 플랫폼"),
* @OA\Property(property="device_name", type="string", nullable=true, maxLength=255, example="Samsung Galaxy S24", description="디바이스명"),
* @OA\Property(property="app_version", type="string", nullable=true, maxLength=50, example="1.0.0", description="앱 버전")
* )
*
* @OA\Schema(
* schema="UnregisterTokenRequest",
* type="object",
* required={"token"},
* @OA\Property(property="token", type="string", example="dGhpcyBpcyBhIHNhbXBsZSBGQ00gdG9rZW4...", description="해제할 FCM 토큰")
* )
*
* @OA\Schema(
* schema="UpdatePushSettingsRequest",
* type="object",
* required={"settings"},
* @OA\Property(
* property="settings",
* type="array",
* description="알림 설정 배열",
* @OA\Items(
* type="object",
* required={"notification_type", "is_enabled"},
* @OA\Property(property="notification_type", type="string", enum={"deposit", "withdrawal", "order", "approval", "attendance", "notice", "system"}, example="deposit"),
* @OA\Property(property="is_enabled", type="boolean", example=true),
* @OA\Property(property="sound", type="string", nullable=true, enum={"default.wav", "deposit.wav", "withdrawal.wav", "order.wav", "approval.wav", "urgent.wav"}, example="deposit.wav"),
* @OA\Property(property="vibrate", type="boolean", nullable=true, example=true),
* @OA\Property(property="show_preview", type="boolean", nullable=true, example=true)
* )
* )
* )
*
* @OA\Schema(
* schema="NotificationTypesResponse",
* type="object",
* @OA\Property(
* property="types",
* type="array",
* description="지원하는 알림 유형 목록",
* @OA\Items(type="string", example="deposit")
* ),
* @OA\Property(
* property="sounds",
* type="array",
* description="지원하는 알림음 목록",
* @OA\Items(type="string", example="deposit.wav")
* )
* )
*/
class PushApi
{
/**
* @OA\Post(
* path="/api/push/register-token",
* tags={"Push"},
* summary="FCM 토큰 등록",
* description="디바이스의 FCM 토큰을 등록합니다. 동일한 토큰이 이미 존재하면 업데이트됩니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/RegisterTokenRequest")
* ),
*
* @OA\Response(
* response=200,
* description="토큰 등록 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="message", type="string", example="푸시 토큰이 등록되었습니다."),
* @OA\Property(property="data", ref="#/components/schemas/PushDeviceToken")
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function registerToken() {}
/**
* @OA\Post(
* path="/api/push/unregister-token",
* tags={"Push"},
* summary="FCM 토큰 해제",
* description="디바이스의 FCM 토큰을 비활성화합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/UnregisterTokenRequest")
* ),
*
* @OA\Response(
* response=200,
* description="토큰 해제 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="message", type="string", example="푸시 토큰이 해제되었습니다."),
* @OA\Property(
* property="data",
* type="object",
* @OA\Property(property="unregistered", type="boolean", example=true)
* )
* )
* }
* )
* ),
*
* @OA\Response(response=400, description="토큰 누락", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function unregisterToken() {}
/**
* @OA\Get(
* path="/api/push/tokens",
* tags={"Push"},
* summary="사용자 토큰 목록 조회",
* description="현재 사용자의 활성화된 디바이스 토큰 목록을 조회합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/PushDeviceToken")
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function getTokens() {}
/**
* @OA\Get(
* path="/api/push/settings",
* tags={"Push"},
* summary="알림 설정 조회",
* description="현재 사용자의 알림 유형별 설정을 조회합니다. 설정이 없는 유형은 기본값으로 반환됩니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(
* property="data",
* type="object",
* description="알림 유형별 설정 (키: notification_type)",
* @OA\Property(property="deposit", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="withdrawal", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="order", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="approval", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="attendance", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="notice", ref="#/components/schemas/PushNotificationSetting"),
* @OA\Property(property="system", ref="#/components/schemas/PushNotificationSetting")
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function getSettings() {}
/**
* @OA\Put(
* path="/api/push/settings",
* tags={"Push"},
* summary="알림 설정 업데이트",
* description="사용자의 알림 설정을 업데이트합니다. 여러 알림 유형을 한 번에 설정할 수 있습니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\RequestBody(
* required=true,
* @OA\JsonContent(ref="#/components/schemas/UpdatePushSettingsRequest")
* ),
*
* @OA\Response(
* response=200,
* description="설정 업데이트 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="message", type="string", example="알림 설정이 업데이트되었습니다."),
* @OA\Property(
* property="data",
* type="array",
* @OA\Items(ref="#/components/schemas/PushNotificationSetting")
* )
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
* @OA\Response(response=422, description="검증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function updateSettings() {}
/**
* @OA\Get(
* path="/api/push/notification-types",
* tags={"Push"},
* summary="알림 유형 목록 조회",
* description="지원하는 알림 유형과 알림음 목록을 조회합니다.",
* security={{"ApiKeyAuth": {}}, {"BearerAuth": {}}},
*
* @OA\Response(
* response=200,
* description="조회 성공",
* @OA\JsonContent(
* allOf={
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
* @OA\Schema(
* @OA\Property(property="data", ref="#/components/schemas/NotificationTypesResponse")
* )
* }
* )
* ),
*
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
* )
*/
public function getNotificationTypes() {}
}