# GLOBEAU 뷰티샵 플랫폼 알림 서비스 매뉴얼

---

## 문서 제어 정보 (Document Control)

| 항목 | 내용 |
|---|---|
| **문서명** | GLOBEAU 뷰티샵 플랫폼 알림 서비스 매뉴얼 (Notification Service Manual) |
| **문서 버전** | v1.0.0 |
| **작성일자** | 2026-10-12 |
| **적용 대상** | GLOBEAU 웹 플랫폼 (사용자 마이페이지, 뷰티샵 파트너 센터, 본사 통합 어드민) |
| **문서 상태** | 공식 승인 (Approved) |

---

## 제1장. 시스템 개요 및 운영

### 1.1 서비스 목적 및 운영 안내
#### 1. **플랫폼 참여자 간의 원활하고 신속한 소통 지원**
   - 본 시스템의 가장 큰 목적은 GLOBEAU 플랫폼에서 일어나는 모든 소식을 **시스템, 일반 사용자, 뷰티샵 운영자, 본사 운영자가 서로 막힘없이 소통하도록 돕는 것**입니다.
   - **사용자**: 사이트 접속 중에는 마이페이지 알림함(웹 알림)에서, 이동 중에는 스마트폰(문자, 알림톡, 이메일)으로 본인의 예약이나 신청 결과를 즉시 확인합니다.
   - **본사 및 뷰티샵 운영자**: 관리자 전용 화면과 업무용 문자를 통해 신규 접수, 심사 요청, 예약 변동 사항을 실시간으로 확인하고 빠르게 대응합니다.
#### 2. **내 소식과 상태를 한곳에서 모아보는 마이페이지 연계**
   - 마이페이지는 로그인한 회원이 본인의 예약 내역, 신청 현황, 회원 정보를 확인하는 가장 익숙한 공간입니다.
   - 알림함을 엉뚱한 곳에 두지 않고 마이페이지 안에 배치하여, 사용자가 "내 정보와 진행 상태를 보러 가는 곳"에서 나에게 온 알림까지 한눈에 편안하게 찾아볼 수 있도록 구성했습니다.
#### 3. **알림 시스템을 구성하는 주요 5대 발송 채널의 역할
GLOBEAU 알림 시스템은 아래 5개 핵심 채널이 유기적으로 결합되어 운영됩니다.
   - **웹 알림 (Web Notification)**: 사이트 이용 중인 사용자가 다른 앱으로 이동할 필요 없이 화면 안에서 즉시 확인하고 다음 작업(매장 관리, 예약 확인 등)으로 연결되는 온사이트 편의성 제공. 마이페이지 내 개인화된 소식 집약.
   - **단문 문자 (SMS)**: 90바이트 이내의 핵심 요약 정보를 데이터 네트워크 연결 상태와 무관하게 모든 모바일 기기에 즉각적이고 확실하게 도달시키는 채널.
   - **장문 문자 (LMS)**: 상세한 안내 본문과 바로가기 URL을 포함하여 최대 2,000바이트까지 안정적으로 전달하는 대용량 모바일 채널.
   - **카카오 알림톡 (Alimtalk)**: 국민 메신저 카카오톡을 통해 공식 인증된 브랜드 프로필과 편리한 웹링크 버튼을 함께 전달하여 높은 신뢰도와 조작 편의성을 제공하는 메신저 채널.
   - **이메일 (Email)**: 장문의 상세 정책, 신청 명세서, 증빙 자료 등 데이터 보관성과 공식 문서 성격이 강한 소통을 담당하는 채널.

#### 4. 사용자 중심의 알림 보관 및 관리 정책 (수동 삭제 원칙)
- 사용자가 수신한 알림(웹 알림)은 시스템이 30일/90일 경과 후 임의로 파기하지 않습니다.
- 중요한 예약 번호, 입점 피드백 안내, 혜택 링크를 사용자가 스크랩처럼 언제든 다시 찾아볼 수 있도록 영구 보관하며, 사용자가 본인의 의사에 따라 직접 [삭제] 버튼을 눌러 관리하도록 지원합니다.


---

## 제2장. 알림 발송 정책 및 운영 체계

### 2.1 발송 방식 구분 (자동 발송 vs 수동 발송)
GLOBEAU 플랫폼의 알림 발송은 크게 시스템 비즈니스 로직에 의해 발송되는 **'자동 발송'**과 관리자가 의도적으로 전송하는 **'수동 발송'**으로 명확히 구분됩니다.

| 구분 | 발송 주체 | 트리거 방식 | 대상 지정 | 주요 사용 목적 |
|---|---|---|---|---|
| **자동 발송 (AUTO)** | 시스템 Bot | 비즈니스 로직 이벤트 감지 | 단일 수신자 (신청자, 담당자) | 입점 승인/반려, 예약 상태 변경, 결제/취소 |
| **본사 수동 발송 (MANUAL)** | 본사 최고 관리자 | 어드민 [메시지 직접 발송] 화면 | 전체 회원, 전체 뷰티샵, 특정인 | 서비스 점검 공지, 정책 변경, 마케팅 프로모션 |
| **뷰티샵 수동 발송 (MANUAL)** | 뷰티샵 원장/매니저 | 뷰티샵 [고객 관리 센터] 화면 | 해당 뷰티샵 이용/예약 고객 | 매장 임시 휴무 안내, 시술 전/후 주의사항 통보 |

---

### 2.2 자동 발송(System Trigger) 세부 정책 및 안전장치

#### 1. 템플릿 코드 식별 표준화
- 모든 자동 발송 템플릿은 `[도메인]_[행위]_[수신주체]` 구조의 고유 코드로 명명하여 코드만으로도 발송 목적과 대상을 즉시 식별할 수 있도록 합니다.
  - 예시: `SHOP_APPLY_ADMIN` (입점 신청 접수 시 본사 통보)
  - 예시: `SHOP_APPLY_USER` (입점 신청 승인 시 신청자 통보)
  - 예시: `RESERVATION_CONFIRM_USER` (예약 확정 시 고객 통보)

#### 2. 설정 파일 기반 치환 변수 중앙 레지스트리 (`config/notifications.php`)
하드코딩된 변수로 인한 오류를 방지하고, 템플릿 등록 시 허용된 변수를 한눈에 파악할 수 있도록 라라벨 설정 파일에서 치환 변수를 중앙 관리합니다.

    // config/notifications.php 설정 구조
    return [
        'common_variables' => [
            'user_name'    => '수신 회원명',
            'created_at'   => '신청/처리 일시',
            'service_name' => '플랫폼명 (기본: GLOBEAU)',
        ],
        'templates' => [
            'SHOP_APPLY_APPROVE_USER' => [
                'name' => '뷰티샵 입점 승인 안내 (신청자용)',
                'allowed_variables' => [
                    'shop_name' => '승인된 매장명',
                    'shop_code' => '매장 고유 코드',
                ],
                'default_action_url' => '/shops/#{shop_code}/status',
            ],
            'RESERVATION_CONFIRM_USER' => [
                'name' => '방문 예약 확정 안내 (고객용)',
                'allowed_variables' => [
                    'shop_name'        => '예약 뷰티샵명',
                    'reservation_date' => '예약 일시',
                    'menu_name'        => '시술 프로그램명',
                ],
                'default_action_url' => '/mypage/reservations',
            ],
        ],
    ];

