Files
sam-docs/dev/dev_plans/archive/fcm-user-targeted-notification-plan.md
권혁성 db63fcff85 refactor: [docs] 팀별 폴더 구조 재편 (공유/개발/프론트/기획)
- 개발팀 전용 폴더 dev/ 생성 (standards, guides, quickstart, changes, deploys, data, history, dev_plans 이동)
- 프론트엔드 전용 폴더 frontend/ 생성 (api/ → frontend/api-specs/)
- 기획팀 폴더 requests/ 생성
- plans/ → dev/dev_plans/ 이름 변경
- README.md 신규 (사람용 안내), INDEX.md 재작성 (Claude Code용)
- resources.md 신규 (노션 링크용, assets/brochure 이관 예정)
- CURRENT_WORKS.md 삭제, TODO.md → dev/ 이동
- 전체 참조 경로 업데이트

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:46:03 +09:00

12 KiB

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: 데이터베이스 변경

마이그레이션 파일:

// 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 모델 수정

// 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 수정

결재요청 - 결재자에게만:

// 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  // 결재자
);

기안 승인/반려/완료 - 기안자에게만 (신규):

// 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 발송 로직 수정

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