# GLOBEAU 뷰티 플랫폼: 뷰티샵 입점 신청 및 심사/알림 아키텍처 기술 명세서

## 1. 시스템 환경 및 기술 스택

* **Server OS:** Ubuntu 22.04 LTS
* **Web Server:** Apache2
* **Backend Framework:** Laravel 13 (PHP 8.4)
* **Database:** MariaDB
* **UI/CSS:** Bootstrap 5, FontAwesome, public/css/admin_custom.css (브랜드 모카 테마: #b18977)
* **Auth & Permission:** Laravel Breeze, Socialite (Kakao, Naver, Google SNS 로그인), Spatie Laravel-Permission
* **Core Modules:**
  * 중앙 집중형 미디어 탐색기 (Media Explorer)
  * 다계층 카테고리 연동 시스템
  * Daum 우편번호 API + Kakao Local Geocoder (LBS 위치 좌표 자동 산출)
  * Cafe24 SMS / LMS 연동 및 NotificationTemplate 기반 비동기 알림 시스템

---

## 2. 입점 신청 프로세스 및 라이프사이클 (State Lifecycle)

        [비회원/일반회원]
        │
        ▼ (SNS 로그인 / 소셜 인증 완료)
        [인증 회원 (Role: user)] ──> 헤더 별도 파트너 유입 배너/아이콘 진입 (/shops/create)
        │
        ├─ 휴대폰 본인인증 (VerifyPhoneController: 타 계정 중복 검증 필수)
        ├─ 사업자등록번호 진위확인 (VerifyBusinessController)
        ├─ 매장 주소 검색 (Daum Postcode API) ──> LBS 좌표(Lat/Lng) 자동 산출
        │
        ▼ (POST /shops -> ShopController@store)
        [입점 신청 완료: pending]
        │
        ├─ DB 트랜잭션: Shop 생성 + User 휴대폰번호 연동 업데이트
        ├─ 알림 발송: 신청자(LMS: SHOP_APPLY_USER) / 최고관리자(LMS: SHOP_APPLY_ADMIN)
        │
        ▼
        [본사 관리자 심사 대기]
        │
        ├─ [반려: rejected] ──> 반려사유(reject_reason) 등록
        │      │
        │      └─ 헤더 드롭다운: [뷰티샵 반려] (NEW 배지 점등)
        │         마이페이지(/mypage/shop-apply)에서 반려 사유 확인 및 재수정
        │
        └─ [승인: approved] ──> Spatie 권한/역할 갱신 (shop_admin 부여)
        │
        └─ 헤더 드롭다운: [뷰티샵 관리자 센터] 전환 (승인 배지 점등)
        클릭 시 뷰티샵 백오피스(/shop-admin) 진입

## 3. 데이터베이스 스키마 확장 명세 (Migration)

### 3.1 뷰티샵 테이블(Shops)
* 파일 위치: database/migrations/2026_07_22_051130_create_shops_table.php


| 번호 | 구분 | 컬럼명 | 타입 | Null | 기본값 | 인덱스/제약조건 | 설명 및 상태값 |
| :---: | :--- | :--- | :--- | :---: | :---: | :---: | :--- |
| 1 | 기본키 | `id` | Bigint (unsigned) | N | Auto | PK | 뷰티샵 고유 식별 번호 |
| 2 | 관계성 연결 | `center_id` | Bigint (unsigned) | Y | null | FK (`centers.id`) | 소속 센터 ID (ON DELETE SET NULL) |
| 3 | 관계성 연결 | `user_id` | Bigint (unsigned) | Y | null | FK (`users.id`) | 샵 원장 계정 ID (ON DELETE SET NULL) |
| 4 | 기본 정보 | `name` | String(255) | N | - | - | 뷰티샵 상호명 (예: 글로뷰 강남 1호점) |
| 5 | 기본 정보 | `code` | String(20) | Y | null | Unique | 샵 고유 코드 (시스템 자동 생성) |
| 6 | 기본 정보 | `business_number` | String(15) | Y | null | Unique | 사업자등록번호 (10자리/하이픈 포함) |
| 7 | 기본 정보 | `ceo_name` | String(30) | Y | null | - | 지점 대표자/원장 성명 |
| 8 | 기본 정보 | `ceo_phone` | String(20) | Y | null | - | 대표자 연락처 (휴대폰 번호) |
| 9 | 기본 정보 | `ceo_email` | String(255) | Y | null | - | 대표자 이메일 주소 |
| 10 | 연락처/주소 | `tel` | String(20) | Y | null | - | 뷰티샵 대표 유선 전화번호 |
| 11 | 연락처/주소 | `fax` | String(20) | Y | null | - | 팩스 번호 |
| 12 | 연락처/주소 | `zipcode` | String(10) | Y | null | - | 우편번호 (5자리 국가기초구역번호) |
| 13 | 연락처/주소 | `address1` | String(255) | Y | null | - | 기본 도로명/지번 주소 |
| 14 | 연락처/주소 | `address2` | String(255) | Y | null | - | 상세 주소 (동/호수, 층 등) |
| 15 | 연락처/주소 | `region_sido` | String(30) | Y | null | Index | 지역 시/도 (예: 서울특별시, 경기도) |
| 16 | 연락처/주소 | `region_sigungu` | String(30) | Y | null | Index | 지역 구/군 (예: 강남구, 성남시 분당구) |
| 17 | 연락처/주소 | `region_category` | String(50) | Y | null | Index | 지역 통합 분류 키 (예: 서울-강남) |
| 18 | 위치 (LBS) | `latitude` | Decimal(10, 8) | Y | null | Index | 위도 좌표 (예: 37.56653500) |
| 19 | 위치 (LBS) | `longitude` | Decimal(11, 8) | Y | null | Index | 경도 좌표 (예: 126.97796900) |
| 20 | 위치 (LBS) | `location_coords` | String(100) | Y | null | - | 공간 검색 최적화용 결합 좌표 텍스트 |
| 21 | 영업/예약 | `is_reservation_enabled` | Boolean (Tinyint 1) | N | false | - | 온라인 예약 활성화 여부 (true/false) |
| 22 | 영업/예약 | `booking_unit_minutes` | Integer | N | 30 | - | 예약 슬롯 단위 (기본 30분) |
| 23 | 영업/예약 | `business_start_time` | Time | Y | null | - | 일일 영업 시작 시간 (HH:MM:SS) |
| 24 | 영업/예약 | `business_end_time` | Time | Y | null | - | 일일 영업 종료 시간 (HH:MM:SS) |
| 25 | 영업/예약 | `break_start_time` | Time | Y | null | - | 브레이크 타임 시작 시간 |
| 26 | 영업/예약 | `break_end_time` | Time | Y | null | - | 브레이크 타임 종료 시간 |
| 27 | 영업/예약 | `holiday_configs` | Json (LongText) | Y | null | - | 정기 휴무 요일 및 공휴일 설정 데이터 |
| 28 | 정산 계좌 | `bank_name` | String(50) | Y | null | - | 정산 입금 은행명 |
| 29 | 정산 계좌 | `bank_account` | String(255) | Y | null | - | 정산 계좌번호 |
| 30 | 정산 계좌 | `bank_holder` | String(50) | Y | null | - | 예금주 성명 |
| 31 | 마케팅/전시 | `tags` | Json (LongText) | Y | null | - | 검색/필터용 키워드 태그 배열 |
| 32 | 마케팅/전시 | `logo_path` | String(255) | Y | null | - | 뷰티샵 로고 이미지 저장 경로 |
| 33 | 마케팅/전시 | `banner_path` | String(255) | Y | null | - | PC용 메인 배너 이미지 경로 (1200x400) |
| 34 | 마케팅/전시 | `og_image_path` | String(255) | Y | null | - | SNS 공유용 Open Graph 이미지 경로 |
| 35 | 마케팅/전시 | `kakao_channel_url` | String(255) | Y | null | - | 카카오톡 채널 1:1 채팅 연동 URL |
| 36 | 마케팅/전시 | `naver_place_url` | String(255) | Y | null | - | 네이버 플레이스 매장 등록 URL |
| 37 | 마케팅/전시 | `youtube_id` | String(50) | Y | null | - | 소개용 유튜브 비디오 ID |
| 38 | 마케팅/전시 | `map_api` | Text | Y | null | - | 외부 지도 임베드 코드/API 소스 |
| 39 | 마케팅/전시 | `introduction` | Text | Y | null | - | 매장 한 줄 요약 소개글 |
| 40 | 마케팅/전시 | `description` | LongText | Y | null | - | 매장 상세 안내 및 시술 특징 소개문 |
| 41 | 인증 관리 | `is_verified` | Boolean (Tinyint 1) | N | false | - | 사업자등록번호 진위 인증 완료 여부 |
| 42 | 인증 관리 | `is_phone_verified` | Boolean (Tinyint 1) | N | false | - | 대표자 휴대폰 SMS 본인인증 완료 여부 |
| 43 | 인증 관리 | `verified_at` | Timestamp | Y | null | - | 사업자/본인 인증 완료 일시 |
| 44 | 인증 관리 | `verification_expires_at`| Timestamp | Y | null | - | 인증 유효 만료 일시 |
| 45 | 심사/운영 상태 | `status` | String(20) | N | 'pending' | - | 관리자 심사 상태 (`pending`: 대기, `approved`: 승인, `rejected`: 반려) |
| 46 | 심사/운영 상태 | `approved_at` | Timestamp | Y | null | - | 본사 관리자 승인 처리 일시 |
| 47 | 심사/운영 상태 | `rejected_at` | Timestamp | Y | null | - | 본사 관리자 반려 처리 일시 |
| 48 | 심사/운영 상태 | `rejection_reason` | Text | Y | null | - | 입점 심사 반려 사유 (사용자 화면 노출) |
| 49 | 심사/운영 상태 | `operating_status` | String(20) | N | 'temp_closed' | - | 원장 제어 운영 상태 (`active`, `temp_closed`, `suspended`, `closed`) |
| 50 | 전시/통제 | `is_pinned` | Boolean (Tinyint 1) | N | false | - | 목록 상단 고정 샵 여부 |
| 51 | 전시/통제 | `pinned_at` | Timestamp | Y | null | - | 상단 고정 처리 일시 |
| 52 | 전시/통제 | `is_main_displayed` | Boolean (Tinyint 1) | N | false | - | 메인 홈 추천 매장 노출 여부 |
| 53 | 전시/통제 | `sort_order` | Integer | N | 0 | - | 노출 정렬 순서 (낮을수록 우선) |
| 54 | 전시/통제 | `admin_memo` | Text | Y | null | - | 본사 운영팀 내부 관리용 비공개 메모 |
| 55 | 전시/통제 | `is_active` | Boolean (Tinyint 1) | N | true | - | 시스템 전체 활성화/노출 플래그 |
| 56 | 시스템 관리 | `created_at` | Timestamp | Y | null | - | 샵 최초 생성/입점 신청 일시 |
| 57 | 시스템 관리 | `updated_at` | Timestamp | Y | null | - | 정보 최종 수정 일시 |
| 58 | 시스템 관리 | `deleted_at` | Timestamp | Y | null | - | 소프트 삭제 일시 (Soft Delete) |

### 3.2 알림 템플릿 Seeder 등록
* 파일 위치: database/seeders/NotificationTemplateSeeder.php

#### [NotificationTemplateSeeder.php]
    namespace Database\Seeders;

    use Illuminate\Database\Seeder;
    use Illuminate\Support\Facades\DB;

    class NotificationTemplateSeeder extends Seeder
    {
        public function run(): void
        {
            $templates = [
                [
                    'code'         => 'SHOP_APPLY_USER',
                    'category'     => 'shop',
                    'subcategory'  => 'apply',
                    'target'       => 'user',
                    'type'         => 'LMS',
                    'title'        => '뷰티샵 입점 신청이 접수되었습니다.',
                    'content'      => "안녕하세요, #{user_name} 원장님/대표님.\nGLOBEAU 뷰티 플랫폼 입니다.\n\n대표님의 소중한 입점 신청서가 정상적으로 접수되었습니다.\n\n■ 신청 내역\n- 샵 명칭: #{shop_name}\n- 신청 일시: #{created_at}\n- 접수 번호: #{shop_code}\n\n■ 향후 진행 절차\n1. 담당 부서 서류 및 입점 적격 심사 (영업일 기준 1~3일 소요)\n2. 심사 결과 안내 (알림톡/문자)\n3. 관리자 계정 생성 및 서비스 등록 안내\n\n신청 내용 수정이나 기타 문의 사항은 고객센터(#{cs_number}) 또는 1:1 문의를 이용해 주시기 바랍니다.\n감사합니다.",
                    'template_key' => null,
                    'description'  => '입점 신청 완료 시 신청자에게 발송. 변수: #{user_name}, #{shop_name}, #{shop_code}, #{created_at}, #{cs_number}',
                    'is_active'    => true,
                ],
                [
                    'code'         => 'SHOP_APPLY_ADMIN',
                    'category'     => 'shop',
                    'subcategory'  => 'apply',
                    'target'       => 'admin',
                    'type'         => 'LMS',
                    'title'        => '신규 입점 신청 접수 알림',
                    'content'      => "[GLOBEAU 관리자] 신규 입점 신청이 접수되었습니다.\n\n- 샵명: #{shop_name}\n- 대표자: #{user_name}\n- 연락처: #{user_phone}\n- 접수일시: #{created_at}\n\n관리자 페이지에서 서류 검토 및 승인 처리를 진행해 주세요.",
                    'template_key' => null,
                    'description'  => '입점 신청 시 본사 운영팀 발송. 변수: #{shop_name}, #{user_name}, #{user_phone}, #{created_at}',
                    'is_active'    => true,
                ],
            ];

            foreach ($templates as$tmpl) {
                DB::table('notification_templates')->updateOrInsert(
                    ['code' => $tmpl['code']],
                    array_merge($tmpl, ['updated_at' => now()])
                );
            }
        }
    }

---

## 4. 백엔드 컨트롤러 구현 명세

### 4.1 본인인증 전송 전 중복 차단 (VerifyPhoneController)
* 파일 위치: app/Http/Controllers/Auth/VerifyPhoneController.php

    namespace App\Http\Controllers\Auth;

    use App\Http\Controllers\Controller;
    use App\Services\Cafe24SmsService;
    use App\Models\User;
    use App\Models\SmsLog;
    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Cache;
    use Illuminate\Support\Facades\Session;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Validator;
    use Illuminate\Support\Facades\RateLimiter;

    class VerifyPhoneController extends Controller
    {
        protected Cafe24SmsService $smsService;

        public function __construct(Cafe24SmsService $smsService)
        {
            $this->smsService =$smsService;
        }

        public function sendSms(Request $request)
        {
            $validator = Validator::make($request->all(), [
                'phone_number' => ['required', 'string'],
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'success' => false,
                    'message' => '휴대전화 번호 항목은 필수입니다.'
                ], 200);
            }

            $phone = preg_replace('/[^0-9]/', '', (string)$request->phone_number);

            if (strlen($phone) < 10 \vert{}\vert{} strlen($phone) > 11) {
                return response()->json([
                    'success' => false,
                    'message' => '올바른 휴대전화 번호를 입력해주세요.'
                ], 200);
            }

            // 타 계정 중복 가입/인증 방어 (본인 계정 제외)
            $userQuery = User::where('phone',$phone);
            if (Auth::check()) {
                $userQuery->where('id', '!=', Auth::id());
            }

            if ($userQuery->exists()) {
                return response()->json([
                    'success' => false,
                    'message' => '이미 다른 회원이 사용 중인 휴대폰 번호입니다. 다른 번호를 입력해 주세요.'
                ], 200);
            }

            $ip = $request->ip();$sessionKey = Session::getId();
            $cooldownKey = 'phone_cooldown_' .$sessionKey;
            $ipRateLimitKey = 'sms_ip_limit_' .$ip;
            $phoneDailyKey = 'phone_daily_limit_' .$phone;
            $sessionDailyKey = 'session_daily_limit_' .$sessionKey;

            if (RateLimiter::tooManyAttempts($ipRateLimitKey, 10)) {
                $seconds = RateLimiter::availableIn($ipRateLimitKey);
                return response()->json([
                    'success' => false,
                    'message' => "요청이 너무 많습니다. 잠시 후 ({$seconds}초 뒤) 다시 시도해주세요."
                ], 200);
            }
            RateLimiter::hit($ipRateLimitKey, 60);

            $phoneDailyCount = Cache::get($phoneDailyKey, 0);
            if ($phoneDailyCount >= 5) {
                return response()->json([
                    'success' => false,
                    'message' => '해당 번호로 오늘 요청 가능한 인증 횟수(5회)를 모두 초과했습니다. 내일 다시 시도해주세요.'
                ], 200);
            }

            $sessionDailyCount = Cache::get($sessionDailyKey, 0);
            if ($sessionDailyCount >= 20) {
                return response()->json([
                    'success' => false,
                    'message' => '오늘 인증 요청 가능 횟수를 모두 초과했습니다. 내일 다시 시도해주세요.'
                ], 200);
            }

            if (Cache::has($cooldownKey)) {
                return response()->json([
                    'success' => false,
                    'message' => '인증번호가 이미 발송되었습니다. 3분 뒤에 다시 시도해주세요.'
                ], 200);
            }

            $code = rand(100000, 999999);
            $cacheKey = 'phone_verify_' .$sessionKey;

            Cache::put($cacheKey, ['phone' => $phone, 'code' =>$code], now()->addMinutes(3));
            Cache::put($cooldownKey, true, now()->addMinutes(3));
            Cache::put($phoneDailyKey,$phoneDailyCount + 1, now()->addDay());
            Cache::put($sessionDailyKey,$sessionDailyCount + 1, now()->addDay());

            if (env('SMS_TEST_MODE', false)) {
                SmsLog::create([
                    'user_id'       => Auth::id(),
                    'phone'         => $phone,
                    'message'       => "[글로보우 뷰티샵] 본인인증 인증번호는 [{$code}] 입니다. (테스트 모드)",
                    'type'          => 'AUTH_VERIFY_CODE',
                    'status'        => 'success',
                    'error_message' => null,
                    'ip_address'    => $ip,
                ]);

                return response()->json([
                    'success'    => true,
                    'message'    => "인증번호가 발송되었습니다. (테스트 번호: {$code})",
                    'debug_code' => $code
                ], 200);
            }

            $isSent =$this->smsService->sendVerificationCode($phone, (string)$code);
            if (!$isSent) {
                return response()->json([
                    'success' => false,
                    'message' => '인증번호 발송에 실패했습니다. 관리자에게 문의해주세요.'
                ], 500);
            }

            return response()->json([
                'success' => true,
                'message' => '인증번호가 발송되었습니다.'
            ], 200);
        }

        public function verifySms(Request $request)
        {
            $validator = Validator::make($request->all(), [
                'verification_code' => ['required', 'string', 'size:6'],
            ]);

            if ($validator->fails()) {
                return response()->json([
                    'success' => false,
                    'message' => '올바른 인증번호 6자리를 입력해주세요.'
                ], 200);
            }

            $sessionKey = Session::getId();
            $cacheKey = 'phone_verify_' .$sessionKey;
            $storedData = Cache::get($cacheKey);

            if (!$storedData) {
                return response()->json([
                    'success' => false,
                    'message' => '인증 시간이 만료되었거나 요청 내역이 없습니다. 다시 인증번호를 전송해주세요.'
                ], 200);
            }

            if ($storedData['code'] !== (int)$request->verification_code) {
                return response()->json([
                    'success' => false,
                    'message' => '인증번호가 일치하지 않습니다.'
                ], 200);
            }

            Session::put('verified_phone_number', $storedData['phone']);

            if (Auth::check()) {
                $targetPhone = preg_replace('/[^0-9]/', '',$storedData['phone']);
                $isDuplicate = User::where('phone',$targetPhone)
                    ->where('id', '!=', Auth::id())
                    ->exists();

                if ($isDuplicate) {
                    return response()->json([
                        'success' => false,
                        'message' => '이미 다른 계정에 등록된 휴대폰 번호입니다.'
                    ], 200);
                }

                $user = Auth::user();$user->is_phone_verified = true;
                $user->phone =$targetPhone;
                $user->phone_verified_at = now();$user->save();
            }

            Cache::forget($cacheKey);
            Cache::forget('phone_cooldown_' . $sessionKey);

            return response()->json([
                'success'        => true,
                'message'        => '휴대전화 본인인증이 성공적으로 완료되었습니다!',
                'verified_phone' => $storedData['phone']
            ], 200);
        }
    }

### 4.2 일반 입점 신청 전용 저장 및 문자 통보 (ShopController@store)
* 파일 위치: app/Http/Controllers/ShopController.php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;
    use App\Models\Shop;
    use App\Models\User;
    use Illuminate\Support\Facades\Session;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Validation\Rule;
    use App\Services\NotificationService;

    class ShopController extends Controller
    {
        public function store(Request $request)
        {
            $currentUser = Auth::user();

            if ($request->input('is_verified') !== 'true') {
                return back()->withErrors(['business_number' => '사업자등록번호 진위 인증을 완료해 주세요.'])->withInput();
            }

            if ($request->input('is_phone_verified') !== 'true') {
                return back()->withErrors(['phone' => '신청인 본인 인증을 완료해 주세요.'])->withInput();
            }

            $cleanPhone = preg_replace('/[^0-9]/', '', (string)$request->input('phone'));
            $request->merge(['phone' =>$cleanPhone]);

            $validated =$request->validate([
                'name'            => 'required|string|max:255',
                'business_number' => 'required|string|max:15|unique:shops,business_number',
                'ceo_name'        => 'required|string|max:30',
                'phone'           => [
                    'required',
                    'string',
                    'max:20',
                    Rule::unique('users', 'phone')->ignore($currentUser?->id),
                ],
                'zipcode'         => 'required|string|max:10',
                'address1'        => 'required|string|max:255',
                'address2'        => 'nullable|string|max:255',
                'region_sido'     => 'required|string|max:30',
                'region_sigungu'  => 'required|string|max:30',
                'region_category' => 'required|string|max:50',
                'latitude'        => 'nullable|numeric|between:-90,90',
                'longitude'       => 'nullable|numeric|between:-180,180',
                'location_coords' => 'nullable|string|max:100',
                'introduction'    => 'nullable|string',
                'description'     => 'nullable|string',
            ], [
                'business_number.unique' => '이미 등록되었거나 심사 중인 사업자등록번호입니다.',
                'phone.unique'           => '이미 다른 회원이 등록하여 사용 중인 휴대폰 번호입니다.',
            ]);

            DB::beginTransaction();
            try {
                if ($currentUser) {$currentUser->update([
                        'phone'             => $cleanPhone,
                        'is_phone_verified' => true,
                        'phone_verified_at' => now(),
                    ]);
                }

                $locationCoords =$validated['location_coords'] ?? null;
                if (!empty($validated['latitude']) && !empty($validated['longitude']) && empty($locationCoords)) {$locationCoords = $validated['latitude'] . ',' .$validated['longitude'];
                }

                $shop = Shop::create([
                    'user_id'           => $currentUser?->id,
                    'name'              => $validated['name'],
                    'business_number'   => $validated['business_number'],
                    'ceo_name'          => $validated['ceo_name'],
                    'zipcode'           => $validated['zipcode'],
                    'address1'          => $validated['address1'],
                    'address2'          => $validated['address2'] ?? null,
                    'region_sido'       => $validated['region_sido'],
                    'region_sigungu'    => $validated['region_sigungu'],
                    'region_category'   => $validated['region_category'],
                    'latitude'          => $validated['latitude'] ?? null,
                    'longitude'         => $validated['longitude'] ?? null,
                    'location_coords'   => $locationCoords,
                    'introduction'      => $validated['introduction'] ?? null,
                    'description'       => $validated['description'] ?? null,
                    'status'            => 'pending',
                    'operating_status'  => 'suspended',
                    'is_verified'       => true,
                    'is_phone_verified' => true,
                    'is_active'         => false,
                ]);

                if ($currentUser) {
                    session(['active_shop_id' => $shop->id]);
                }

                DB::commit();
            } catch (\Exception $e) {
                DB::rollBack();
                return back()->withErrors(['error' => '입점 신청 처리 중 오류가 발생했습니다: ' . $e->getMessage()])->withInput();
            }

            Session::forget(['sms_verify_code', 'sms_verify_phone', 'sms_verified', 'sms_sent_at']);

            // 알림 발송 모듈 호출
            $notificationService = app(NotificationService::class);
            $applicantName  = $currentUser?->name ?? $shop->ceo_name;
            $applicantPhone = $currentUser?->phone ?? $cleanPhone;
            $shopCode       = $shop->code ?? ('SHOP-' . str_pad($shop->id, 6, '0', STR_PAD_LEFT));
            $createdAtText  =$shop->created_at?->format('Y-m-d H:i') ?? now()->format('Y-m-d H:i');
            $csNumber       = env('CS_PHONE_NUMBER', '1588-0000');

            $variables = [
                'shop_name'   => $shop->name,
                'user_name'   => $applicantName,
                'user_phone'  => $applicantPhone,
                'shop_code'   => $shopCode,
                'created_at'  => $createdAtText,
                'cs_number'   => $csNumber,
            ];

            // 1. 최고관리자 통보
            $adminPhone = env('ADMIN_PHONE_NUMBER', '010-0000-0000');
            if ($adminPhone) {
                $notificationService->send(
                    templateCode: 'SHOP_APPLY_ADMIN',
                    receiver:     $adminPhone,
                    variables:    $variables,
                    userId:       null
                );
            }

            // 2. 신청자 접수증 통보
            if ($applicantPhone) {
                $notificationService->send(
                    templateCode: 'SHOP_APPLY_USER',
                    receiver:     $applicantPhone,
                    variables:    $variables,
                    userId:       $currentUser?->id
                );
            }

            return redirect('/ttt')->with('success', '뷰티샵 입점 심사 신청이 성공적으로 접수되었습니다!');
        }
    }