- **UI 연동**: 본사 어드민 템플릿 수정 화면에서는 위 설정 파일의 정보를 읽어와 **"사용 가능한 치환 변수 뱃지"**를 화면에 자동으로 노출하며, 클릭 시 본문에 즉시 삽입되도록 지원합니다.

#### 3. 치환 검증 및 안전장치 (Fail-Safe)
- **변수 누락 방어**: 컨트롤러에서 `$variables` 배열에 일부 변수값을 누락하여 전달하더라도 시스템 Fatal Error가 발생하지 않도록, 미정의된 변수는 공백(`""`) 또는 기본 대시(`"-"`)로 안전하게 치환됩니다.
- **예상치 못한 미치환 태그 잔존 방지**: 발송 직전 정규식을 검사하여 미치환된 `#{...}` 패턴이 남아있을 경우 시스템 로그에 경고(`Log::warning`)를 기록하고 원문 형태를 정돈하여 발송합니다.

#### 4. 실시간 활성화 제어 및 채널별 토글
- 소스코드 수정이나 배포 없이 본사 관리자 화면에서 다음 항목을 실시간으로 켜고 끌 수 있습니다.
  - **전체 활성화 (`is_active`)**: 끄면 해당 템플릿의 모든 발송(웹, 문자)이 즉시 차단됩니다.
  - **웹 알림 여부 (`is_web_notify`)**: 회원 화면용 인앱 알림 적재 여부를 독립 제어합니다.
  - **문자 발송 여부 (`is_sms_notify`)**: 유료 문자(SMS/LMS) 전송 여부를 독립 제어합니다.

#### 5. 용량 기반 발송 타입 자동 승격 (SMS -> LMS)
- 본문 텍스트 치환 완료 후, EUC-KR 인코딩 기준 바이트 길이를 실시간 검사합니다.
- **90바이트 이하**: 일반 단문 `SMS`로 발송
- **90바이트 초과**: 시스템이 자동으로 장문 `LMS`로 승격 처리하여 텍스트 잘림을 방지하고, 통신사 요금 과금 구분을 정확히 기록합니다.

---

### 2.3 수동 발송(Manual Broadcast) 대량 발송 및 성능 방어 대책

수백~수천 명을 대상으로 하는 관리자 수동 공지 발송 시 발생할 수 있는 **웹 서버 타임아웃(PHP `max_execution_time` 30초 초과, Nginx 504 Gateway Timeout)**을 원천 차단하기 위한 아키텍처입니다.

#### 1. 웹 요청 즉시 분리 (비동기 처리 원칙)
- 관리자가 수동 발송 버튼을 누르면, 웹 컨트롤러는 동기 방식으로 외부 통신사 API를 호출하지 않습니다.
- 발송 기본 정보와 수신 대상 ID 목록을 담아 **`BulkNotificationDispatchJob`**을 라라벨 큐(Queue)로 디스패치한 뒤 **0.1초 만에 화면에 "발송 작업이 등록되었습니다"를 응답**합니다.

#### 2. 청크(Chunking) 분할 및 API 부하 제어
- 큐 워커(Queue Worker)는 백그라운드에서 수신 대상을 100~200명 단위의 청크(`chunkById`)로 분할하여 처리합니다.
- 통신사(카페24) API의 초당 요청 한도(TPS)를 준수할 수 있도록 배치 간 미세 지연(Sleep/Rate Limiting)을 두어 통신사 서버 차단 및 실패를 방지합니다.

#### 3. 발송 진행률(Progress) 실시간 추적
- 발송 시작 시 `notification_logs`에 대표 마스터 레코드를 `PENDING` 상태로 생성하고, 큐 워커가 발송을 완료할 때마다 처리 건수를 누적 갱신하여 관리자 화면에서 실시간 진행률(예: 850/1,000건 완료)을 확인할 수 있도록 합니다.

---

### 2.4 뷰티샵 관리자(Shop Admin)의 알림 발송 정책 및 권한 통제

뷰티샵 원장 및 매니저가 매장 고객에게 안내 알림을 발송할 때 적용되는 엄격한 격리 정책입니다.

#### 1. 수신 대상 범위의 엄격한 격리 (Tenant Isolation)
- 뷰티샵 관리자는 **자신의 매장을 이용한 이력이 있거나 현재 예약이 체결된 회원**에게만 알림을 보낼 수 있습니다.
- 전체 회원 DB나 타 매장 고객에 대한 임의 조회 및 발송 권한은 백엔드 쿼리 레벨에서 차단됩니다 (`where('shop_id', $currentShopId)`).

#### 2. 허용 발송 목적 및 채널 범위
- 예약 변경, 시술 전 주의사항 안내, 노쇼 확인, 매장 임시 휴무 공지 등 **매장 운영에 직접 관련된 안내**로 용도를 제한합니다.
- 광고성 홍보 메시지는 정보통신망법 준수를 위해 본사의 사전 승인된 양식 또는 정해진 마케팅 수신 동의 회원에게만 발송 가능합니다.

#### 3. 과금 및 발송 비용 정책
- 뷰티샵이 발송하는 외부 유료 문자(SMS/LMS)는 뷰티샵별 충전 포인트에서 차감하거나, 입점 계약에 따른 월간 무료 지원 한도 내에서 차감합니다.

#### 4. 본사 통합 감사(Audit) 및 모니터링
- 뷰티샵 관리자가 발송한 모든 내역 역시 중앙 `notification_logs`에 `sender_type='SHOP'`, `sender_id={shop_id}`로 빠짐없이 기록됩니다.
- 본사 운영자는 불법 스팸, 비방, 부적절한 홍보 발송 여부를 상시 모니터링하고 해당 매장의 발송 권한을 즉시 정지할 수 있습니다.

---

### 2.5 메시지 분기 및 이동 경로 정책 (방안 A)
- **본문(`content`) 순수 텍스트화**: 템플릿 본문에는 군더더기 없는 순수 안내 문구만 작성하며, 본문 안에 URL 주소를 직접 적지 않습니다.
- **이동 경로(`action_url`) 분리 관리**: 템플릿 설정의 전용 URL 필드에 이동할 경로를 따로 관리하며, 상대 경로(`/shops/#{shop_code}/status`)를 입력해 두면 시스템이 발송할 때 현재 도메인 주소를 자동으로 결합하여 완성합니다.
- **웹 알림 (인앱)**: 본문 내에 복잡한 인터넷 주소를 노출하지 않고, 상세 화면 하단의 전용 버튼([관련 페이지 바로가기])을 누르면 해당 화면으로 바로 이동하도록 처리합니다.
- **문자 메시지 (SMS/LMS)**: 문자는 버튼 기능이 없으므로, 시스템이 문자를 보낼 때 자동으로 본문 맨 아래에 `\n\n▶ 바로가기: {전체 도메인 URL}`을 덧붙여 전송합니다.
- **카카오 알림톡 규격 100% 호환**: 본문과 버튼 링크를 엄격히 분리하여 심사하는 카카오 알림톡 기준에 맞춰져 있으므로, 향후 알림톡을 도입할 때 문구를 다시 고칠 필요 없이 그대로 활용할 수 있습니다.

