# Cafe24 SMS 연동 및 알림 서비스

## 1. 개요
- **목적**: 플랫폼 관리자의 모듈형 SMS 알림 템플릿 관리 체계 구축 및 뷰티샵 입점 신청 완료 시 관리자에게 자동으로 SMS/LMS를 발송하는 파이프라인 완성.
- **핵심 아키텍처**: 
  - 설정 파일 기반(`config/notification_categories.php`)의 동적 카테고리/소분류 매핑 및 유효성 검증.
  - 범용 `NotificationService->send()` 메서드를 통한 단일화된 알림 발송 래퍼 구축.
  - EUC-KR 기준 90바이트 초과 시 SMS/LMS 자동 분기 및 `sms_logs` 테이블 연동.
  - `.env` 기반의 `ADMIN_PHONE_NUMBER` 연동 및 `SMS_TEST_MODE`를 통한 개발 비용 절감(샌드박스) 방어 체계 적용.

---

## 2. 주요 구현 내용 및 코드 구조

### 2.1. 범용 알림 서비스 (`NotificationService.php`)
- 템플릿 코드를 기반으로 활성화 상태를 확인하고, 내장된 제목/본문 파서를 통해 치환 변수를 바인딩한 뒤 발송 서비스로 전달합니다.

#### <NotificationService.php>

    namespace App\Services;

    use App\Models\NotificationTemplate;
    use Illuminate\Support\Facades\Log;

    class NotificationService
    {
        public function __construct(
            protected Cafe24SmsService $smsService
        ) {}

        public function send(string $templateCode, string$receiver, array $variables = [], ?int $userId = null): bool
        {
            try {
                $template = NotificationTemplate::where('code',$templateCode)->first();

                if (!$template \vert{}\vert{} !$template->is_active) {
                    Log::warning("[NotificationService] 비활성화되었거나 존재하지 않는 템플릿: {$templateCode}");
                    return false;
                }

                $title   =$template->parseTitle($variables);$content = $template->parseContent($variables);

                return $this->smsService->send(
                    receiverPhone: $receiver,
                    message: $content,
                    title: $title ?: '[GLOBEAU]',
                    type: $templateCode,
                    userId: $userId
                );
            } catch (\Exception $e) {
                Log::error("[NotificationService Exception] Code: {$templateCode} \vert{} Error: " . $e->getMessage());
                return false;
            }
        }
    }
<br>

### 2.2. SMS 발송 및 테스트 모드가 통합된 서비스 (`Cafe24SmsService.php`)
- 실제 API 통신 전 `SMS_TEST_MODE`를 확인하여 비용 발생 없이 안전하게 로그를 적재할 수 있으며, 90바이트 기준 SMS/LMS 자동 분기를 지원합니다.