### 4.3 마이페이지 입점 상태 조회 및 배지 해제 (MypageController)
* 파일 위치: app/Http/Controllers/MypageController.php

    namespace App\Http\Controllers;

    use Illuminate\Http\Request;
    use App\Models\Shop;
    use Illuminate\Support\Facades\Auth;

    class MypageController extends Controller
    {
        public function shopApplyStatus()
        {
            $user = Auth::user();
            $shop =$user->latestShop;

            // 상태 확인 시각을 업데이트하여 헤더의 미확인 NEW 배지 자동 제거
            if ($shop && is_null($shop->status_checked_at) && in_array($shop->status, ['rejected', 'approved'])) {$shop->update(['status_checked_at' => now()]);
            }

            return view('mypage.shop-apply-status', compact('shop'));
        }
    }

---

## 5. 라우터 설정 (routes/web.php)

    use App\Http\Controllers\ShopController;
    use App\Http\Controllers\MypageController;
    use App\Http\Controllers\Auth\VerifyPhoneController;
    use App\Http\Controllers\Common\VerifyBusinessController;

    // 공용 인증 API (로그인 불필요)
    Route::post('/auth/phone/send', [VerifyPhoneController::class, 'sendSms']);
    Route::post('/auth/phone/verify', [VerifyPhoneController::class, 'verifySms']);
    Route::post('/common/business/verify', [VerifyBusinessController::class, 'verifyBusiness']);

    Route::middleware(['auth'])->group(function () {
        // 마이페이지 그룹
        Route::prefix('mypage')->name('mypage.')->group(function () {
            Route::get('/', [MypageController::class, 'index'])->name('index');
            Route::get('/shop-apply', [MypageController::class, 'shopApplyStatus'])->name('shop.apply');
        });

        // 일반 사용자 입점 신청 및 뷰티샵 CRUD
        Route::resource('shops', ShopController::class)->except(['index', 'show']);
    });

    // 뷰티샵 퍼블릭 라우트
    Route::get('/shops', [ShopController::class, 'index'])->name('shops.index');
    Route::get('/shops/{id}', [ShopController::class, 'show'])->name('shops.show');

