- 완료된 계획 문서 22개를 plans/archive/로 이동 - tracked 16개 (git mv): bending-lot-pipeline, docs-update, fcm-notification 등 - untracked 6개 (mv): bending-worklog, formula-engine, mng-item 등 - index_plans.md 전면 업데이트 - 진행중 44개 / 완료 37개 현황 반영 - 각 문서별 실제 진행률 기재 (0%~94%) - 카테고리별 재정리 (견적/생산/품목/문서/마이그레이션/시스템/UI) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12 KiB
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 스킬로 생성되었습니다.