SpeciesController.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <?php
  2. namespace App\Controllers\Api;
  3. use CodeIgniter\HTTP\ResponseInterface;
  4. class SpeciesController extends BaseApiController
  5. {
  6. protected $format = 'json';
  7. protected $table = 'species_type';
  8. /**
  9. * 어종구분 목록
  10. * GET /api/species/list
  11. */
  12. public function index()
  13. {
  14. $auth = $this->requireAuth();
  15. if ($auth instanceof ResponseInterface) {
  16. return $auth;
  17. }
  18. try {
  19. $page = (int) ($this->request->getGet('page') ?? 1);
  20. $perPage = (int) ($this->request->getGet('per_page') ?? 10);
  21. if ($page < 1) $page = 1;
  22. if ($perPage < 1) $perPage = 10;
  23. $offset = ($page - 1) * $perPage;
  24. $search = trim((string) $this->request->getGet('search'));
  25. $status = trim((string) $this->request->getGet('status'));
  26. $db = $this->getDB();
  27. $builder = $db->table($this->table)->where('deleted_YN', 'N');
  28. if ($search !== '') $builder->like('name', $search);
  29. if ($status === 'Y' || $status === 'N') $builder->where('status_YN', $status);
  30. $total = $builder->countAllResults(false);
  31. $items = $builder
  32. ->select('id, name, sort_order, status_YN, created_at')
  33. ->orderBy('sort_order', 'ASC')
  34. ->orderBy('id', 'ASC')
  35. ->limit($perPage, $offset)
  36. ->get()
  37. ->getResult();
  38. return $this->respondSuccess([
  39. 'items' => $items,
  40. 'total' => $total,
  41. 'page' => $page,
  42. 'per_page' => $perPage,
  43. 'total_pages' => (int) ceil($total / $perPage),
  44. ]);
  45. } catch (\Exception $e) {
  46. log_message('error', 'SpeciesController index error: ' . $e->getMessage());
  47. return $this->respondError('목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  48. }
  49. }
  50. /**
  51. * 어종구분 등록
  52. * POST /api/species
  53. */
  54. public function create()
  55. {
  56. $auth = $this->requireAuth();
  57. if ($auth instanceof ResponseInterface) {
  58. return $auth;
  59. }
  60. try {
  61. $payload = $this->request->getJSON(true);
  62. if (!is_array($payload) || empty($payload)) {
  63. $payload = $this->request->getPost() ?? [];
  64. }
  65. $name = trim((string) ($payload['name'] ?? ''));
  66. $sortOrder = $payload['sort_order'] ?? 1;
  67. $status = trim((string) ($payload['status_YN'] ?? 'Y'));
  68. if ($name === '') {
  69. return $this->respondError('구분명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  70. }
  71. if (mb_strlen($name) > 30) {
  72. return $this->respondError('구분명은 30자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  73. }
  74. if (!is_numeric($sortOrder) || (int) $sortOrder < 0) {
  75. return $this->respondError('정렬순서는 0 이상의 숫자여야 합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  76. }
  77. $status = ($status === 'N') ? 'N' : 'Y';
  78. $db = $this->getDB();
  79. // 중복 검사
  80. $dupe = $db->table($this->table)
  81. ->where('name', $name)->where('deleted_YN', 'N')->countAllResults();
  82. if ($dupe > 0) {
  83. return $this->respondError('이미 등록된 구분명입니다.', ResponseInterface::HTTP_CONFLICT);
  84. }
  85. $insertData = [
  86. 'name' => $name,
  87. 'sort_order' => (int) $sortOrder,
  88. 'status_YN' => $status,
  89. 'created_at' => date('Y-m-d H:i:s'),
  90. ];
  91. if (!$db->table($this->table)->insert($insertData)) {
  92. return $this->respondError('등록에 실패했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  93. }
  94. $newId = $db->insertID();
  95. $row = $db->table($this->table)->where('id', $newId)->get()->getRow();
  96. return $this->respondSuccess($row, '어종구분이 등록되었습니다.', ResponseInterface::HTTP_CREATED);
  97. } catch (\Exception $e) {
  98. log_message('error', 'SpeciesController create error: ' . $e->getMessage());
  99. return $this->respondError('등록 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  100. }
  101. }
  102. /**
  103. * 어종구분 수정 (인라인)
  104. * PUT /api/species/:id
  105. */
  106. public function update($id = null)
  107. {
  108. $auth = $this->requireAuth();
  109. if ($auth instanceof ResponseInterface) {
  110. return $auth;
  111. }
  112. if (empty($id)) {
  113. return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  114. }
  115. try {
  116. $payload = $this->request->getJSON(true);
  117. if (!is_array($payload) || empty($payload)) {
  118. $payload = $this->request->getRawInput() ?? [];
  119. }
  120. $name = trim((string) ($payload['name'] ?? ''));
  121. $sortOrder = $payload['sort_order'] ?? 1;
  122. $status = trim((string) ($payload['status_YN'] ?? 'Y'));
  123. if ($name === '') {
  124. return $this->respondError('구분명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  125. }
  126. if (mb_strlen($name) > 30) {
  127. return $this->respondError('구분명은 30자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  128. }
  129. if (!is_numeric($sortOrder) || (int) $sortOrder < 0) {
  130. return $this->respondError('정렬순서는 0 이상의 숫자여야 합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  131. }
  132. $status = ($status === 'N') ? 'N' : 'Y';
  133. $db = $this->getDB();
  134. $exists = $db->table($this->table)
  135. ->where('id', (int) $id)->where('deleted_YN', 'N')->countAllResults();
  136. if ($exists === 0) {
  137. return $this->respondError('해당 항목을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  138. }
  139. // 자기 자신 제외 중복 검사
  140. $dupe = $db->table($this->table)
  141. ->where('name', $name)
  142. ->where('id !=', (int) $id)
  143. ->where('deleted_YN', 'N')
  144. ->countAllResults();
  145. if ($dupe > 0) {
  146. return $this->respondError('이미 등록된 구분명입니다.', ResponseInterface::HTTP_CONFLICT);
  147. }
  148. $db->table($this->table)->where('id', (int) $id)->update([
  149. 'name' => $name,
  150. 'sort_order' => (int) $sortOrder,
  151. 'status_YN' => $status,
  152. ]);
  153. $row = $db->table($this->table)->where('id', (int) $id)->get()->getRow();
  154. return $this->respondSuccess($row, '수정되었습니다.');
  155. } catch (\Exception $e) {
  156. log_message('error', 'SpeciesController update error: ' . $e->getMessage());
  157. return $this->respondError('수정 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  158. }
  159. }
  160. /**
  161. * 어종구분 일괄 저장 (신규 + 수정 한 번에, 트랜잭션)
  162. * POST /api/species/bulk-save
  163. * { creates: [{name, sort_order, status_YN}, ...],
  164. * updates: [{id, name, sort_order, status_YN}, ...] }
  165. */
  166. public function bulkSave()
  167. {
  168. $auth = $this->requireAuth();
  169. if ($auth instanceof ResponseInterface) {
  170. return $auth;
  171. }
  172. try {
  173. $payload = $this->request->getJSON(true);
  174. if (!is_array($payload) || empty($payload)) {
  175. $payload = $this->request->getRawInput() ?? [];
  176. }
  177. $creates = is_array($payload['creates'] ?? null) ? $payload['creates'] : [];
  178. $updates = is_array($payload['updates'] ?? null) ? $payload['updates'] : [];
  179. $deletes = is_array($payload['deletes'] ?? null) ? $payload['deletes'] : [];
  180. if (empty($creates) && empty($updates) && empty($deletes)) {
  181. return $this->respondError('저장할 내용이 없습니다.', ResponseInterface::HTTP_BAD_REQUEST);
  182. }
  183. $db = $this->getDB();
  184. $db->transBegin();
  185. $seenNames = [];
  186. $createdCount = 0;
  187. $updatedCount = 0;
  188. $deletedCount = 0;
  189. // creates
  190. foreach ($creates as $i => $c) {
  191. $rowLabel = '신규 ' . ($i + 1) . '행';
  192. $name = trim((string) ($c['name'] ?? ''));
  193. $sortOrder = $c['sort_order'] ?? 1;
  194. $status = trim((string) ($c['status_YN'] ?? 'Y'));
  195. if ($name === '') {
  196. $db->transRollback();
  197. return $this->respondError("{$rowLabel}: 구분명을 입력하세요.", ResponseInterface::HTTP_BAD_REQUEST);
  198. }
  199. if (mb_strlen($name) > 30) {
  200. $db->transRollback();
  201. return $this->respondError("{$rowLabel}: 구분명은 30자 이내", ResponseInterface::HTTP_BAD_REQUEST);
  202. }
  203. if (!is_numeric($sortOrder) || (int) $sortOrder < 0) {
  204. $db->transRollback();
  205. return $this->respondError("{$rowLabel}: 정렬순서는 0 이상 숫자", ResponseInterface::HTTP_BAD_REQUEST);
  206. }
  207. if (isset($seenNames[$name])) {
  208. $db->transRollback();
  209. return $this->respondError("{$rowLabel}: 같은 batch에 '{$name}' 중복", ResponseInterface::HTTP_CONFLICT);
  210. }
  211. $dupe = $db->table($this->table)->where('name', $name)->where('deleted_YN', 'N')->countAllResults();
  212. if ($dupe > 0) {
  213. $db->transRollback();
  214. return $this->respondError("{$rowLabel}: 이미 등록된 '{$name}'", ResponseInterface::HTTP_CONFLICT);
  215. }
  216. $seenNames[$name] = true;
  217. $db->table($this->table)->insert([
  218. 'name' => $name,
  219. 'sort_order' => (int) $sortOrder,
  220. 'status_YN' => ($status === 'N') ? 'N' : 'Y',
  221. 'created_at' => date('Y-m-d H:i:s'),
  222. ]);
  223. $createdCount++;
  224. }
  225. // updates
  226. foreach ($updates as $i => $u) {
  227. $rowLabel = '수정 ' . ($i + 1) . '행';
  228. $id = (int) ($u['id'] ?? 0);
  229. $name = trim((string) ($u['name'] ?? ''));
  230. $sortOrder = $u['sort_order'] ?? 1;
  231. $status = trim((string) ($u['status_YN'] ?? 'Y'));
  232. if ($id <= 0) {
  233. $db->transRollback();
  234. return $this->respondError("{$rowLabel}: ID가 올바르지 않습니다.", ResponseInterface::HTTP_BAD_REQUEST);
  235. }
  236. if ($name === '') {
  237. $db->transRollback();
  238. return $this->respondError("{$rowLabel}: 구분명을 입력하세요.", ResponseInterface::HTTP_BAD_REQUEST);
  239. }
  240. if (mb_strlen($name) > 30) {
  241. $db->transRollback();
  242. return $this->respondError("{$rowLabel}: 구분명은 30자 이내", ResponseInterface::HTTP_BAD_REQUEST);
  243. }
  244. if (!is_numeric($sortOrder) || (int) $sortOrder < 0) {
  245. $db->transRollback();
  246. return $this->respondError("{$rowLabel}: 정렬순서는 0 이상 숫자", ResponseInterface::HTTP_BAD_REQUEST);
  247. }
  248. if (isset($seenNames[$name])) {
  249. $db->transRollback();
  250. return $this->respondError("{$rowLabel}: 같은 batch에 '{$name}' 중복", ResponseInterface::HTTP_CONFLICT);
  251. }
  252. $exists = $db->table($this->table)->where('id', $id)->where('deleted_YN', 'N')->countAllResults();
  253. if ($exists === 0) {
  254. $db->transRollback();
  255. return $this->respondError("{$rowLabel}: 대상이 없습니다.", ResponseInterface::HTTP_NOT_FOUND);
  256. }
  257. $dupe = $db->table($this->table)
  258. ->where('name', $name)->where('id !=', $id)->where('deleted_YN', 'N')->countAllResults();
  259. if ($dupe > 0) {
  260. $db->transRollback();
  261. return $this->respondError("{$rowLabel}: 이미 등록된 '{$name}'", ResponseInterface::HTTP_CONFLICT);
  262. }
  263. $seenNames[$name] = true;
  264. $db->table($this->table)->where('id', $id)->update([
  265. 'name' => $name,
  266. 'sort_order' => (int) $sortOrder,
  267. 'status_YN' => ($status === 'N') ? 'N' : 'Y',
  268. ]);
  269. $updatedCount++;
  270. }
  271. // deletes 처리 (soft delete)
  272. $deleteIds = [];
  273. foreach ($deletes as $d) {
  274. $id = (int) (is_array($d) ? ($d['id'] ?? 0) : $d);
  275. if ($id > 0) $deleteIds[] = $id;
  276. }
  277. if (!empty($deleteIds)) {
  278. $db->table($this->table)
  279. ->whereIn('id', array_values(array_unique($deleteIds)))
  280. ->where('deleted_YN', 'N')
  281. ->update(['deleted_YN' => 'Y']);
  282. $deletedCount = count(array_unique($deleteIds));
  283. }
  284. if ($db->transStatus() === false) {
  285. $db->transRollback();
  286. return $this->respondError('저장 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  287. }
  288. $db->transCommit();
  289. $total = $createdCount + $updatedCount + $deletedCount;
  290. return $this->respondSuccess(
  291. ['created' => $createdCount, 'updated' => $updatedCount, 'deleted' => $deletedCount],
  292. "{$total}건이 저장되었습니다. (신규 {$createdCount} / 수정 {$updatedCount} / 삭제 {$deletedCount})"
  293. );
  294. } catch (\Exception $e) {
  295. log_message('error', 'SpeciesController bulkSave error: ' . $e->getMessage());
  296. return $this->respondError('저장 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  297. }
  298. }
  299. /**
  300. * 어종구분 일괄 삭제 (soft delete)
  301. * POST /api/species/bulk-delete { ids: [1,2,3] }
  302. */
  303. public function bulkDelete()
  304. {
  305. $auth = $this->requireAuth();
  306. if ($auth instanceof ResponseInterface) {
  307. return $auth;
  308. }
  309. try {
  310. $payload = $this->request->getJSON(true);
  311. if (!is_array($payload) || empty($payload)) {
  312. $payload = $this->request->getPost() ?? [];
  313. }
  314. $ids = $payload['ids'] ?? [];
  315. if (!is_array($ids) || empty($ids)) {
  316. return $this->respondError('삭제할 항목을 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  317. }
  318. $intIds = [];
  319. foreach ($ids as $v) {
  320. $n = (int) $v;
  321. if ($n > 0) $intIds[] = $n;
  322. }
  323. if (empty($intIds)) {
  324. return $this->respondError('올바른 ID가 없습니다.', ResponseInterface::HTTP_BAD_REQUEST);
  325. }
  326. $db = $this->getDB();
  327. $db->table($this->table)
  328. ->whereIn('id', $intIds)
  329. ->where('deleted_YN', 'N')
  330. ->update(['deleted_YN' => 'Y']);
  331. return $this->respondSuccess(['count' => count($intIds)], count($intIds) . '건이 삭제되었습니다.');
  332. } catch (\Exception $e) {
  333. log_message('error', 'SpeciesController bulkDelete error: ' . $e->getMessage());
  334. return $this->respondError('삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  335. }
  336. }
  337. }