관리자 환경설정이 값을 저장은 하지만 실제 동작에는 닿지 않던 결함군을 정리했다. 설정 화면(min/max)·저장 검증(FormRequest)·런타임 적용(Service) 세 계층이 각자 리터럴을 들고 있어 서로 조용히 갈라지던 것이 공통 원인이다. 경계값 SSoT 도입: - config/core.php `settings_limits` 를 단일 출처로 두고, FormRequest 는 이 값으로 규칙을 만들고 화면은 설정 응답의 `_meta.limits` 로 같은 값을 받아 바인딩한다. - 게시판·이커머스도 각 모듈 config 의 limits 를 동일 구조로 노출한다. 적용되지 않던 정책을 실제로 적용: - 비밀번호 최소 길이·특수문자 필수(신규 지정 7개 경로. 로그인은 제외 — 정책을 올렸을 때 기존 회원이 자기 계정에 접근조차 못 하는 데드락을 막기 위함) - 계정 잠금 시간 0 = 무한대(전용 컬럼 분리. timestamp 상한과 NULL 규약 때문에 기존 컬럼 재사용이 불가능하다) + 관리자 수동 해제 경로 - 업로드 최대 크기·허용 확장자, 게시판 첨부 개수·댓글 깊이, 페이지 첨부 정책, 장바구니 수량·구매 한도, 마일리지 사용 한도, 주문 부가세 파생 결함: - 큐 Job(sync 드라이버)이 진행 중인 요청의 Request 바인딩을 덮어써, 뒤이어 실행되는 훅 리스너가 헤더를 잃고 조용히 건너뛰었다. Job 이 원 바인딩을 복원하도록 고치고, 전송 헤더가 필요한 리스너는 동기 실행으로 선언한다. - 확장 업데이트 실패 시 상태만 되돌리고 상태 캐시를 비우지 않아, 그 확장의 관리자 화면만 캐시 TTL(기본 하루) 동안 404 로 남았다. 실패 경로에도 무효화를 대칭으로 두고, 빈 목록 자체를 캐시하지 않는다. - 글 작성 시 첨부 파일이 Service 로 전달되지 않아 검증·권한은 통과하고 첨부만 사라졌다. - 환경설정 저장 실패 안내가 내부 설정 키를 그대로 노출했다(105개 항목에 라벨 부여). 재발 방지: - audit 룰 3종(레이아웃 경계 리터럴 / FormRequest 경계 불일치 / Service 재클램프) - 테스트 실행 범위 강제 훅(check-test-scope) - 트러블슈팅 사례 2건(큐 Job 요청 오염 / 확장 상태 캐시 고착) 공개 이슈: gnuboard/g7
331 lines
12 KiB
PHP
331 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\Admin;
|
|
|
|
use App\Exceptions\CannotDeleteSuperAdminException;
|
|
use App\Http\Controllers\Api\Base\AdminBaseController;
|
|
use App\Http\Requests\User\BulkUpdateUserStatusRequest;
|
|
use App\Http\Requests\User\CheckEmailRequest;
|
|
use App\Http\Requests\User\CreateUserRequest;
|
|
use App\Http\Requests\User\DeleteUserRequest;
|
|
use App\Http\Requests\User\SearchUserRequest;
|
|
use App\Http\Requests\User\UpdateLanguageRequest;
|
|
use App\Http\Requests\User\UpdateUserRequest;
|
|
use App\Http\Requests\User\UserListRequest;
|
|
use App\Http\Resources\UserCollection;
|
|
use App\Http\Resources\UserResource;
|
|
use App\Models\User;
|
|
use App\Services\UserService;
|
|
use Exception;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
/**
|
|
* 관리자용 사용자 관리 컨트롤러
|
|
*
|
|
* 관리자가 시스템 사용자들을 관리할 수 있는 기능을 제공합니다.
|
|
*/
|
|
class UserController extends AdminBaseController
|
|
{
|
|
public function __construct(
|
|
private UserService $userService
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
/**
|
|
* 필터링된 사용자 목록을 조회합니다.
|
|
*
|
|
* @param UserListRequest $request 사용자 목록 요청 데이터
|
|
* @return JsonResponse 사용자 목록과 통계 정보를 포함한 JSON 응답
|
|
*/
|
|
public function index(UserListRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$filters = $request->validated();
|
|
|
|
$users = $this->userService->getPaginatedUsers($filters);
|
|
$statistics = $this->userService->getStatistics();
|
|
|
|
$collection = new UserCollection($users);
|
|
|
|
return $this->success(
|
|
'user.fetch_success',
|
|
$collection->withStatistics($statistics)
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.fetch_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 새로운 사용자를 생성합니다.
|
|
*
|
|
* @param CreateUserRequest $request 사용자 생성 요청 데이터
|
|
* @return JsonResponse 생성된 사용자 정보를 포함한 JSON 응답
|
|
*/
|
|
public function store(CreateUserRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$user = $this->userService->createUser($request->validated());
|
|
|
|
return $this->successWithResource(
|
|
'user.create_success',
|
|
new UserResource($user),
|
|
201
|
|
);
|
|
} catch (ValidationException $e) {
|
|
return $this->error('user.create_failed', 422, $e->errors());
|
|
} catch (Exception $e) {
|
|
return $this->error('user.create_failed', 500, $e, ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 사용자의 상세 정보를 조회합니다.
|
|
*
|
|
* @param User $user 조회할 사용자 모델
|
|
* @return JsonResponse 사용자 상세 정보를 포함한 JSON 응답
|
|
*/
|
|
public function show(User $user): JsonResponse
|
|
{
|
|
try {
|
|
// 관계형 데이터 로드
|
|
$user->load(['modules', 'plugins', 'menus', 'roles', 'consents']);
|
|
$user->loadCount(['modules', 'plugins', 'menus']);
|
|
|
|
// withAdminInfo()를 호출하여 관리자용 상세 정보 및 모듈 훅 데이터 포함
|
|
$resource = new UserResource($user);
|
|
|
|
return $this->success(
|
|
'user.fetch_success',
|
|
$resource->withAdminInfo()
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.fetch_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기존 사용자 정보를 수정합니다.
|
|
*
|
|
* @param UpdateUserRequest $request 사용자 수정 요청 데이터
|
|
* @param User $user 수정할 사용자 모델
|
|
* @return JsonResponse 수정된 사용자 정보를 포함한 JSON 응답
|
|
*/
|
|
public function update(UpdateUserRequest $request, User $user): JsonResponse
|
|
{
|
|
try {
|
|
$updatedUser = $this->userService->updateUser($user, $request->validated());
|
|
|
|
return $this->successWithResource(
|
|
'user.update_success',
|
|
new UserResource($updatedUser)
|
|
);
|
|
} catch (ValidationException $e) {
|
|
return $this->error('user.update_failed', 422, $e->errors());
|
|
} catch (Exception $e) {
|
|
return $this->error('user.update_failed', 500, $e, ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용자의 계정 잠금을 해제합니다.
|
|
*
|
|
* 로그인 실패 누적으로 잠긴 계정(특히 잠금 시간 0 = 무한대 설정으로 영구 잠긴 계정)을
|
|
* 관리자가 수동으로 풀어 줍니다. 이 경로가 없으면 영구 잠금 계정은 성공 로그인 자체가
|
|
* 불가하므로 복구 수단이 없습니다.
|
|
*
|
|
* @param User $user 잠금을 해제할 사용자 모델
|
|
* @return JsonResponse 갱신된 사용자 정보를 포함한 JSON 응답
|
|
*/
|
|
public function unlock(User $user): JsonResponse
|
|
{
|
|
try {
|
|
$unlocked = $this->userService->unlockAccount($user);
|
|
|
|
return $this->successWithResource(
|
|
'auth.account_unlocked',
|
|
new UserResource($unlocked)
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.update_failed', 500, $e, ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용자를 삭제합니다.
|
|
*
|
|
* @param DeleteUserRequest $request 사용자 삭제 요청 데이터
|
|
* @param User $user 삭제할 사용자 모델
|
|
* @return JsonResponse 삭제 결과 JSON 응답
|
|
*/
|
|
public function destroy(DeleteUserRequest $request, User $user): JsonResponse
|
|
{
|
|
try {
|
|
$result = $this->userService->deleteUser($user);
|
|
|
|
if ($result) {
|
|
return $this->success('user.delete_success');
|
|
} else {
|
|
return $this->error('user.delete_failed');
|
|
}
|
|
} catch (CannotDeleteSuperAdminException $e) {
|
|
return $this->error('exceptions.cannot_delete_super_admin', 422);
|
|
} catch (ValidationException $e) {
|
|
// UserService 가 던진 ValidationException 의 general[0] 에는 이미 `:error` 가
|
|
// 치환된 완성 메시지(예: "사용자 삭제에 실패했습니다: <상세 사유>")가 들어있다.
|
|
// 이를 최상위 message 로도 노출해 토스트에 `:error` 가 그대로 보이지 않게 한다
|
|
// (에러 상세 표시 기능 유지). errors 배열도 함께 전달.
|
|
$detail = $e->errors()['general'][0] ?? null;
|
|
|
|
return $this->error($detail ?? 'user.delete_failed', 422, $e->errors());
|
|
} catch (Exception $e) {
|
|
return $this->error('user.delete_failed', 500, $e, ['error' => $e->getMessage()]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 사용자 관련 통계 정보를 조회합니다.
|
|
*
|
|
* @return JsonResponse 사용자 통계 데이터를 포함한 JSON 응답
|
|
*/
|
|
public function statistics(): JsonResponse
|
|
{
|
|
try {
|
|
$statistics = $this->userService->getStatistics();
|
|
$languageDistribution = $this->userService->getUserLanguageDistribution();
|
|
|
|
$data = array_merge($statistics, [
|
|
'language_distribution' => $languageDistribution,
|
|
]);
|
|
|
|
return $this->success('user.statistics_success', $data);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.statistics_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 최근 등록된 사용자들을 조회합니다.
|
|
*
|
|
* @return JsonResponse 최근 사용자 목록을 포함한 JSON 응답
|
|
*/
|
|
public function recent(): JsonResponse
|
|
{
|
|
try {
|
|
$users = $this->userService->getRecentUsers(10);
|
|
|
|
return $this->successWithResource(
|
|
'user.fetch_success',
|
|
UserResource::collection($users)
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.fetch_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 키워드로 사용자를 검색합니다. (이름, 닉네임, 이메일)
|
|
*
|
|
* @param SearchUserRequest $request 사용자 검색 요청 데이터
|
|
* @return JsonResponse 검색된 사용자 목록을 포함한 JSON 응답
|
|
*/
|
|
public function search(SearchUserRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$validated = $request->validated();
|
|
|
|
if (isset($validated['uuid'])) {
|
|
$user = $this->userService->getUserByUuid($validated['uuid']);
|
|
$users = $user ? collect([$user]) : collect();
|
|
} elseif (isset($validated['id'])) {
|
|
$user = $this->userService->getUserById($validated['id']);
|
|
$users = $user ? collect([$user]) : collect();
|
|
} else {
|
|
$users = $this->userService->searchByKeyword($validated['keyword']);
|
|
}
|
|
|
|
return $this->successWithResource(
|
|
'user.search_success',
|
|
UserResource::collection($users)
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.search_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 이메일 주소의 중복 여부를 확인합니다.
|
|
*
|
|
* @param CheckEmailRequest $request 이메일 중복 확인 요청 데이터
|
|
* @return JsonResponse 이메일 사용 가능 여부를 포함한 JSON 응답
|
|
*/
|
|
public function checkEmail(CheckEmailRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$validated = $request->validated();
|
|
$email = $validated['email'];
|
|
$excludeUserId = $validated['exclude_user_id'] ?? null;
|
|
|
|
$isAvailable = $this->userService->isEmailAvailable($email, $excludeUserId);
|
|
|
|
return $this->success(
|
|
$isAvailable ? 'user.email_available' : 'user.email_unavailable',
|
|
['available' => $isAvailable]
|
|
);
|
|
} catch (Exception $e) {
|
|
return $this->error('user.email_check_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 현재 로그인된 사용자의 언어 설정을 업데이트합니다.
|
|
*
|
|
* @param UpdateLanguageRequest $request 언어 변경 요청 데이터
|
|
* @return JsonResponse 변경된 사용자 정보를 포함한 JSON 응답
|
|
*/
|
|
public function updateMyLanguage(UpdateLanguageRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$user = $request->user();
|
|
$validated = $request->validated();
|
|
$oldLanguage = $user->language;
|
|
|
|
$updatedUser = $this->userService->updateUserLanguage($user, $validated['language']);
|
|
|
|
return $this->successWithResource(
|
|
'user.language_update_success',
|
|
new UserResource($updatedUser)
|
|
);
|
|
} catch (ValidationException $e) {
|
|
return $this->error('user.language_update_failed', 422, $e->errors());
|
|
} catch (Exception $e) {
|
|
return $this->error('user.language_update_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 여러 사용자의 상태를 일괄 변경합니다.
|
|
*
|
|
* @param BulkUpdateUserStatusRequest $request 일괄 상태 변경 요청 데이터
|
|
* @return JsonResponse 일괄 변경 결과를 포함한 JSON 응답
|
|
*/
|
|
public function bulkUpdateStatus(BulkUpdateUserStatusRequest $request): JsonResponse
|
|
{
|
|
try {
|
|
$validated = $request->validated();
|
|
$result = $this->userService->bulkUpdateStatus($validated['ids'], $validated['status']);
|
|
|
|
return $this->success(
|
|
'user.bulk_status_updated',
|
|
$result
|
|
);
|
|
} catch (ValidationException $e) {
|
|
return $this->error('user.bulk_update_status_failed', 422, $e->errors());
|
|
} catch (Exception $e) {
|
|
return $this->error('user.bulk_update_status_failed', 500, $e->getMessage());
|
|
}
|
|
}
|
|
}
|