Files
sam-docs/dev/dev_plans/archive/fcm-user-targeted-notification-plan.md

369 lines
12 KiB
Markdown
Raw Normal View History

# 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 스킬로 생성되었습니다.*