---

## 제3장. 데이터베이스 아키텍처 및 테이블 설계

본 장은 GLOBEAU 플랫폼 알림 시스템의 데이터베이스 구조와 설계 사상, 각 테이블의 역할 분류, 상세 테이블 명세서(DDL), 테이블 간의 상관관계 및 매핑 규칙, 데이터 적재 순서, 그리고 무결성 제약 규정을 체계적으로 기술합니다.

---

### 3.1 데이터베이스 구조 개요 및 역할 분류

GLOBEAU 알림 데이터베이스는 무분별하게 테이블을 단일화하거나 파편화하지 않고, **알림의 생성 규칙**, **수신자 확인 및 채널별 실행 로그**, 그리고 **본사 운영자의 총괄 관리**라는 3대 목적에 따라 명확히 역할을 분리하여 설계합니다.

```
[1. 규칙 정의 레이어]
notification_templates (자동 알림 생성 규칙 및 치환변수 마스터)
* 수동 발송 전용 테이블 신설 여부는 향후 업무 협의 후 결정

[2. 수신자 확인 및 개별 채널 실행/증빙 레이어]
├── notifications   : 사용자/관리자가 직접 화면에서 열람하는 인앱 웹 알림함
├── sms_logs        : 통신사(카페24) 문자 발송 결과 및 유료 과금 증빙 로그
├── email_logs      : 이메일 전송 결과 및 수신 이력 로그
└── alimtalk_logs   : [차후 확장] 카카오 알림톡 발송 상세 및 대체발송 증빙 로그

[3. 본사 운영자 통합 관리 레이어]
notification_logs (본사 운영자가 전체 발송 현황을 한곳에서 추적·모니터링하는 통합 관제 로그)
```

#### 1. 발송 규칙 및 메시지 생성 테이블 (발송 전 단계)
- **`notification_templates` (자동 템플릿 마스터)**:
  - 시스템 이벤트(입점 승인, 예약 체결 등)가 발생했을 때, 사전 정의된 치환변수(`#{...}`)를 통해 실제 본문과 `action_url`을 동적으로 조합합니다.
  - 해당 알림을 어떤 수신자에게, 어떤 채널(웹 알림, 문자 등)을 통해 발송할지 결정하는 기준 데이터입니다.
- **수동 발송 관련 향후 과제**:
  - 관리자가 비정기적으로 대량 공지나 마케팅 메시지를 작성하여 발송하는 '수동 발송'의 경우, 별도의 수동 발송 템플릿/캠페인 테이블을 신설할지, 아니면 기존 테이블 구조를 확장하여 수용할지는 향후 운영 정책 논의를 통해 결정합니다.

#### 2. 수신자 확인 및 개별 채널 실행/증빙 테이블 (수신자 접점 및 통신망 로그)
수신 주체(고객, 매장 원장, 관리자)가 각자의 화면이나 기기에서 개별적으로 확인하는 접점이자, 각 통신망별 실행/과금을 증빙하는 독립 테이블입니다.
- **`notifications`**: 사용자가 마이페이지나 상단 바에서 직접 조회하고 필요 시 수동 삭제하는 인앱 전용 사물함입니다.
- **`sms_logs`**: 인증번호나 알림 문자 등 카페24를 통해 발송된 모든 문자의 통신사 원천 영수증입니다.
- **`email_logs`**: 시스템이 발송한 공식 이메일의 전송 결과 로그입니다.
- **`alimtalk_logs`**: 카카오 알림톡 도달 결과와 LMS 대체발송(Fallback) 이력을 증빙하는 전용 로그입니다.

#### 3. 본사 운영자 통합 관리 테이블 (운영자 관제)
- **`notification_logs`**:
  - 본사 관리자가 "어느 채널로 발송되었고 성공/실패했는가"를 단일 화면에서 총괄 확인하기 위한 **유일한 통합 관제 테이블**입니다.
  - 하나의 비즈니스 사건으로 인해 파생된 여러 채널의 발송 내역들을 단일 묶음키(**`batch_id`**)로 묶어 관리합니다.
  - **독립성 원칙**: `notifications`나 `sms_logs`에 불필요하게 `batch_id`를 강제 결합하지 않으며, 본사 관리자는 오직 `notification_logs` 하나만 조회하여 전체 현황을 파악합니다.

---

### 3.2 테이블별 상세 명세서 (Schema Definition)

#### 1. 알림 템플릿 테이블 (`notification_templates`)
- **목적**: 시스템 자동 발송 문구 조립 규칙, 치환변수, 2단 업무 분류, 이동 경로 및 채널 정의
- **물리 테이블명**: `notification_templates`

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Bigint (unsigned) | N | Auto Increment | PK | 템플릿 고유 식별 일련번호 |
| **2** | 식별값 | `code` | Varchar(50) | N | - | Unique Index | 템플릿 고유 식별 코드 (예: `RESERVE_APPLY_USER`) |
| **3** | 업무분류 | `category` | Varchar(30) | N | - | Composite Index<br>(`category`, `subcategory`) | 대분류: `auth`, `reservation`, `billing`, `notice` |
| **4** | 업무분류 | `subcategory` | Varchar(30) | N | - | Composite Index<br>(`category`, `subcategory`) | 소분류: `verify`, `apply`, `confirm`, `cancel`, `remind`, `payment` |
| **5** | 수신대상 | `target` | Varchar(20) | N | 'user' | Index | 수신 대상: `user`(일반회원), `shop`(뷰티샵), `admin`(최고관리자) |
| **6** | 채널구분 | `type` | Varchar(20) | N | 'SMS' | - | 기본 전송 채널: `SMS`, `LMS`, `ALIMTALK`, `EMAIL` |
| **7** | 내용/제목 | `title` | Varchar(255) | Y | NULL | - | LMS 제목 / 알림톡 강조 타이틀 / 이메일 제목 |
| **8** | 내용/본문 | `content` | Text | N | - | - | 발송 본문 전문 (치환 변수 포함: `#{name}`, `#{shop_name}` 등) |
| **9** | 외부연동 | `template_key` | Varchar(100) | Y | NULL | - | 카카오 비즈니스 사전 승인 템플릿 코드 |
| **10** | 설명/가이드 | `description` | Text | Y | NULL | - | 관리자용 템플릿 설명 및 치환 가능 변수 가이드 |
| **11** | 제어스위치 | `is_active` | Boolean | N | 1 (true) | Index | 발송 활성화 여부 (1: 발송ON, 0: 발송OFF) |
| **12** | 제어스위치 | `is_web_notify` | Boolean | N | 1 (true) | - | 웹 내부 알림함(`notifications`) 적재 여부 |
| **13** | 연결링크 | `action_url` | Varchar(255) | Y | NULL | - | 웹 알림 클릭 시 이동할 기본 URL 또는 라우트명 |
| **14** | 일시 | `created_at` | Timestamp | Y | NULL | - | 레코드 생성 일시 |
| **15** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |
| **16** | 일시 | `deleted_at` | Timestamp | Y | NULL | - | 삭제 일시 (소프트 딜리트용) |

