feat: FCM 사용자별 타겟 알림 발송 기능 추가
- today_issues 테이블에 target_user_id 컬럼 추가 (마이그레이션) - TodayIssue 모델: target_user_id 필드, targetUser 관계, forUser/targetedTo 스코프 추가 - TodayIssue 모델: 기안 상태 뱃지 상수 추가 (BADGE_DRAFT_APPROVED/REJECTED/COMPLETED) - TodayIssueObserverService: createIssueWithFcm, sendFcmNotification, getEnabledUserTokens에 targetUserId 파라미터 추가 - TodayIssueObserverService: handleApprovalStepChange - 결재자에게만 발송 - TodayIssueObserverService: handleApprovalStatusChange 추가 - 기안자에게만 발송 - ApprovalIssueObserver 신규 생성 및 AppServiceProvider에 등록 - i18n: 기안 승인/반려/완료 알림 메시지 추가 결재요청은 결재자(ApprovalStep.user_id)에게만, 기안 승인/반려는 기안자(Approval.drafter_id)에게만 FCM 발송 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
use App\Models\Orders\Client;
|
||||
use App\Models\Orders\Order;
|
||||
use App\Models\PushDeviceToken;
|
||||
use App\Models\Tenants\Approval;
|
||||
use App\Models\Tenants\ApprovalStep;
|
||||
use App\Models\Tenants\Deposit;
|
||||
use App\Models\Tenants\ExpectedExpense;
|
||||
@@ -202,6 +203,8 @@ public function handleExpectedExpenseDeleted(ExpectedExpense $expense): void
|
||||
|
||||
/**
|
||||
* 결재 요청 이슈 생성/삭제
|
||||
*
|
||||
* 결재자(ApprovalStep.user_id)에게만 알림 발송
|
||||
*/
|
||||
public function handleApprovalStepChange(ApprovalStep $step): void
|
||||
{
|
||||
@@ -223,7 +226,8 @@ public function handleApprovalStepChange(ApprovalStep $step): void
|
||||
]),
|
||||
path: '/approval/inbox',
|
||||
needsApproval: true,
|
||||
expiresAt: null // 결재 완료 시까지 유지
|
||||
expiresAt: null, // 결재 완료 시까지 유지
|
||||
targetUserId: $step->user_id // 결재자에게만 발송
|
||||
);
|
||||
} else {
|
||||
if ($approval) {
|
||||
@@ -242,6 +246,52 @@ public function handleApprovalStepDeleted(ApprovalStep $step): void
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 상태 변경 시 기안자에게 알림 발송
|
||||
*
|
||||
* 승인(approved), 반려(rejected), 완료(approved 최종) 시 기안자에게만 발송
|
||||
*/
|
||||
public function handleApprovalStatusChange(Approval $approval): void
|
||||
{
|
||||
// 상태에 따른 뱃지 결정
|
||||
$badge = match ($approval->status) {
|
||||
Approval::STATUS_APPROVED => TodayIssue::BADGE_DRAFT_APPROVED,
|
||||
Approval::STATUS_REJECTED => TodayIssue::BADGE_DRAFT_REJECTED,
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $badge) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 메시지 키 결정
|
||||
$messageKey = match ($approval->status) {
|
||||
Approval::STATUS_APPROVED => 'message.today_issue.draft_approved',
|
||||
Approval::STATUS_REJECTED => 'message.today_issue.draft_rejected',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $messageKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title = $approval->title ?? __('message.today_issue.approval_request');
|
||||
|
||||
$this->createIssueWithFcm(
|
||||
tenantId: $approval->tenant_id,
|
||||
sourceType: TodayIssue::SOURCE_APPROVAL,
|
||||
sourceId: $approval->id,
|
||||
badge: $badge,
|
||||
content: __($messageKey, [
|
||||
'title' => $title,
|
||||
]),
|
||||
path: '/approval/draft',
|
||||
needsApproval: false,
|
||||
expiresAt: Carbon::now()->addDays(7),
|
||||
targetUserId: $approval->drafter_id // 기안자에게만 발송
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 신규 거래처 이슈 생성
|
||||
*/
|
||||
@@ -425,6 +475,7 @@ public function updateTaxIssues(int $tenantId): void
|
||||
/**
|
||||
* TodayIssue 생성 후 FCM 푸시 알림 발송
|
||||
*
|
||||
* target_user_id가 있으면 해당 사용자에게만, 없으면 테넌트 전체에게 발송
|
||||
* 알림 설정이 활성화된 사용자에게만 발송
|
||||
* 중요 알림(수주등록, 신규거래처, 결재요청)은 알림음 다르게 발송
|
||||
*/
|
||||
@@ -436,12 +487,18 @@ public function sendFcmNotification(TodayIssue $issue): void
|
||||
|
||||
try {
|
||||
// 해당 테넌트의 활성 토큰 조회 (알림 설정 활성화된 사용자만)
|
||||
$tokens = $this->getEnabledUserTokens($issue->tenant_id, $issue->notification_type);
|
||||
// target_user_id가 있으면 해당 사용자만, 없으면 테넌트 전체
|
||||
$tokens = $this->getEnabledUserTokens(
|
||||
$issue->tenant_id,
|
||||
$issue->notification_type,
|
||||
$issue->target_user_id
|
||||
);
|
||||
|
||||
if (empty($tokens)) {
|
||||
Log::info('[TodayIssue] No enabled tokens found for FCM', [
|
||||
'tenant_id' => $issue->tenant_id,
|
||||
'notification_type' => $issue->notification_type,
|
||||
'target_user_id' => $issue->target_user_id,
|
||||
]);
|
||||
|
||||
return;
|
||||
@@ -466,6 +523,7 @@ public function sendFcmNotification(TodayIssue $issue): void
|
||||
Log::info('[TodayIssue] FCM notification sent', [
|
||||
'issue_id' => $issue->id,
|
||||
'notification_type' => $issue->notification_type,
|
||||
'target_user_id' => $issue->target_user_id,
|
||||
'channel_id' => $channelId,
|
||||
'token_count' => count($tokens),
|
||||
'success_count' => $result->getSuccessCount(),
|
||||
@@ -476,6 +534,7 @@ public function sendFcmNotification(TodayIssue $issue): void
|
||||
Log::error('[TodayIssue] FCM notification failed', [
|
||||
'issue_id' => $issue->id,
|
||||
'notification_type' => $issue->notification_type,
|
||||
'target_user_id' => $issue->target_user_id,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
@@ -483,15 +542,25 @@ public function sendFcmNotification(TodayIssue $issue): void
|
||||
|
||||
/**
|
||||
* 알림 설정이 활성화된 사용자들의 FCM 토큰 조회
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $notificationType 알림 타입
|
||||
* @param int|null $targetUserId 특정 대상 사용자 ID (null이면 테넌트 전체)
|
||||
*/
|
||||
private function getEnabledUserTokens(int $tenantId, string $notificationType): array
|
||||
private function getEnabledUserTokens(int $tenantId, string $notificationType, ?int $targetUserId = null): array
|
||||
{
|
||||
// 해당 테넌트의 활성 토큰 조회
|
||||
$tokens = PushDeviceToken::withoutGlobalScopes()
|
||||
$query = PushDeviceToken::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('is_active', true)
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
->whereNull('deleted_at');
|
||||
|
||||
// 특정 대상자가 지정된 경우 해당 사용자만 조회
|
||||
if ($targetUserId !== null) {
|
||||
$query->where('user_id', $targetUserId);
|
||||
}
|
||||
|
||||
$tokens = $query->get();
|
||||
|
||||
if ($tokens->isEmpty()) {
|
||||
return [];
|
||||
@@ -534,6 +603,16 @@ private function isNotificationEnabledForUser(int $tenantId, int $userId, string
|
||||
* TodayIssue 생성 시 FCM 발송 포함 (래퍼 메서드)
|
||||
*
|
||||
* createIssue 호출 후 자동으로 FCM 발송
|
||||
*
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $sourceType 소스 타입
|
||||
* @param int|null $sourceId 소스 ID
|
||||
* @param string $badge 뱃지 타입
|
||||
* @param string $content 알림 내용
|
||||
* @param string|null $path 이동 경로
|
||||
* @param bool $needsApproval 승인 필요 여부
|
||||
* @param \DateTime|null $expiresAt 만료 시간
|
||||
* @param int|null $targetUserId 특정 대상 사용자 ID (null이면 테넌트 전체)
|
||||
*/
|
||||
public function createIssueWithFcm(
|
||||
int $tenantId,
|
||||
@@ -543,7 +622,8 @@ public function createIssueWithFcm(
|
||||
string $content,
|
||||
?string $path = null,
|
||||
bool $needsApproval = false,
|
||||
?\DateTime $expiresAt = null
|
||||
?\DateTime $expiresAt = null,
|
||||
?int $targetUserId = null
|
||||
): TodayIssue {
|
||||
$issue = TodayIssue::createIssue(
|
||||
tenantId: $tenantId,
|
||||
@@ -553,7 +633,8 @@ public function createIssueWithFcm(
|
||||
content: $content,
|
||||
path: $path,
|
||||
needsApproval: $needsApproval,
|
||||
expiresAt: $expiresAt
|
||||
expiresAt: $expiresAt,
|
||||
targetUserId: $targetUserId
|
||||
);
|
||||
|
||||
// FCM 발송
|
||||
|
||||
Reference in New Issue
Block a user