getDB()->table('admin_users') ->select('role') ->where('id', (int) $authData->admin_id) ->get()->getRow(); return $admin && $admin->role === 'super_admin'; } // TODO: 권한관리 시스템 구축 후, '관리자관리 권한'을 가진 admin만 // create/update/delete/changePassword/unlockAccount 가능하도록 가드 재추가 /** * 관리자의 메뉴 권한 동기화 (DELETE + INSERT) * - super_admin은 row 박지 않음 (role 자체가 모든 권한) * - admin은 검증된 배열만 INSERT */ private function syncPermissions(int $adminId, string $role, $permissions): void { $db = $this->getDB(); // 기존 권한 전부 제거 $db->table('admin_permissions')->where('admin_id', $adminId)->delete(); if ($role !== 'admin') return; if (!is_array($permissions) || empty($permissions)) return; // 허용 목록만 INSERT (중복 제거) $clean = array_values(array_unique(array_filter( $permissions, fn($p) => is_string($p) && in_array($p, self::ALLOWED_PERMISSIONS, true) ))); if (empty($clean)) return; $rows = array_map(fn($p) => [ 'admin_id' => $adminId, 'permission' => $p, 'created_at' => date('Y-m-d H:i:s'), ], $clean); $db->table('admin_permissions')->insertBatch($rows); } /** * 관리자의 메뉴 권한 배열 반환 * - super_admin은 'all' 반환 * - admin은 ['field', 'fishing', ...] 형태 */ private function getPermissions(int $adminId, string $role) { if ($role === 'super_admin') return 'all'; $rows = $this->getDB()->table('admin_permissions') ->select('permission') ->where('admin_id', $adminId) ->get()->getResult(); return array_map(fn($r) => $r->permission, $rows); } /** * Get all admins (관리자 목록) — 인증된 모두 가능 * GET /api/admin */ public function index() { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } try { $page = (int) ($this->request->getGet('page') ?? 1); $perPage = (int) ($this->request->getGet('per_page') ?? 10); if ($page < 1) $page = 1; if ($perPage < 1) $perPage = 10; $offset = ($page - 1) * $perPage; $db = $this->getDB(); $builder = $db->table('admin_users'); // 삭제된 계정만 / 활성만 분기 (deleted=1이면 삭제된 것만) $showDeleted = $this->request->getGet('deleted') === '1'; $builder->where('deleted_YN', $showDeleted ? 'Y' : 'N'); // 검색 $search = trim((string) $this->request->getGet('search')); $searchField = $this->request->getGet('search_field'); // username / name / email / '' if ($search !== '') { if ($searchField === 'username') { $builder->like('username', $search); } elseif ($searchField === 'name') { $builder->like('name', $search); } elseif ($searchField === 'email') { $builder->like('email', $search); } else { $builder->groupStart() ->like('username', $search) ->orLike('name', $search) ->orLike('email', $search) ->groupEnd(); } } // 역할 필터 $role = $this->request->getGet('role'); if (!empty($role) && in_array($role, self::ALLOWED_ROLES, true)) { $builder->where('role', $role); } // 상태 필터 $status = $this->request->getGet('status'); if (!empty($status) && in_array($status, self::ALLOWED_STATUSES, true)) { $builder->where('status', $status); } $total = $builder->countAllResults(false); $items = $builder ->select('id, username, name, email, phone, role, status, COALESCE(login_attempts, 0) as login_attempts, last_failed_login, last_login, created_at, updated_at') ->orderBy('id', 'DESC') ->limit($perPage, $offset) ->get() ->getResult(); // permissions 일괄 조회 후 attach if (!empty($items)) { $ids = array_map(fn($i) => (int) $i->id, $items); $rows = $db->table('admin_permissions') ->select('admin_id, permission') ->whereIn('admin_id', $ids) ->get()->getResult(); $permsByAdmin = []; foreach ($rows as $r) { $permsByAdmin[$r->admin_id][] = $r->permission; } foreach ($items as $item) { $item->permissions = $item->role === 'super_admin' ? 'all' : ($permsByAdmin[$item->id] ?? []); } } return $this->respondSuccess([ 'items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => (int) ceil($total / $perPage), ]); } catch (\Exception $e) { log_message('error', 'AdminController index error: ' . $e->getMessage()); return $this->respondError('관리자 목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Get single admin (관리자 상세) — 인증된 모두 가능 * GET /api/admin/:id */ public function show($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $admin = $this->getDB()->table('admin_users') ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at') ->where('id', (int) $id) ->where('deleted_YN', 'N') ->get() ->getRow(); if (!$admin) { return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } $admin->permissions = $this->getPermissions((int) $id, $admin->role); return $this->respondSuccess($admin); } catch (\Exception $e) { log_message('error', 'AdminController show error: ' . $e->getMessage()); return $this->respondError('관리자 조회 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Check if username is available (아이디 중복 체크) * GET /api/admin/check-username */ public function checkUsername() { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } $username = trim((string) $this->request->getGet('username')); if ($username === '') { return $this->respondError('아이디를 입력하세요.'); } try { $existing = $this->getDB()->table('admin_users') ->where('username', $username) ->where('deleted_YN', 'N') ->get() ->getRow(); return $this->respondSuccess(['available' => !$existing]); } catch (\Exception $e) { log_message('error', 'AdminController checkUsername error: ' . $e->getMessage()); return $this->respondError('중복 체크 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Create new admin (관리자 생성) — 슈퍼 관리자만 * POST /api/admin */ public function create() { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } try { $data = $this->request->getJSON(true) ?? []; // 필수 필드 검증 $required = ['username', 'password', 'name', 'email']; foreach ($required as $field) { if (empty($data[$field])) { return $this->respondError("{$field}는 필수 항목입니다.", ResponseInterface::HTTP_BAD_REQUEST); } } $username = trim((string) $data['username']); // role / status 검증 $role = $data['role'] ?? 'admin'; if (!in_array($role, self::ALLOWED_ROLES, true)) { return $this->respondError('올바르지 않은 역할입니다.', ResponseInterface::HTTP_BAD_REQUEST); } $status = $data['status'] ?? 'active'; if (!in_array($status, self::ALLOWED_STATUSES, true)) { return $this->respondError('올바르지 않은 상태입니다.', ResponseInterface::HTTP_BAD_REQUEST); } // 호출자가 슈퍼관리자가 아니면 super_admin 등록 차단 if ($role === 'super_admin' && !$this->isCallerSuperAdmin($auth)) { return $this->respondError('슈퍼 관리자 권한 부여는 슈퍼 관리자만 가능합니다.', ResponseInterface::HTTP_FORBIDDEN); } // 일반 admin은 권한 1개 이상 필수 $permissions = $data['permissions'] ?? []; if ($role === 'admin') { if (!is_array($permissions) || empty($permissions)) { return $this->respondError('관리자에게 부여할 메뉴 권한을 1개 이상 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST); } } // 중복 체크 (삭제된 계정 제외 — 재사용 허용) $existing = $this->getDB()->table('admin_users') ->groupStart() ->where('username', $username) ->orWhere('email', $data['email']) ->groupEnd() ->where('deleted_YN', 'N') ->get() ->getRow(); if ($existing) { if ($existing->username === $username) { return $this->respondError('이미 사용 중인 아이디입니다.', ResponseInterface::HTTP_BAD_REQUEST); } if ($existing->email === $data['email']) { return $this->respondError('이미 사용 중인 이메일입니다.', ResponseInterface::HTTP_BAD_REQUEST); } } $insertData = [ 'username' => $username, 'password' => password_hash($data['password'], PASSWORD_DEFAULT), 'password_changed_at' => date('Y-m-d H:i:s'), 'name' => trim((string) $data['name']), 'email' => trim((string) $data['email']), 'phone' => trim((string) ($data['phone'] ?? '')), 'role' => $role, 'status' => $status, 'login_attempts' => 0, 'deleted_YN' => 'N', 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $this->getDB()->table('admin_users')->insert($insertData); $insertId = $this->getDB()->insertID(); // 권한 저장 (super_admin은 row 안 박음) $this->syncPermissions((int) $insertId, $role, $permissions); $admin = $this->getDB()->table('admin_users') ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at') ->where('id', $insertId) ->get() ->getRow(); $admin->permissions = $this->getPermissions((int) $insertId, $admin->role); return $this->respondSuccess($admin, '관리자가 생성되었습니다.', ResponseInterface::HTTP_CREATED); } catch (\Exception $e) { log_message('error', 'AdminController create error: ' . $e->getMessage()); return $this->respondError('관리자 생성 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Update admin (관리자 수정) — 슈퍼 관리자만 * PUT /api/admin/:id */ public function update($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $existing = $this->getDB()->table('admin_users') ->where('id', (int) $id) ->where('deleted_YN', 'N') ->get()->getRow(); if (!$existing) { return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } $data = $this->request->getJSON(true) ?? []; // 일반 admin은 role/permissions 변경 불가 if (isset($data['role']) || array_key_exists('permissions', $data)) { if (!$this->isCallerSuperAdmin($auth)) { return $this->respondError('권한 변경은 슈퍼 관리자만 가능합니다.', ResponseInterface::HTTP_FORBIDDEN); } } // 이메일 중복 체크 (자신 제외, 삭제된 계정 제외) if (!empty($data['email']) && $data['email'] !== $existing->email) { $duplicate = $this->getDB()->table('admin_users') ->where('email', $data['email']) ->where('id !=', (int) $id) ->where('deleted_YN', 'N') ->get() ->getRow(); if ($duplicate) { return $this->respondError('이미 사용 중인 이메일입니다.', ResponseInterface::HTTP_BAD_REQUEST); } } $updateData = ['updated_at' => date('Y-m-d H:i:s')]; if (isset($data['name'])) $updateData['name'] = trim((string) $data['name']); if (isset($data['email'])) $updateData['email'] = trim((string) $data['email']); if (isset($data['phone'])) $updateData['phone'] = trim((string) $data['phone']); if (isset($data['role'])) { if (!in_array($data['role'], self::ALLOWED_ROLES, true)) { return $this->respondError('올바르지 않은 역할입니다.', ResponseInterface::HTTP_BAD_REQUEST); } $updateData['role'] = $data['role']; } if (isset($data['status'])) { if (!in_array($data['status'], self::ALLOWED_STATUSES, true)) { return $this->respondError('올바르지 않은 상태입니다.', ResponseInterface::HTTP_BAD_REQUEST); } $updateData['status'] = $data['status']; } $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData); // 권한 동기화: role 또는 permissions가 들어왔을 때만 $touchedRole = isset($data['role']); $touchedPerms = array_key_exists('permissions', $data); if ($touchedRole || $touchedPerms) { $finalRole = $touchedRole ? $data['role'] : $existing->role; $finalPermissions = $touchedPerms ? ($data['permissions'] ?? []) : []; // role='admin'으로 바뀌었는데 권한 비어있으면 차단 if ($finalRole === 'admin' && $touchedPerms && empty($finalPermissions)) { return $this->respondError('관리자에게 부여할 메뉴 권한을 1개 이상 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST); } // role만 super_admin → admin으로 바꿨는데 permissions 안 보낸 경우도 차단 if ($touchedRole && $finalRole === 'admin' && !$touchedPerms) { return $this->respondError('일반 관리자로 변경 시 메뉴 권한을 함께 지정해야 합니다.', ResponseInterface::HTTP_BAD_REQUEST); } $this->syncPermissions((int) $id, $finalRole, $finalPermissions); } $admin = $this->getDB()->table('admin_users') ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at') ->where('id', (int) $id) ->where('deleted_YN', 'N') ->get() ->getRow(); $admin->permissions = $this->getPermissions((int) $id, $admin->role); return $this->respondSuccess($admin, '관리자 정보가 수정되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController update error: ' . $e->getMessage()); return $this->respondError('관리자 수정 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Delete admin (관리자 삭제) — 슈퍼 관리자만, 본인은 삭제 불가 * DELETE /api/admin/:id */ public function delete($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $targetId = (int) $id; // 본인 삭제 방지 if ((int) $auth->admin_id === $targetId) { return $this->respondError('본인 계정은 삭제할 수 없습니다.', ResponseInterface::HTTP_FORBIDDEN); } $existing = $this->getDB()->table('admin_users') ->where('id', $targetId) ->where('deleted_YN', 'N') ->get()->getRow(); if (!$existing) { return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } // soft delete — 데이터는 보존, 플래그만 변경 $this->getDB()->table('admin_users') ->where('id', $targetId) ->update([ 'deleted_YN' => 'Y', 'updated_at' => date('Y-m-d H:i:s'), ]); // 토큰만 무효화 (admin_permissions는 복구 시를 위해 보존) $this->getDB()->table('admin_tokens')->where('admin_id', $targetId)->delete(); return $this->respondSuccess(null, '관리자가 삭제되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController delete error: ' . $e->getMessage()); return $this->respondError('관리자 삭제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Change admin password (비밀번호 변경) — 슈퍼 관리자만 * POST /api/admin/:id/password */ public function changePassword($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $data = $this->request->getJSON(true) ?? []; if (empty($data['new_password'])) { return $this->respondError('새 비밀번호가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } $existing = $this->getDB()->table('admin_users') ->where('id', (int) $id) ->where('deleted_YN', 'N') ->get()->getRow(); if (!$existing) { return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } $updateData = [ 'password' => password_hash($data['new_password'], PASSWORD_DEFAULT), 'password_changed_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData); return $this->respondSuccess(null, '비밀번호가 변경되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController changePassword error: ' . $e->getMessage()); return $this->respondError('비밀번호 변경 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Unlock admin account (계정 잠금 해제) — 슈퍼 관리자만 * POST /api/admin/:id/unlock */ public function unlockAccount($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $existing = $this->getDB()->table('admin_users') ->where('id', (int) $id) ->where('deleted_YN', 'N') ->get()->getRow(); if (!$existing) { return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } $updateData = [ 'login_attempts' => 0, 'last_failed_login' => null, 'updated_at' => date('Y-m-d H:i:s'), ]; $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData); return $this->respondSuccess(null, '계정 잠금이 해제되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController unlockAccount error: ' . $e->getMessage()); return $this->respondError('계정 잠금 해제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Restore deleted admin (삭제된 관리자 복구) * POST /api/admin/:id/restore */ public function restore($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $db = $this->getDB(); $existing = $db->table('admin_users') ->where('id', (int) $id) ->where('deleted_YN', 'Y') ->get()->getRow(); if (!$existing) { return $this->respondError('삭제된 관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND); } // 동일 아이디 충돌 검사 $dupeUsername = $db->table('admin_users') ->where('username', $existing->username) ->where('deleted_YN', 'N') ->countAllResults(); if ($dupeUsername > 0) { return $this->respondError( "동일 아이디 '{$existing->username}'가 이미 사용 중이라 복구할 수 없습니다.", ResponseInterface::HTTP_CONFLICT ); } // 이메일 충돌 검사 $dupeEmail = $db->table('admin_users') ->where('email', $existing->email) ->where('deleted_YN', 'N') ->countAllResults(); if ($dupeEmail > 0) { return $this->respondError( "동일 이메일 '{$existing->email}'가 이미 사용 중이라 복구할 수 없습니다.", ResponseInterface::HTTP_CONFLICT ); } $db->table('admin_users') ->where('id', (int) $id) ->update([ 'deleted_YN' => 'N', 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->respondSuccess(null, '관리자가 복구되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController restore error: ' . $e->getMessage()); return $this->respondError('복구 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * Hard delete admin (영구 삭제 — 이미 soft 삭제된 계정만) * DELETE /api/admin/:id/hard */ public function hardDelete($id = null) { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } if (empty($id)) { return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST); } try { $targetId = (int) $id; // 본인 영구 삭제 방지 if ((int) $auth->admin_id === $targetId) { return $this->respondError('본인 계정은 영구 삭제할 수 없습니다.', ResponseInterface::HTTP_FORBIDDEN); } $db = $this->getDB(); $existing = $db->table('admin_users') ->where('id', $targetId) ->where('deleted_YN', 'Y') ->get()->getRow(); if (!$existing) { return $this->respondError('영구 삭제 대상이 없습니다. (이미 삭제된 계정만 영구 삭제 가능)', ResponseInterface::HTTP_NOT_FOUND); } // 권한 row 정리 (FK CASCADE 있어도 명시적으로) $db->table('admin_permissions')->where('admin_id', $targetId)->delete(); $db->table('admin_tokens')->where('admin_id', $targetId)->delete(); $db->table('admin_users')->where('id', $targetId)->delete(); return $this->respondSuccess(null, '관리자가 영구 삭제되었습니다.'); } catch (\Exception $e) { log_message('error', 'AdminController hardDelete error: ' . $e->getMessage()); return $this->respondError('영구 삭제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } }