---
title: 플랫폼 사용자 계정 라이프사이클 및 공용 UI 컴포넌트 기술 명세서
section_key: chapter_1
section_label: 제1장 - 회원 관리 및 공용 UI 가이드
---

# GLOBEAU 뷰티 플랫폼 - 사용자 계정 라이프사이클 및 공용 UI 컴포넌트 기술 명세서
(Technical Specification & Operations Manual for User Lifecycle, Data Integrity & Common Frontend UI)

---

## 문서 제어 및 개정 이력 (Document Control & Revision History)

### 기본 제어 정보
| 항목 | 내용 |
|---|---|
| **문서명** | GLOBEAU 플랫폼 사용자 계정 라이프사이클 및 공용 UI 컴포넌트 기술 명세서 |
| **현재 버전** | v1.0.0 |
| **최종 개정일** | 2026-10-20 |
| **작성/관리자** | 본사 시스템개발팀 |
| **적용 시스템** | GLOBEAU 본사 백오피스 (/admin), 회원 관리 시스템, 공용 프론트엔드 유틸리티 |
| **문서 목적** | 회원 상태 제어(active/suspended), 오등록 계정 해소 및 이력 보존을 위한 2-Track 삭제 정책, User 모델 도메인 무결성 헬퍼, SweetAlert2 기반 공용 컨펌 유틸리티(showSwalConfirm)의 기술 표준 수립 |
| **문서 상태** | 개발 및 운영 공식 표준 (Production Standard) |

### 개정 및 변경 이력
| 버전 | 개정일자 | 구분 | 주요 변경 내역 | 작업자 |
|:---:|:---:|:---:|---|:---:|
| **v1.0.0** | 2026-10-20 | 신규제정 | • 관리자 직권 활성화/정지 및 영구 보호 계정 통제 원칙 수립<br>• 오등록 소프트 딜리트 및 활동 계정 영구 보존 2-Track 삭제 정책 확립<br>• User 모델 직관적 헬퍼(isActive, hasLoggedIn 등) 및 도메인 무결성(shops, centers, manufacturers) 가드 구현<br>• 객체형 옵션, 위험도별 동적 색상, 엔터키 실수 방어(focusCancel), 미로드 폴백을 결합한 showSwalConfirm 표준 명세화 | 개발팀 |

---

## 1. 사용자 계정 라이프사이클 종합 운영 정책 (User Account Lifecycle)

### 1.1 계정 상태 제어 정책 (State Control & Admin Provisioning)
1. **관리자 직권 활성화 보장**: 본사 운영자(`admin`)는 회원의 휴대폰 본인인증 여부와 관계없이 필요 시 회원의 상태를 일시중지(`suspended`) 또는 정상(`active`)으로 언제든 즉시 전환할 수 있습니다.
2. **신규 계정 수동 발급(Admin Provisioning) 정책**:
   - 관리자가 본사 백오피스에서 수동 생성하는 부운영자 및 뷰티샵 관리자 계정은 기본적으로 `active`(정상)로 발급하여 즉시 업무에 투입될 수 있도록 지원합니다.
   - 대기 목적으로 `suspended`로 등록했더라도 관리자가 목록에서 [활성화] 토글 클릭 한 번으로 즉시 정상 전환이 가능해야 합니다.
3. **보호 계정 예외 규정**:
   - **셀프 잠금 방지**: 현재 로그인한 관리자 본인 계정은 스스로 본인의 상태를 일시중지로 변경하거나 삭제할 수 없습니다.
   - **최상위 최고관리자 영구 보호**: 시스템 최상위 마스터 계정(ID: 1 또는 `admin` 롤 보유자)은 시스템 연속성을 위해 타 관리자에 의해 정지되거나 삭제될 수 없습니다.

---

### 1.2 계정 2-Track 삭제 및 권한 박탈 정책 (2-Track Deletion Standard)

