diff --git a/INDEX.md b/INDEX.md index 19f7060..15bdd9a 100644 --- a/INDEX.md +++ b/INDEX.md @@ -188,6 +188,7 @@ DB 도메인별: | [construction-pmis/bim-viewer.md](features/construction-pmis/bim-viewer.md) | BIM 뷰어 (Three.js 기반 웹 3D 건물 모델 뷰어) | | [rd/README.md](features/rd/README.md) | R&D 메뉴 전체 개요 | | [rd/fire-shutter-drawing-guide-rail.md](features/rd/fire-shutter-drawing-guide-rail.md) | 방화셔터 가이드레일 SVG/3D 렌더링 기술 명세 | +| [notification-settings/README.md](features/notification-settings/README.md) | 서비스 알림설정 (5개 테이블, API 엔드포인트, 알림음 시스템, Gap 분석) | --- diff --git a/features/notification-settings/README.md b/features/notification-settings/README.md new file mode 100644 index 0000000..9094226 --- /dev/null +++ b/features/notification-settings/README.md @@ -0,0 +1,554 @@ +# 서비스 알림설정 (Notification Settings) + +> **작성일**: 2026-03-18 +> **상태**: 구현 완료 (soundType 연동 미완) +> **대상**: API (`sam/api`) + React (`sam/react`) + +--- + +## 1. 개요 + +### 1.1 목적 + +사용자가 알림 유형별로 수신 채널(푸시/이메일/SMS/인앱/카카오)과 알림음을 개별 설정할 수 있는 기능이다. + +### 1.2 핵심 원칙 + +- **그룹 기반 UI**: 알림 유형을 8개 그룹으로 분류하여 React UI에서 카테고리별 관리 +- **채널별 ON/OFF**: 푸시, 이메일, SMS, 인앱, 카카오톡 5개 채널 독립 제어 +- **알림음 선택**: 기본음 / SAM 보이스 / 무음 3종 선택 +- **멀티테넌트 격리**: 모든 설정은 `tenant_id` 기반 격리 +- **기본값 정책**: 설정 미저장 시 알림 유형별 기본값 자동 적용 + +--- + +## 2. 테이블 구조 + +### 2.1 ERD 개요 + +``` +User (1) ─── (N) notification_settings # 채널별 ON/OFF +User (1) ─── (N) push_notification_settings # 푸시 전용 (알림음/진동/미리보기) +User (1) ─── (N) notification_setting_group_states # 그룹 전체 ON/OFF +User (1) ─── (N) push_device_tokens # FCM 토큰 + +Tenant (1) ── (N) notification_setting_groups # 그룹 메타데이터 +Group (1) ── (N) notification_setting_group_items # 그룹-알림유형 매핑 +``` + +### 2.2 notification_settings + +사용자별 알림 채널 ON/OFF 설정. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint PK | — | +| `tenant_id` | bigint FK | 테넌트 | +| `user_id` | bigint FK | 사용자 | +| `notification_type` | varchar(50) | 알림 유형 (approval, order 등) | +| `push_enabled` | boolean | 푸시 (기본: true) | +| `email_enabled` | boolean | 이메일 (기본: false) | +| `sms_enabled` | boolean | SMS (기본: false) | +| `in_app_enabled` | boolean | 인앱 (기본: true) | +| `kakao_enabled` | boolean | 카카오 알림톡 (기본: false) | +| `settings` | json | 추가 설정 (우선순위, 알림 시간대 등) | + +> **UNIQUE**: `(tenant_id, user_id, notification_type)` + +### 2.3 push_notification_settings + +모바일 앱 푸시 전용 고급 설정 (알림음, 진동, 미리보기). + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint PK | — | +| `tenant_id` | bigint FK | 테넌트 | +| `user_id` | bigint FK | 사용자 | +| `notification_type` | varchar(50) | 알림 유형 | +| `is_enabled` | boolean | 푸시 알림 활성화 (기본: true) | +| `sound` | varchar(100) | 알림음 파일명 (기본: 'default') | +| `vibrate` | boolean | 진동 (기본: true) | +| `show_preview` | boolean | 미리보기 (기본: true) | + +> **UNIQUE**: `(tenant_id, user_id, notification_type)` + +### 2.4 notification_setting_groups + +알림을 UI에서 그룹으로 표시하기 위한 메타데이터 (테넌트별 커스터마이징 가능). + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint PK | — | +| `tenant_id` | bigint FK | 테넌트 | +| `code` | varchar(50) | 그룹 코드 | +| `name` | varchar(100) | 그룹명 | +| `sort_order` | smallint | 정렬 순서 | +| `is_active` | boolean | 활성화 여부 | + +> **UNIQUE**: `(tenant_id, code)` + +### 2.5 notification_setting_group_items + +그룹-알림유형 매핑 (어떤 알림 타입이 어떤 그룹에 속하는지). + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint PK | — | +| `group_id` | bigint FK | 그룹 | +| `notification_type` | varchar(50) | 알림 타입 | +| `label` | varchar(100) | 항목 라벨 | +| `sort_order` | smallint | 정렬 순서 | + +> **UNIQUE**: `(group_id, notification_type)` + +### 2.6 notification_setting_group_states + +사용자가 그룹 전체를 ON/OFF 할 수 있도록 그룹별 활성화 상태 저장. + +| 컬럼 | 타입 | 설명 | +|------|------|------| +| `id` | bigint PK | — | +| `tenant_id` | bigint FK | 테넌트 | +| `user_id` | bigint FK | 사용자 | +| `group_code` | varchar(50) | 그룹 코드 | +| `enabled` | boolean | 그룹 전체 활성화 (기본: true) | + +> **UNIQUE**: `(tenant_id, user_id, group_code)` + +--- + +## 3. 알림 유형 분류 + +### 3.1 알림 채널 설정 유형 (NotificationSetting — 8가지) + +| 상수 | 값 | 한글명 | 기본 활성 채널 | +|------|------|--------|---------------| +| `TYPE_APPROVAL` | `approval` | 전자결재 | 푸시, 인앱 | +| `TYPE_ORDER` | `order` | 수주 | 푸시, 인앱 | +| `TYPE_DEPOSIT` | `deposit` | 입금 | 푸시, 인앱 | +| `TYPE_WITHDRAWAL` | `withdrawal` | 출금 | 푸시, 인앱 | +| `TYPE_NOTICE` | `notice` | 공지사항 | 푸시, 인앱 | +| `TYPE_SYSTEM` | `system` | 시스템 | 푸시, 인앱 | +| `TYPE_MARKETING` | `marketing` | 마케팅 | 모두 OFF | +| `TYPE_SECURITY` | `security` | 보안 | 푸시, 이메일, 인앱 | + +### 3.2 그룹 기반 세부 알림 유형 (20가지) + +| 그룹 코드 | 그룹명 | 포함 알림 유형 | 항목 수 | +|-----------|--------|---------------|---------| +| `notice` | 공지 알림 | notice, event | 2 | +| `schedule` | 일정 알림 | vat_report, income_tax_report | 2 | +| `vendor` | 거래처 알림 | new_vendor, credit_rating | 2 | +| `attendance` | 근태 알림 | annual_leave, clock_in, late, absent | 4 | +| `order` | 수주/발주 알림 | sales_order, purchase_order | 2 | +| `approval` | 전자결재 알림 | approval_request, draft_approved, draft_rejected, draft_completed | 4 | +| `production` | 생산 알림 | safety_stock, production_complete | 2 | +| `collection` | 채권/지출 알림 | bad_debt, expected_expense | 2 | + +### 3.3 snake_case ↔ camelCase 매핑 + +React(camelCase) ↔ API(snake_case) 변환이 `NotificationSettingGroup::CAMEL_CASE_MAP`에 정의: + +``` +vat_report ↔ vatReport new_vendor ↔ newVendor +income_tax_report ↔ incomeTaxReport credit_rating ↔ creditRating +annual_leave ↔ annualLeave clock_in ↔ clockIn +sales_order ↔ salesOrder purchase_order ↔ purchaseOrder +approval_request ↔ approvalRequest draft_approved ↔ draftApproved +draft_rejected ↔ draftRejected draft_completed ↔ draftCompleted +safety_stock ↔ safetyStock production_complete ↔ productionComplete +bad_debt ↔ badDebt expected_expense ↔ expectedExpense +``` + +--- + +## 4. API 엔드포인트 + +### 4.1 그룹 기반 설정 (React UI 연동) + +| Method | Path | 설명 | 인증 | +|--------|------|------|------| +| GET | `/api/v1/settings/notifications` | 그룹 기반 알림 설정 조회 | auth:sanctum | +| PUT | `/api/v1/settings/notifications` | 그룹 기반 알림 설정 업데이트 | auth:sanctum | + +**컨트롤러**: `NotificationSettingController::indexGrouped()`, `updateGrouped()` + +#### GET 응답 구조 + +```json +{ + "success": true, + "data": { + "notice": { + "enabled": true, + "notice": { "enabled": true, "email": false }, + "event": { "enabled": true, "email": false } + }, + "approval": { + "enabled": true, + "approvalRequest": { "enabled": true, "email": true }, + "draftApproved": { "enabled": true, "email": false }, + "draftRejected": { "enabled": true, "email": false }, + "draftCompleted": { "enabled": true, "email": false } + } + } +} +``` + +#### PUT 요청 구조 + +```json +{ + "notice": { + "enabled": true, + "notice": { "enabled": true, "email": false }, + "event": { "enabled": true, "email": false } + } +} +``` + +### 4.2 플랫 구조 설정 (개별 타입) + +| Method | Path | 설명 | 인증 | +|--------|------|------|------| +| GET | `/api/v1/users/me/notification-settings` | 알림 설정 조회 | auth:sanctum | +| PUT | `/api/v1/users/me/notification-settings` | 단일 타입 업데이트 | auth:sanctum | +| PUT | `/api/v1/users/me/notification-settings/bulk` | 일괄 업데이트 | auth:sanctum | + +**컨트롤러**: `NotificationSettingController::index()`, `update()`, `bulkUpdate()` + +### 4.3 푸시 알림 전용 설정 + +| Method | Path | 설명 | 인증 | +|--------|------|------|------| +| POST | `/api/v1/push/register-token` | FCM 토큰 등록/갱신 | auth:sanctum | +| POST | `/api/v1/push/unregister-token` | FCM 토큰 비활성화 | auth:sanctum | +| GET | `/api/v1/push/tokens` | 등록된 토큰 목록 | auth:sanctum | +| GET | `/api/v1/push/settings` | 푸시 설정 조회 | auth:sanctum | +| PUT | `/api/v1/push/settings` | 푸시 설정 업데이트 | auth:sanctum | +| GET | `/api/v1/push/notification-types` | 알림 유형/알림음 목록 | auth:sanctum | + +**컨트롤러**: `PushNotificationController` + +--- + +## 5. 알림음 시스템 + +### 5.1 알림음 옵션 (React UI) + +```typescript +export const SOUND_OPTIONS = [ + { value: 'default', label: '기본 알림음' }, + { value: 'sam_voice', label: 'SAM 보이스' }, + { value: 'mute', label: '무음' } +] +``` + +> **React types.ts에 정의됨**: `soundType: 'default' | 'sam_voice' | 'mute'` + +### 5.2 음원 파일 현황 + +#### 물리적 파일 위치 + +| 위치 | 파일 | 크기 | 상태 | +|------|------|------|------| +| `mng/public/sounds/default.wav` | 기본 알림음 | 788KB | ✅ 실제 파일 | +| `mng/public/sounds/push_notification.wav` | 푸시 알림음 | 788KB | ✅ 실제 파일 | +| `react/public/sounds/default.wav` | 기본 알림음 | 0 bytes | ❌ 빈 placeholder | +| `react/public/sounds/push_notification.wav` | 푸시 알림음 | 0 bytes | ❌ 빈 placeholder | + +#### 앱(Android) 알림음 채널 + +앱에서는 Android NotificationChannel로 채널별 알림음이 동작한다. + +| 채널 ID | 알림음 파일 | 용도 | +|---------|-----------|------| +| `push_default` | `res/raw/push_default.wav` | 일반 알림 | +| `push_urgent` | `res/raw/push_urgent.wav` | 긴급 알림 (신규업체) | +| `push_payment` | `res/raw/push_payment.wav` | 결제 알림 | +| `push_sales_order` | `res/raw/push_sales_order.wav` | 수주 알림 | +| `push_purchase_order` | `res/raw/push_purchase_order.wav` | 발주 알림 | +| `push_contract` | `res/raw/push_contract.wav` | 계약 알림 | + +### 5.3 FCM 채널 매핑 (config/fcm.php) + +```php +'channels' => [ + 'default' => 'push_default', + 'vendor_register' => 'push_vendor_register', + 'approval_request' => 'push_approval_request', + 'income' => 'push_income', + 'sales_order' => 'push_sales_order', + 'purchase_order' => 'push_purchase_order', + 'contract' => 'push_contract', +], +``` + +### 5.4 FcmSender 사운드 매핑 + +`FcmSender::getSoundForChannel()`: + +```php +return match ($channelId) { + 'push_vendor_register' => 'push_vendor_register', + 'push_approval_request' => 'push_approval_request', + 'push_income' => 'push_income', + 'push_sales_order' => 'push_sales_order', + 'push_purchase_order' => 'push_purchase_order', + 'push_contract' => 'push_contract', + default => 'push_default', +}; +``` + +### 5.5 PushNotificationSetting 알림음 상수 + +```php +const SOUND_DEFAULT = 'default'; +const SOUND_DEPOSIT = 'deposit.wav'; +const SOUND_WITHDRAWAL = 'withdrawal.wav'; +const SOUND_ORDER = 'order.wav'; +const SOUND_APPROVAL = 'approval.wav'; +const SOUND_URGENT = 'urgent.wav'; +``` + +--- + +## 6. 알림 발송 흐름 + +### 6.1 비즈니스 이벤트 발송 + +``` +비즈니스 이벤트 (예: 수주 생성) + ↓ +PushNotificationService::notifySalesOrder() + ↓ +PushNotificationService::sendByEvent() + ├── event → channelId 매핑 + ├── PushDeviceToken 조회 (tenant_id, is_active=true) + └── FcmSender::sendToMany() + ├── 토큰 chunk 분할 (200개/배치) + ├── FCM HTTP v1 API 발송 + │ ├── android: channel_id + sound 파일명 + │ └── apns: sound = 'default' (고정) + └── FcmBatchResult 반환 +``` + +### 6.2 사용자별 타겟 발송 + +``` +결재 이벤트 (예: 결재요청) + ↓ +TodayIssueObserverService::handleApprovalStepChange() + ├── target_user_id = step->user_id (결재자) + ├── TodayIssue 생성 (target_user_id 포함) + └── sendFcmNotification() + ├── getEnabledUserTokens(tenantId, type, targetUserId) + │ ├── targetUserId 있음 → 해당 사용자 토큰만 조회 + │ └── targetUserId 없음 → 테넌트 전체 토큰 조회 + └── 알림 설정 확인 후 발송 +``` + +### 6.3 발송 대상 정책 + +| 이슈 타입 | 발송 대상 | 비고 | +|-----------|----------|------| +| 결재요청 | 결재자 (step.user_id) | 타겟 발송 | +| 기안 승인/반려/완료 | 기안자 (approval.drafter_id) | 타겟 발송 | +| 수주등록 | 테넌트 전체 | 브로드캐스트 | +| 입금/출금 | 테넌트 전체 | 브로드캐스트 | +| 신규업체 등록 | 테넌트 전체 | 브로드캐스트 | +| 안전재고/추심 | 테넌트 전체 | 브로드캐스트 | + +--- + +## 7. React 프론트엔드 + +### 7.1 컴포넌트 구조 + +``` +settings/notification-settings/page.tsx ← 페이지 + └── NotificationSettings/index.tsx ← 메인 컴포넌트 + ├── 섹션별 카드 (8개 그룹) + │ ├── 그룹 토글 (전체 ON/OFF) + │ └── 개별 항목 (NotificationItemRow) + │ ├── 알림 ON/OFF 토글 + │ ├── 알림음 선택 (Select + Play 버튼) + │ └── 이메일 체크박스 + └── ItemSettingsDialog.tsx ← 항목 표시/숨김 모달 +``` + +### 7.2 Server Action (API 호출) + +| 함수 | 엔드포인트 | 설명 | +|------|-----------|------| +| `getNotificationSettings()` | `GET /api/v1/settings/notifications` | 설정 조회 | +| `saveNotificationSettings()` | `PUT /api/v1/settings/notifications` | 설정 저장 | + +**데이터 변환**: +- `transformApiToFrontend()` — API 응답을 React 타입으로 변환 (soundType 기본값 병합) +- `transformFrontendToApi()` — React 구조를 API 형식으로 변환 + +### 7.3 항목 표시/숨김 (ItemVisibility) + +- localStorage에 저장 (서버 X) +- 사용자가 불필요한 알림 항목을 UI에서 숨길 수 있음 +- 알림 설정 자체는 변경하지 않음 (UI 커스터마이징만) + +--- + +## 8. 서비스 계층 + +### 8.1 NotificationSettingService + +| 메서드 | 설명 | +|--------|------| +| `getSettings()` | 사용자의 알림 설정 조회 (기본값 포함) | +| `updateSetting()` | 단일 알림 타입 설정 업데이트 | +| `bulkUpdateSettings()` | 일괄 업데이트 (트랜잭션) | +| `initializeDefaultSettings()` | 새 사용자 기본 설정 초기화 | +| `isChannelEnabled()` | 특정 채널 활성화 여부 확인 | +| `getGroupedSettings()` | 그룹 기반 조회 (React 구조) | +| `updateGroupedSettings()` | 그룹 기반 업데이트 (React 구조) | +| `initializeGroupsIfNeeded()` | 테넌트 그룹 정의 자동 초기화 | + +### 8.2 PushNotificationService + +| 메서드 | 설명 | +|--------|------| +| `registerToken()` | FCM 토큰 등록/갱신 | +| `unregisterToken()` | 토큰 비활성화 | +| `getSettings()` | 푸시 설정 조회 | +| `updateSettings()` | 푸시 설정 업데이트 | +| `sendByEvent()` | 이벤트 기반 푸시 발송 | +| `notifyNewClient()` | 신규 거래처 알림 | +| `notifyPayment()` | 결제 알림 | +| `notifySalesOrder()` | 수주 알림 | + +### 8.3 FcmSender + +| 메서드 | 설명 | +|--------|------| +| `sendToToken()` | 단일 토큰 발송 | +| `sendToMany()` | 대량 발송 (chunk 200개, delay 100ms) | +| `getSoundForChannel()` | 채널 ID → 사운드 파일명 매핑 | +| `getAccessToken()` | OAuth2 토큰 발급 (캐싱) | + +### 8.4 AdminFcmService + +MNG 관리자가 보내는 수동 푸시 알림 관리. + +| 메서드 | 설명 | +|--------|------| +| `send()` | 대상 필터링 → 발송 → 이력 저장 | +| `previewCount()` | 발송 대상 수 미리보기 | +| `getHistory()` | 발송 이력 조회 | + +--- + +## 9. Gap 분석 — soundType 미연동 + +### 9.1 현재 상태 + +| 항목 | React | API | 상태 | +|------|-------|-----|------| +| `soundType` 타입 정의 | ✅ `types.ts` | — | React만 선행 | +| `soundType` UI (드롭다운) | ✅ `index.tsx` | — | 렌더링됨 | +| `soundType` 미리듣기 | 🟡 Mock (토스트만) | — | 실제 재생 없음 | +| `soundType` 저장 | ✅ API 호출 시 전송 | ❌ 무시됨 | **Gap** | +| `soundType` 조회 | ✅ 기본값 'default' 병합 | ❌ 반환 안 함 | **Gap** | +| 음원 파일 서빙 | ❌ 빈 placeholder | — | **Gap** | + +### 9.2 React types.ts 주석 (2026-01-05) + +```typescript +/** + * [2026-01-05] 백엔드 API 수정 필요 사항 + * 1. NotificationItem에 soundType 필드 추가 + * - 기존: { enabled: boolean, email: boolean } + * - 변경: { enabled: boolean, email: boolean, soundType: 'default' | 'sam_voice' | 'mute' } + */ +``` + +### 9.3 구현 필요 항목 + +#### API 측 + +1. **그룹 기반 응답에 soundType 포함** + - `NotificationSettingService::getGroupedSettings()` 수정 + - 각 항목에 `soundType` 필드 추가 (기본값: `'default'`) + +2. **그룹 기반 업데이트에 soundType 저장** + - `NotificationSettingService::updateGroupedSettings()` 수정 + - `soundType` 값을 `notification_settings.settings` JSON 또는 `push_notification_settings.sound` 컬럼에 저장 + +3. **음원 파일 API 제공** + - 옵션 A: `GET /api/v1/sounds/{soundType}.wav` — API에서 직접 서빙 + - 옵션 B: React `public/sounds/` 경로에 실제 음원 파일 배치 — 정적 파일로 서빙 + - **권장**: 옵션 B (React에서 직접 서빙, CDN 캐싱 가능) + +#### React 측 + +4. **음원 파일 배치** + - `mng/public/sounds/default.wav` → `react/public/sounds/default.wav` 복사 + - SAM 보이스 음원 제작 후 `react/public/sounds/sam_voice.wav` 배치 + +5. **미리듣기 실제 구현** + - `playPreviewSound()` 함수에서 `Audio` 객체로 실제 재생 + - 음원 URL: `/sounds/{soundType}.wav` + +--- + +## 10. 관련 파일 + +### API (`sam/api`) + +| 파일 | 설명 | +|------|------| +| `app/Models/NotificationSetting.php` | 알림 채널 설정 모델 | +| `app/Models/PushNotificationSetting.php` | 푸시 전용 설정 모델 | +| `app/Models/NotificationSettingGroup.php` | 알림 그룹 모델 (DEFAULT_GROUPS, CAMEL_CASE_MAP) | +| `app/Models/NotificationSettingGroupItem.php` | 그룹 아이템 모델 | +| `app/Models/NotificationSettingGroupState.php` | 그룹 상태 모델 | +| `app/Models/PushDeviceToken.php` | FCM 토큰 모델 | +| `app/Http/Controllers/Api/V1/NotificationSettingController.php` | 알림 설정 컨트롤러 | +| `app/Http/Controllers/Api/V1/PushNotificationController.php` | 푸시 설정 컨트롤러 | +| `app/Services/NotificationSettingService.php` | 알림 설정 서비스 | +| `app/Services/PushNotificationService.php` | 푸시 알림 서비스 | +| `app/Services/Fcm/FcmSender.php` | FCM 발송 서비스 | +| `app/Services/AdminFcmService.php` | 관리자 FCM 서비스 | +| `app/Services/TodayIssueObserverService.php` | 이슈 알림 발송 | +| `config/fcm.php` | FCM 채널 설정 | + +### React (`sam/react`) + +| 파일 | 설명 | +|------|------| +| `src/components/settings/NotificationSettings/types.ts` | 타입 정의 (soundType 포함) | +| `src/components/settings/NotificationSettings/index.tsx` | 메인 UI 컴포넌트 | +| `src/components/settings/NotificationSettings/actions.ts` | Server Action (API 호출) | +| `src/components/settings/NotificationSettings/ItemSettingsDialog.tsx` | 항목 표시/숨김 모달 | +| `src/app/[locale]/(protected)/settings/notification-settings/page.tsx` | 페이지 | +| `src/lib/capacitor/fcm.ts` | FCM 핸들러 (sound_key 참조) | +| `public/sounds/` | 알림음 파일 (현재 빈 placeholder) | + +### MNG (`sam/mng`) + +| 파일 | 설명 | +|------|------| +| `app/Http/Controllers/FcmController.php` | FCM 테스트 발송 | +| `app/Services/FcmApiService.php` | MNG → API FCM 발송 | +| `resources/views/fcm/send.blade.php` | 테스트 발송 UI | +| `public/sounds/default.wav` | ✅ 실제 기본 알림음 (788KB) | +| `public/sounds/push_notification.wav` | ✅ 실제 푸시 알림음 (788KB) | + +### 관련 문서 + +| 문서 | 설명 | +|------|------| +| `docs/dev/dev_plans/flow-tests/notification-settings-flow.json` | 플로우 테스트 시나리오 | +| `docs/dev/dev_plans/archive/notification-sound-system-plan.md` | 알림음 시스템 구현 계획 (완료) | +| `docs/dev/dev_plans/archive/fcm-user-targeted-notification-plan.md` | FCM 사용자별 발송 계획 (완료) | + +--- + +**최종 업데이트**: 2026-03-18