#### 2. 웹 알림함 테이블 (`notifications`)
- **목적**: 일반 사용자 및 뷰티샵 관리자가 마이페이지/상단 바에서 조회하는 인앱 알림 데이터
- **물리 테이블명**: `notifications` (라라벨 공식 표준)

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Char(36) / Uuid | N | - | PK | 알림 고유 식별자 (UUID v4) |
| **2** | 식별값 | `type` | Varchar(255) | N | - | - | 알림 클래스 네임스페이스<br>(예: `App\Notifications\SystemWebNotification`) |
| **3** | 다형성 | `notifiable_type` | Varchar(255) | N | - | Composite Index<br>(`notifiable_type`, `notifiable_id`) | 수신 대상 모델 클래스 (예: `App\Models\User`) |
| **4** | 다형성 | `notifiable_id` | Bigint (unsigned) | N | - | Composite Index<br>(`notifiable_type`, `notifiable_id`) | 수신 대상 모델의 기본키 PK (`user_id`) |
| **5** | 페이로드 | `data` | Text / LongText | N | - | - | 알림 본문 데이터 JSON 규격<br>- `title`: 알림 제목<br>- `message`: 알림 본문 전문<br>- `url`: 연동 목적지 링크<br>- `batch_id`: 관제 묶음키 (JSON 내 포함) |
| **6** | 상태값 | `read_at` | Timestamp | Y | NULL | Index | 알림 열람/확인 일시<br>- `NULL`: 미열람 (안 읽음, N 배지 활성화 대상)<br>- `Timestamp`: 열람 완료 (읽음) |
| **7** | 일시 | `created_at` | Timestamp | Y | NULL | - | 알림 발송/생성 일시 (목록 최신순 정렬 기준) |
| **8** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |

#### 3. 통신사 문자 전송 로그 테이블 (`sms_logs`)
- **목적**: 카페24 통신사를 통한 문자 발송 결과 증빙 및 유료 포인트 정산 내역 보관
- **물리 테이블명**: `sms_logs`

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Bigint (unsigned) | N | Auto Increment | PK | SMS 발송 로그 고유 식별자 |
| **2** | 식별값 | `user_id` | Bigint (unsigned) | Y | NULL | - | 수신 회원 ID (비회원/게스트 발송 시 NULL) |
| **3** | 수신대상 | `phone` | Varchar(20) | N | - | Composite Index<br>(`phone`, `type`) | 실제 수신 휴대폰 번호 (숫자만 정제 저장) |
| **4** | 내용/제목 | `title` | Varchar(100) | Y | NULL | - | 문자 제목 (LMS/알림톡 발송 시 표기) |
| **5** | 내용/본문 | `message` | Text | N | - | - | 실제 통신사로 전송된 문자 전체 본문 내용 |
| **6** | 업무구분 | `type` | Varchar(30) | N | 'verify' | Composite Index<br>(`phone`, `type`) | 발송 업무 구분 (`phone_verification`, `RESERVE_APPLY`, `NOTICE` 등) |
| **7** | 상태값 | `status` | Varchar(20) | N | 'success' | - | 전송 상태 (`success`: 성공, `failed`: 실패) |
| **8** | 실패사유 | `error_message` | Text | Y | NULL | - | 발송 실패 시 통신사 게이트웨이 응답 전문 또는 예외 사유 |
| **9** | 클라이언트 | `ip_address` | Varchar(45) | Y | NULL | - | 발송 요청 클라이언트 IP 주소 (어뷰징 방어 및 추적용) |
| **10** | 일시 | `created_at` | Timestamp | Y | NULL | Index | 발송 요청/생성 일시 (일자별 검색 기준) |
| **11** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |

#### 4. 이메일 발송 로그 테이블 (`email_logs`)
- **목적**: 플랫폼에서 발송된 공식 이메일의 전송 결과 및 도달 여부 기록
- **물리 테이블명**: `email_logs`

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Bigint (unsigned) | N | Auto Increment | PK | 이메일 로그 고유 식별 번호 |
| **2** | 식별값 | `user_id` | Bigint (unsigned) | Y | NULL | Index | 수신 회원 ID (비회원 발송 시 NULL) |
| **3** | 수신대상 | `recipient_email` | Varchar(255) | N | - | Index | 수신자 이메일 주소 |
| **4** | 내용/제목 | `subject` | Varchar(255) | N | - | - | 발송 메일 제목 |
| **5** | 내용/본문 | `content` | LongText | N | - | - | 발송된 실제 메일 본문 (HTML 전문) |
| **6** | 업무구분 | `type` | Varchar(50) | N | 'NOTICE' | - | 발송 업무 목적 (예: `RESERVE_NOTIFY`, `NOTICE`) |
| **7** | 상태값 | `status` | Varchar(20) | N | 'PENDING' | - | 전송 상태 (`PENDING`: 대기, `SUCCESS`: 성공, `FAIL`: 실패) |
| **8** | 실패사유 | `error_message` | Text | Y | NULL | - | 메일 발송 실패 시 SMTP 응답 에러 메시지 |
| **9** | 일시 | `created_at` | Timestamp | Y | NULL | - | 로그 생성/발송 일시 |
| **10** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |

#### 5. 카카오 알림톡 로그 테이블 (`alimtalk_logs` - 차후 신설)
- **목적**: 카카오 비즈메시지 API를 통한 알림톡 발송 결과 및 문자 대체발송(Fallback) 이력 관리
- **물리 테이블명**: `alimtalk_logs`

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Bigint (unsigned) | N | Auto Increment | PK | 알림톡 로그 고유 식별자 |
| **2** | 식별값 | `user_id` | Bigint (unsigned) | Y | NULL | Index | 수신 회원 ID (비회원 시 NULL) |
| **3** | 수신대상 | `phone` | Varchar(20) | N | - | Composite Index<br>(`phone`, `type`) | 수신 휴대폰 번호 (숫자만 정제 저장) |
| **4** | 외부식별 | `template_code` | Varchar(100) | N | - | Index | 카카오 비즈니스 사전 승인 템플릿 키 (`template_key`) |
| **5** | 내용/제목 | `title` | Varchar(100) | Y | NULL | - | 알림톡 강조 표기 타이틀 |
| **6** | 내용/본문 | `message` | Text | N | - | - | 발송된 알림톡 본문 전문 |
| **7** | 부가기능 | `buttons` | Json | Y | NULL | - | 알림톡에 부착된 링크 버튼 JSON (버튼명, 웹링크 URL 등) |
| **8** | 업무구분 | `type` | Varchar(30) | N | 'NOTICE' | Composite Index<br>(`phone`, `type`) | 발송 업무 구분 (`RESERVE_CONFIRM`, `SHOP_APPROVE` 등) |
| **9** | 상태값 | `status` | Varchar(20) | N | 'PENDING' | Composite Index<br>(`status`, `created_at`) | 전송 상태 (`SUCCESS`: 성공, `FAIL`: 실패, `PENDING`: 대기) |
| **10** | 결과코드 | `result_code` | Varchar(30) | Y | NULL | - | 카카오/중계사 반환 응답 코드 (`0000`: 성공 등) |
| **11** | 실패사유 | `error_message` | Text | Y | NULL | - | 발송 실패 상세 사유 |
| **12** | 대체발송 | `is_fallback_lms` | Boolean | N | 0 (false) | - | 카카오톡 실패로 인한 문자 대체발송(Fallback) 실행 여부 |
| **13** | 대체발송 | `fallback_sms_log_id` | Bigint (unsigned) | Y | NULL | Index | 대체발송 시 생성된 `sms_logs` 테이블의 ID 연동 |
| **14** | 클라이언트 | `ip_address` | Varchar(45) | Y | NULL | - | 발송 요청 IP 주소 |
| **15** | 일시 | `created_at` | Timestamp | Y | NULL | Composite Index<br>(`status`, `created_at`) | 생성 일시 (status와 복합 인덱스) |
| **16** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |

#### 6. 본사 운영자 통합 관리 로그 테이블 (`notification_logs` - 신설)
- **목적**: 본사 운영자가 전체 채널의 발송 사건을 한곳에서 추적·검색·감사하기 위한 최상위 관제 테이블
- **물리 테이블명**: `notification_logs`

| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :---: | :--- | :--- | :---: | :---: | :--- | :--- |
| **1** | 기본키 | `id` | Bigint (unsigned) | N | Auto Increment | PK | 통합 발송 로그 고유 식별자 |
| **2** | 묶음키 | `batch_id` | Varchar(64) | N | - | Index | 동일 사건 다중 채널 발송 묶음키 (`EVT_YYYYMMDD_랜덤문자`) |
| **3** | 발송유형 | `dispatch_type` | Varchar(20) | N | 'AUTO' | - | 발송 유형: `AUTO`(시스템 자동), `MANUAL`(관리자 수동) |
| **4** | 업무식별 | `trigger_code` | Varchar(50) | Y | NULL | Index | 템플릿 코드(`code`) 또는 수동 업무 식별자 |
| **5** | 발송주체 | `sender_type` | Varchar(20) | N | 'SYSTEM' | - | 발송 주체 구분: `SYSTEM`, `ADMIN`, `SHOP` |
| **6** | 발송주체 | `sender_id` | Bigint (unsigned) | Y | NULL | Index | 발송자 ID (관리자 `user_id` 또는 뷰티샵 `shop_id`) |
| **7** | 수신대상 | `receiver_type` | Varchar(20) | N | 'USER' | - | 수신 대상 구분: `USER`, `SHOP_OWNER`, `ADMIN`, `GUEST` |
| **8** | 수신대상 | `receiver_id` | Bigint (unsigned) | Y | NULL | Index | 수신 회원 ID (비회원 발송 시 NULL) |
| **9** | 수신대상 | `receiver_contact` | Varchar(100) | Y | NULL | Index | 수신 연락처 (전화번호 또는 이메일 주소) |
| **10** | 채널구분 | `channel` | Varchar(20) | N | - | Composite Index<br>(`created_at`, `channel`) | 전송 채널: `WEB`, `SMS`, `LMS`, `ALIMTALK`, `EMAIL` |
| **11** | 내용/제목 | `title` | Varchar(255) | Y | NULL | - | 메시지 제목 |
| **12** | 내용/본문 | `content` | Text | N | - | - | 치환 완료된 실제 발송 본문 전문 |
| **13** | 연결링크 | `action_url` | Varchar(255) | Y | NULL | - | 도메인이 결합된 연결 이동 전체 URL |
| **14** | 상태값 | `status` | Varchar(20) | N | 'SUCCESS' | Composite Index<br>(`channel`, `status`) | 전송 상태: `SUCCESS`, `FAIL`, `PENDING` |
| **15** | 결과코드 | `error_code` | Varchar(50) | Y | NULL | - | 에러 코드 |
| **16** | 실패사유 | `error_message` | Text | Y | NULL | - | 실패 상세 사유 (결번, 수신거부, 통신사 장애 등) |
| **17** | 클라이언트 | `ip_address` | Varchar(45) | Y | NULL | - | 발송 요청 IP 주소 |
| **18** | 일시 | `created_at` | Timestamp | Y | NULL | Composite Index<br>(`created_at`, `channel`) | 발송 생성 일시 (channel과 복합 인덱스) |
| **19** | 일시 | `updated_at` | Timestamp | Y | NULL | - | 레코드 최종 수정 일시 |

---

### 3.3 테이블 간 상관관계 및 데이터 연계 구조

#### 1. 테이블 연계 개념도

    [발송 규칙 마스터]
    notification_templates (템플릿 코드: code)
      │
      │ 1. 템플릿 참조 및 치환 (본문/URL/발송방식 결정)
      ▼
    [본사 운영자 통합 관리 테이블]
    notification_logs (운영자 관제 로그) ───────────── batch_id (사건 묶음키)
      │
      ├───────────────────────┬────────────────────────┬───────────────────────┐
      │ 채널: 'WEB'           │ 채널: 'SMS'/'LMS'      │ 채널: 'EMAIL'         │ [차후 확장]
      ▼                       ▼                        ▼                       ▼
    notifications           sms_logs                 email_logs              alimtalk_logs
    (사용자 웹 알림함)        (통신사 문자 로그)        (이메일 발송 로그)        (알림톡/대체발송 로그)
    - id (UUID)             - id                     - id                    - id
    - notifiable_id=회원ID  - phone (수신번호)       - recipient_email (수신) - phone (수신번호)
    - data[url]=action_url  - type (업무구분)        - type (업무구분)        - template_code (카카오키)
    - read_at (열람 확인)   - status (성공/실패)     - status (전송상태)      - status / result_code
                            - message (발송본문)     - content (메일본문)     - is_fallback_lms
                                                                             - buttons (링크버튼)

#### 2. 테이블 간 매핑 키 및 참조 규칙
- **발송 근거 추적**: `notification_templates.code`는 `notification_logs.trigger_code`에 매핑되어 어떤 비즈니스 규칙에 의해 알림이 발생했는지 추적합니다.
- **다중 채널 묶음 키 (`batch_id`)**: 단일 비즈니스 사건(예: 입점 승인)으로 인해 웹 알림 1건, 신청자 LMS 1건, 본사 담당자 LMS 1건이 발생할 경우, 3건 모두 동일한 `batch_id`를 부여하여 본사 관리자 화면에서 1건의 사건으로 묶어서 조회합니다.
- **수신 접점 데이터 연동 (실제 마이그레이션 기준)**:
  - `notification_logs.receiver_id` ⟷ `notifications.notifiable_id` (회원 웹 알림함 매핑)
  - `notification_logs.receiver_contact` ⟷ `sms_logs.phone` (문자 수신 휴대폰 번호 매핑)
  - `notification_logs.receiver_contact` ⟷ `email_logs.recipient_email` (수신자 이메일 주소 매핑)
  - `notification_logs.receiver_contact` ⟷ `alimtalk_logs.phone` (알림톡 수신 휴대폰 번호 매핑)
- **알림톡 대체발송(Fallback) 연동**:
  - 카카오톡 미설치나 수신 차단 등으로 알림톡 전송 실패 시, 자동으로 문자로 전환 발송되며 생성된 `sms_logs.id`가 `alimtalk_logs.fallback_sms_log_id`에 기록되어 과금 및 대체 이력을 교차 증빙합니다.

---

### 3.4 데이터 적재 흐름 및 생명주기 (Data Flow & Sequence)

