---
title: (임시)본사 운영자 회원 관리 기술 명세서 01
---

# GLOBEAU 백오피스 관리자 사용자 관리 및 공용 폼 컴포넌트 표준화 기술 문서

## 1. 개요 및 이전 작업 히스토리 (Context & Background)
본 문서는 GLOBEAU 플랫폼 백오피스 관리자 시스템의 회원/부운영자 관리 모듈 고도화 및 공용 폼 컴포넌트 표준화 작업 내역을 정리한 기술 문서입니다.

### 1.1 이전 선행 작업 요약
- **권한 체계 기본 인프라 구축**:
    - Spatie Laravel-Permission 기반으로 핵심 역할(`admin`, `operator`, `center`, `shop`, `manufacturer`, `user`) 및 대단위 메뉴 권한 키(`users.menu`, `shops.menu`, `programs.menu`, `images.menu`, `categories.menu`, `notifications.menu`) 정의 완료.
    - `RoleAndAdminSeeder`를 통한 초기 DB 시딩 및 최고관리자(`admin`) 권한 자동 동기화(`syncPermissions`) 체계 구성.
- **백오피스 회원 수정/등록 컨트롤러 고도화**:
    - `UserController` 내 등록(`store`), 수정(`update`) 트랜잭션 처리 및 다중 역할(`roles[]`), 세부 권한(`permissions[]`) 동기화 구조 설계.
    - 수정 후 기존 검색 필터 조건 유지를 위한 `_query_string` 리다이렉트 복귀 체계 확립.
- **UI/UX 테마 및 디자인 규격 준수**:
    - 브론즈 모카 테마 컬러(`--primary-color: #b18977`), 딥 오렌지 포인트, 고정 6px 라운드(`--radius-fixed: 6px`) 가이드라인 준수.
    - SweetAlert2 기반 폼 유효성 검사 경고 및 비동기 폼 제어 표준화.

---

## 2. 공용 폼 컴포넌트 표준화 및 활용 가이드 (Blade Components)
기존에 검색 바와 등록/수정 폼에서 혼용되던 마크업을 Blade 컴포넌트화하고, `isFilter` 불리언 인자를 기준으로 UI와 동작 방식을 명확히 분리하였습니다.

### 2.1 공용 폼 컴포넌트 핵심 설계 원칙
1. **ID 부여 원칙 (`id` 속성 일관성)**:
    - `input`, `select`, `checkbox`: 외부에서 명시적 `id`를 넘기지 않을 경우 `$id ?? $name` 규칙을 적용하여 `name`을 기본 ID로 사용.
    - `radio`: 여러 옵션이 하나의 그룹을 이루므로 외부에서 단일 `id`를 넘기지 않고, 내부 루프에서 `$name . '_' . ($value !== '' ? $value : 'all')` 형태로 고유 ID를 자동 생성하여 라벨 클릭(`label for`) 오작동 방지.
2. **모드 분기 플래그 (`isFilter`)**:
    - `isFilter=true`: 검색 필터바 전용 컴팩트 스타일(라벨 폰트 크기 `0.75rem`, 높이 축소, 세그먼트 버튼 트랙 적용).
    - `isFilter=false` (기본값): 등록/수정 폼 본문 스타일(라벨 `small fw-bold text-secondary`, 기본 원형 라디오/체크박스).
3. **`autoSubmit` 플래그 독립화**:
    - 뷰의 디자인 형태를 바꾸지 않고, `onchange="this.form.submit()"` 동작 트리거 여부만 독립적으로 제어.
4. **웹에디터(TinyMCE) 및 일반 Textarea 분리 운영**:
    - 상세 소개 등 TinyMCE 에디터가 필요한 영역은 초기화 타이밍 및 스크립트 종속성 분리를 위해 표준 HTML 태그 기반 수동 연결 방식 유지.
    - 관리자 내부 메모(`admin_memo`) 등 순수 텍스트 필드는 표준 `textarea` 규격 적용.

### 2.2 컴포넌트별 구현 코드 및 가이드

