# 뷰티샵 관리자(Shop Admin) 시스템 종합 개발 및 아키텍처 가이드 명세서

## 1. 업무 플로어 (Development & Operations Workflow)
뷰티샵 관리자가 시스템을 도입하고 실무 운영을 해나가는 전반적인 업무 흐름은 다음과 같은 단계로 체계화되어 진행됩니다.

1. **최고관리자 프로그램 배포 단계**
   - 최고관리자(Super Admin)가 전체 시술/상품 프로그램을 등록하고 모든 뷰티샵에 N:M 관계로 대기 상태로 일괄 배포합니다.
2. **뷰티샵 기본 정보 등록 단계**
   - 뷰티샵 원장님이 로그인하여 [기본 정보 관리] 화면에서 매장 주소, 연락처, 대표자명, 로고/배너 등을 등록합니다.
3. **영업 상태 활성화 단계**
   - 필수 정보(주소, 연락처, 대표자명)가 모두 입력되면, 영업 상태를 [영업중(active)]으로 전환하여 본격적인 운영 채비를 마칩니다.
4. **영업시간 및 휴무일 설정 단계**
   - [영업시간 및 휴무일 설정] 화면에서 오픈/마감 시간, 예약 단위, 요일별 정기 휴무 및 매월 고정일 휴무를 세팅하여 `holiday_configs` JSON으로 저장합니다.
5. **프로그램 판매 대기 해제 단계**
   - 샵 전용 프로그램 관리 화면에서 최고관리자가 배포한 프로그램 중 우리 샵에서 실제로 판매할 항목만 골라 판매 대기를 풀고(`is_active = true`) 활성화합니다.
6. **캘린더 대시보드 및 예약 연동 운영 단계**
   - 앞선 모든 설정(기본정보, 영업일정, 판매 프로그램)을 바탕으로 대시보드 캘린더와 예약 시스템이 유기적으로 구동되어 실무 운영이 이루어집니다.

---

## 2. 멀티샵 운영 및 세션 보안 설계

### 2.1 세션 기반 샵 검증 (`active_shop_id`)
- 복수의 뷰티샵을 소유하고 운영하는 원장님 환경을 고려하여, 단순히 첫 번째 샵을 조회하는 방식을 배제했습니다.
- 사용자가 현재 선택한 샵의 ID를 세션(`session('active_shop_id')`)에 안전하게 저장하고, 컨트롤러에서 접근할 때마다 해당 유저의 소유 여부와 승인 상태(`status = approved`)를 엄격하게 검증하도록 설계되었습니다.

### 2.2 컨트롤러 상속 구조 분리 (DRY 원칙)
- 전역 베이스 컨트롤러(`App\Http\Controllers\Controller`)가 오염되는 것을 방지하기 위해, 뷰티샵 관리자 전용 베이스 컨트롤러를 별도로 분리했습니다.
- **파일 경로**: `app/Http/Controllers/ShopAdmin/Controller.php`

#### <ShopAdmin/Controller.php>
    namespace App\Http\Controllers\ShopAdmin;

    use App\Http\Controllers\Controller as BaseController;
    use Illuminate\Http\Request;
    use App\Models\Shop;

    class Controller extends BaseController
    {
        protected function getActiveShop(Request $request)
        {
            $user = $request->user();$activeShopId = session('active_shop_id');

            if ($activeShopId) {
                $shop = Shop::where('id',$activeShopId)->first();
                if ($shop) {
                    return $shop;
                }
            }

            $shop =$user->shop ?? Shop::first();
            
            if ($shop) {
                session(['active_shop_id' => $shop->id]);
            }

            return $shop;
        }
    }

---

## 3. 뷰티샵 기본 정보 및 영업 상태 제어

### 3.1 필수 정보 누락 방지 로직 (`updateStatus`)
- 뷰티샵의 영업 상태를 '영업중(`active`)'으로 변경할 때, 매장의 핵심 정보가 누락된 채 영업이 시작되는 대참사를 방지하기 위한 실무형 검증 로직을 도입했습니다.
- 매장 주소(`zipcode`, `address1`), 대표 연락처(`tel`), 대표자명(`ceo_name`) 등의 필수 항목이 비어 있는 경우 상태 변경을 원천 차단하고, 관리자에게 명확한 안내 메시지를 반환하도록 구현했습니다.