비즈니스 이벤트(예: 뷰티샵 입점 승인 완료) 발생 시 시스템이 데이터를 처리하고 기록하는 물리적 순서는 다음과 같습니다.

    [단계 1: 템플릿 조회 및 활성화 검증]
    NotificationService가 notification_templates에서 code='SHOP_APPLY_APPROVE_USER'인
    레코드를 SELECT하여 본문, action_url, 활성화 여부(is_active), 웹알림 여부(is_web_notify)를 로드합니다.
    (is_active=false일 경우 발송 즉시 중단)
            │
            ▼
    [단계 2: 본문 치환 및 고유 batch_id 발행]
    전달된 변수(매장명, 매장코드 등)를 대입하여 본문과 action_url을 완성하고,
    본사 운영자 관제를 위한 고유 'batch_id' (예: EVT_20261012_A8F9D2)를 메모리에 생성합니다.
            │
            ▼
    [단계 3: 수신자 웹 알림함 적재 및 본사 관제 로그 기록 (채널: WEB)]
    is_web_notify=true이고 수신 회원 ID가 존재할 때:
    1) notifications 테이블에 신규 UUID 레코드 INSERT ($user->notify()) ──> 사용자가 화면에서 열람
    2) notification_logs 테이블에 batch_id='EVT_...', channel='WEB', status='SUCCESS' INSERT ──> 본사 기록
            │
            ▼
    [단계 4: 외부 통신사 문자 발송 및 영수증/본사 관제 로그 기록 (채널: LMS/SMS)]
    수신 전화번호가 존재할 때:
    1) EUC-KR 90바이트 초과 여부를 검사하여 SMS 또는 LMS 확정 후 하단에 바로가기 URL 첨부
    2) Cafe24SmsService 호출 ──> 통신사 전송 실행
    3) sms_logs 테이블에 통신사 결과 코드와 함께 INSERT ──> 통신사 원천 영수증 보관
    4) notification_logs 테이블에 동일한 batch_id='EVT_...', channel='LMS', status='SUCCESS'/'FAIL' INSERT ──> 본사 기록
            │
            ▼
    [단계 5: 본사 운영자 화면 조회]
    본사 관리자는 다른 테이블과 복잡하게 조인(JOIN)할 필요 없이,
    오직 notification_logs 테이블에서 WHERE batch_id = 'EVT_20261012_A8F9D2' 조건으로
    해당 사건으로 나간 웹 알림과 문자 발송 성공/실패 여부를 단일 팝업 모달에서 일괄 확인합니다.

---

### 3.5 데이터 무결성 및 시스템 제약 규정 (Integrity & Constraints)

#### 1. 논리적 참조 원칙 (물리 외래키 제약 배제)
- `notification_logs`, `sms_logs`, `email_logs`, `alimtalk_logs`는 회원(`users`)이나 매장(`shops`) 테이블에 대해 물리적인 데이터베이스 외래키(`FOREIGN KEY ... ON DELETE CASCADE`)를 설정하지 않습니다.
- **사유**: 회원이 서비스를 탈퇴하거나 매장 정보가 삭제되더라도, **법적 분쟁 소명 및 과거 발송 내역에 대한 감사(Audit) 증빙 데이터는 법정 의무 보관 기간 동안 영구 보존**되어야 하기 때문입니다.
- 회원이 탈퇴하더라도 `receiver_id`는 과거 식별자로 남고 `receiver_contact`에 발송 당시의 전화번호나 이메일이 고스란히 남아 증빙 능력을 온전히 유지합니다.

#### 2. 성능 최적화를 위한 인덱스(Index) 전략
- **`notification_logs`**:
  - `batch_id`: 사건별 다중 채널 그룹화 및 상세 조회 속도 극대화 (단일 Index)
  - `['created_at', 'channel']`: 일자별 채널 통계 및 기간별 검색 쿼리 커버링
  - `receiver_contact`: 전화번호/이메일 기반 특정 회원 CS 검색 최적화
- **`notifications`**:
  - `['notifiable_type', 'notifiable_id']`: 마이페이지 알림함 로딩 커버링 인덱스
  - `read_at`: 상단 헤더의 미확인 알림 건수(`unreadCount`) 카운팅 속도 보장
- **`sms_logs` / `alimtalk_logs`**:
  - `['phone', 'type']`: 특정 수신자의 업무별 발송 이력 조회 최적화

#### 3. 채널 간 부분 실패(Partial Failure) 격리 규정
- 특정 채널(예: 통신망 일시 장애로 인한 SMS 전송 실패)의 발송이 실패하더라도, 정상 발송된 내부 웹 알림(`notifications`)이나 이메일까지 함께 롤백(Rollback)시키지 않습니다.
- 각 채널 발송 로직은 개별 예외 처리(`try-catch`)로 격리되며, 실패한 채널의 `notification_logs.status`만 `FAIL`로 기록하고 `error_message`에 실패 원인을 남깁니다.
- 이를 통해 본사 운영자는 추후 관리자 화면에서 '실패한 특정 채널만 골라 재발송'하는 작업을 안전하게 수행할 수 있습니다.

---

## 제4장. 백엔드 서비스 파이프라인

### 4.1 발송 처리 흐름도

    [업무 이벤트 발생 (자동) 또는 관리자 발송 요청 (수동)]
                        │
                        ▼
           NotificationService::send() 호출
                        │
       ┌────────────────┴────────────────┐
       ▼                                 ▼
    1. config 기반 변수 검증 & 치환    2. action_url 도메인 완성
    (누락 변수 기본값 처리 안전장치)     (상대경로 -> 전체 도메인)
       │                                 │
       └────────────────┬────────────────┘
                        │
                        ▼
         [EUC-KR 90바이트 자동 검사]
         ├── 90바이트 이하 -> SMS 채널 유지
         └── 90바이트 초과 -> LMS 채널 자동 승격
                        │
                        ▼ batch_id 생성 ('EVT_날짜_랜덤값')
         ┌──────────────┴──────────────┐
         ▼                             ▼
    [채널 1: 웹 알림]             [채널 2: 문자 (SMS/LMS)]
    - notifications에 저장        - 본문 맨 아래에 링크 자동 첨부
    - channel='WEB' 기록          - 카페24 전송 API 호출
                                  - sms_logs에 영수증 기록
                                  - channel='SMS/LMS' 기록
                        │
                        ▼
        notification_logs 테이블에 최종 발송 성공/실패 기록 완료

