| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- <?php
- namespace App\Controllers\Api;
- use CodeIgniter\HTTP\ResponseInterface;
- class OnboardController extends BaseApiController
- {
- protected $format = 'json';
- protected $table = 'onboard';
- /**
- * 선상 목록
- * GET /api/onboard/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;
- $searchField = trim((string) $this->request->getGet('search_field')); // '', field, area, name
- $search = trim((string) $this->request->getGet('search'));
- $partnership = trim((string) $this->request->getGet('partnership'));
- $status = trim((string) $this->request->getGet('status'));
- $startDate = trim((string) $this->request->getGet('start_date')); // YYYY-MM-DD
- $endDate = trim((string) $this->request->getGet('end_date')); // YYYY-MM-DD
- $db = $this->getDB();
- $builder = $db->table($this->table . ' o');
- $builder->join('fishing_field f', 'f.id = o.field_id', 'left');
- $builder->join('fishing_area a', 'a.id = o.area_id', 'left');
- $builder->where('o.deleted_YN', 'N');
- if ($search !== '') {
- if ($searchField === 'field') {
- $builder->like('f.name', $search);
- } elseif ($searchField === 'area') {
- $builder->like('a.name', $search);
- } elseif ($searchField === 'name') {
- $builder->like('o.name', $search);
- } else {
- // 전체: 분야 / 지역명 / 선상명
- $builder->groupStart()
- ->like('f.name', $search)
- ->orLike('a.name', $search)
- ->orLike('o.name', $search)
- ->groupEnd();
- }
- }
- if ($partnership === 'Y' || $partnership === 'N') {
- $builder->where('o.partnership_YN', $partnership);
- }
- if ($status === 'Y' || $status === 'N') {
- $builder->where('o.status_YN', $status);
- }
- // 등록일 기간 필터 (YYYY-MM-DD)
- if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
- $builder->where('o.created_at >=', $startDate . ' 00:00:00');
- }
- if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
- $builder->where('o.created_at <=', $endDate . ' 23:59:59');
- }
- $total = $builder->countAllResults(false);
- // 계좌번호는 목록에서 제외 (민감정보)
- $items = $builder
- ->select('o.id, o.name, o.field_id, o.area_id, o.area_detail, o.partnership_YN, o.status_YN, o.created_at, f.name as field_name, a.name as area_name')
- ->orderBy('o.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', 'OnboardController index error: ' . $e->getMessage());
- return $this->respondError('목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 등록
- * POST /api/onboard
- */
- public function create()
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- try {
- $payload = $this->request->getJSON(true);
- if (!is_array($payload) || empty($payload)) {
- $payload = $this->request->getPost() ?? [];
- }
- $fieldId = (int) ($payload['field_id'] ?? 0);
- $areaId = (int) ($payload['area_id'] ?? 0);
- $name = trim((string) ($payload['name'] ?? ''));
- // 필수값 검증
- if ($fieldId <= 0) {
- return $this->respondError('분야를 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if ($areaId <= 0) {
- return $this->respondError('지역을 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if ($name === '') {
- return $this->respondError('선상명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if (mb_strlen($name) > 100) {
- return $this->respondError('선상명은 100자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $db = $this->getDB();
- // 분야 / 지역 존재 확인
- $fieldExists = $db->table('fishing_field')
- ->where('id', $fieldId)->where('deleted_YN', 'N')->countAllResults();
- if ($fieldExists === 0) {
- return $this->respondError('존재하지 않는 분야입니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $areaExists = $db->table('fishing_area')
- ->where('id', $areaId)->where('deleted_YN', 'N')->countAllResults();
- if ($areaExists === 0) {
- return $this->respondError('존재하지 않는 지역입니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- // Y/N 정규화
- $partnership = (($payload['partnership_YN'] ?? 'N') === 'Y') ? 'Y' : 'N';
- $status = (($payload['status_YN'] ?? 'Y') === 'N') ? 'N' : 'Y';
- $insertData = [
- 'field_id' => $fieldId,
- 'area_id' => $areaId,
- 'name' => $name,
- 'area_detail' => trim((string) ($payload['area_detail'] ?? '')),
- 'tonnage' => trim((string) ($payload['tonnage'] ?? '')),
- 'capacity' => trim((string) ($payload['capacity'] ?? '')),
- 'zip_code' => trim((string) ($payload['zip_code'] ?? '')),
- 'address' => trim((string) ($payload['address'] ?? '')),
- 'address_detail' => trim((string) ($payload['address_detail'] ?? '')),
- 'address_refer' => trim((string) ($payload['address_refer'] ?? '')),
- 'lat' => trim((string) ($payload['lat'] ?? '')),
- 'lng' => trim((string) ($payload['lng'] ?? '')),
- 'partnership_YN' => $partnership,
- 'status_YN' => $status,
- 'created_at' => date('Y-m-d H:i:s'),
- ];
- // 제휴인 경우에만 계좌 정보 저장 (비제휴면 빈 값)
- // 계좌번호는 양방향 암호화하여 저장
- if ($partnership === 'Y') {
- $insertData['bank_code'] = trim((string) ($payload['bank_code'] ?? ''));
- $insertData['account_number'] = $this->encryptValue(trim((string) ($payload['account_number'] ?? '')));
- $insertData['account_holder'] = trim((string) ($payload['account_holder'] ?? ''));
- } else {
- $insertData['bank_code'] = '';
- $insertData['account_number'] = '';
- $insertData['account_holder'] = '';
- }
- if (!$db->table($this->table)->insert($insertData)) {
- return $this->respondError('등록에 실패했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- $newId = $db->insertID();
- $row = $db->table($this->table)->where('id', $newId)->get()->getRow();
- // 응답 시 계좌번호 복호화
- if ($row) {
- $row->account_number = $this->decryptValue($row->account_number);
- }
- return $this->respondSuccess($row, '선상이 등록되었습니다.', ResponseInterface::HTTP_CREATED);
- } catch (\Exception $e) {
- log_message('error', 'OnboardController create error: ' . $e->getMessage());
- return $this->respondError('등록 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 상세 조회
- * GET /api/onboard/:id
- */
- public function show($id = null)
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- if (empty($id)) {
- return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- try {
- $row = $this->getDB()->table($this->table . ' o')
- ->select('o.*, f.name as field_name, a.name as area_name')
- ->join('fishing_field f', 'f.id = o.field_id', 'left')
- ->join('fishing_area a', 'a.id = o.area_id', 'left')
- ->where('o.id', (int) $id)
- ->where('o.deleted_YN', 'N')
- ->get()
- ->getRow();
- if (!$row) {
- return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
- }
- // 계좌번호 복호화
- $row->account_number = $this->decryptValue($row->account_number);
- // 사진 목록 (정렬순)
- $row->photos = $this->getDB()->table('onboard_photos')
- ->where('onboard_id', (int) $id)
- ->orderBy('sort_order', 'ASC')
- ->orderBy('id', 'ASC')
- ->get()
- ->getResult();
- return $this->respondSuccess($row);
- } catch (\Exception $e) {
- log_message('error', 'OnboardController show error: ' . $e->getMessage());
- return $this->respondError('조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 수정
- * PUT /api/onboard/:id
- */
- public function update($id = null)
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- if (empty($id)) {
- return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- try {
- $payload = $this->request->getJSON(true);
- if (!is_array($payload) || empty($payload)) {
- $payload = $this->request->getRawInput() ?? [];
- }
- $fieldId = (int) ($payload['field_id'] ?? 0);
- $areaId = (int) ($payload['area_id'] ?? 0);
- $name = trim((string) ($payload['name'] ?? ''));
- if ($fieldId <= 0) {
- return $this->respondError('분야를 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if ($areaId <= 0) {
- return $this->respondError('지역을 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if ($name === '') {
- return $this->respondError('선상명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- if (mb_strlen($name) > 100) {
- return $this->respondError('선상명은 100자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $db = $this->getDB();
- // 대상 존재 확인
- $exists = $db->table($this->table)
- ->where('id', (int) $id)->where('deleted_YN', 'N')->countAllResults();
- if ($exists === 0) {
- return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
- }
- // 분야 / 지역 존재 확인
- $fieldExists = $db->table('fishing_field')
- ->where('id', $fieldId)->where('deleted_YN', 'N')->countAllResults();
- if ($fieldExists === 0) {
- return $this->respondError('존재하지 않는 분야입니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $areaExists = $db->table('fishing_area')
- ->where('id', $areaId)->where('deleted_YN', 'N')->countAllResults();
- if ($areaExists === 0) {
- return $this->respondError('존재하지 않는 지역입니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $partnership = (($payload['partnership_YN'] ?? 'N') === 'Y') ? 'Y' : 'N';
- $status = (($payload['status_YN'] ?? 'Y') === 'N') ? 'N' : 'Y';
- $updateData = [
- 'field_id' => $fieldId,
- 'area_id' => $areaId,
- 'name' => $name,
- 'area_detail' => trim((string) ($payload['area_detail'] ?? '')),
- 'tonnage' => trim((string) ($payload['tonnage'] ?? '')),
- 'capacity' => trim((string) ($payload['capacity'] ?? '')),
- 'zip_code' => trim((string) ($payload['zip_code'] ?? '')),
- 'address' => trim((string) ($payload['address'] ?? '')),
- 'address_detail' => trim((string) ($payload['address_detail'] ?? '')),
- 'address_refer' => trim((string) ($payload['address_refer'] ?? '')),
- 'lat' => trim((string) ($payload['lat'] ?? '')),
- 'lng' => trim((string) ($payload['lng'] ?? '')),
- 'partnership_YN' => $partnership,
- 'status_YN' => $status,
- 'updated_at' => date('Y-m-d H:i:s'),
- ];
- // 제휴면 계좌 정보 (계좌번호 재암호화), 비제휴면 빈 값
- if ($partnership === 'Y') {
- $updateData['bank_code'] = trim((string) ($payload['bank_code'] ?? ''));
- $updateData['account_number'] = $this->encryptValue(trim((string) ($payload['account_number'] ?? '')));
- $updateData['account_holder'] = trim((string) ($payload['account_holder'] ?? ''));
- } else {
- $updateData['bank_code'] = '';
- $updateData['account_number'] = '';
- $updateData['account_holder'] = '';
- }
- $db->table($this->table)->where('id', (int) $id)->update($updateData);
- $row = $db->table($this->table)->where('id', (int) $id)->get()->getRow();
- if ($row) {
- $row->account_number = $this->decryptValue($row->account_number);
- }
- return $this->respondSuccess($row, '선상이 수정되었습니다.');
- } catch (\Exception $e) {
- log_message('error', 'OnboardController update error: ' . $e->getMessage());
- return $this->respondError('수정 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 사진 삭제 (파일 + DB hard delete)
- * DELETE /api/onboard/photo/:photoId
- */
- public function deletePhoto($photoId = null)
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- if (empty($photoId)) {
- return $this->respondError('사진 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- try {
- $db = $this->getDB();
- $photo = $db->table('onboard_photos')->where('id', (int) $photoId)->get()->getRow();
- if (!$photo) {
- return $this->respondError('해당 사진을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
- }
- // 실제 파일 삭제
- $fullPath = FCPATH . ltrim($photo->file_path, '/');
- if (is_file($fullPath)) {
- @unlink($fullPath);
- }
- $db->table('onboard_photos')->where('id', (int) $photoId)->delete();
- return $this->respondSuccess(null, '사진이 삭제되었습니다.');
- } catch (\Exception $e) {
- log_message('error', 'OnboardController deletePhoto error: ' . $e->getMessage());
- return $this->respondError('사진 삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 사진 업로드 (다중)
- * POST /api/onboard/:id/photos (multipart, photos[])
- */
- public function uploadPhotos($id = null)
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- if (empty($id)) {
- return $this->respondError('선상 ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- try {
- $db = $this->getDB();
- // 선상 존재 확인
- $exists = $db->table($this->table)
- ->where('id', (int) $id)->where('deleted_YN', 'N')->countAllResults();
- if ($exists === 0) {
- return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
- }
- $files = $this->request->getFileMultiple('photos');
- if (empty($files)) {
- return $this->respondError('업로드할 사진이 없습니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- $allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
- $uploadPath = FCPATH . 'uploads/onboard/';
- if (!is_dir($uploadPath)) {
- mkdir($uploadPath, 0755, true);
- }
- // 기존 사진의 최대 sort_order 다음부터 부여
- $maxRow = $db->table('onboard_photos')
- ->selectMax('sort_order')
- ->where('onboard_id', (int) $id)
- ->get()->getRow();
- $order = $maxRow && $maxRow->sort_order !== null ? (int) $maxRow->sort_order + 1 : 0;
- $saved = [];
- foreach ($files as $file) {
- if (!$file->isValid()) {
- continue;
- }
- // 실제 파일 기반 MIME 검증
- $mime = $file->getMimeType();
- if (!in_array($mime, $allowed, true)) {
- continue;
- }
- $originalName = $file->getClientName();
- $size = $file->getSize();
- $newName = $file->getRandomName();
- $file->move($uploadPath, $newName);
- $fullPath = $uploadPath . $newName;
- // 이미지 크기 추출
- $width = null;
- $height = null;
- $info = @getimagesize($fullPath);
- if ($info) {
- $width = $info[0];
- $height = $info[1];
- }
- $photoData = [
- 'onboard_id' => (int) $id,
- 'original_name' => $originalName,
- 'stored_name' => $newName,
- 'file_path' => '/uploads/onboard/' . $newName,
- 'file_size' => $size,
- 'mime_type' => $mime,
- 'width' => $width,
- 'height' => $height,
- 'sort_order' => $order,
- 'created_at' => date('Y-m-d H:i:s'),
- ];
- $db->table('onboard_photos')->insert($photoData);
- $photoData['id'] = $db->insertID();
- $saved[] = $photoData;
- $order++;
- }
- if (empty($saved)) {
- return $this->respondError('유효한 이미지 파일이 없습니다. (JPG/PNG/GIF/WebP만 허용)', ResponseInterface::HTTP_BAD_REQUEST);
- }
- return $this->respondSuccess($saved, count($saved) . '장의 사진이 업로드되었습니다.', ResponseInterface::HTTP_CREATED);
- } catch (\Exception $e) {
- log_message('error', 'OnboardController uploadPhotos error: ' . $e->getMessage());
- return $this->respondError('사진 업로드 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 선상 삭제 (soft delete)
- * DELETE /api/onboard/:id
- */
- public function delete($id = null)
- {
- $auth = $this->requireAuth();
- if ($auth instanceof ResponseInterface) {
- return $auth;
- }
- if (empty($id)) {
- return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
- }
- try {
- $db = $this->getDB();
- $exists = $db->table($this->table)
- ->where('id', (int) $id)
- ->where('deleted_YN', 'N')
- ->countAllResults();
- if ($exists === 0) {
- return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
- }
- $db->table($this->table)
- ->where('id', (int) $id)
- ->update([
- 'deleted_YN' => 'Y',
- 'updated_at' => date('Y-m-d H:i:s'),
- ]);
- return $this->respondSuccess(null, '선상이 삭제되었습니다.');
- } catch (\Exception $e) {
- log_message('error', 'OnboardController delete error: ' . $e->getMessage());
- return $this->respondError('삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
- }
- }
- /**
- * 값 암호화 (빈 값은 그대로 빈 문자열)
- */
- private function encryptValue(string $plain): string
- {
- if ($plain === '') {
- return '';
- }
- $encrypter = \Config\Services::encrypter();
- return base64_encode($encrypter->encrypt($plain));
- }
- /**
- * 값 복호화 (실패/빈 값이면 빈 문자열)
- */
- private function decryptValue(?string $cipher): string
- {
- if (empty($cipher)) {
- return '';
- }
- try {
- $encrypter = \Config\Services::encrypter();
- return $encrypter->decrypt(base64_decode($cipher));
- } catch (\Exception $e) {
- log_message('error', 'Account decrypt error: ' . $e->getMessage());
- return '';
- }
- }
- }
|