# 서비스 알림설정 (Notification Settings)

> **작성일**: 2026-03-18
> **상태**: API 구현 완료 (React 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 음원 파일 현황

#### 물리적 파일 위치

| 위치 | 파일 | 크기 | 상태 |
|------|------|------|------|
| `api/public/sounds/default.wav` | 기본 알림음 | 788KB | ✅ API에서 서빙 |
| `api/public/sounds/sam_voice.wav` | SAM 보이스 | 788KB | ✅ API에서 서빙 (임시로 기본음 동일) |
| `mng/public/sounds/default.wav` | 기본 알림음 | 788KB | ✅ 실제 파일 (원본) |
| `mng/public/sounds/push_notification.wav` | 푸시 알림음 | 788KB | ✅ 실제 파일 |

> React에는 음원 파일을 두지 않는다. API 서버가 정적 파일로 서빙하며, 응답의 `_soundUrls`로 URL을 제공한다.

#### 앱(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. soundType 연동 현황

### 9.1 현재 상태 (2026-03-18)

| 항목 | API | React | 상태 |
|------|-----|-------|------|
| `soundType` 저장 | ✅ `settings.sound_type` JSON에 저장 | ✅ PUT 요청 시 전송 | **완료** |
| `soundType` 조회 | ✅ 응답에 `soundType` 포함 | ✅ 기본값 병합 | **완료** |
| `soundType` 검증 | ✅ `in:default,sam_voice,mute` | ✅ 타입 정의 | **완료** |
| `soundType` UI | — | ✅ 드롭다운 + Play 버튼 | **완료** |
| 음원 파일 서빙 | ✅ `api/public/sounds/` | — | **API에서 서빙** |
| `_soundUrls` 응답 | ✅ GET 응답에 URL 맵 포함 | — | **완료** |
| 미리듣기 실제 재생 | — | ❌ Mock (토스트만) | **React 작업 대기** |

### 9.2 API 구현 완료 내용

**저장 흐름**:
```
React PUT → { soundType: "sam_voice" }
  → NotificationSettingService::updateGroupedSettings()
    → notification_settings.settings JSON = { "sound_type": "sam_voice" }
```

**조회 흐름**:
```
NotificationSettingService::getGroupedSettings()
  → notification_settings.settings['sound_type'] 읽기
    → 미저장 시 기본값 'default'
      → React 응답: { enabled, email, soundType }
```

### 9.3 React 구현 대기 항목

> **요청 문서**: `docs/plans/notification-sound-react-request.md`

1. **미리듣기 실제 재생** — `playPreviewSound()`에서 `_soundUrls`의 URL로 `Audio` API 재생
2. **`_soundUrls` 수신** — `actions.ts`에서 API 응답의 `_soundUrls` 추출
3. **types.ts 주석 정리** — "백엔드 API 수정 필요" 블록 제거

> 음원 파일은 API 서버에서 서빙한다. React에 파일 배치 불필요.

---

## 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/plans/notification-sound-react-request.md` | soundType React 구현 요청서 (프론트 전달용) |
| `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