### 4.2 통합 알림 서비스 구현 (`app/Services/NotificationService.php`)

    namespace App\Services;

    use App\Models\NotificationTemplate;
    use App\Models\User;
    use App\Notifications\SystemWebNotification;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Support\Facades\Log;
    use Illuminate\Support\Str;

    class NotificationService
    {
        public function __construct(
            protected Cafe24SmsService $smsService
        ) {}

        /**
         * [시스템 자동 알림 통합 발송]
         */
        public function send(
            string $templateCode,
            string $receiverPhone,
            array $variables = [],
            ?int $userId = null,
            ?string $actionUrl = null,
            array $extraData = []
        ): bool {
            $batchId = 'EVT_' . now()->format('YmdHis') . '_' . Str::upper(Str::random(6));

            try {
                // 1. 템플릿 조회 및 실시간 활성화 여부 검증
                $template = NotificationTemplate::where('code', $templateCode)->first();
                if (!$template || !$template->is_active) {
                    Log::warning("[NotificationService] 비활성화 또는 누락된 템플릿: {$templateCode}");
                    return false;
                }

                // 2. 제목, 본문, 이동 링크 치환 (Fail-Safe 변수 안전장치 적용)
                $title   = $this->replaceVariables($template->title ?? '', $variables);
                $content = $this->replaceVariables($template->content ?? '', $variables);
                $rawActionUrl = $actionUrl ?: ($template->action_url ?? null);
                $fullActionUrl = $this->resolveActionUrl($rawActionUrl, $variables);

                // 3. 웹 알림 처리 (템플릿별 On/Off 토글 반영)
                $isWebNotify = isset($template->is_web_notify) ? (bool)$template->is_web_notify : true;
                if ($userId && $isWebNotify) {
                    $this->dispatchWebNotification(
                        batchId: $batchId,
                        templateCode: $templateCode,
                        userId: $userId,
                        title: $title ?: '[GLOBEAU 알림]',
                        content: $content,
                        actionUrl: $fullActionUrl,
                        extraData: $extraData
                    );
                }

                // 4. 문자 발송 처리 (cleanPhone 존재 시)
                $cleanPhone = preg_replace('/[^0-9]/', '', $receiverPhone);
                if (!empty($cleanPhone)) {
                    $this->dispatchSmsNotification(
                        batchId: $batchId,
                        templateCode: $templateCode,
                        receiverPhone: $cleanPhone,
                        title: $title ?: '[GLOBEAU]',
                        content: $content,
                        fullActionUrl: $fullActionUrl,
                        userId: $userId
                    );
                }

                return true;
            } catch (\Exception $e) {
                Log::error("[NotificationService Exception] Code: {$templateCode} | Msg: " . $e->getMessage());
                return false;
            }
        }

        protected function replaceVariables(string $text, array $variables): string
        {
            foreach ($variables as $key => $val) {
                $text = str_replace("#{{$key}}", (string)$val, $text);
            }
            return preg_replace('/#\{[a-zA-Z0-9_]+\}/', '-', $text);
        }

        protected function dispatchWebNotification(
            string $batchId,
            string $templateCode,
            int $userId,
            string $title,
            string $content,
            ?string $actionUrl,
            array $extraData
        ): void {
            $user = User::find($userId);
            if (!$user) return;

            // 1. 사용자 웹 알림함 저장 (data JSON 내부에 batch_id 포함)
            $user->notify(new SystemWebNotification([
                'batch_id'    => $batchId,
                'code'        => $templateCode,
                'title'       => $title,
                'message'     => $content,
                'url'         => $actionUrl,
                'source_type' => $extraData['source_type'] ?? 'system',
                'source_id'   => $extraData['source_id'] ?? null,
            ]));

            // 2. 본사 운영자 관제 로그 적재
            DB::table('notification_logs')->insert([
                'batch_id'         => $batchId,
                'dispatch_type'    => 'AUTO',
                'trigger_code'     => $templateCode,
                'sender_type'      => 'SYSTEM',
                'sender_id'        => null,
                'receiver_type'    => 'USER',
                'receiver_id'      => $userId,
                'receiver_contact' => $user->phone ?? $user->email,
                'channel'          => 'WEB',
                'title'            => $title,
                'content'          => $content,
                'action_url'       => $actionUrl,
                'status'           => 'SUCCESS',
                'created_at'       => now(),
                'updated_at'       => now(),
            ]);
        }

        protected function dispatchSmsNotification(
            string $batchId,
            string $templateCode,
            string $receiverPhone,
            string $title,
            string $content,
            ?string $fullActionUrl,
            ?int $userId
        ): void {
            $smsBody = $content;
            if (!empty($fullActionUrl)) {
                $smsBody .= "\n\n▶ 바로가기: " . $fullActionUrl;
            }

            // 용량 기반 자동 승격 (EUC-KR 90바이트 초과 여부 검사)
            $channelType = (mb_strwidth($smsBody, 'euc-kr') > 90) ? 'LMS' : 'SMS';

            // 1. 통신사 발송 호출 (sms_logs는 서비스 내부에서 별도 기록)
            $smsResult = $this->smsService->send(
                receiverPhone: $receiverPhone,
                message: $smsBody,
                title: $title,
                type: $templateCode,
                userId: $userId
            );

            // 2. 본사 운영자 관제 로그 적재
            DB::table('notification_logs')->insert([
                'batch_id'         => $batchId,
                'dispatch_type'    => 'AUTO',
                'trigger_code'     => $templateCode,
                'sender_type'      => 'SYSTEM',
                'sender_id'        => null,
                'receiver_type'    => $userId ? 'USER' : 'GUEST',
                'receiver_id'      => $userId,
                'receiver_contact' => $receiverPhone,
                'channel'          => $channelType,
                'title'            => $title,
                'content'          => $smsBody,
                'action_url'       => $fullActionUrl,
                'status'           => $smsResult ? 'SUCCESS' : 'FAIL',
                'error_message'    => $smsResult ? null : 'SMS 전송 실패',
                'created_at'       => now(),
                'updated_at'       => now(),
            ]);
        }

        protected function resolveActionUrl(?string $actionUrl, array $variables = []): ?string
        {
            if (empty($actionUrl)) return null;

            $actionUrl = $this->replaceVariables($actionUrl, $variables);

            return str_starts_with($actionUrl, 'http://') || str_starts_with($actionUrl, 'https://')
                ? $actionUrl
                : url($actionUrl);
        }
    }

### 4.3 전역 뷰 컴포저 설정 (`app/Providers/AppServiceProvider.php`)

    namespace App\Providers;

    use Illuminate\Support\ServiceProvider;
    use Illuminate\Support\Facades\View;
    use Illuminate\Support\Facades\Auth;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            View::composer(['layouts.*', 'mypage.*'], function ($view) {
                $view->with(
                    'unreadCount',
                    Auth::check() ? Auth::user()->unreadNotifications()->count() : 0
                );
            });
        }
    }

---

## 제5장. 프론트엔드 UI·UX 가이드라인

### 5.1 글로벌 헤더(GNB/서브헤더) [N] 배지 동작 기준
- **오직 안 읽은 알림 기준**: 매장 승인 여부나 심사 상태와 무관하게, **확인하지 않은 알림(`$unreadCount > 0`)이 있을 때만 프로필/종 아이콘 우측 상단에 붉은색 [N] 배지**가 뜹니다.
- **수량 표기**: 드롭다운 메뉴 안에는 실제 안 읽은 개수를 숫자로 표시하며, 100건 이상일 때는 `99+`로 단축 표기합니다.

    <!-- 상단 헤더 프로필 버튼 뱃지 표준 -->
    <button class="btn btn-sm border-0 p-0 text-dark fs-5 dropdown-toggle position-relative" ...>
        <i class="fas fa-user-circle text-brand"></i>
        @if(($unreadCount ?? 0) > 0)
            <span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger border border-white" style="font-size: 0.55rem; padding: 0.25em 0.4em;">
                N
            </span>
        @endif
    </button>

