OnboardController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. <?php
  2. namespace App\Controllers\Api;
  3. use CodeIgniter\HTTP\ResponseInterface;
  4. class OnboardController extends BaseApiController
  5. {
  6. protected $format = 'json';
  7. protected $table = 'onboard';
  8. /**
  9. * 선상 목록
  10. * GET /api/onboard/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 . ' o');
  28. $builder->join('fishing_field f', 'f.id = o.field_id', 'left');
  29. $builder->join('fishing_area a', 'a.id = o.area_id', 'left');
  30. $builder->where('o.deleted_YN', 'N');
  31. if ($search !== '') {
  32. $builder->groupStart()
  33. ->like('o.name', $search)
  34. ->orLike('a.name', $search)
  35. ->groupEnd();
  36. }
  37. if ($status === 'Y' || $status === 'N') {
  38. $builder->where('o.status_YN', $status);
  39. }
  40. $total = $builder->countAllResults(false);
  41. // 계좌번호는 목록에서 제외 (민감정보)
  42. $items = $builder
  43. ->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')
  44. ->orderBy('o.id', 'DESC')
  45. ->limit($perPage, $offset)
  46. ->get()
  47. ->getResult();
  48. return $this->respondSuccess([
  49. 'items' => $items,
  50. 'total' => $total,
  51. 'page' => $page,
  52. 'per_page' => $perPage,
  53. 'total_pages' => (int) ceil($total / $perPage),
  54. ]);
  55. } catch (\Exception $e) {
  56. log_message('error', 'OnboardController index error: ' . $e->getMessage());
  57. return $this->respondError('목록 조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  58. }
  59. }
  60. /**
  61. * 선상 등록
  62. * POST /api/onboard
  63. */
  64. public function create()
  65. {
  66. $auth = $this->requireAuth();
  67. if ($auth instanceof ResponseInterface) {
  68. return $auth;
  69. }
  70. try {
  71. $payload = $this->request->getJSON(true);
  72. if (!is_array($payload) || empty($payload)) {
  73. $payload = $this->request->getPost() ?? [];
  74. }
  75. $fieldId = (int) ($payload['field_id'] ?? 0);
  76. $areaId = (int) ($payload['area_id'] ?? 0);
  77. $name = trim((string) ($payload['name'] ?? ''));
  78. // 필수값 검증
  79. if ($fieldId <= 0) {
  80. return $this->respondError('분야를 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  81. }
  82. if ($areaId <= 0) {
  83. return $this->respondError('지역을 선택하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  84. }
  85. if ($name === '') {
  86. return $this->respondError('선상명을 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  87. }
  88. if (mb_strlen($name) > 100) {
  89. return $this->respondError('선상명은 100자 이내로 입력하세요.', ResponseInterface::HTTP_BAD_REQUEST);
  90. }
  91. $db = $this->getDB();
  92. // 분야 / 지역 존재 확인
  93. $fieldExists = $db->table('fishing_field')
  94. ->where('id', $fieldId)->where('deleted_YN', 'N')->countAllResults();
  95. if ($fieldExists === 0) {
  96. return $this->respondError('존재하지 않는 분야입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  97. }
  98. $areaExists = $db->table('fishing_area')
  99. ->where('id', $areaId)->where('deleted_YN', 'N')->countAllResults();
  100. if ($areaExists === 0) {
  101. return $this->respondError('존재하지 않는 지역입니다.', ResponseInterface::HTTP_BAD_REQUEST);
  102. }
  103. // Y/N 정규화
  104. $partnership = (($payload['partnership_YN'] ?? 'N') === 'Y') ? 'Y' : 'N';
  105. $status = (($payload['status_YN'] ?? 'Y') === 'N') ? 'N' : 'Y';
  106. $insertData = [
  107. 'field_id' => $fieldId,
  108. 'area_id' => $areaId,
  109. 'name' => $name,
  110. 'area_detail' => trim((string) ($payload['area_detail'] ?? '')),
  111. 'tonnage' => trim((string) ($payload['tonnage'] ?? '')),
  112. 'capacity' => trim((string) ($payload['capacity'] ?? '')),
  113. 'zip_code' => trim((string) ($payload['zip_code'] ?? '')),
  114. 'address' => trim((string) ($payload['address'] ?? '')),
  115. 'address_detail' => trim((string) ($payload['address_detail'] ?? '')),
  116. 'address_refer' => trim((string) ($payload['address_refer'] ?? '')),
  117. 'lat' => trim((string) ($payload['lat'] ?? '')),
  118. 'lng' => trim((string) ($payload['lng'] ?? '')),
  119. 'partnership_YN' => $partnership,
  120. 'status_YN' => $status,
  121. 'created_at' => date('Y-m-d H:i:s'),
  122. ];
  123. // 제휴인 경우에만 계좌 정보 저장 (비제휴면 빈 값)
  124. // 계좌번호는 양방향 암호화하여 저장
  125. if ($partnership === 'Y') {
  126. $insertData['bank_code'] = trim((string) ($payload['bank_code'] ?? ''));
  127. $insertData['account_number'] = $this->encryptValue(trim((string) ($payload['account_number'] ?? '')));
  128. $insertData['account_holder'] = trim((string) ($payload['account_holder'] ?? ''));
  129. } else {
  130. $insertData['bank_code'] = '';
  131. $insertData['account_number'] = '';
  132. $insertData['account_holder'] = '';
  133. }
  134. if (!$db->table($this->table)->insert($insertData)) {
  135. return $this->respondError('등록에 실패했습니다.', ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  136. }
  137. $newId = $db->insertID();
  138. $row = $db->table($this->table)->where('id', $newId)->get()->getRow();
  139. // 응답 시 계좌번호 복호화
  140. if ($row) {
  141. $row->account_number = $this->decryptValue($row->account_number);
  142. }
  143. return $this->respondSuccess($row, '선상이 등록되었습니다.', ResponseInterface::HTTP_CREATED);
  144. } catch (\Exception $e) {
  145. log_message('error', 'OnboardController create error: ' . $e->getMessage());
  146. return $this->respondError('등록 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  147. }
  148. }
  149. /**
  150. * 선상 상세 조회
  151. * GET /api/onboard/:id
  152. */
  153. public function show($id = null)
  154. {
  155. $auth = $this->requireAuth();
  156. if ($auth instanceof ResponseInterface) {
  157. return $auth;
  158. }
  159. if (empty($id)) {
  160. return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  161. }
  162. try {
  163. $row = $this->getDB()->table($this->table . ' o')
  164. ->select('o.*, f.name as field_name, a.name as area_name')
  165. ->join('fishing_field f', 'f.id = o.field_id', 'left')
  166. ->join('fishing_area a', 'a.id = o.area_id', 'left')
  167. ->where('o.id', (int) $id)
  168. ->where('o.deleted_YN', 'N')
  169. ->get()
  170. ->getRow();
  171. if (!$row) {
  172. return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  173. }
  174. // 계좌번호 복호화
  175. $row->account_number = $this->decryptValue($row->account_number);
  176. return $this->respondSuccess($row);
  177. } catch (\Exception $e) {
  178. log_message('error', 'OnboardController show error: ' . $e->getMessage());
  179. return $this->respondError('조회 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  180. }
  181. }
  182. /**
  183. * 선상 삭제 (soft delete)
  184. * DELETE /api/onboard/:id
  185. */
  186. public function delete($id = null)
  187. {
  188. $auth = $this->requireAuth();
  189. if ($auth instanceof ResponseInterface) {
  190. return $auth;
  191. }
  192. if (empty($id)) {
  193. return $this->respondError('ID가 필요합니다.', ResponseInterface::HTTP_BAD_REQUEST);
  194. }
  195. try {
  196. $db = $this->getDB();
  197. $exists = $db->table($this->table)
  198. ->where('id', (int) $id)
  199. ->where('deleted_YN', 'N')
  200. ->countAllResults();
  201. if ($exists === 0) {
  202. return $this->respondError('해당 선상을 찾을 수 없습니다.', ResponseInterface::HTTP_NOT_FOUND);
  203. }
  204. $db->table($this->table)
  205. ->where('id', (int) $id)
  206. ->update([
  207. 'deleted_YN' => 'Y',
  208. 'updated_at' => date('Y-m-d H:i:s'),
  209. ]);
  210. return $this->respondSuccess(null, '선상이 삭제되었습니다.');
  211. } catch (\Exception $e) {
  212. log_message('error', 'OnboardController delete error: ' . $e->getMessage());
  213. return $this->respondError('삭제 중 오류가 발생했습니다: ' . $e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
  214. }
  215. }
  216. /**
  217. * 값 암호화 (빈 값은 그대로 빈 문자열)
  218. */
  219. private function encryptValue(string $plain): string
  220. {
  221. if ($plain === '') {
  222. return '';
  223. }
  224. $encrypter = \Config\Services::encrypter();
  225. return base64_encode($encrypter->encrypt($plain));
  226. }
  227. /**
  228. * 값 복호화 (실패/빈 값이면 빈 문자열)
  229. */
  230. private function decryptValue(?string $cipher): string
  231. {
  232. if (empty($cipher)) {
  233. return '';
  234. }
  235. try {
  236. $encrypter = \Config\Services::encrypter();
  237. return $encrypter->decrypt(base64_decode($cipher));
  238. } catch (\Exception $e) {
  239. log_message('error', 'Account decrypt error: ' . $e->getMessage());
  240. return '';
  241. }
  242. }
  243. }