369 lines
12 KiB
Markdown
369 lines
12 KiB
Markdown
|
|
# FCM 사용자별 알림 발송 계획
|
||
|
|
|
||
|
|
> **작성일**: 2026-01-28
|
||
|
|
> **목적**: FCM 푸시 알림을 테넌트 전체 브로드캐스트에서 사용자별 타겟 발송으로 변경
|
||
|
|
> **상태**: ✅ 구현 완료
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 📍 현재 진행 상태
|
||
|
|
|
||
|
|
| 항목 | 내용 |
|
||
|
|
|------|------|
|
||
|
|
| **마지막 완료 작업** | Phase 4 - FCM 발송 로직 수정 완료 |
|
||
|
|
| **다음 작업** | 테스트 검증 |
|
||
|
|
| **진행률** | 8/8 (100%) |
|
||
|
|
| **마지막 업데이트** | 2026-01-28 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. 개요
|
||
|
|
|
||
|
|
### 1.1 배경
|
||
|
|
|
||
|
|
현재 TodayIssue 생성 시 FCM 푸시 알림이 **테넌트 전체 사용자** 중 알림 설정이 켜진 모든 사용자에게 발송됨.
|
||
|
|
|
||
|
|
**문제점**:
|
||
|
|
- 결재요청 알림이 결재자가 아닌 사람에게도 발송됨
|
||
|
|
- 기안 승인/반려/완료 알림이 기안자가 아닌 사람에게도 발송됨
|
||
|
|
- 불필요한 알림으로 사용자 경험 저하
|
||
|
|
|
||
|
|
### 1.2 목표
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ 🎯 핵심 목표 │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ 1. 이슈 타입에 따라 특정 대상자에게만 FCM 발송 │
|
||
|
|
│ 2. 사용자별 알림 설정(ON/OFF)이 정상 동작하도록 보장 │
|
||
|
|
│ 3. 근태 알림은 제외 (정책 미확정) │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### 1.3 발송 대상 정책
|
||
|
|
|
||
|
|
| 이슈 타입 | 현재 | 변경 후 대상 |
|
||
|
|
|-----------|------|-------------|
|
||
|
|
| **결재요청** | 테넌트 전체 | **결재자(나)** - ApprovalStep.user_id |
|
||
|
|
| **기안 승인** | 테넌트 전체 | **기안자** - Approval.drafter_id |
|
||
|
|
| **기안 반려** | 테넌트 전체 | **기안자** - Approval.drafter_id |
|
||
|
|
| **기안 완료** | 테넌트 전체 | **기안자** - Approval.drafter_id |
|
||
|
|
| 수주등록 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 추심이슈 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 안전재고 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 지출승인 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 세금신고 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 신규업체 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 입금 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| 출금 | 테넌트 전체 | 테넌트 전체 (변경 없음) |
|
||
|
|
| **근태 알림** | - | **제외** (정책 미확정) |
|
||
|
|
|
||
|
|
### 1.4 변경 승인 정책
|
||
|
|
|
||
|
|
| 분류 | 예시 | 승인 |
|
||
|
|
|------|------|------|
|
||
|
|
| ✅ 즉시 가능 | 필드 추가, 로직 수정, 문서 수정 | 불필요 |
|
||
|
|
| ⚠️ 컨펌 필요 | 마이그레이션, 새 테이블/컬럼 | **필수** |
|
||
|
|
| 🔴 금지 | 기존 테이블 구조 변경, 파괴적 변경 | 별도 협의 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 대상 범위
|
||
|
|
|
||
|
|
### 2.1 Phase 1: 데이터베이스 변경
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 1.1 | TodayIssue 테이블에 `target_user_id` 컬럼 추가 | ✅ | nullable, FK |
|
||
|
|
| 1.2 | 마이그레이션 파일 생성 | ✅ | 2026_01_28_132426 |
|
||
|
|
|
||
|
|
### 2.2 Phase 2: 모델 수정
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 2.1 | TodayIssue 모델에 target_user_id 추가 | ✅ | fillable, relation, scopes |
|
||
|
|
| 2.2 | TodayIssue::createIssue() 메서드에 targetUserId 파라미터 추가 | ✅ | |
|
||
|
|
|
||
|
|
### 2.3 Phase 3: Observer 수정
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 3.1 | handleApprovalStepChange() - 결재요청 시 결재자 지정 | ✅ | step->user_id 전달 |
|
||
|
|
| 3.2 | 기안 승인/반려/완료 알림 추가 (기안자 지정) | ✅ | ApprovalIssueObserver 신규 |
|
||
|
|
|
||
|
|
### 2.4 Phase 4: FCM 발송 로직 수정
|
||
|
|
|
||
|
|
| # | 작업 항목 | 상태 | 비고 |
|
||
|
|
|---|----------|:----:|------|
|
||
|
|
| 4.1 | sendFcmNotification() - target_user_id 있으면 해당 사용자만 | ✅ | |
|
||
|
|
| 4.2 | getEnabledUserTokens() - 특정 사용자 필터링 로직 추가 | ✅ | |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. 작업 절차
|
||
|
|
|
||
|
|
### 3.1 단계별 절차
|
||
|
|
|
||
|
|
```
|
||
|
|
Step 1: 데이터베이스 변경
|
||
|
|
├── today_issues 테이블에 target_user_id 컬럼 추가
|
||
|
|
├── 마이그레이션 실행
|
||
|
|
└── 검증: 테이블 구조 확인
|
||
|
|
|
||
|
|
Step 2: TodayIssue 모델 수정
|
||
|
|
├── target_user_id fillable 추가
|
||
|
|
├── targetUser() relation 추가
|
||
|
|
└── createIssue() 파라미터 추가
|
||
|
|
|
||
|
|
Step 3: TodayIssueObserverService 수정
|
||
|
|
├── createIssueWithFcm() 파라미터 추가
|
||
|
|
├── handleApprovalStepChange() 수정 - 결재자 지정
|
||
|
|
├── 기안 상태 변경 알림 추가 (신규)
|
||
|
|
└── 근태 알림 비활성화
|
||
|
|
|
||
|
|
Step 4: FCM 발송 로직 수정
|
||
|
|
├── sendFcmNotification() 수정
|
||
|
|
├── getEnabledUserTokens() 수정 - targetUserId 파라미터 추가
|
||
|
|
└── 검증: 대상자만 수신 확인
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. 상세 작업 내용
|
||
|
|
|
||
|
|
### 4.1 Phase 1: 데이터베이스 변경
|
||
|
|
|
||
|
|
**마이그레이션 파일**:
|
||
|
|
```php
|
||
|
|
// database/migrations/xxxx_add_target_user_id_to_today_issues_table.php
|
||
|
|
|
||
|
|
Schema::table('today_issues', function (Blueprint $table) {
|
||
|
|
$table->unsignedBigInteger('target_user_id')
|
||
|
|
->nullable()
|
||
|
|
->after('source_id')
|
||
|
|
->comment('특정 대상 사용자 ID (null이면 테넌트 전체)');
|
||
|
|
|
||
|
|
$table->foreign('target_user_id')
|
||
|
|
->references('id')
|
||
|
|
->on('users')
|
||
|
|
->onDelete('cascade');
|
||
|
|
|
||
|
|
$table->index(['tenant_id', 'target_user_id']);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.2 Phase 2: TodayIssue 모델 수정
|
||
|
|
|
||
|
|
```php
|
||
|
|
// app/Models/Tenants/TodayIssue.php
|
||
|
|
|
||
|
|
protected $fillable = [
|
||
|
|
// ... 기존 필드
|
||
|
|
'target_user_id', // 추가
|
||
|
|
];
|
||
|
|
|
||
|
|
public function targetUser(): BelongsTo
|
||
|
|
{
|
||
|
|
return $this->belongsTo(User::class, 'target_user_id');
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function createIssue(
|
||
|
|
int $tenantId,
|
||
|
|
string $sourceType,
|
||
|
|
?int $sourceId,
|
||
|
|
string $badge,
|
||
|
|
string $content,
|
||
|
|
?string $path = null,
|
||
|
|
bool $needsApproval = false,
|
||
|
|
?\DateTime $expiresAt = null,
|
||
|
|
?int $targetUserId = null // 추가
|
||
|
|
): self {
|
||
|
|
// ... 기존 로직 + target_user_id 저장
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.3 Phase 3: Observer 수정
|
||
|
|
|
||
|
|
**결재요청 - 결재자에게만**:
|
||
|
|
```php
|
||
|
|
// handleApprovalStepChange() 수정
|
||
|
|
|
||
|
|
$this->createIssueWithFcm(
|
||
|
|
tenantId: $approval->tenant_id,
|
||
|
|
sourceType: TodayIssue::SOURCE_APPROVAL,
|
||
|
|
sourceId: $step->id,
|
||
|
|
badge: TodayIssue::BADGE_APPROVAL_REQUEST,
|
||
|
|
content: __('message.today_issue.approval_pending', [...]),
|
||
|
|
path: '/approval/inbox',
|
||
|
|
needsApproval: true,
|
||
|
|
expiresAt: null,
|
||
|
|
targetUserId: $step->user_id // 결재자
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**기안 승인/반려/완료 - 기안자에게만** (신규):
|
||
|
|
```php
|
||
|
|
// handleApprovalStatusChange() 신규 메서드
|
||
|
|
|
||
|
|
public function handleApprovalStatusChange(Approval $approval): void
|
||
|
|
{
|
||
|
|
$badge = match($approval->status) {
|
||
|
|
'approved' => TodayIssue::BADGE_DRAFT_APPROVED,
|
||
|
|
'rejected' => TodayIssue::BADGE_DRAFT_REJECTED,
|
||
|
|
'completed' => TodayIssue::BADGE_DRAFT_COMPLETED,
|
||
|
|
default => null,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!$badge) return;
|
||
|
|
|
||
|
|
$this->createIssueWithFcm(
|
||
|
|
tenantId: $approval->tenant_id,
|
||
|
|
sourceType: TodayIssue::SOURCE_APPROVAL,
|
||
|
|
sourceId: $approval->id,
|
||
|
|
badge: $badge,
|
||
|
|
content: __('message.today_issue.'.$approval->status, [...]),
|
||
|
|
path: '/approval/draft',
|
||
|
|
needsApproval: false,
|
||
|
|
expiresAt: Carbon::now()->addDays(7),
|
||
|
|
targetUserId: $approval->drafter_id // 기안자
|
||
|
|
);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4.4 Phase 4: FCM 발송 로직 수정
|
||
|
|
|
||
|
|
```php
|
||
|
|
// sendFcmNotification() 수정
|
||
|
|
|
||
|
|
public function sendFcmNotification(TodayIssue $issue): void
|
||
|
|
{
|
||
|
|
// target_user_id가 있으면 해당 사용자만, 없으면 테넌트 전체
|
||
|
|
$tokens = $this->getEnabledUserTokens(
|
||
|
|
$issue->tenant_id,
|
||
|
|
$issue->notification_type,
|
||
|
|
$issue->target_user_id // 추가
|
||
|
|
);
|
||
|
|
|
||
|
|
// ... 기존 발송 로직
|
||
|
|
}
|
||
|
|
|
||
|
|
// getEnabledUserTokens() 수정
|
||
|
|
|
||
|
|
private function getEnabledUserTokens(
|
||
|
|
int $tenantId,
|
||
|
|
string $notificationType,
|
||
|
|
?int $targetUserId = null // 추가
|
||
|
|
): array {
|
||
|
|
$query = PushDeviceToken::withoutGlobalScopes()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('is_active', true)
|
||
|
|
->whereNull('deleted_at');
|
||
|
|
|
||
|
|
// 특정 대상자가 지정된 경우
|
||
|
|
if ($targetUserId !== null) {
|
||
|
|
$query->where('user_id', $targetUserId);
|
||
|
|
}
|
||
|
|
|
||
|
|
$tokens = $query->get();
|
||
|
|
|
||
|
|
// 알림 설정 확인 후 필터링
|
||
|
|
$enabledTokens = [];
|
||
|
|
foreach ($tokens as $token) {
|
||
|
|
if ($this->isNotificationEnabledForUser($tenantId, $token->user_id, $notificationType)) {
|
||
|
|
$enabledTokens[] = $token->token;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $enabledTokens;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. 제외 항목
|
||
|
|
|
||
|
|
### 5.1 근태 알림 (정책 미확정)
|
||
|
|
|
||
|
|
다음 알림 타입은 이번 작업에서 **제외**:
|
||
|
|
- 연차 알림
|
||
|
|
- 출근 알림
|
||
|
|
- 지각 알림
|
||
|
|
- 결근 알림
|
||
|
|
|
||
|
|
**사유**: 정책이 모호하여 추후 별도 작업
|
||
|
|
|
||
|
|
### 5.2 알림 소리 커스터마이징
|
||
|
|
|
||
|
|
현재는 **하드코딩된 채널별 알림음** 사용:
|
||
|
|
- `push_urgent`: 긴급 (신규업체)
|
||
|
|
- `push_payment`: 결재
|
||
|
|
- `push_sales_order`: 수주
|
||
|
|
- `push_default`: 기타
|
||
|
|
|
||
|
|
**추후 작업**: 사용자별 알림 설정의 `soundType` 값 기준으로 발송
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. 영향받는 파일
|
||
|
|
|
||
|
|
### API (api/)
|
||
|
|
|
||
|
|
| 파일 | 변경 내용 |
|
||
|
|
|------|----------|
|
||
|
|
| `database/migrations/2026_01_28_132426_add_target_user_id_to_today_issues_table.php` | 신규 - 마이그레이션 |
|
||
|
|
| `app/Models/Tenants/TodayIssue.php` | target_user_id 추가, 신규 뱃지 상수, targetUser 관계, forUser/targetedTo 스코프 |
|
||
|
|
| `app/Services/TodayIssueObserverService.php` | createIssueWithFcm, sendFcmNotification, getEnabledUserTokens 수정, handleApprovalStatusChange 추가 |
|
||
|
|
| `app/Observers/TodayIssue/ApprovalIssueObserver.php` | 신규 - 기안 상태 변경 Observer |
|
||
|
|
| `app/Providers/AppServiceProvider.php` | ApprovalIssueObserver 등록 |
|
||
|
|
| `lang/ko/message.php` | 신규 메시지 키 추가 (draft_approved/rejected/completed) |
|
||
|
|
|
||
|
|
### React (react/) - 변경 없음
|
||
|
|
|
||
|
|
프론트엔드 알림 설정 UI는 이미 사용자별로 구현되어 있음.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. 검증 방법
|
||
|
|
|
||
|
|
### 7.1 테스트 시나리오
|
||
|
|
|
||
|
|
| # | 시나리오 | 예상 결과 |
|
||
|
|
|---|----------|----------|
|
||
|
|
| 1 | A가 B에게 결재 요청 | B에게만 FCM 발송 |
|
||
|
|
| 2 | B가 A의 기안 승인 | A에게만 FCM 발송 |
|
||
|
|
| 3 | B가 A의 기안 반려 | A에게만 FCM 발송 |
|
||
|
|
| 4 | 수주 등록 | 테넌트 전체 (알림 ON인 사용자만) |
|
||
|
|
| 5 | A가 알림 OFF → 수주 등록 | A에게는 발송 안됨 |
|
||
|
|
|
||
|
|
### 7.2 성공 기준
|
||
|
|
|
||
|
|
- [ ] 결재요청 알림이 결재자에게만 발송됨
|
||
|
|
- [ ] 기안 상태 변경 알림이 기안자에게만 발송됨
|
||
|
|
- [ ] 사용자별 알림 설정(ON/OFF)이 정상 동작함
|
||
|
|
- [ ] 기존 브로드캐스트 이슈(수주, 입금 등)는 정상 동작함
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. 참고 문서
|
||
|
|
|
||
|
|
- `api/app/Services/TodayIssueObserverService.php` - 현재 발송 로직
|
||
|
|
- `api/app/Models/NotificationSetting.php` - 알림 설정 모델
|
||
|
|
- `react/src/components/settings/NotificationSettings/types.ts` - 프론트엔드 알림 설정 타입
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. 변경 이력
|
||
|
|
|
||
|
|
| 날짜 | 항목 | 변경 내용 | 파일 | 승인 |
|
||
|
|
|------|------|----------|------|------|
|
||
|
|
| 2026-01-28 | - | 계획 문서 초안 작성 | - | - |
|
||
|
|
| 2026-01-28 | Phase 1 | target_user_id 컬럼 추가 마이그레이션 | migrations/2026_01_28_132426_* | ✅ |
|
||
|
|
| 2026-01-28 | Phase 2 | TodayIssue 모델 수정 (fillable, relation, scopes) | TodayIssue.php | ✅ |
|
||
|
|
| 2026-01-28 | Phase 3 | Observer 수정 (결재자/기안자 타겟팅) | TodayIssueObserverService.php, ApprovalIssueObserver.php | ✅ |
|
||
|
|
| 2026-01-28 | Phase 4 | FCM 발송 로직 수정 | TodayIssueObserverService.php | ✅ |
|
||
|
|
| 2026-01-28 | 신규 | ApprovalIssueObserver 생성 | ApprovalIssueObserver.php | ✅ |
|
||
|
|
| 2026-01-28 | i18n | 기안 상태 알림 메시지 추가 | lang/ko/message.php | ✅ |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*이 문서는 /sc:plan 스킬로 생성되었습니다.*
|