플랫폼의 법적 감사 추적(Audit Trail)과 관리자의 오등록 실수 수습을 동시에 달성하기 위해 2-Track 정책을 엄격히 적용합니다.

    [트랙 1: 미사용 오등록 계정]
    접속 이력 없음 (last_login_at === null) & 도메인 연결 데이터 없음
    ──▶ 최고관리자 직권 [삭제] 버튼 노출
    ──▶ 이메일/전화번호 뒤에 _deleted_{timestamp} 마킹 (유니크 제약 해제)
    ──▶ Spatie 권한 회수 및 소프트 딜리트 처리
    ──▶ 동일한 정상 이메일로 즉시 재등록 가능

    [트랙 2: 활동 이력 계정]
    로그인 이력 존재 (last_login_at !== null) 또는 매장/센터/제조사/주문 데이터 존재
    ──▶ 화면상 [삭제] 버튼 영구 숨김 처리 & 백엔드 403 차단
    ──▶ 퇴사/보직 해제 시 status = 'suspended' 전환
    ──▶ Spatie 역할 전면 회수 (syncRoles([]))
    ──▶ 시스템 접근 차단 및 법적 감사 데이터 영구 보존

#### 트랙 1: 접속 및 활동 이력이 전혀 없는 경우 (오등록 ➔ 제한적 소프트 딜리트 허용)
- **판정 조건**: 최초 로그인 이력이 없고(`last_login_at === null`), 매장(`shops`), 소속 센터(`centers`), 협력 제조사(`manufacturers`), 결제 등 외래키 연결 데이터가 전혀 없는 경우.
- **처리 절차**:
  1. 최고관리자(`admin`) 등급에 한해서만 삭제 버튼이 노출되며 백엔드 요청을 허가합니다.
  2. 삭제 직전 회원의 이메일과 전화번호에 `_deleted_{timestamp}`를 결합(마킹)합니다. 이를 통해 DB의 고유키(Unique Index) 충돌을 해소하므로, 오등록했던 정상 이메일/전화번호를 즉시 시스템에 올바르게 재등록할 수 있습니다.
  3. 부여된 역할을 회수하고 라라벨 Eloquent `SoftDeletes`를 수행합니다.

#### 트랙 2: 이미 로그인하여 활동한 경우 (삭제 절대 불가 ➔ 권한 박탈 + 영구 정지)
- **판정 조건**: `last_login_at`이 존재하거나 시스템 업무 로그, 결재, 샵 운영 이력이 엮여 있는 경우.
- **처리 절차**:
  1. 프론트엔드 목록 및 상세 화면에서 삭제 버튼 자체가 렌더링되지 않습니다.
  2. 백엔드 `destroy` API로 변조 호출이 들어오더라도 403 예외(`abort(403)`)를 발생시켜 원천 차단합니다.
  3. 퇴사나 보직 해제 시 계정 상태를 일시중지(`suspended`)로 잠그고, Spatie 권한/역할을 전면 회수(`$user->syncRoles([])`)하여 접근을 영구 차단하되 데이터베이스의 외래키 무결성과 감사 로그는 온전히 보존합니다.

---

### 1.3 계정 상태 및 삭제 권한 제어 매트릭스

| 대상 계정 유형 | 상태 전환 (정상 ↔ 일시중지) | 계정 삭제 허용 여부 | 시스템 조치 및 데이터 처리 규약 |
|---|:---:|:---:|---|
| **최고관리자 (ID: 1, admin)** | ❌ 불가 | ❌ 불가 | 시스템 필수 계정으로 영구 보호 (버튼 비활성화) |
| **로그인 이력 보유 계정** | ⭕ 자유 전환 | ❌ 불가 (보존) | 삭제 버튼 미노출, 퇴사 시 `suspended` 및 `syncRoles([])` |
| **접속 이력 없는 미사용 계정** | ⭕ 자유 전환 | ⭕ 허용 (최고관리자) | `_deleted_{timestamp}` 마킹 후 소프트 딜리트 (재등록 보장) |
| **로그인 본인 계정** | ❌ 불가 | ❌ 불가 | 셀프 계정 잠금 및 셀프 삭제 방지 가드 |

