AdminController.php 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. <?php
  2. namespace App\Controllers\Api;
  3. use CodeIgniter\HTTP\ResponseInterface;
  4. class AdminController extends BaseApiController
  5. {
  6. protected $format = 'json';
  7. private const ALLOWED_ROLES = ['super_admin', 'admin'];
  8. private const ALLOWED_STATUSES = ['active', 'inactive', 'suspended'];
  9. // 허용 메뉴 권한 (admin.vue의 menuItems id와 동일)
  10. private const ALLOWED_PERMISSIONS = [
  11. 'admin', 'field', 'fishing', 'challenge', 'quest', 'item', 'species', 'user',
  12. ];
  13. /**
  14. * 호출한 관리자가 슈퍼관리자인지 확인
  15. */
  16. private function isCallerSuperAdmin($authData): bool
  17. {
  18. $admin = $this->getDB()->table('admin_users')
  19. ->select('role')
  20. ->where('id', (int) $authData->admin_id)
  21. ->get()->getRow();
  22. return $admin && $admin->role === 'super_admin';
  23. }
  24. // TODO: 권한관리 시스템 구축 후, '관리자관리 권한'을 가진 admin만
  25. // create/update/delete/changePassword/unlockAccount 가능하도록 가드 재추가
  26. /**
  27. * 관리자의 메뉴 권한 동기화 (DELETE + INSERT)
  28. * - super_admin은 row 박지 않음 (role 자체가 모든 권한)
  29. * - admin은 검증된 배열만 INSERT
  30. */
  31. private function syncPermissions(int $adminId, string $role, $permissions): void
  32. {
  33. $db = $this->getDB();
  34. // 기존 권한 전부 제거
  35. $db->table('admin_permissions')->where('admin_id', $adminId)->delete();
  36. if ($role !== 'admin') return;
  37. if (!is_array($permissions) || empty($permissions)) return;
  38. // 허용 목록만 INSERT (중복 제거)
  39. $clean = array_values(array_unique(array_filter(
  40. $permissions,
  41. fn($p) => is_string($p) && in_array($p, self::ALLOWED_PERMISSIONS, true)
  42. )));
  43. if (empty($clean)) return;
  44. $rows = array_map(fn($p) => [
  45. 'admin_id' => $adminId,
  46. 'permission' => $p,
  47. 'created_at' => date('Y-m-d H:i:s'),
  48. ], $clean);
  49. $db->table('admin_permissions')->insertBatch($rows);
  50. }
  51. /**
  52. * 관리자의 메뉴 권한 배열 반환
  53. * - super_admin은 'all' 반환
  54. * - admin은 ['field', 'fishing', ...] 형태
  55. */
  56. private function getPermissions(int $adminId, string $role)
  57. {
  58. if ($role === 'super_admin') return 'all';
  59. $rows = $this->getDB()->table('admin_permissions')
  60. ->select('permission')
  61. ->where('admin_id', $adminId)
  62. ->get()->getResult();
  63. return array_map(fn($r) => $r->permission, $rows);
  64. }
  65. /**
  66. * Get all admins (관리자 목록) — 인증된 모두 가능
  67. * GET /api/admin
  68. */
  69. public function index()
  70. {
  71. $auth = $this->requireAuth();
  72. if ($auth instanceof ResponseInterface) {
  73. return $auth;
  74. }
  75. try {
  76. $page = (int) ($this->request->getGet('page') ?? 1);
  77. $perPage = (int) ($this->request->getGet('per_page') ?? 10);
  78. if ($page < 1) $page = 1;
  79. if ($perPage < 1) $perPage = 10;
  80. $offset = ($page - 1) * $perPage;
  81. $db = $this->getDB();
  82. $builder = $db->table('admin_users');
  83. // 삭제된 계정만 / 활성만 분기 (deleted=1이면 삭제된 것만)
  84. $showDeleted = $this->request->getGet('deleted') === '1';
  85. $builder->where('deleted_YN', $showDeleted ? 'Y' : 'N');
  86. // 검색
  87. $search = trim((string) $this->request->getGet('search'));
  88. $searchField = $this->request->getGet('search_field'); // username / name / email / ''
  89. if ($search !== '') {
  90. if ($searchField === 'username') {
  91. $builder->like('username', $search);
  92. } elseif ($searchField === 'name') {
  93. $builder->like('name', $search);
  94. } elseif ($searchField === 'email') {
  95. $builder->like('email', $search);
  96. } else {
  97. $builder->groupStart()
  98. ->like('username', $search)
  99. ->orLike('name', $search)
  100. ->orLike('email', $search)
  101. ->groupEnd();
  102. }
  103. }
  104. // 역할 필터
  105. $role = $this->request->getGet('role');
  106. if (!empty($role) && in_array($role, self::ALLOWED_ROLES, true)) {
  107. $builder->where('role', $role);
  108. }
  109. // 상태 필터
  110. $status = $this->request->getGet('status');
  111. if (!empty($status) && in_array($status, self::ALLOWED_STATUSES, true)) {
  112. $builder->where('status', $status);
  113. }
  114. $total = $builder->countAllResults(false);
  115. $items = $builder
  116. ->select('id, username, name, email, phone, role, status, COALESCE(login_attempts, 0) as login_attempts, last_failed_login, last_login, created_at, updated_at')
  117. ->orderBy('id', 'DESC')
  118. ->limit($perPage, $offset)
  119. ->get()
  120. ->getResult();
  121. // permissions 일괄 조회 후 attach
  122. if (!empty($items)) {
  123. $ids = array_map(fn($i) => (int) $i->id, $items);
  124. $rows = $db->table('admin_permissions')
  125. ->select('admin_id, permission')
  126. ->whereIn('admin_id', $ids)
  127. ->get()->getResult();
  128. $permsByAdmin = [];
  129. foreach ($rows as $r) {
  130. $permsByAdmin[$r->admin_id][] = $r->permission;
  131. }
  132. foreach ($items as $item) {
  133. $item->permissions = $item->role === 'super_admin'
  134. ? 'all'
  135. : ($permsByAdmin[$item->id] ?? []);
  136. }
  137. }
  138. return $this->respondSuccess([
  139. 'items' => $items,
  140. 'total' => $total,
  141. 'page' => $page,
  142. 'per_page' => $perPage,
  143. 'total_pages' => (int) ceil($total / $perPage),
  144. ]);
  145. } catch (\Exception $e) {
  146. log_message('error', 'AdminController index error: ' . $e->getMessage());
  147. return $this->respondError('관리자 목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  148. }
  149. }
  150. /**
  151. * Get single admin (관리자 상세) — 인증된 모두 가능
  152. * GET /api/admin/:id
  153. */
  154. public function show($id = null)
  155. {
  156. $auth = $this->requireAuth();
  157. if ($auth instanceof ResponseInterface) {
  158. return $auth;
  159. }
  160. if (empty($id)) {
  161. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  162. }
  163. try {
  164. $admin = $this->getDB()->table('admin_users')
  165. ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at')
  166. ->where('id', (int) $id)
  167. ->where('deleted_YN', 'N')
  168. ->get()
  169. ->getRow();
  170. if (!$admin) {
  171. return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  172. }
  173. $admin->permissions = $this->getPermissions((int) $id, $admin->role);
  174. return $this->respondSuccess($admin);
  175. } catch (\Exception $e) {
  176. log_message('error', 'AdminController show error: ' . $e->getMessage());
  177. return $this->respondError('관리자 조회 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  178. }
  179. }
  180. /**
  181. * Check if username is available (아이디 중복 체크)
  182. * GET /api/admin/check-username
  183. */
  184. public function checkUsername()
  185. {
  186. $auth = $this->requireAuth();
  187. if ($auth instanceof ResponseInterface) {
  188. return $auth;
  189. }
  190. $username = trim((string) $this->request->getGet('username'));
  191. if ($username === '') {
  192. return $this->respondError('아이디를 입력하세요.');
  193. }
  194. try {
  195. $existing = $this->getDB()->table('admin_users')
  196. ->where('username', $username)
  197. ->where('deleted_YN', 'N')
  198. ->get()
  199. ->getRow();
  200. return $this->respondSuccess(['available' => !$existing]);
  201. } catch (\Exception $e) {
  202. log_message('error', 'AdminController checkUsername error: ' . $e->getMessage());
  203. return $this->respondError('중복 체크 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  204. }
  205. }
  206. /**
  207. * Create new admin (관리자 생성) — 슈퍼 관리자만
  208. * POST /api/admin
  209. */
  210. public function create()
  211. {
  212. $auth = $this->requireAuth();
  213. if ($auth instanceof ResponseInterface) {
  214. return $auth;
  215. }
  216. try {
  217. $data = $this->request->getJSON(true) ?? [];
  218. // 필수 필드 검증
  219. $required = ['username', 'password', 'name', 'email'];
  220. foreach ($required as $field) {
  221. if (empty($data[$field])) {
  222. return $this->respondError("{$field}는 필수 항목입니다.", ResponseInterface::HTTP_BAD_REQUEST);
  223. }
  224. }
  225. $username = trim((string) $data['username']);
  226. // role / status 검증
  227. $role = $data['role'] ?? 'admin';
  228. if (!in_array($role, self::ALLOWED_ROLES, true)) {
  229. return $this->respondError('올바르지 않은 역할입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  230. }
  231. $status = $data['status'] ?? 'active';
  232. if (!in_array($status, self::ALLOWED_STATUSES, true)) {
  233. return $this->respondError('올바르지 않은 상태입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  234. }
  235. // 호출자가 슈퍼관리자가 아니면 super_admin 등록 차단
  236. if ($role === 'super_admin' && !$this->isCallerSuperAdmin($auth)) {
  237. return $this->respondError('슈퍼 관리자 권한 부여는 슈퍼 관리자만 가능합니다.', ResponseInterface::HTTP_FORBIDDEN);
  238. }
  239. // 일반 admin은 권한 1개 이상 필수
  240. $permissions = $data['permissions'] ?? [];
  241. if ($role === 'admin') {
  242. if (!is_array($permissions) || empty($permissions)) {
  243. return $this->respondError('관리자에게 부여할 메뉴 권한을 1개 이상 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  244. }
  245. }
  246. // 중복 체크 (삭제된 계정 제외 — 재사용 허용)
  247. $existing = $this->getDB()->table('admin_users')
  248. ->groupStart()
  249. ->where('username', $username)
  250. ->orWhere('email', $data['email'])
  251. ->groupEnd()
  252. ->where('deleted_YN', 'N')
  253. ->get()
  254. ->getRow();
  255. if ($existing) {
  256. if ($existing->username === $username) {
  257. return $this->respondError('이미 사용 중인 아이디입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  258. }
  259. if ($existing->email === $data['email']) {
  260. return $this->respondError('이미 사용 중인 이메일입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  261. }
  262. }
  263. $insertData = [
  264. 'username' => $username,
  265. 'password' => password_hash($data['password'], PASSWORD_DEFAULT),
  266. 'password_changed_at' => date('Y-m-d H:i:s'),
  267. 'name' => trim((string) $data['name']),
  268. 'email' => trim((string) $data['email']),
  269. 'phone' => trim((string) ($data['phone'] ?? '')),
  270. 'role' => $role,
  271. 'status' => $status,
  272. 'login_attempts' => 0,
  273. 'deleted_YN' => 'N',
  274. 'created_at' => date('Y-m-d H:i:s'),
  275. 'updated_at' => date('Y-m-d H:i:s'),
  276. ];
  277. $this->getDB()->table('admin_users')->insert($insertData);
  278. $insertId = $this->getDB()->insertID();
  279. // 권한 저장 (super_admin은 row 안 박음)
  280. $this->syncPermissions((int) $insertId, $role, $permissions);
  281. $admin = $this->getDB()->table('admin_users')
  282. ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at')
  283. ->where('id', $insertId)
  284. ->get()
  285. ->getRow();
  286. $admin->permissions = $this->getPermissions((int) $insertId, $admin->role);
  287. return $this->respondSuccess($admin, '관리자가 생성되었습니다.', ResponseInterface::HTTP_CREATED);
  288. } catch (\Exception $e) {
  289. log_message('error', 'AdminController create error: ' . $e->getMessage());
  290. return $this->respondError('관리자 생성 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  291. }
  292. }
  293. /**
  294. * Update admin (관리자 수정) — 슈퍼 관리자만
  295. * PUT /api/admin/:id
  296. */
  297. public function update($id = null)
  298. {
  299. $auth = $this->requireAuth();
  300. if ($auth instanceof ResponseInterface) {
  301. return $auth;
  302. }
  303. if (empty($id)) {
  304. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  305. }
  306. try {
  307. $existing = $this->getDB()->table('admin_users')
  308. ->where('id', (int) $id)
  309. ->where('deleted_YN', 'N')
  310. ->get()->getRow();
  311. if (!$existing) {
  312. return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  313. }
  314. $data = $this->request->getJSON(true) ?? [];
  315. // 일반 admin은 role/permissions 변경 불가
  316. if (isset($data['role']) || array_key_exists('permissions', $data)) {
  317. if (!$this->isCallerSuperAdmin($auth)) {
  318. return $this->respondError('권한 변경은 슈퍼 관리자만 가능합니다.', ResponseInterface::HTTP_FORBIDDEN);
  319. }
  320. }
  321. // 이메일 중복 체크 (자신 제외, 삭제된 계정 제외)
  322. if (!empty($data['email']) && $data['email'] !== $existing->email) {
  323. $duplicate = $this->getDB()->table('admin_users')
  324. ->where('email', $data['email'])
  325. ->where('id !=', (int) $id)
  326. ->where('deleted_YN', 'N')
  327. ->get()
  328. ->getRow();
  329. if ($duplicate) {
  330. return $this->respondError('이미 사용 중인 이메일입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  331. }
  332. }
  333. $updateData = ['updated_at' => date('Y-m-d H:i:s')];
  334. if (isset($data['name'])) $updateData['name'] = trim((string) $data['name']);
  335. if (isset($data['email'])) $updateData['email'] = trim((string) $data['email']);
  336. if (isset($data['phone'])) $updateData['phone'] = trim((string) $data['phone']);
  337. if (isset($data['role'])) {
  338. if (!in_array($data['role'], self::ALLOWED_ROLES, true)) {
  339. return $this->respondError('올바르지 않은 역할입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  340. }
  341. $updateData['role'] = $data['role'];
  342. }
  343. if (isset($data['status'])) {
  344. if (!in_array($data['status'], self::ALLOWED_STATUSES, true)) {
  345. return $this->respondError('올바르지 않은 상태입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  346. }
  347. $updateData['status'] = $data['status'];
  348. }
  349. $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData);
  350. // 권한 동기화: role 또는 permissions가 들어왔을 때만
  351. $touchedRole = isset($data['role']);
  352. $touchedPerms = array_key_exists('permissions', $data);
  353. if ($touchedRole || $touchedPerms) {
  354. $finalRole = $touchedRole ? $data['role'] : $existing->role;
  355. $finalPermissions = $touchedPerms ? ($data['permissions'] ?? []) : [];
  356. // role='admin'으로 바뀌었는데 권한 비어있으면 차단
  357. if ($finalRole === 'admin' && $touchedPerms && empty($finalPermissions)) {
  358. return $this->respondError('관리자에게 부여할 메뉴 권한을 1개 이상 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  359. }
  360. // role만 super_admin → admin으로 바꿨는데 permissions 안 보낸 경우도 차단
  361. if ($touchedRole && $finalRole === 'admin' && !$touchedPerms) {
  362. return $this->respondError('일반 관리자로 변경 시 메뉴 권한을 함께 지정해야 합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  363. }
  364. $this->syncPermissions((int) $id, $finalRole, $finalPermissions);
  365. }
  366. $admin = $this->getDB()->table('admin_users')
  367. ->select('id, username, name, email, phone, role, status, login_attempts, last_failed_login, last_login, created_at, updated_at')
  368. ->where('id', (int) $id)
  369. ->where('deleted_YN', 'N')
  370. ->get()
  371. ->getRow();
  372. $admin->permissions = $this->getPermissions((int) $id, $admin->role);
  373. return $this->respondSuccess($admin, '관리자 정보가 수정되었습니다.');
  374. } catch (\Exception $e) {
  375. log_message('error', 'AdminController update error: ' . $e->getMessage());
  376. return $this->respondError('관리자 수정 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  377. }
  378. }
  379. /**
  380. * Delete admin (관리자 삭제) — 슈퍼 관리자만, 본인은 삭제 불가
  381. * DELETE /api/admin/:id
  382. */
  383. public function delete($id = null)
  384. {
  385. $auth = $this->requireAuth();
  386. if ($auth instanceof ResponseInterface) {
  387. return $auth;
  388. }
  389. if (empty($id)) {
  390. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  391. }
  392. try {
  393. $targetId = (int) $id;
  394. // 본인 삭제 방지
  395. if ((int) $auth->admin_id === $targetId) {
  396. return $this->respondError('본인 계정은 삭제할 수 없습니다.', ResponseInterface::HTTP_FORBIDDEN);
  397. }
  398. $existing = $this->getDB()->table('admin_users')
  399. ->where('id', $targetId)
  400. ->where('deleted_YN', 'N')
  401. ->get()->getRow();
  402. if (!$existing) {
  403. return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  404. }
  405. // soft delete — 데이터는 보존, 플래그만 변경
  406. $this->getDB()->table('admin_users')
  407. ->where('id', $targetId)
  408. ->update([
  409. 'deleted_YN' => 'Y',
  410. 'updated_at' => date('Y-m-d H:i:s'),
  411. ]);
  412. // 토큰만 무효화 (admin_permissions는 복구 시를 위해 보존)
  413. $this->getDB()->table('admin_tokens')->where('admin_id', $targetId)->delete();
  414. return $this->respondSuccess(null, '관리자가 삭제되었습니다.');
  415. } catch (\Exception $e) {
  416. log_message('error', 'AdminController delete error: ' . $e->getMessage());
  417. return $this->respondError('관리자 삭제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  418. }
  419. }
  420. /**
  421. * Change admin password (비밀번호 변경) — 슈퍼 관리자만
  422. * POST /api/admin/:id/password
  423. */
  424. public function changePassword($id = null)
  425. {
  426. $auth = $this->requireAuth();
  427. if ($auth instanceof ResponseInterface) {
  428. return $auth;
  429. }
  430. if (empty($id)) {
  431. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  432. }
  433. try {
  434. $data = $this->request->getJSON(true) ?? [];
  435. if (empty($data['new_password'])) {
  436. return $this->respondError('새 비밀번호가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  437. }
  438. $existing = $this->getDB()->table('admin_users')
  439. ->where('id', (int) $id)
  440. ->where('deleted_YN', 'N')
  441. ->get()->getRow();
  442. if (!$existing) {
  443. return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  444. }
  445. $updateData = [
  446. 'password' => password_hash($data['new_password'], PASSWORD_DEFAULT),
  447. 'password_changed_at' => date('Y-m-d H:i:s'),
  448. 'updated_at' => date('Y-m-d H:i:s'),
  449. ];
  450. $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData);
  451. return $this->respondSuccess(null, '비밀번호가 변경되었습니다.');
  452. } catch (\Exception $e) {
  453. log_message('error', 'AdminController changePassword error: ' . $e->getMessage());
  454. return $this->respondError('비밀번호 변경 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  455. }
  456. }
  457. /**
  458. * Unlock admin account (계정 잠금 해제) — 슈퍼 관리자만
  459. * POST /api/admin/:id/unlock
  460. */
  461. public function unlockAccount($id = null)
  462. {
  463. $auth = $this->requireAuth();
  464. if ($auth instanceof ResponseInterface) {
  465. return $auth;
  466. }
  467. if (empty($id)) {
  468. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  469. }
  470. try {
  471. $existing = $this->getDB()->table('admin_users')
  472. ->where('id', (int) $id)
  473. ->where('deleted_YN', 'N')
  474. ->get()->getRow();
  475. if (!$existing) {
  476. return $this->respondError('관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  477. }
  478. $updateData = [
  479. 'login_attempts' => 0,
  480. 'last_failed_login' => null,
  481. 'updated_at' => date('Y-m-d H:i:s'),
  482. ];
  483. $this->getDB()->table('admin_users')->where('id', (int) $id)->update($updateData);
  484. return $this->respondSuccess(null, '계정 잠금이 해제되었습니다.');
  485. } catch (\Exception $e) {
  486. log_message('error', 'AdminController unlockAccount error: ' . $e->getMessage());
  487. return $this->respondError('계정 잠금 해제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  488. }
  489. }
  490. /**
  491. * Restore deleted admin (삭제된 관리자 복구)
  492. * POST /api/admin/:id/restore
  493. */
  494. public function restore($id = null)
  495. {
  496. $auth = $this->requireAuth();
  497. if ($auth instanceof ResponseInterface) {
  498. return $auth;
  499. }
  500. if (empty($id)) {
  501. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  502. }
  503. try {
  504. $db = $this->getDB();
  505. $existing = $db->table('admin_users')
  506. ->where('id', (int) $id)
  507. ->where('deleted_YN', 'Y')
  508. ->get()->getRow();
  509. if (!$existing) {
  510. return $this->respondError('삭제된 관리자를 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  511. }
  512. // 동일 아이디 충돌 검사
  513. $dupeUsername = $db->table('admin_users')
  514. ->where('username', $existing->username)
  515. ->where('deleted_YN', 'N')
  516. ->countAllResults();
  517. if ($dupeUsername > 0) {
  518. return $this->respondError(
  519. "동일 아이디 '{$existing->username}'가 이미 사용 중이라 복구할 수 없습니다.",
  520. ResponseInterface::HTTP_CONFLICT
  521. );
  522. }
  523. // 이메일 충돌 검사
  524. $dupeEmail = $db->table('admin_users')
  525. ->where('email', $existing->email)
  526. ->where('deleted_YN', 'N')
  527. ->countAllResults();
  528. if ($dupeEmail > 0) {
  529. return $this->respondError(
  530. "동일 이메일 '{$existing->email}'가 이미 사용 중이라 복구할 수 없습니다.",
  531. ResponseInterface::HTTP_CONFLICT
  532. );
  533. }
  534. $db->table('admin_users')
  535. ->where('id', (int) $id)
  536. ->update([
  537. 'deleted_YN' => 'N',
  538. 'updated_at' => date('Y-m-d H:i:s'),
  539. ]);
  540. return $this->respondSuccess(null, '관리자가 복구되었습니다.');
  541. } catch (\Exception $e) {
  542. log_message('error', 'AdminController restore error: ' . $e->getMessage());
  543. return $this->respondError('복구 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  544. }
  545. }
  546. /**
  547. * Hard delete admin (영구 삭제 — 이미 soft 삭제된 계정만)
  548. * DELETE /api/admin/:id/hard
  549. */
  550. public function hardDelete($id = null)
  551. {
  552. $auth = $this->requireAuth();
  553. if ($auth instanceof ResponseInterface) {
  554. return $auth;
  555. }
  556. if (empty($id)) {
  557. return $this->respondError('관리자 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  558. }
  559. try {
  560. $targetId = (int) $id;
  561. // 본인 영구 삭제 방지
  562. if ((int) $auth->admin_id === $targetId) {
  563. return $this->respondError('본인 계정은 영구 삭제할 수 없습니다.', ResponseInterface::HTTP_FORBIDDEN);
  564. }
  565. $db = $this->getDB();
  566. $existing = $db->table('admin_users')
  567. ->where('id', $targetId)
  568. ->where('deleted_YN', 'Y')
  569. ->get()->getRow();
  570. if (!$existing) {
  571. return $this->respondError('영구 삭제 대상이 없습니다. (이미 삭제된 계정만 영구 삭제 가능)', ResponseInterface::HTTP_NOT_FOUND);
  572. }
  573. // 권한 row 정리 (FK CASCADE 있어도 명시적으로)
  574. $db->table('admin_permissions')->where('admin_id', $targetId)->delete();
  575. $db->table('admin_tokens')->where('admin_id', $targetId)->delete();
  576. $db->table('admin_users')->where('id', $targetId)->delete();
  577. return $this->respondSuccess(null, '관리자가 영구 삭제되었습니다.');
  578. } catch (\Exception $e) {
  579. log_message('error', 'AdminController hardDelete error: ' . $e->getMessage());
  580. return $this->respondError('영구 삭제 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  581. }
  582. }
  583. }