#### (1) 라디오 버튼 컴포넌트 (`resources/views/components/admin/forms/radio.blade.php`)
    {{-- radio.blade.php 전체 코드 --}}
    @props([
        'name',
        'options' => [],
        'selected' => null,
        'label' => null,
        'inline' => true,
        'isFilter' => false,
        'autoSubmit' => false,
        'required' => false,
        'nativeRequired' => false,
        'disabled' => false,
    ])

    @php
        $currentSelected = old($name, $selected ?? request($name, ''));
    @endphp

    <div class="d-flex flex-column gap-1">
        @if($label)
            <label class="form-label text-secondary mb-0 {{ $isFilter ? 'fw-bold' : 'small fw-bold' }}" 
                   @if($isFilter) style="font-size: 0.75rem;" @endif>
                {{ $label }}
                @if($required)
                    <span class="text-danger fw-bold ms-0.5">*</span>
                @endif
            </label>
        @endif

        @if($isFilter)
            {{-- 검색 필터 모드: 세그먼트 캡슐 버튼 그룹 --}}
            <div class="btn-group btn-group-sm filter-radio-group" role="group">
                @foreach($options as $value =>$optionLabel)
                    @php
                        $isChecked = (string)$currentSelected === (string)$value;
                        $uniqueId =$name . '_' . ($value !== '' ?$value : 'all');
                    @endphp
                    <input type="radio" 
                           name="{{ $name }}" 
                           value="{{ $value }}" 
                           class="btn-check filter-radio-input" 
                           id="{{ $uniqueId }}"
                           {{ $isChecked ? 'checked' : '' }}
                           @if($autoSubmit) onchange="this.form.submit()" @endif
                           @disabled($disabled)>
                    <label class="btn py-1 px-2.5 text-nowrap filter-radio-label" 
                           for="{{ $uniqueId }}" 
                           style="font-size: 0.8rem;">
                        {{ $optionLabel }}
                    </label>
                @endforeach
            </div>
        @else
            {{-- 본문 폼 모드: 표준 원형 라디오 박스 --}}
            <div class="d-flex {{ $inline ? 'flex-row gap-3' : 'flex-column gap-1' }} align-items-center py-1">
                @foreach($options as $value =>$optionLabel)
                    @php
                        $isChecked = (string)$currentSelected === (string)$value;
                        $uniqueId =$name . '_' . ($value !== '' ?$value : 'all');
                    @endphp
                    <div class="form-check m-0">
                        <input type="radio" 
                               name="{{ $name }}" 
                               value="{{ $value }}" 
                               class="form-check-input @error($name) is-invalid @enderror" 
                               id="{{ $uniqueId }}"
                               {{ $isChecked ? 'checked' : '' }}
                               @if($autoSubmit) onchange="this.form.submit()" @endif
                               @if($nativeRequired) required @endif
                               @if($required) data-required="true" data-label="{{ $label ?? $name }}" @endif
                               @disabled($disabled)
                               style="cursor: pointer;">
                        <label class="form-check-label small text-dark" for="{{ $uniqueId }}" style="cursor: pointer;">
                            {{ $optionLabel }}
                        </label>
                    </div>
                @endforeach
            </div>

            @error($name)
                <div class="invalid-feedback d-block" style="font-size: 0.75rem;">
                    {{ $message }}
                </div>
            @enderror
        @endif
    </div>

#### (2) 체크박스 컴포넌트 (`resources/views/components/admin/forms/checkbox.blade.php`)
    {{-- checkbox.blade.php 사용 가이드 --}}
    - 다중 선택 배열: name="roles[]" 형태로 전달 시 old('roles', []) 및 배열 체크 판별 내장.
    - 스위치 토글: isSwitch=true 설정 시 Bootstrap form-switch UI 자동 렌더링.
    - 필터 바: isFilter=true 설정 시 0.75rem 폰트 규격 적용.

#### (3) 셀렉트 박스 및 입력창 (`select.blade.php`, `input.blade.php`)
    - input: type="text|email|password|number", readonly, disabled, class="input-phone" 자동 하이픈 연동 지원.
    - select: placeholder 속성 지정 시 첫 번째 빈 옵션('') 자동 생성, isFilter=true 시 form-select-sm 스타일 적용.

---

## 3. 관리자 메뉴 및 부운영자 권한 설정 체계 (`config/admin_menu.php`)
기존 Blade 뷰 파일에 하드코딩되어 있던 메뉴 권한 목록 배열을 `config/admin_menu.php` 내 **`operator_permissions`** 키로 분리하여 단일 진실 공급원(SSOT)을 확립하였습니다.

### 3.1 `config/admin_menu.php` 핵심 구조
    <?php

    return [
        // 1. 관리자 상단/좌측 메인 메뉴 계층 구조
        'main_menu' => [
            // 상점관리 (뷰티샵 관리, 신규 입점, 프로그램 관리, 정산)
            // 회원 관리 (전체 회원 목록)
            // 알림·메시지 관리 (통합 발송 내역, 메시지 발송, 알림 템플릿 관리)
            // 콘텐츠&자산관리 (이미지 관리)
        ],

        // 2. 관리자 하단 푸터 메뉴
        'footer_menu' => [
            // 환경설정, 카테고리 관리, 공통 코드, 통합 휴지통
        ],

        // 3. 본사 부운영자(Operator) 세부 메뉴 접근 권한 정의
        'operator_permissions' => [
            [
                'group'       => '회원 관리',
                'name'        => '회원 관리 메뉴 전체',
                'permission'  => 'users.menu',
                'description' => '회원 목록 조회, 상세 조회 및 파트너 직원 계정 관리',
            ],
            [
                'group'       => '상점 관리',
                'name'        => '뷰티샵 관리 메뉴 전체',
                'permission'  => 'shops.menu',
                'description' => '뷰티샵 등록, 입점 심사 및 상세 정보 관리',
            ],
            [
                'group'       => '상점 관리',
                'name'        => '프로그램 관리 메뉴 전체',
                'permission'  => 'programs.menu',
                'description' => '시술/체험 프로그램 등록 및 예약 정책 관리',
            ],
            [
                'group'       => '알림·메시지',
                'name'        => '알림·메시지 관리 메뉴 전체',
                'permission'  => 'notifications.menu',
                'description' => '알림 발송 내역 조회, 수동 발송 및 알림톡/SMS 템플릿 관리',
            ],
            [
                'group'       => '콘텐츠&자산',
                'name'        => '이미지 관리 메뉴 전체',
                'permission'  => 'images.menu',
                'description' => '서버 내 미디어 라이브러리 및 이미지 에셋 관리',
            ],
            [
                'group'       => '환경설정',
                'name'        => '카테고리 관리 메뉴',
                'permission'  => 'categories.menu',
                'description' => '업종별 카테고리 분류 및 트리 구조 관리',
            ],
        ],
    ];

### 3.2 분리 설계에 따른 기술적 이점
1. **Blade 뷰의 독립화**: `create.blade.php`, `edit.blade.php`에서는 단순히 `config('admin_menu.operator_permissions')`만 순회하므로 신규 메뉴 추가 시 뷰 수정이 일절 불필요.
2. **역할별 권한 확장 안전성**: 키 이름을 단순 `permissions`가 아닌 `operator_permissions`로 지정하여, 추후 지사 관리자(`center_permissions`) 등의 새로운 권한 풀이 추가되어도 상호 충돌 없이 독립 유지.
3. **DB 및 Seeder와의 무결성**: DB `permissions` 테이블(`guard_name='web'`) 및 `RoleAndAdminSeeder`의 권한 키 목록과 1:1로 정확하게 일치.

---

## 4. 부운영자 등록 및 수정 화면 연동 내역

### 4.1 등록 화면 (`resources/views/admin/users/create.blade.php`)
- **다중 시스템 권한 선택 (`roles[]`)**:
    - 기존의 단일 `<select name="role">`을 제거하고, `x-admin.forms.checkbox`를 활용한 다중 선택 그룹(`roles[]`)으로 전면 전환.
    - 역할 옵션: 본사 부운영자(`operator`), 뷰티샵 운영자(`shop`), 센터 운영자(`center`), 제조사 운영자(`manufacturer`).
- **부운영자 메뉴 권한 모달 연동**:
    - 본사 부운영자 체크박스에 `id="roleOperator"`를 부여하고, 체크 시 `#operatorPermissionSection`이 노출되도록 JS 연동.
    - `permissionModal` 내부에서 `config('admin_menu.operator_permissions')`를 순회하며 메뉴별 세부 권한 체크박스 렌더링.

### 4.2 수정 화면 (`resources/views/admin/users/edit.blade.php`)
- **최고관리자(`admin`) 보호 로직**:
    - 대상 회원이 최고관리자일 경우 상태 및 시스템 권한 필드를 disabled 고정하고 안내 경고창 노출.
- **공용 컴포넌트 전면 적용**:
    - 성명, 이메일, 휴대폰 번호 필드를 `x-admin.forms.input`으로 표준화.
    - 계정 상태 필드를 `x-admin.forms.select`로 전환.
    - 시스템 역할 및 모달 권한 목록을 `x-admin.forms.checkbox`로 전환하고 `hasRole()`, `hasPermissionTo()`를 통해 기존 권한 복원 자동화.

---

## 5. 프론트엔드 검증 스크립트 규격 (`admin_user.js`)
라디오 및 체크박스 그룹의 필수 검증을 위해 그룹 단위 체크 판별 로직을 구축하였습니다.

    // admin_user.js 라디오/체크박스 검증 패턴
    const requiredRadios = form.querySelectorAll('input[type="radio"][data-required="true"]');
    const checkedRadioNames = new Set();

    for (const radio of requiredRadios) {
        const groupName = radio.name;
        if (checkedRadioNames.has(groupName)) continue;
        checkedRadioNames.add(groupName);

        const isChecked = form.querySelector('input[type="radio"][name="' + groupName + '"]:checked');
        if (!isChecked) {
            e.preventDefault();
            const label = radio.dataset.label || groupName;
            showSwalAlert('<strong>[' + label + ']</strong> 항목을 선택해 주세요.', radio, null, 'warning', '선택 확인');
            return false;
        }
    }

---

## 6. 운영 및 배포 주의사항
- 설정 파일(`config/admin_menu.php`)을 수정한 후에는 반드시 라라벨 설정 캐시를 갱신해야 브라우저에 즉각 반영됩니다.
    
    php artisan config:clear
    php artisan config:cache