### 5.2 마이페이지 대시보드 진입 동선
- **프로필 카드**: 상단 내 정보 영역 우측에 [알림함] 바로가기 버튼을 두고 붉은 배지를 연결합니다.
- **메뉴 그리드**: 6대 핵심 메뉴 중 1번 카드에 [나의 알림함] 큰 카드를 배치하여 `N건 안 읽음`을 한눈에 보여줍니다.
- **헤더와 연결**: 헤더 드롭다운 첫 번째 메뉴를 `마이페이지 홈`으로 두어 언제든 대시보드로 쉽게 돌아올 수 있게 합니다.

### 5.3 알림함 목록(Index) - 1줄 게시판 구조
- **한눈에 보기**: 카드 형태 대신 1줄 게시판 형태를 사용하여, 화면 스크롤을 많이 내리지 않고도 10~15개 이상의 알림 제목과 시간을 편안하게 훑어볼 수 있습니다.
- **읽음/안 읽음 구분**: 안 읽은 알림은 새하얀 배경에 굵은 글씨와 붉은색 N 배지를 붙이고, 이미 읽은 알림은 연한 회색 배경(`bg-light bg-opacity-50`)과 보통 글씨로 차분하게 보여줍니다.
- **무한 스크롤**: 50개 단위로 끊어서 보여주며, 화면 맨 아래로 내리면 다음 알림이 자동으로 로딩됩니다.

### 5.4 알림 상세(Show) - 버튼 크기 및 역할 정리
- **버튼 높이 통일**: 아래쪽에 나란히 놓이는 [삭제], [목록으로], [관련 페이지 바로가기] 3개 버튼의 높이를 모두 `40px`로 맞추어 줄이 삐뚤빼뚤하지 않고 단정합니다.
- **버튼 역할 강조**:
  - **관련 페이지 바로가기**: 사용자가 눌러야 할 가장 중요한 버튼이므로, 우측에 넓은 패딩(`px-4`)과 브랜드 색상(`btn-premium`)으로 돋보이게 만듭니다.
  - **알림 삭제**: 보조 기능이므로 좌측에 아담한 크기(`12px` 패딩)와 부드러운 테두리(`btn-outline-secondary`)로 배치하여 실수로 누르지 않게 합니다.

### 5.5 알림 삭제 확인 창(SweetAlert2)
- 알림 삭제는 결제 취소처럼 위험한 작업이 아니므로 빨간색 경고창을 띄우지 않고, 친절한 물음표(`question`) 아이콘과 함께 "[알림 제목] 알림을 삭제하시겠습니까?"라는 1문장 확인 창을 띄워 사용자의 불편함을 덜어줍니다.

---

## 제6장. 본사 및 샵 관리자 운영 매뉴얼

### 6.1 관리자 통합 메시지 센터 메뉴 구조

    [본사 관리자 (Super Admin)]
    └── 메시지 / 알림 센터
        ├── 1. 알림 템플릿 관리   (routes: admin.notifications.templates.*)
        │      └── 시스템 자동 발송 문구, #{변수}, 이동 경로, 채널별 On/Off 관리
        ├── 2. 메시지 직접 발송   (routes: admin.notifications.send.*)
        │      └── 대상 선택(전체 회원, 뷰티샵 전체, 특정 회원) + 채널 선택 후 수동 발송
        └── 3. 통합 발송 내역     (routes: admin.notifications.logs.*)
               └── notification_logs 기반 단일 검색 및 batch_id 모달 상세 조회

### 6.2 알림 템플릿 문구 및 변수 작성 방법
- **변수 작성**: 문구나 이동 주소 안에 `#{변수명}` 형태로 적습니다. (예: `#{name}`, `#{shop_name}`, `#{code}`)
- **주소 입력**: 이동 주소 입력칸에는 도메인 주소(`https://...`)를 뺀 뒷부분(`/shops/#{shop_code}/status`)만 적습니다. 시스템이 알아서 완전한 주소로 결합합니다.
- **자동 승격 확인**: 입력 중인 본문이 한글 45자(90바이트)를 넘으면 화면에 `[LMS 승격]` 배지가 실시간으로 표기됩니다.

### 6.3 CS 응대 및 발송 실패 확인 방법
1. 고객이나 매장 원장님이 "문자를 못 받았다"고 문의할 때
2. 본사 관리자 화면의 [통합 발송 내역] 메뉴에서 해당 전화번호나 이름으로 검색
3. 상태가 `FAIL`(실패)로 되어 있다면 우측 [상세보기]를 눌러 실패 사유(번호 오류, 스팸 차단, 통신사 일시 오류 등)를 확인
4. 정확한 번호를 확인한 후 화면에서 [수동 재발송] 버튼 클릭

---

## 제7장. 컴플라이언스 및 기술 확장 로드맵

### 7.1 정보통신망법 준수 (광고성 메시지 분리)
- **정보성 안내**: 입점 심사, 예약 확정, 결제 내역 등 서비스 이용에 꼭 필요한 알림은 수신 동의 여부와 상관없이 즉시 발송할 수 있으며 밤 시간 발송 제한이 없습니다.
- **광고/홍보성 안내**: 할인 쿠폰, 기획전, 이벤트 안내는 반드시 사전에 마케팅 수신에 동의한 회원(`marketing_opt_in = true`)에게만 발송합니다.
- **표기 의무**: 광고 메시지에는 문장 맨 앞에 `(광고)`, 맨 뒤에 `무료수신거부: 080-XXX-XXXX`를 시스템에서 자동으로 붙여줍니다.
- **야간 발송 제한**: 밤 8시(20:00)부터 다음 날 아침 8시(08:00)까지는 광고 메시지를 보낼 수 없으므로, 이 시간에 등록된 광고는 스케줄러가 다음 날 오전 8시 이후로 발송을 자동 이월합니다.

### 7.2 비동기 대기열(Queue Worker) 전환 계획
수백 명 이상의 회원에게 단체 공지를 보낼 때 화면이 하얗게 멈추거나 서버 시간 초과(504 에러)가 나지 않도록, 백그라운드 작업(Queue Job)으로 분리 발송합니다.

    // 관리자 화면은 즉시 완료 응답 처리
    SendNotificationJob::dispatch($templateCode, $receiverPhone, $variables, $userId);

- DB에 먼저 발송 요청을 대기(`PENDING`) 상태로 적재하고, 서버 백그라운드 프로세스가 차례대로 문자를 발송한 뒤 성공(`SUCCESS`)으로 상태를 갱신합니다.

### 7.3 뷰티 O2O 예약 스케줄러(CRM / 노쇼 방지) 연동
라라벨 스케줄러(`Cron`)를 통해 시간이 되면 자동으로 알림을 보냅니다.
1. **노쇼 방지 전날 알림**: 매일 오후 6시(18:00) 실행 → 다음 날 방문 예약이 있는 고객에게 리마인더 발송.
2. **당일 매장 길 안내**: 매 시간 실행 → 시술 2시간 전 매장 위치와 주차 안내 발송.
3. **시술 후기 작성 안내**: 매 시간 실행 → 시술 완료 2시간 후 포인트 적립 안내와 리뷰 작성 링크 발송.
4. **정기 관리 주기 안내**: 네일이나 피부 관리 주기(약 3~4주)가 지나면 단골 케어 추천 알림 발송.