---

## 6. 프론트엔드 UI/UX 명세

### 6.1 GNB 상단 드롭다운 메뉴 (헤더 컴포넌트)
* 규칙:
  * 미신청자에게는 드롭다운 내 입점 메뉴를 표시하지 않음 (신청 유도는 GNB 우측 상단/푸터 전용 링크 활용).
  * 신청 이력이 있는 사용자에게만 파트너 관리 섹션을 활성화.
  * 승인/반려 시 `status_checked_at`이 null이면 드롭다운 버튼 우측 상단에 알림 배지(Red/Green Dot) 노출.

    @auth
        @php
            $latestShop = Auth::user()->latestShop;
            $hasUnreadStatus = false;
            if ($latestShop && in_array($latestShop->status, ['approved', 'rejected'])) {
                $hasUnreadStatus = is_null($latestShop->status_checked_at);
            }
        @endphp

        <div class="dropdown">
            <button class="btn btn-outline-secondary position-relative dropdown-toggle d-flex align-items-center gap-2" 
                    type="button" 
                    id="userMenuDropdown" 
                    data-bs-toggle="dropdown" 
                    aria-expanded="false">
                <i class="fas fa-user-circle fs-5"></i>
                <span>{{ Auth::user()->name }}님</span>

                @if($hasUnreadStatus)
                    <span class="position-absolute top-0 start-100 translate-middle p-1 {{ $latestShop->status === 'rejected' ? 'bg-danger' : 'bg-success' }} border border-light rounded-circle">
                        <span class="visually-hidden">새로운 알림</span>
                    </span>
                @endif
            </button>

            <ul class="dropdown-menu dropdown-menu-end shadow-sm border-0 py-2" aria-labelledby="userMenuDropdown" style="min-width: 210px;">
                <li><h6 class="dropdown-header text-muted small">내 계정</h6></li>
                <li>
                    <a class="dropdown-item" href="{{ route('mypage.index') }}">
                        <i class="fas fa-home me-2 text-secondary"></i> 마이페이지
                    </a>
                </li>
                <li>
                    <a class="dropdown-item" href="{{ route('mypage.reservations') }}">
                        <i class="fas fa-calendar-check me-2 text-secondary"></i> 예약 내역
                    </a>
                </li>
                <li>
                    <a class="dropdown-item" href="{{ route('mypage.tickets') }}">
                        <i class="fas fa-ticket-alt me-2 text-secondary"></i> 티켓 보관함
                    </a>
                </li>

                @if($latestShop)
                    <li><hr class="dropdown-divider"></li>
                    <li><h6 class="dropdown-header text-muted small">파트너 관리</h6></li>

                    @if($latestShop->status === 'pending')
                        <li>
                            <a class="dropdown-item text-secondary d-flex justify-content-between align-items-center" 
                               href="{{ route('mypage.shop.apply') }}">
                                <span><i class="fas fa-hourglass-half me-2 text-warning"></i> 입점 심사중</span>
                                <span class="badge bg-warning-subtle text-warning-emphasis border border-warning-subtle" style="font-size: 0.7rem;">대기</span>
                            </a>
                        </li>
                    @elseif($latestShop->status === 'rejected')
                        <li>
                            <a class="dropdown-item text-danger d-flex justify-content-between align-items-center" 
                               href="{{ route('mypage.shop.apply') }}">
                                <span><i class="fas fa-exclamation-circle me-2"></i> 뷰티샵 반려</span>
                                <span class="badge bg-danger text-white rounded-pill" style="font-size: 0.65rem;">
                                    {{ is_null($latestShop->status_checked_at) ? 'NEW' : '사유확인' }}
                                </span>
                            </a>
                        </li>
                    @elseif($latestShop->status === 'approved')
                        <li>
                            <a class="dropdown-item fw-bold text-brand d-flex justify-content-between align-items-center" 
                               href="{{ route('shop_admin.index') }}" 
                               target="_blank">
                                <span><i class="fas fa-store-alt me-2"></i> 뷰티샵 관리자 센터</span>
                                @if(is_null($latestShop->status_checked_at))
                                    <span class="badge bg-success rounded-pill" style="font-size: 0.65rem;">승인</span>
                                @endif
                            </a>
                        </li>
                    @endif
                @endif

                <li><hr class="dropdown-divider"></li>
                <li>
                    <form method="POST" action="{{ route('logout') }}">
                        @csrf
                        <button type="submit" class="dropdown-item text-secondary">
                            <i class="fas fa-sign-out-alt me-2"></i> 로그아웃
                        </button>
                    </form>
                </li>
            </ul>
        </div>
    @endauth