---

## 2. 백엔드 도메인 무결성 및 Eloquent 모델 규약 (Backend Integrity & User Model)

### 2.1 User 모델 헬퍼 메서드 및 도메인 무결성 확장 (`app/Models/User.php`)
비즈니스 로직의 오타를 방지하고 가독성을 극대화하기 위해 직관적인 상태 판별 메서드를 캡슐화하며, 플랫폼 핵심 도메인(`shops`, `centers`, `manufacturers`)과의 연결 관계를 통합 검증합니다.

    namespace App\Models;

    use Illuminate\Foundation\Auth\User as Authenticatable;
    use Illuminate\Database\Eloquent\SoftDeletes;
    use Spatie\Permission\Traits\HasRoles;

    class User extends Authenticatable
    {
        use HasRoles, SoftDeletes;

        protected $guarded = [];

        protected $casts = [
            'last_login_at'     => 'datetime',
            'is_phone_verified' => 'boolean',
        ];

        /**
         * 계정 활성 상태 판별 헬퍼
         */
        public function isActive(): bool
        {
            return $this->status === 'active';
        }

        /**
         * 계정 정지 상태 판별 헬퍼
         */
        public function isSuspended(): bool
        {
            return $this->status === 'suspended';
        }

        /**
         * 시스템 로그인 접속 이력 판별 헬퍼
         */
        public function hasLoggedIn(): bool
        {
            return !is_null($this->last_login_at);
        }

        // ==========================================
        // 플랫폼 3대 공급자 도메인 연관관계 정의
        // ==========================================

        public function shops()
        {
            return $this->hasMany(Shop::class);
        }

        public function centers()
        {
            return $this->hasMany(Center::class);
        }

        public function manufacturers()
        {
            return $this->hasMany(Manufacturer::class);
        }

        /**
         * 2-Track 삭제 정책 가드: 오등록 계정 여부 판별
         */
        public function canBeDeleted(): bool
        {
            // 접속 이력이 1회라도 있다면 영구 보존
            if ($this->hasLoggedIn()) {
                return false;
            }

            // 도메인 핵심 데이터가 연결되어 있다면 삭제 불가
            if ($this->shops()->exists() || $this->centers()->exists() \vert{}\vert{}$this->manufacturers()->exists()) {
                return false;
            }

            return true;
        }

        /**
         * 유니크 제약 해제 및 오등록 계정 안전 삭제 처리
         */
        public function safeDeleteForReRegistration(): void
        {
            $timestamp = now()->timestamp;

            $this->update([
                'email'  => $this->email . '_deleted_' .$timestamp,
                'phone'  => $this->phone ? $this->phone . '_deleted_' .$timestamp : null,
                'status' => 'suspended',
            ]);

            $this->syncRoles([]);$this->delete();
        }
    }

---

