apiUserId(); $user = User::find($userId); if (! $user) { throw new NotFoundHttpException(__('error.not_found_resource', ['resource' => '사용자'])); } // 비밀번호 확인 if (! password_verify($data['password'], $user->password)) { throw new BadRequestHttpException(__('error.account.invalid_password')); } return DB::transaction(function () use ($user, $data) { // 1. 모든 테넌트 연결 해제 (soft delete) UserTenant::where('user_id', $user->id)->delete(); // 2. 탈퇴 사유 저장 (options에 기록) $options = $user->options ?? []; $options['withdrawal'] = [ 'reason' => $data['reason'] ?? null, 'detail' => $data['detail'] ?? null, 'withdrawn_at' => now()->toIso8601String(), ]; $user->options = $options; $user->save(); // 3. 사용자 soft delete $user->delete(); // 4. 토큰 삭제 $user->tokens()->delete(); return [ 'withdrawn_at' => now()->toIso8601String(), ]; }); } /** * 사용 중지 (특정 테넌트에서만 탈퇴) */ public function suspend(): array { $userId = $this->apiUserId(); $tenantId = $this->tenantId(); $userTenant = UserTenant::where('user_id', $userId) ->where('tenant_id', $tenantId) ->first(); if (! $userTenant) { throw new NotFoundHttpException(__('error.account.tenant_membership_not_found')); } return DB::transaction(function () use ($userTenant, $userId, $tenantId) { // 1. 현재 테넌트에서 비활성화 $userTenant->is_active = false; $userTenant->left_at = now(); $userTenant->save(); // 2. 다른 활성 테넌트가 있으면 기본 테넌트 변경 $otherActiveTenant = UserTenant::where('user_id', $userId) ->where('tenant_id', '!=', $tenantId) ->where('is_active', true) ->first(); if ($otherActiveTenant) { // 다른 테넌트로 기본 변경 UserTenant::where('user_id', $userId)->update(['is_default' => false]); $otherActiveTenant->is_default = true; $otherActiveTenant->save(); return [ 'suspended' => true, 'new_default_tenant_id' => $otherActiveTenant->tenant_id, ]; } return [ 'suspended' => true, 'new_default_tenant_id' => null, ]; }); } /** * 약관 동의 정보 조회 */ public function getAgreements(): array { $userId = $this->apiUserId(); $user = User::find($userId); if (! $user) { throw new NotFoundHttpException(__('error.not_found_resource', ['resource' => '사용자'])); } $options = $user->options ?? []; $agreements = $options['agreements'] ?? self::getDefaultAgreements(); return [ 'agreements' => $agreements, 'types' => self::getAgreementTypes(), ]; } /** * 약관 동의 정보 수정 */ public function updateAgreements(array $data): array { $userId = $this->apiUserId(); $user = User::find($userId); if (! $user) { throw new NotFoundHttpException(__('error.not_found_resource', ['resource' => '사용자'])); } $options = $user->options ?? []; $currentAgreements = $options['agreements'] ?? self::getDefaultAgreements(); // 수신 데이터로 업데이트 foreach ($data['agreements'] as $agreement) { $type = $agreement['type']; if (isset($currentAgreements[$type])) { $currentAgreements[$type]['agreed'] = $agreement['agreed']; $currentAgreements[$type]['agreed_at'] = $agreement['agreed'] ? now()->toIso8601String() : null; } } $options['agreements'] = $currentAgreements; $user->options = $options; $user->save(); return [ 'agreements' => $currentAgreements, ]; } /** * 기본 약관 동의 항목 */ public static function getDefaultAgreements(): array { return [ 'terms' => [ 'type' => 'terms', 'label' => '이용약관', 'required' => true, 'agreed' => false, 'agreed_at' => null, ], 'privacy' => [ 'type' => 'privacy', 'label' => '개인정보 처리방침', 'required' => true, 'agreed' => false, 'agreed_at' => null, ], 'marketing' => [ 'type' => 'marketing', 'label' => '마케팅 정보 수신', 'required' => false, 'agreed' => false, 'agreed_at' => null, ], 'push' => [ 'type' => 'push', 'label' => '푸시 알림 수신', 'required' => false, 'agreed' => false, 'agreed_at' => null, ], 'email' => [ 'type' => 'email', 'label' => '이메일 수신', 'required' => false, 'agreed' => false, 'agreed_at' => null, ], 'sms' => [ 'type' => 'sms', 'label' => 'SMS 수신', 'required' => false, 'agreed' => false, 'agreed_at' => null, ], ]; } /** * 약관 유형 레이블 */ public static function getAgreementTypes(): array { return [ 'terms' => '이용약관', 'privacy' => '개인정보 처리방침', 'marketing' => '마케팅 정보 수신', 'push' => '푸시 알림 수신', 'email' => '이메일 수신', 'sms' => 'SMS 수신', ]; } /** * 탈퇴 사유 목록 */ public static function getWithdrawalReasons(): array { return [ 'not_using' => '서비스를 더 이상 사용하지 않음', 'difficult' => '사용하기 어려움', 'alternative' => '다른 서비스 이용', 'privacy' => '개인정보 보호 우려', 'other' => '기타', ]; } }