### 6.2 마이페이지 입점 상태 상세 뷰 (resources/views/mypage/shop-apply-status.blade.php)

    @extends('layouts.app-sub')

    @section('title', 'Globeau - 뷰티샵 입점 신청 현황')

    @section('content')
    <div class="container py-5">
        <div class="row justify-content-center">
            <div class="col-lg-8">
                <div class="d-flex align-items-center justify-content-between mb-4">
                    <h4 class="fw-bold text-dark mb-0">
                        <i class="fas fa-store me-2 text-brand"></i>뷰티샵 입점 신청 현황
                    </h4>
                    <a href="{{ route('mypage.index') }}" class="btn btn-sm btn-outline-secondary">
                        <i class="fas fa-arrow-left me-1"></i> 마이페이지로 이동
                    </a>
                </div>

                @if(!$shop)
                    <div class="card p-5 text-center bg-light border-0 shadow-sm rounded-3">
                        <i class="fas fa-store-slash fa-3x text-muted mb-3"></i>
                        <p class="text-secondary mb-3">신청하신 입점 내역이 없습니다.</p>
                        <div>
                            <a href="{{ route('shops.create') }}" class="btn btn-premium px-4">뷰티샵 입점 신청하기</a>
                        </div>
                    </div>
                @else
                    <div class="card card-premium p-4 shadow-sm border mb-4">
                        <div class="d-flex justify-content-between align-items-start pb-3 border-bottom mb-3">
                            <div>
                                <span class="badge bg-light text-brand border border-brand px-2 py-1 mb-2 fw-medium" style="font-size: 0.7rem;">
                                    신청번호: {{ $shop->code ?? ('SHOP-' . str_pad($shop->id, 6, '0', STR_PAD_LEFT)) }}
                                </span>
                                <h4 class="fw-bold text-dark mb-1">{{ $shop->name }}</h4>
                                <span class="text-secondary small">신청 일시: {{ $shop->created_at->format('Y-m-d H:i') }}</span>
                            </div>
                            <div>
                                @if($shop->status === 'pending')
                                    <span class="badge bg-warning-subtle text-warning-emphasis border border-warning px-3 py-2 fs-6">심사 대기중</span>
                                @elseif($shop->status === 'rejected')
                                    <span class="badge bg-danger-subtle text-danger border border-danger px-3 py-2 fs-6">심사 반려</span>
                                @elseif($shop->status === 'approved')
                                    <span class="badge bg-success-subtle text-success border border-success px-3 py-2 fs-6">입점 승인 완료</span>
                                @endif
                            </div>
                        </div>

                        <!-- 샵 기본 신청 정보 요약 테이블 -->
                        <div class="table-responsive mb-4">
                            <table class="table table-bordered align-middle small mb-0">
                                <tbody>
                                    <tr>
                                        <th class="bg-light text-secondary" style="width: 25%;">대표자 성명</th>
                                        <td class="text-dark fw-medium">{{ $shop->ceo_name }}</td>
                                        <th class="bg-light text-secondary" style="width: 25%;">사업자등록번호</th>
                                        <td class="text-dark fw-medium">{{ $shop->business_number }}</td>
                                    </tr>
                                    <tr>
                                        <th class="bg-light text-secondary">사업장 소재지</th>
                                        <td colspan="3" class="text-dark">
                                            ({{ $shop->zipcode }}) {{ $shop->address1 }} {{$shop->address2 }}
                                        </td>
                                    </tr>
                                    <tr>
                                        <th class="bg-light text-secondary">한 줄 소개</th>
                                        <td colspan="3" class="text-dark">{{ $shop->introduction ?: '-' }}</td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>

                        <!-- 상태별 후속 액션 카드 -->
                        @if($shop->status === 'rejected')
                            <div class="alert alert-danger border-danger p-3 mb-4 rounded-3">
                                <h6 class="fw-bold text-danger mb-2">
                                    <i class="fas fa-exclamation-triangle me-1"></i> 입점 심사 반려 사유
                                </h6>
                                <p class="mb-0 text-dark small lh-base">
                                    {{ $shop->reject_reason ?: '제출 서류 또는 사업자 정보가 일치하지 않아 반려되었습니다. 보완 후 재신청해 주시기 바랍니다.' }}
                                </p>
                            </div>
                            <div class="d-flex justify-content-end gap-2">
                                <a href="{{ route('shops.edit', $shop->id) }}" class="btn btn-danger px-4 fw-semibold">
                                    신청 내용 수정 및 재심사 요청
                                </a>
                            </div>
                        @elseif($shop->status === 'approved')
                            <div class="alert alert-success border-success p-3 mb-4 rounded-3">
                                <h6 class="fw-bold text-success mb-2">
                                    <i class="fas fa-check-circle me-1"></i> 입점 승인이 완료되었습니다!
                                </h6>
                                <p class="mb-0 text-dark small">
                                    뷰티샵 관리자 센터로 이동하여 시술 프로그램 등록, 매장 영업 일정 설정, 갤러리 이미지 관리를 시작할 수 있습니다.
                                </p>
                            </div>
                            <div class="d-flex justify-content-end">
                                <a href="{{ route('shop_admin.index') }}" target="_blank" class="btn btn-premium px-4 fw-semibold">
                                    뷰티샵 관리자 센터 바로가기 <i class="fas fa-external-link-alt ms-1"></i>
                                </a>
                            </div>
                        @else
                            <div class="p-3 bg-light rounded-3 text-secondary border small mb-0">
                                <i class="fas fa-info-circle me-1 text-brand"></i>
                                본사 심사 담당자가 제출 서류 및 적격 여부를 확인하고 있습니다 (영업일 기준 1~3일 소요). 결과는 등록된 연락처로 안내됩니다.
                            </div>
                        @endif
                    </div>
                @endif
            </div>
        </div>
    </div>
    @endsection