### 2.2 본사 관리자 계정 통제 컨트롤러 구현 (`App\Http\Controllers\Admin\UserController.php`)

    namespace App\Http\Controllers\Admin;

    use App\Http\Controllers\Controller;
    use Illuminate\Http\Request;
    use App\Models\User;

    class UserController extends Controller
    {
        /**
         * 계정 상태 직권 전환 (active <-> suspended)
         */
        public function updateStatus(Request $request, User$user)
        {
            // 1. 본인 계정 변경 차단
            if (auth()->id() === $user->id) {
                return back()->with('error', '본인 계정의 상태는 직접 변경할 수 없습니다.');
            }

            // 2. 최고관리자(ID: 1) 보호
            if ($user->id === 1) {
                return back()->with('error', '최고 관리자 계정은 정지할 수 없습니다.');
            }

            $validated =$request->validate([
                'status' => 'required|in:active,suspended',
            ]);

            $newStatus =$validated['status'];

            // 3. 중복 갱신 방어
            if ($user->status ===$newStatus) {
                return back()->with('info', '이미 해당 상태로 설정되어 있습니다.');
            }

            $user->update(['status' =>$newStatus]);

            // 계정 정지 시 Spatie 역할 회수 조치
            if ($user->isSuspended()) {$user->syncRoles([]);
            }

            $statusText =$user->isActive() ? '정상 활성화' : '일시중지(잠금)';
            return back()->with('success', "[{$user->name}] 계정이 {$statusText} 상태로 변경되었습니다.");
        }

        /**
         * 계정 삭제 (오등록 계정에 한해 제한적 삭제 허용)
         */
        public function destroy(Request $request, User$user)
        {
            // 1. 최고관리자 및 본인 계정 삭제 차단
            if ($user->id === 1 \vert{}\vert{} auth()->id() ===$user->id) {
                abort(403, '보호된 계정이거나 본인 계정은 삭제할 수 없습니다.');
            }

            // 2. 활동 이력 및 도메인 무결성 검증 가드
            if (!$user->canBeDeleted()) {
                return back()->with('error', '로그인 이력이 있거나 연결된 매장/데이터가 존재하는 계정은 시스템 감사 추적을 위해 삭제할 수 없습니다. 일시중지(suspended)로 변경해 주세요.');
            }

            // 3. 유니크 해제 마킹 및 소프트 딜리트 실행
            $user->safeDeleteForReRegistration();

            return back()->with('success', '오등록된 계정이 안전하게 삭제 처리되었습니다. (해당 이메일로 즉시 재등록 가능)');
        }
    }

---

## 3. 공용 프론트엔드 UI 컴포넌트 및 컨펌창 표준 규약 (Frontend Standards)

### 3.1 SweetAlert2 기반 showSwalConfirm 아키텍처 및 진화 배경
기존 `user_custom.js`의 단순 위치형 인자 방식은 코드가 짧은 장점이 있으나 실무 운영 환경에서 치명적인 한계가 발생했습니다.

#### 기존 방식 vs 신규 표준 비교 분석
| 비교 항목 | 기존 user_custom.js 방식 | 신규 표준 admin_custom.js 규약 | 개선 효과 |
|---|---|---|---|
| **인자 전달 방식** | **위치형 인자 (Positional)**<br>`showSwalConfirm(msg, title, confirm, cancel, icon)` | **객체형 옵션 (Options Object)**<br>`showSwalConfirm({ message, icon, confirmColor, ... })` | 인자 순서 암기 불필요, 필요한 속성만 선언하여 가독성 극대화 |
| **호출 편의성** | 아이콘(`icon`) 하나만 바꾸려 해도 중간의 제목, 버튼 텍스트를 모두 채워야 함 | 순서와 무관하게 변경할 키-값만 전달 가능 | 코드 중복 제거 및 유지보수성 향상 |
| **버튼 색상 제어** | 브랜드 컬러(`#b18977`) 고정 | `confirmColor` 동적 지정 지원<br>(일반: `#b18977`, 삭제: `#dc3545`, 정지: `#f0ad4e`) | 파괴적 작업(삭제) 시 사용자에게 시각적 경고 제공 |
| **엔터키 오실행 방어** | 기본 포커스가 [확인]에 위치 | **`focusCancel: true` 강제 적용** | 팝업이 뜨자마자 엔터를 연타하여 발생하는 오삭제 원천 방어 |
| **미로드 방어 (Fallback)** | 브라우저 기본 `window.confirm()` 폴백 제공 | 폴백 로직을 온전히 계승하여 유지 | 네트워크 지연이나 CDN 장애 시에도 무중단 작동 |
| **부트스트랩 모달 호환** | 미지원 (백드롭 뒤로 숨김 가능성) | `target: currentModal ? currentModal : 'body'` | 모달 내부에서 호출되어도 z-index 및 포커스 충돌 없음 |

---

