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