### 3.2 뷰티샵 관리자 샵 컨트롤러
- **파일 경로**: `app/Http/Controllers/ShopAdmin/ShopController.php`

    namespace App\Http\Controllers\ShopAdmin;

    use App\Http\Controllers\ShopAdmin\Controller as ShopAdminBaseController;
    use Illuminate\Http\Request;
    use App\Models\Shop;
    use App\Models\Image;
    use App\Services\ImageService;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\Facades\Storage;
    use Illuminate\Support\Facades\DB;

    class ShopController extends ShopAdminBaseController
    {
        protected $imageService;

        public function __construct(ImageService $imageService)
        {
            $this->imageService =$imageService;
        }

        public function edit()
        {
            $activeShopId = session('active_shop_id');$shop = Auth::user()->shops()
                ->where('id', $activeShopId)
                ->where('status', 'approved')
                ->with(['categories.parent', 'images'])
                ->first();

            if (!$shop) {$shop = Auth::user()->shops()
                    ->where('status', 'approved')
                    ->with(['categories.parent', 'images'])
                    ->firstOrFail();
            }

            return view('shop_admin.shop.edit', compact('shop'));
        }

        public function update(Request $request)
        {
            $activeShopId = session('active_shop_id');
            
            $shop =$request->user()->shops()
                ->where('id', $activeShopId)
                ->where('status', 'approved')
                ->firstOrFail();

            $validated =$request->validate([
                'name'            => 'required|string|max:255',
                'ceo_name'        => 'required|string|max:100',
                'ceo_phone'       => 'nullable|string|max:50',
                'ceo_email'       => 'nullable|email|max:255',
                'tel'             => 'required|string|max:50',
                'fax'             => 'nullable|string|max:50',
                'zipcode'         => 'required|string|max:20',
                'address1'        => 'required|string|max:255',
                'address2'        => 'nullable|string|max:255',
                'region_sido'     => 'nullable|string|max:30',
                'region_sigungu'  => 'nullable|string|max:30',
                'region_category' => 'nullable|string|max:50',
                'latitude'        => 'nullable|numeric|between:-90,90',
                'longitude'       => 'nullable|numeric|between:-180,180',
                'location_coords' => 'nullable|string|max:100',
                'logo_path'       => 'nullable|image|max:2048',
                'banner_path'     => 'nullable|image|max:4096',
                'og_image_path'   => 'nullable|image|max:2048',
                'naver_place_url' => 'nullable|url|max:255',
                'youtube_id'      => 'nullable|string|max:100',
                'introduction'    => 'nullable|string|max:255',
                'description'     => 'nullable|string',
                'category_ids'    => 'nullable|array',
                'category_ids.*'  => 'exists:categories,id',
            ]);

            $shop->update(collect($validated)->except(['category_ids'])->toArray());
            $shop->categories()->sync($request->input('category_ids', []));

            return back()->with('success', '매장 기본 정보가 성공적으로 수정되었습니다.');
        }

        public function updateStatus(Request $request)
        {
            $activeShopId = session('active_shop_id');$shop = $request->user()->shops()->where('id',$activeShopId)->where('status', 'approved')->firstOrFail();

            $request->validate([
                'operating_status' => 'required|in:active,temp_closed,suspended,closed',
            ]);

            $newStatus =$request->operating_status;

            if ($newStatus === 'active') {$missingFields = [];
                if (empty($shop->zipcode) || empty($shop->address1))$missingFields[] = '매장 위치(주소)';
                if (empty($shop->tel))$missingFields[] = '매장 대표 연락처';
                if (empty($shop->ceo_name))$missingFields[] = '대표자명';

                if (!empty($missingFields)) {
                    $message = '다음 필수 정보가 등록되지 않아 [영업중]으로 변경할 수 없습니다: ' . implode(', ', $missingFields);
                    return back()->with('error', $message);
                }
            }

            $shop->update([
                'operating_status' => $newStatus,
                'is_active' => ($newStatus === 'active')
            ]);

            return back()->with('success', '영업 상태가 성공적으로 변경되었습니다.');
        }
    }

---

## 4. 영업시간 및 휴무일 설정 (Schedule Configs)

### 4.1 JSON 컬럼 구조화 (`holiday_configs`)
- 매장의 복잡한 휴무 및 영업 예외 패턴을 유연하게 담기 위해 `shop->holiday_configs` JSON 컬럼 구조를 도입했습니다.
- 요일별 정기 휴무(`weekly_off`), 매월 고정일 휴무(`monthly_date_off`), 일정 기간 휴무(`date_range_off`) 데이터가 하나의 JSON으로 구조화되어 저장됩니다.

### 4.2 영업시간 및 휴무일 설정 컨트롤러 메서드
- **파일 경로**: `app/Http/Controllers/ShopAdmin/ShopController.php` (추가 메서드)

        public function scheduleEdit()
        {
            $activeShopId = session('active_shop_id');
            $shop = Auth::user()->shops()->where('id',$activeShopId)->where('status', 'approved')->firstOrFail();
            return view('shop_admin.shop.schedule', compact('shop'));
        }

        public function scheduleUpdate(Request $request)
        {
            $activeShopId = session('active_shop_id');$shop = $request->user()->shops()->where('id',$activeShopId)->where('status', 'approved')->firstOrFail();

            $validated =$request->validate([
                'is_reservation_enabled' => 'nullable|boolean',
                'booking_unit_minutes' => 'required|integer|in:10,15,20,30,60',
                'business_start_time' => 'nullable|date_format:H:i',
                'business_end_time' => 'nullable|date_format:H:i',
                'weekly_off' => 'nullable|array',
                'weekly_off.*.day' => 'required|string|in:mon,tue,wed,thu,fri,sat,sun',
                'weekly_off.*.memo' => 'nullable|string|max:100',
                'monthly_date_off' => 'nullable|array',
                'monthly_date_off.*.date' => 'required|integer|min:1|max:31',
                'monthly_date_off.*.memo' => 'nullable|string|max:100',
            ]);

            $shop->is_reservation_enabled =$request->has('is_reservation_enabled') ? 1 : 0;
            $shop->booking_unit_minutes =$validated['booking_unit_minutes'];
            $shop->business_start_time =$validated['business_start_time'] ?? null;
            $shop->business_end_time =$validated['business_end_time'] ?? null;

            $shop->holiday_configs = [
                'weekly_off' => $validated['weekly_off'] ?? [],
                'monthly_date_off' => $validated['monthly_date_off'] ?? [],
            ];
            $shop->save();

            return redirect()->route('shop_admin.settings.schedule.edit')
                ->with('success', '영업시간 및 휴무일 설정이 저장되었습니다.');
        }

### 4.3 영업시간 및 휴무일 설정 뷰
- **파일 경로**: `resources/views/shop_admin/shop/schedule.blade.php`

    @extends('layouts.admin')

    @section('title', '영업시간 및 휴무일 설정')

    @section('content')
    <div class="content-header-box mb-4 px-0">
        <div class="content-title-wrap">
            <h1 class="content-title">영업시간 및 휴무일 설정</h1>
            <span class="text-muted small">매장의 기본 영업 시간과 정기/고정 휴무일 정책을 상세히 관리하세요.</span>
        </div>
    </div>

    <form action="{{ route('shop_admin.settings.schedule.update') }}" method="POST">
        @csrf
        @method('PUT')

        <div class="row g-4 mb-4">
            <!-- 요일별 정기 휴무 (2열 그리드 col-md-6) -->
            <div class="col-md-6">
                <div class="card border-0 shadow-sm rounded-4 p-4 h-100">
                    <h5 class="fw-bold text-dark mb-3"><i class="fa-solid fa-calendar-week text-danger me-2"></i>요일별 정기 휴무</h5>
                    <div id="weekly-off-container">
                        <!-- 동적 행 렌더링 영역 -->
                    </div>
                </div>
            </div>

            <!-- 매월 고정일 휴무 (max="31" 적용) -->
            <div class="col-md-6">
                <div class="card border-0 shadow-sm rounded-4 p-4 h-100">
                    <h5 class="fw-bold text-dark mb-3"><i class="fa-solid fa-calendar-days text-danger me-2"></i>매월 고정일 휴무</h5>
                    <div id="monthly-off-container">
                        <!-- 동적 행 렌더링 영역 -->
                    </div>
                </div>
            </div>
        </div>

        <div class="text-end">
            <button type="submit" class="btn btn-primary px-5 py-3 rounded-pill fw-bold">설정 저장하기</button>
        </div>
    </form>
    @endsection

---

## 5. 프로그램 판매 제어 및 모니터링 시스템 개요
- **데이터 구조**: 최고관리자가 전체 프로그램을 등록하고 N:M 피벗 테이블(`shop_program`)을 통해 모든 뷰티샵에 대기 상태로 일괄 배포합니다.
- **제어 방식**: 각 뷰티샵 원장님은 개별 샵의 판매 상태(`is_active`)를 토글하여 고객용 UI 노출 여부를 실시간으로 통제합니다.
- **핵심 규약**: `unique(['shop_id', 'program_id'])` 제약을 통해 동일 샵에 동일 프로그램이 중복 매핑되는 것을 원천 차단합니다.