### 3.2 안전 설계 핵심 5대 메커니즘
1. **객체형 옵션 파라미터 구조 분해**: 기본값을 내장하여 필수 메시지만 넘겨도 즉시 동작하며 인자 순서 꼬임을 방지합니다.
2. **동적 버튼 컬러 및 위험도 플래그(`isDanger`)**: 계정 삭제나 권한 박탈과 같은 파괴적 동작에는 자동으로 경고 레드(`#dc3545`)를 바인딩합니다.
3. **엔터키 실수 방지 가드 (`focusCancel: true`)**: 확인 팝업 생성 시 기본 커서를 [취소] 버튼에 위치시켜 엔터키 연타로 인한 오삭제를 방지합니다.
4. **SweetAlert2 미로드 시 브라우저 confirm 폴백**: `typeof Swal === 'undefined'` 환경에서도 브라우저 표준 대화상자를 호출하여 스크립트 에러를 방어합니다.
5. **부트스트랩 모달 DOM 타겟팅**: 현재 화면에 열려 있는 부트스트랩 모달(`.modal.show`)이 있을 경우 해당 모달 내부를 타겟으로 지정하여 포커스 트랩과 레이아웃 깨짐을 방지합니다.

---

### 3.3 admin_custom.js 내 showSwalConfirm 완결 구현체

    // public/js/admin/admin_custom.js - 표준 공용 컨펌 유틸리티
    function showSwalConfirm(options = {}) {
        const {
            message = '진행하시겠습니까?',
            title = '확인',
            confirmText = '확인',
            cancelText = '취소',
            icon = 'question',
            confirmColor = '#b18977', // 기본 브랜드 모카
            cancelColor = '#6c757d',
            isDanger = false,          // 삭제 등 위험 작업 시 true 전달 (레드 자동 바인딩)
            onConfirm = null,
            onCancel = null
        } = options;

        // 1. SweetAlert2 CDN/라이브러리 미로드 시 브라우저 기본 confirm 폴백 가드
        if (typeof Swal === 'undefined') {
            if (window.confirm(message)) {
                if (typeof onConfirm === 'function') onConfirm();
            } else {
                if (typeof onCancel === 'function') onCancel();
            }
            return;
        }

        // 2. 위험도에 따른 확인 버튼 색상 동적 결정
        const activeConfirmColor = isDanger ? '#dc3545' : confirmColor;

        // 3. 모달 내부 호출 호환을 위한 동적 타겟 분기
        const currentModal = document.querySelector('.modal.show');

        // 4. 안전 가드가 적용된 SweetAlert2 컨펌창 실행
        Swal.fire({
            target: currentModal ? currentModal : 'body',
            title: title,
            html: message,
            icon: icon,
            showCancelButton: true,
            confirmButtonText: confirmText,
            cancelButtonText: cancelText,
            confirmButtonColor: activeConfirmColor,
            cancelButtonColor: cancelColor,
            focusCancel: true, // 엔터키 연타로 인한 파괴적 작업 오실행 원천 차단
            customClass: {
                popup: 'badge-rounded',
                confirmButton: 'badge-rounded',
                cancelButton: 'badge-rounded'
            }
        }).then((result) => {
            if (result.isConfirmed) {
                if (typeof onConfirm === 'function') {
                    onConfirm();
                }
            } else if (result.isDismissed) {
                if (typeof onCancel === 'function') {
                    onCancel();
                }
            }
        });
    }

---

### 3.4 블레이드 템플릿 연동 실무 마크업 예시

