서비스 알림설정 (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 응답 구조
{
"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 요청 구조
{
"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)
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)
'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():
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 알림음 상수
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 버튼 | 완료 |
| 미리듣기 실제 재생 | — | ❌ Mock (토스트만) | React 작업 대기 |
| 음원 파일 서빙 | — | ❌ 빈 placeholder | 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
- 음원 파일 배치 —
mng/public/sounds/default.wav→react/public/sounds/복사 - 미리듣기 실제 재생 —
playPreviewSound()에서AudioAPI 사용 - types.ts 주석 정리 — "백엔드 API 수정 필요" 블록 제거
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