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; $search = trim((string) $this->request->getGet('search')); $typeIdRaw = $this->request->getGet('type_id'); // '', 'null', 또는 숫자 $startDate = trim((string) $this->request->getGet('start_date')); $endDate = trim((string) $this->request->getGet('end_date')); $db = $this->getDB(); $builder = $db->table($this->table . ' sc') ->join('species_type st', 'st.id = sc.type_id', 'left') ->where('sc.deleted_YN', 'N'); if ($search !== '') $builder->like('sc.name', $search); if ($typeIdRaw === 'null') { $builder->where('sc.type_id IS NULL', null, false); } elseif (is_numeric($typeIdRaw) && (int) $typeIdRaw > 0) { $builder->where('sc.type_id', (int) $typeIdRaw); } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { $builder->where('sc.created_at >=', $startDate . ' 00:00:00'); } if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { $builder->where('sc.created_at <=', $endDate . ' 23:59:59'); } $total = $builder->countAllResults(false); $items = $builder ->select('sc.*, st.name as type_name') ->orderBy('sc.id', 'DESC') ->limit($perPage, $offset) ->get() ->getResult(); return $this->respondSuccess([ 'items' => $items, 'total' => $total, 'page' => $page, 'per_page' => $perPage, 'total_pages' => (int) ceil($total / $perPage), ]); } catch (\Exception $e) { log_message('error', 'SpeciesChallengeController index error: ' . $e->getMessage()); return $this->respondError('목록 조회 중 오류: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * 어종 챌린지 일괄 저장 (creates + updates + deletes, 트랜잭션) * POST /api/species-challenge/bulk-save */ public function bulkSave() { $auth = $this->requireAuth(); if ($auth instanceof ResponseInterface) { return $auth; } try { $payload = $this->request->getJSON(true); if (!is_array($payload) || empty($payload)) { $payload = $this->request->getRawInput() ?? []; } $creates = is_array($payload['creates'] ?? null) ? $payload['creates'] : []; $updates = is_array($payload['updates'] ?? null) ? $payload['updates'] : []; $deletes = is_array($payload['deletes'] ?? null) ? $payload['deletes'] : []; if (empty($creates) && empty($updates) && empty($deletes)) { return $this->respondError('저장할 내용이 없습니다.', ResponseInterface::HTTP_BAD_REQUEST); } $db = $this->getDB(); $db->transBegin(); $createdCount = 0; $updatedCount = 0; $deletedCount = 0; // creates foreach ($creates as $i => $c) { $err = $this->validateRow($c, '신규 ' . ($i + 1) . '행', $db); if ($err) { $db->transRollback(); return $this->respondError($err, ResponseInterface::HTTP_BAD_REQUEST); } $db->table($this->table)->insert([ 'type_id' => ((int) $c['type_id']) > 0 ? (int) $c['type_id'] : null, 'name' => trim((string) $c['name']), 'min' => (int) $c['min'], 'max' => (int) $c['max'], 'round1_min' => (int) $c['round1_min'], 'round1_max' => (int) $c['round1_max'], 'round2_min' => (int) $c['round2_min'], 'round2_max' => (int) $c['round2_max'], 'round3_min' => (int) $c['round3_min'], 'round3_max' => (int) $c['round3_max'], 'round4_min' => (int) $c['round4_min'], 'round4_max' => (int) $c['round4_max'], 'round5_min' => (int) $c['round5_min'], 'round5_max' => (int) $c['round5_max'], 'created_at' => date('Y-m-d H:i:s'), ]); $createdCount++; } // updates foreach ($updates as $i => $u) { $rowLabel = '수정 ' . ($i + 1) . '행'; $id = (int) ($u['id'] ?? 0); if ($id <= 0) { $db->transRollback(); return $this->respondError("{$rowLabel}: ID가 올바르지 않습니다.", ResponseInterface::HTTP_BAD_REQUEST); } $exists = $db->table($this->table)->where('id', $id)->where('deleted_YN', 'N')->countAllResults(); if ($exists === 0) { $db->transRollback(); return $this->respondError("{$rowLabel}: 대상이 없습니다.", ResponseInterface::HTTP_NOT_FOUND); } $err = $this->validateRow($u, $rowLabel, $db); if ($err) { $db->transRollback(); return $this->respondError($err, ResponseInterface::HTTP_BAD_REQUEST); } $db->table($this->table)->where('id', $id)->update([ 'type_id' => ((int) $u['type_id']) > 0 ? (int) $u['type_id'] : null, 'name' => trim((string) $u['name']), 'min' => (int) $u['min'], 'max' => (int) $u['max'], 'round1_min' => (int) $u['round1_min'], 'round1_max' => (int) $u['round1_max'], 'round2_min' => (int) $u['round2_min'], 'round2_max' => (int) $u['round2_max'], 'round3_min' => (int) $u['round3_min'], 'round3_max' => (int) $u['round3_max'], 'round4_min' => (int) $u['round4_min'], 'round4_max' => (int) $u['round4_max'], 'round5_min' => (int) $u['round5_min'], 'round5_max' => (int) $u['round5_max'], ]); $updatedCount++; } // deletes $deleteIds = []; foreach ($deletes as $d) { $id = (int) (is_array($d) ? ($d['id'] ?? 0) : $d); if ($id > 0) $deleteIds[] = $id; } if (!empty($deleteIds)) { $db->table($this->table) ->whereIn('id', array_values(array_unique($deleteIds))) ->where('deleted_YN', 'N') ->update(['deleted_YN' => 'Y']); $deletedCount = count(array_unique($deleteIds)); } if ($db->transStatus() === false) { $db->transRollback(); return $this->respondError('저장 중 오류가 발생했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } $db->transCommit(); $total = $createdCount + $updatedCount + $deletedCount; return $this->respondSuccess( ['created' => $createdCount, 'updated' => $updatedCount, 'deleted' => $deletedCount], "{$total}건이 저장되었습니다. (신규 {$createdCount} / 수정 {$updatedCount} / 삭제 {$deletedCount})" ); } catch (\Exception $e) { log_message('error', 'SpeciesChallengeController bulkSave error: ' . $e->getMessage()); return $this->respondError('저장 중 오류: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); } } /** * 한 행 검증 — 에러 메시지 반환 (null이면 OK) */ private function validateRow($row, string $label, $db) { $typeId = (int) ($row['type_id'] ?? 0); $name = trim((string) ($row['name'] ?? '')); $min = $row['min'] ?? null; $max = $row['max'] ?? null; if ($name === '') return "{$label}: 어종명을 입력하세요."; if (mb_strlen($name) > 50) return "{$label}: 어종명은 50자 이내"; // type_id가 지정된 경우에만 존재 확인 (미선택 허용) if ($typeId > 0) { $typeExists = $db->table('species_type') ->where('id', $typeId)->where('deleted_YN', 'N')->countAllResults(); if ($typeExists === 0) return "{$label}: 존재하지 않는 구분입니다."; } if ($min === null || $min === '' || !is_numeric($min)) return "{$label}: 최소금지를 입력하세요."; if ($max === null || $max === '' || !is_numeric($max)) return "{$label}: 최대길이를 입력하세요."; if ((int) $min < 0) return "{$label}: 최소금지는 0 이상"; if ((int) $max < (int) $min) return "{$label}: 최대길이는 최소금지 이상이어야 합니다."; for ($r = 1; $r <= 5; $r++) { $rmin = $row["round{$r}_min"] ?? null; $rmax = $row["round{$r}_max"] ?? null; if ($rmin === null || $rmin === '' || !is_numeric($rmin)) return "{$label}: {$r}라운드 최소를 입력하세요."; if ($rmax === null || $rmax === '' || !is_numeric($rmax)) return "{$label}: {$r}라운드 최대를 입력하세요."; $rmin = (int) $rmin; $rmax = (int) $rmax; if ($rmin > $rmax) return "{$label}: {$r}라운드 최소는 최대보다 작거나 같아야 합니다."; // 각 라운드 max <= 최대길이 if ($rmax > (int) $max) { return "{$label}: {$r}라운드 최대는 최대길이({$max})보다 클 수 없습니다."; } } // 1라운드 min >= 최소금지 if ((int) $row['round1_min'] < (int) $min) { return "{$label}: 1라운드 최소는 최소금지({$min})보다 작을 수 없습니다."; } return null; } }