#### <Cafe24SmsService.php>
    namespace App\Services;

    use Illuminate\Support\Facades\Http;
    use Illuminate\Support\Facades\Log;
    use Illuminate\Support\Facades\DB;

    class Cafe24SmsService
    {
        protected string $userId;
        protected string $apiKey;
        protected string $sender;
        protected string $apiUrl;

        public function __construct()
        {
            $this->userId = config('services.cafe24.user_id');$this->apiKey = config('services.cafe24.api_key');
            $this->sender = config('services.cafe24.sender');$this->apiUrl = 'https://sslsms.cafe24.com/sms_sender.php'; 
        }

        public function send(
            string $receiverPhone,
            string $message,
            ?string $title = null,
            string $type = 'SMS',
            ?int $userId = null
        ): bool {
            $cleanReceiver = preg_replace('/[^0-9]/', '',$receiverPhone);
            $cleanSender   = preg_replace('/[^0-9]/', '',$this->sender);

            if (env('SMS_TEST_MODE', false)) {
                Log::info("[Cafe24SmsService] 테스트 모드 작동 (실제 발송 차단)", [
                    'type'     => $type,
                    'receiver' => $cleanReceiver,
                    'message'  => $message,
                ]);

                try {
                    DB::table('sms_logs')->insert([
                        'user_id'       => $userId,
                        'phone'         => $cleanReceiver,
                        'message'       => $message,
                        'type'          => $type,
                        'status'        => 'success',
                        'error_message' => '[SMS_TEST_MODE SIMULATION]',
                        'ip_address'    => request()->ip() ?? '127.0.0.1',
                        'created_at'    => now(),
                        'updated_at'    => now(),
                    ]);
                } catch (\Exception $e) {
                    Log::error("[Cafe24SmsService] 테스트 로그 적재 실패: " . $e->getMessage());
                }

                return true;
            }

            $byteLength = mb_strwidth($message, 'EUC-KR');
            $msgType = ($byteLength > 90) ? 'L' : 'S';

            $data = [
                'user_id' => $this->userId,
                'secure'  => $this->apiKey,
                'sphone1' => substr($cleanSender, 0, 4),                 'sphone2' => substr($cleanSender, 4, 4),
                'sphone3' => substr($cleanSender, 8, 4),                 'rphone'  =>$cleanReceiver,
                'msg'     => $message,
                'rdate'   => '',
                'rtime'   => '',
                'mode'    => '1',
                'return_url' => '',
                'testflag'    => '',
                'subject'     => ($msgType === 'L') ? ($title ?? '[GLOBEAU]') : '',
            ];

            try {
                $response = Http::asForm()->post($this->apiUrl,$data);
                $resultBody = trim($response->body());

                $isSuccess = str_contains($resultBody, 'success') \vert{}\vert{} str_contains($resultBody, 'Test Success');
                $status =$isSuccess ? 'success' : 'failed';
                $errorMessage = $isSuccess ? null : $resultBody;

                DB::table('sms_logs')->insert([
                    'user_id'       => $userId,
                    'phone'         => $cleanReceiver,
                    'message'       => $message,
                    'type'          => $type,
                    'status'        => $status,
                    'error_message' => $errorMessage,
                    'ip_address'    => request()->ip() ?? '127.0.0.1',
                    'created_at'    => now(),
                    'updated_at'    => now(),
                ]);

                return $isSuccess;

            } catch (\Exception $e) {
                Log::error("[Cafe24SmsService Exception] " . $e->getMessage());

                DB::table('sms_logs')->insert([
                    'user_id'       => $userId,
                    'phone'         => $cleanReceiver,
                    'message'       => $message,
                    'type'          => $type,
                    'status'        => 'failed',
                    'error_message' => $e->getMessage(),
                    'ip_address'    => request()->ip() ?? '127.0.0.1',
                    'created_at'    => now(),
                    'updated_at'    => now(),
                ]);

                return false;
            }
        }
    }
<br>

### 2.3. 뷰티샵 입점 신청 컨트롤러 연동부
- 데이터베이스 트랜잭션 성공(`DB::commit()`) 직후, 뷰티샵 입점신청의 경우 최고관리자 번호(`.env`의 `ADMIN_PHONE_NUMBER`)로 알림을 발송하는 예입니다.

#### <ShopController.php>

    // 뷰티샵 컨트롤의 Create 메소드에서 입점신정을 완료한후 아래 SMS 전송
    ...
    DB::beginTransaction();
    try {
        $shop = Shop::create([...]);
        DB::commit();
    } catch (\Exception $e) {
        DB::rollBack();
        return back()->withErrors(['error' => '입점 신청 처리 중 오류가 발생했습니다: ' . $e->getMessage()])->withInput();
    }

    // 범용 알림서비스로 SMS 전송 양식(중요)
    app(NotificationService::class)->send(
        templateCode: 'SHOP_APPLY_ADMIN', // notification_templates 테이블 code 필드 (알림 템플릿 사용)
        receiver: env('ADMIN_PHONE_NUMBER', '010-0000-0000'), // 등록된 최고관리자 번호가 있다면 사용한다.
        variables: [
            'shop_name'       => $shop->name,
            'ceo_name'        => $shop->ceo_name,
            'applicant_name'  => $currentUser?->name ?? $shop->ceo_name,
            'applicant_phone' => $currentUser?->phone ?? $shop->user?->phone ?? '',
            'created_at'      => $shop->created_at?->format('Y-m-d H:i:s') ?? now()->format('Y-m-d H:i:s'),
        ],
        userId: $currentUser?->id
    );

---

## 3. 검증 및 테스트 방법
- **Laravel Tinker를 활용한 즉시 검증**:

#### <터미널>
    php artisan tinker

    app(App\Services\NotificationService::class)->send(
        templateCode: 'SHOP_APPLY_ADMIN',
        receiver: env('ADMIN_PHONE_NUMBER'),
        variables: [
            'shop_name'       => '테스트뷰티샵',
            'ceo_name'        => '홍길동',
            'applicant_name'  => '테스터',
            'applicant_phone' => '01012345678',
            'created_at'      => now()->format('Y-m-d H:i:s'),
        ],
        userId: null
    );
<br>

- **비용 방어 설정 (`.env`)**:
  - 개발 환경에서는 `SMS_TEST_MODE=true`로 설정하여 실제 통신비 발생 없이 로직 및 `sms_logs` 적재 상태를 검증하고, 실서비스 오픈 시 `false`로 전환합니다.
