SpeciesQuestController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. <?php
  2. namespace App\Controllers\Api;
  3. use CodeIgniter\HTTP\ResponseInterface;
  4. class SpeciesQuestController extends BaseApiController
  5. {
  6. protected $format = 'json';
  7. protected $table = 'species_quest';
  8. /**
  9. * 어종 챌린지 목록 (구분 JOIN)
  10. * GET /api/species-challenge/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. $typeIdRaw = $this->request->getGet('type_id'); // '', 'null', 또는 숫자
  26. $startDate = trim((string) $this->request->getGet('start_date'));
  27. $endDate = trim((string) $this->request->getGet('end_date'));
  28. $db = $this->getDB();
  29. $builder = $db->table($this->table . ' sc')
  30. ->join('species_type st', 'st.id = sc.type_id', 'left')
  31. ->where('sc.deleted_YN', 'N');
  32. if ($search !== '') $builder->like('sc.name', $search);
  33. if ($typeIdRaw === 'null') {
  34. $builder->where('sc.type_id IS NULL', null, false);
  35. } elseif (is_numeric($typeIdRaw) && (int) $typeIdRaw > 0) {
  36. $builder->where('sc.type_id', (int) $typeIdRaw);
  37. }
  38. if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
  39. $builder->where('sc.created_at >=', $startDate . ' 00:00:00');
  40. }
  41. if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
  42. $builder->where('sc.created_at <=', $endDate . ' 23:59:59');
  43. }
  44. $total = $builder->countAllResults(false);
  45. $items = $builder
  46. ->select('sc.*, st.name as type_name')
  47. ->orderBy('sc.id', 'DESC')
  48. ->limit($perPage, $offset)
  49. ->get()
  50. ->getResult();
  51. return $this->respondSuccess([
  52. 'items' => $items,
  53. 'total' => $total,
  54. 'page' => $page,
  55. 'per_page' => $perPage,
  56. 'total_pages' => (int) ceil($total / $perPage),
  57. ]);
  58. } catch (\Exception $e) {
  59. log_message('error', 'SpeciesQuestController index error: ' . $e->getMessage());
  60. return $this->respondError('목록 조회 중 오류: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  61. }
  62. }
  63. /**
  64. * 어종 챌린지 일괄 저장 (creates + updates + deletes, 트랜잭션)
  65. * POST /api/species-challenge/bulk-save
  66. */
  67. public function bulkSave()
  68. {
  69. $auth = $this->requireAuth();
  70. if ($auth instanceof ResponseInterface) {
  71. return $auth;
  72. }
  73. try {
  74. $payload = $this->request->getJSON(true);
  75. if (!is_array($payload) || empty($payload)) {
  76. $payload = $this->request->getRawInput() ?? [];
  77. }
  78. $creates = is_array($payload['creates'] ?? null) ? $payload['creates'] : [];
  79. $updates = is_array($payload['updates'] ?? null) ? $payload['updates'] : [];
  80. $deletes = is_array($payload['deletes'] ?? null) ? $payload['deletes'] : [];
  81. if (empty($creates) && empty($updates) && empty($deletes)) {
  82. return $this->respondError('저장할 내용이 없습니다.', ResponseInterface::HTTP_BAD_REQUEST);
  83. }
  84. $db = $this->getDB();
  85. $db->transBegin();
  86. $createdCount = 0;
  87. $updatedCount = 0;
  88. $deletedCount = 0;
  89. // creates
  90. foreach ($creates as $i => $c) {
  91. $err = $this->validateRow($c, '신규 ' . ($i + 1) . '행', $db);
  92. if ($err) { $db->transRollback(); return $this->respondError($err, ResponseInterface::HTTP_BAD_REQUEST); }
  93. $db->table($this->table)->insert([
  94. 'type_id' => ((int) $c['type_id']) > 0 ? (int) $c['type_id'] : null,
  95. 'name' => trim((string) $c['name']),
  96. 'min' => (int) $c['min'],
  97. 'max' => (int) $c['max'],
  98. 'round1_min' => (int) $c['round1_min'],
  99. 'round1_max' => (int) $c['round1_max'],
  100. 'round2_min' => (int) $c['round2_min'],
  101. 'round2_max' => (int) $c['round2_max'],
  102. 'round3_min' => (int) $c['round3_min'],
  103. 'round3_max' => (int) $c['round3_max'],
  104. 'round4_min' => (int) $c['round4_min'],
  105. 'round4_max' => (int) $c['round4_max'],
  106. 'round5_min' => (int) $c['round5_min'],
  107. 'round5_max' => (int) $c['round5_max'],
  108. 'created_at' => date('Y-m-d H:i:s'),
  109. ]);
  110. $createdCount++;
  111. }
  112. // updates
  113. foreach ($updates as $i => $u) {
  114. $rowLabel = '수정 ' . ($i + 1) . '행';
  115. $id = (int) ($u['id'] ?? 0);
  116. if ($id <= 0) {
  117. $db->transRollback();
  118. return $this->respondError("{$rowLabel}: ID가 올바르지 않습니다.", ResponseInterface::HTTP_BAD_REQUEST);
  119. }
  120. $exists = $db->table($this->table)->where('id', $id)->where('deleted_YN', 'N')->countAllResults();
  121. if ($exists === 0) {
  122. $db->transRollback();
  123. return $this->respondError("{$rowLabel}: 대상이 없습니다.", ResponseInterface::HTTP_NOT_FOUND);
  124. }
  125. $err = $this->validateRow($u, $rowLabel, $db);
  126. if ($err) { $db->transRollback(); return $this->respondError($err, ResponseInterface::HTTP_BAD_REQUEST); }
  127. $db->table($this->table)->where('id', $id)->update([
  128. 'type_id' => ((int) $u['type_id']) > 0 ? (int) $u['type_id'] : null,
  129. 'name' => trim((string) $u['name']),
  130. 'min' => (int) $u['min'],
  131. 'max' => (int) $u['max'],
  132. 'round1_min' => (int) $u['round1_min'],
  133. 'round1_max' => (int) $u['round1_max'],
  134. 'round2_min' => (int) $u['round2_min'],
  135. 'round2_max' => (int) $u['round2_max'],
  136. 'round3_min' => (int) $u['round3_min'],
  137. 'round3_max' => (int) $u['round3_max'],
  138. 'round4_min' => (int) $u['round4_min'],
  139. 'round4_max' => (int) $u['round4_max'],
  140. 'round5_min' => (int) $u['round5_min'],
  141. 'round5_max' => (int) $u['round5_max'],
  142. ]);
  143. $updatedCount++;
  144. }
  145. // deletes
  146. $deleteIds = [];
  147. foreach ($deletes as $d) {
  148. $id = (int) (is_array($d) ? ($d['id'] ?? 0) : $d);
  149. if ($id > 0) $deleteIds[] = $id;
  150. }
  151. if (!empty($deleteIds)) {
  152. $db->table($this->table)
  153. ->whereIn('id', array_values(array_unique($deleteIds)))
  154. ->where('deleted_YN', 'N')
  155. ->update(['deleted_YN' => 'Y']);
  156. $deletedCount = count(array_unique($deleteIds));
  157. }
  158. if ($db->transStatus() === false) {
  159. $db->transRollback();
  160. return $this->respondError('저장 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  161. }
  162. $db->transCommit();
  163. $total = $createdCount + $updatedCount + $deletedCount;
  164. return $this->respondSuccess(
  165. ['created' => $createdCount, 'updated' => $updatedCount, 'deleted' => $deletedCount],
  166. "{$total}건이 저장되었습니다. (신규 {$createdCount} / 수정 {$updatedCount} / 삭제 {$deletedCount})"
  167. );
  168. } catch (\Exception $e) {
  169. log_message('error', 'SpeciesQuestController bulkSave error: ' . $e->getMessage());
  170. return $this->respondError('저장 중 오류: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  171. }
  172. }
  173. /**
  174. * 한 행 검증 — 에러 메시지 반환 (null이면 OK)
  175. */
  176. private function validateRow($row, string $label, $db)
  177. {
  178. $typeId = (int) ($row['type_id'] ?? 0);
  179. $name = trim((string) ($row['name'] ?? ''));
  180. $min = $row['min'] ?? null;
  181. $max = $row['max'] ?? null;
  182. if ($name === '') return "{$label}: 어종명을 입력하세요.";
  183. if (mb_strlen($name) > 50) return "{$label}: 어종명은 50자 이내";
  184. // type_id가 지정된 경우에만 존재 확인 (미선택 허용)
  185. if ($typeId > 0) {
  186. $typeExists = $db->table('species_type')
  187. ->where('id', $typeId)->where('deleted_YN', 'N')->countAllResults();
  188. if ($typeExists === 0) return "{$label}: 존재하지 않는 구분입니다.";
  189. }
  190. if ($min === null || $min === '' || !is_numeric($min)) return "{$label}: 최소금지를 입력하세요.";
  191. if ($max === null || $max === '' || !is_numeric($max)) return "{$label}: 최대길이를 입력하세요.";
  192. if ((int) $min < 0) return "{$label}: 최소금지는 0 이상";
  193. if ((int) $max < (int) $min) return "{$label}: 최대길이는 최소금지 이상이어야 합니다.";
  194. for ($r = 1; $r <= 5; $r++) {
  195. $rmin = $row["round{$r}_min"] ?? null;
  196. $rmax = $row["round{$r}_max"] ?? null;
  197. if ($rmin === null || $rmin === '' || !is_numeric($rmin)) return "{$label}: {$r}라운드 최소를 입력하세요.";
  198. if ($rmax === null || $rmax === '' || !is_numeric($rmax)) return "{$label}: {$r}라운드 최대를 입력하세요.";
  199. $rmin = (int) $rmin;
  200. $rmax = (int) $rmax;
  201. if ($rmin > $rmax) return "{$label}: {$r}라운드 최소는 최대보다 작거나 같아야 합니다.";
  202. // 각 라운드 max <= 최대길이
  203. if ($rmax > (int) $max) {
  204. return "{$label}: {$r}라운드 최대는 최대길이({$max})보다 클 수 없습니다.";
  205. }
  206. }
  207. // 1라운드 min >= 최소금지
  208. if ((int) $row['round1_min'] < (int) $min) {
  209. return "{$label}: 1라운드 최소는 최소금지({$min})보다 작을 수 없습니다.";
  210. }
  211. return null;
  212. }
  213. }