#### ① 오등록 계정 삭제 버튼 (트랙 1 조건 충족 시에만 렌더링)
    @if($user->canBeDeleted())
        <button type="button" 
                class="btn btn-sm btn-outline-danger" 
                onclick="confirmDeleteUser({{ $user->id }}, '{{$user->name }}')">
            <i class="fa-solid fa-trash-can me-1"></i> 오등록 삭제
        </button>

        <form id="delete-user-form-{{ $user->id }}" 
              action="{{ route('admin.users.destroy', $user) }}" 
              method="POST" 
              style="display: none;">
            @csrf
            @method('DELETE')
        </form>
    @endif

    <script>
    function confirmDeleteUser(userId, userName) {
        showSwalConfirm({
            title: '오등록 계정 삭제',
            message: `<strong>[${userName}]</strong> 계정을 삭제하시겠습니까?<br><small class="text-danger">접속 이력이 없는 오등록 계정으로, 고유키 제약이 해제되어 동일 이메일로 즉시 재등록 가능합니다.</small>`,
            icon: 'warning',
            confirmText: '삭제 확정',
            isDanger: true, // 버튼을 빨간색(#dc3545)으로 표시하고 focusCancel 강제
            onConfirm: function () {
                document.getElementById('delete-user-form-' + userId).submit();
            }
        });
    }
    </script>

#### ② 계정 일시중지(정지) 토글 버튼
    <button type="button" 
            class="btn btn-sm {{ $user->isActive() ? 'btn-outline-warning' : 'btn-outline-success' }}" 
            onclick="toggleUserStatus({{ $user->id }}, '{{ $user->name }}', '{{$user->isActive() ? 'suspended' : 'active' }}')">
        <i class="fa-solid {{ $user->isActive() ? 'fa-ban' : 'fa-check' }} me-1"></i>
        {{ $user->isActive() ? '일시중지' : '정상 활성화' }}
    </button>

    <form id="status-user-form-{{ $user->id }}" 
          action="{{ route('admin.users.update_status', $user) }}" 
          method="POST" 
          style="display: none;">
        @csrf
        @method('PATCH')
        <input type="hidden" name="status" id="status-input-{{ $user->id }}">
    </form>

    <script>
    function toggleUserStatus(userId, userName, targetStatus) {
        const isSuspending = (targetStatus === 'suspended');

        showSwalConfirm({
            title: isSuspending ? '계정 일시중지' : '계정 정상 활성화',
            message: isSuspending 
                ? `<strong>[${userName}]</strong> 계정을 일시중지하시겠습니까?<br><small class="text-muted">일시중지 시 Spatie 역할이 회수되며 시스템 접속이 차단됩니다.</small>`
                : `<strong>[${userName}]</strong> 계정을 정상 상태로 활성화하시겠습니까?`,
            icon: isSuspending ? 'warning' : 'question',
            confirmColor: isSuspending ? '#f0ad4e' : '#b18977',
            confirmText: isSuspending ? '네, 일시중지합니다' : '네, 활성화합니다',
            onConfirm: function () {
                document.getElementById('status-input-' + userId).value = targetStatus;
                document.getElementById('status-user-form-' + userId).submit();
            }
        });
    }
    </script>

---

## 4. 시스템 관리자 운영 점검 체크리스트 (Administrator Checklist)

- [ ] **영구 보호 계정 통제**: 최고관리자(ID: 1)와 본인 계정에 대해 상태 변경 및 삭제 버튼이 원천 차단되는가?
- [ ] **활동 계정 삭제 버튼 은닉**: `last_login_at`이 존재하거나 `shops`, `centers`, `manufacturers` 데이터가 연결된 회원의 목록/상세 화면에서 삭제 버튼이 숨겨지는가?
- [ ] **오등록 재등록 검증**: 미사용 계정 삭제 시 DB에서 `_deleted_{timestamp}`가 정상 결합되어 동일 이메일로 신규 회원가입이 즉시 가능한가?
- [ ] **권한 박탈 연동**: 계정을 `suspended`로 변경할 때 `$user->syncRoles([])`가 실행되어 Spatie 인가 권한이 즉시 회수되는가?
- [ ] **컨펌창 엔터키 방어**: `showSwalConfirm` 호출 시 확인창이 뜨자마자 엔터를 눌러도 취소 버튼에 포커스가 머물러 실수로 실행되지 않는가?
- [ ] **CDN 장애 폴백**: SweetAlert2 라이브러리가 로드되지 않은 환경에서도 브라우저 표준 `window.confirm()`으로 안전하게 대체 작동하는가?