|
@@ -0,0 +1,244 @@
|
|
|
|
|
+<?php
|
|
|
|
|
+
|
|
|
|
|
+namespace App\Controllers\Api;
|
|
|
|
|
+
|
|
|
|
|
+use CodeIgniter\HTTP\ResponseInterface;
|
|
|
|
|
+
|
|
|
|
|
+class SpeciesQuestController extends BaseApiController
|
|
|
|
|
+{
|
|
|
|
|
+ protected $format = 'json';
|
|
|
|
|
+ protected $table = 'species_quest';
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 어종 챌린지 목록 (구분 JOIN)
|
|
|
|
|
+ * GET /api/species-challenge/list
|
|
|
|
|
+ */
|
|
|
|
|
+ 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;
|
|
|
|
|
+
|
|
|
|
|
+ $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', 'SpeciesQuestController 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', 'SpeciesQuestController 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;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|