diff --git a/app/Controllers/AbstractCRUDController.php b/app/Controllers/AbstractCRUDController.php index f95e60a..771f3fd 100644 --- a/app/Controllers/AbstractCRUDController.php +++ b/app/Controllers/AbstractCRUDController.php @@ -4,6 +4,7 @@ namespace App\Controllers; use App\Entities\CommonEntity; use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\ResponseInterface; use RuntimeException; /** @@ -53,7 +54,7 @@ abstract class AbstractCRUDController extends AbstractWebController ); } - final public function create(): string|RedirectResponse + public function create(): string|RedirectResponse|ResponseInterface { try { $action = __FUNCTION__; @@ -81,7 +82,7 @@ abstract class AbstractCRUDController extends AbstractWebController return $this->action_render_process($action, $this->getViewDatas(), $this->request->getVar('ActionTemplate')); } - final public function modify_form($uid): string|RedirectResponse + public function modify_form($uid): string|RedirectResponse { try { if (!$uid) { @@ -111,7 +112,7 @@ abstract class AbstractCRUDController extends AbstractWebController $redirect_url ?? '/' . implode('/', [...$this->getActionPaths(), 'view']) . '/' . $entity->getPK() ); } - final public function modify($uid): string|RedirectResponse + public function modify($uid): string|RedirectResponse { try { if (!$uid) { @@ -136,7 +137,7 @@ abstract class AbstractCRUDController extends AbstractWebController { return $this->action_redirect_process('info', "{$this->getTitle()}에서 {$entity->getTitle()} 삭제가 완료되었습니다.", $redirect_url); } - final public function delete($uid): RedirectResponse + public function delete($uid): RedirectResponse { try { if (!$uid) { @@ -160,7 +161,7 @@ abstract class AbstractCRUDController extends AbstractWebController { return $this->action_render_process($action, $this->getViewDatas(), $this->request->getVar('ActionTemplate')); } - final public function view($uid): string|RedirectResponse + public function view($uid): string|RedirectResponse { try { if (!$uid) { diff --git a/app/Controllers/Ajax/AjaxController.php b/app/Controllers/Ajax/AjaxController.php index 65071f1..4af60ce 100644 --- a/app/Controllers/Ajax/AjaxController.php +++ b/app/Controllers/Ajax/AjaxController.php @@ -3,6 +3,7 @@ namespace App\Controllers\Ajax; use App\Controllers\AbstractCRUDController; +use App\Exceptions\FormValidationException; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; @@ -32,4 +33,46 @@ abstract class AjaxController extends AbstractCRUDController $this->addViewDatas('index_batchjobFields', $this->service->getActionForm()->getBatchjobFilters()); $this->addViewDatas('index_batchjobButtons', $this->service->getActionForm()->getBatchjobButtons()); } + + protected function ok(array $data = [], int $status = 200): ResponseInterface + { + return $this->response->setStatusCode($status)->setJSON([ + 'ok' => true, + ...$data, + ]); + } + + protected function fail(string $message, int $status = 400, array $extra = []): ResponseInterface + { + return $this->response->setStatusCode($status)->setJSON([ + 'ok' => false, + 'message' => $message, + ...$extra, + ]); + } + + protected function requireAjax() + { + // fetch + X-Requested-With 로 들어오는 경우 + if (!$this->request->isAJAX()) { + return $this->fail('Invalid request', 400); + } + return null; + } + + protected function handleException(\Throwable $e): ResponseInterface + { + if ($e instanceof FormValidationException) { + // ✅ 필드별 + 전역 메시지 + return $this->fail( + $e->getMessage() ?: '입력값을 확인해 주세요.', + 422, + ['errors' => $e->getErrors()] + ); + } + + log_message('error', '[AJAX] ' . $e->getMessage()); + + return $this->fail('처리 중 오류가 발생했습니다.', 500); + } } diff --git a/app/Controllers/Ajax/InquiryController.php b/app/Controllers/Ajax/InquiryController.php index 3e8e858..35ad16e 100644 --- a/app/Controllers/Ajax/InquiryController.php +++ b/app/Controllers/Ajax/InquiryController.php @@ -2,11 +2,12 @@ namespace App\Controllers\Ajax; -use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; +use App\Exceptions\FormValidationException; + class InquiryController extends AjaxController { public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) @@ -21,11 +22,57 @@ class InquiryController extends AjaxController //기본 함수 작업 //Custom 추가 함수 - protected function create_result_process($entity, ?string $redirect_url = null): string|RedirectResponse + public function create(): ResponseInterface { - return $this->action_redirect_process( - 'info', - "{$this->getTitle()}에서 {$entity->getTitle()} 문의 등록이 완료되었습니다.", - ); + log_message('error', 'HIT InquiryController::create'); + + if (!$this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON([ + 'ok' => false, + 'message' => 'Invalid request' + ]); + } + + try { + $formDatas = $this->request->getPost(); + log_message('error', 'POST=' . json_encode($formDatas, JSON_UNESCAPED_UNICODE)); + + $entity = $this->service->create($formDatas); + + return $this->response->setStatusCode(201)->setJSON([ + 'ok' => true, + 'message' => '문의가 접수되었습니다.', + 'data' => ['pk' => $entity->getPK()], + ]); + + } catch (FormValidationException $e) { + log_message('error', 'CAUGHT FormValidationException: ' . print_r($e->getErrors(), true)); + + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => false, + 'message' => '입력값을 확인해 주세요.', + 'errors' => $e->getErrors(), + ]); + + } catch (\Throwable $e) { + // ✅ 혹시 서비스에서 예외를 감싸버린 경우에도 에러를 최대한 복구 + $errors = service('validation')->getErrors(); + if (!empty($errors)) { + log_message('error', 'FALLBACK validation errors: ' . print_r($errors, true)); + + return $this->response->setStatusCode(422)->setJSON([ + 'ok' => false, + 'message' => '입력값을 확인해 주세요.', + 'errors' => $errors, + ]); + } + + log_message('error', '[AJAX create] ' . $e->getMessage()); + + return $this->response->setStatusCode(500)->setJSON([ + 'ok' => false, + 'message' => '처리 중 오류가 발생했습니다.', + ]); + } } } diff --git a/app/Exceptions/FormValidationException.php b/app/Exceptions/FormValidationException.php index 6f7c205..f7eec2e 100644 --- a/app/Exceptions/FormValidationException.php +++ b/app/Exceptions/FormValidationException.php @@ -6,11 +6,16 @@ use RuntimeException; class FormValidationException extends RuntimeException { - public array $errors; + private array $errors = []; public function __construct(array $errors, string $message = 'Validation failed', int $code = 0, ?\Throwable $previous = null) { - $this->errors = $errors; parent::__construct($message, $code, $previous); + $this->errors = $errors; } -} + + public function getErrors(): array + { + return $this->errors; + } +} \ No newline at end of file diff --git a/app/Forms/CommonForm.php b/app/Forms/CommonForm.php index 7b6ca23..76b3b6b 100644 --- a/app/Forms/CommonForm.php +++ b/app/Forms/CommonForm.php @@ -245,6 +245,7 @@ abstract class CommonForm // 5) run $this->validation->setRules($dynamicRules); if (!$this->validation->run($formDatas)) { + log_message('error', 'CI4 getErrors=' . print_r($this->validation->getErrors(), true)); throw new FormValidationException($this->validation->getErrors()); } diff --git a/app/Services/CommonService.php b/app/Services/CommonService.php index c1de3e8..1bb00d9 100644 --- a/app/Services/CommonService.php +++ b/app/Services/CommonService.php @@ -34,11 +34,18 @@ abstract class CommonService final protected function dbTransaction(callable $callback, string $functionName = ''): mixed { $db = \Config\Database::connect(); + try { $db->transException(true)->transStart(); $result = $callback($db); $db->transComplete(); return $result; + + } catch (FormValidationException $e) { + // ✅ 검증 에러는 감싸지 말고 그대로 던져야 Ajax가 422 + errors 받음 + $db->transRollback(); + throw $e; + } catch (DatabaseException $e) { $errorMessage = sprintf( "\n----[%s]에서 트랜잭션 실패: DB 오류----\n%s\n%s\n------------------------------\n", @@ -48,8 +55,10 @@ abstract class CommonService ); log_message('error', $errorMessage); throw new RuntimeException($errorMessage, $e->getCode(), $e); + } catch (\Throwable $e) { $db->transRollback(); + // ✅ 여기서 FormValidationException까지 RuntimeException으로 바뀌던게 문제였음 throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } } diff --git a/app/Views/front/welcome/inquiry.php b/app/Views/front/welcome/inquiry.php index 4867530..6aa6911 100644 --- a/app/Views/front/welcome/inquiry.php +++ b/app/Views/front/welcome/inquiry.php @@ -29,6 +29,12 @@ const form = document.getElementById('inquiryForm'); if (!form) return; + const setGlobalError = (msg) => { + const globalBox = document.querySelector(`[data-error-for="_global"]`); + if (globalBox) globalBox.textContent = msg; + else alert(msg); + }; + form.addEventListener('submit', async (e) => { e.preventDefault(); @@ -37,18 +43,40 @@ const formData = new FormData(form); - const res = await fetch('/ajax/inquiry/create', { - method: 'POST', - body: formData, - headers: { 'X-Requested-With': 'XMLHttpRequest' } - }); + let res; + try { + res = await fetch('/ajax/inquiry/create', { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + } catch (err) { + setGlobalError('네트워크 오류가 발생했습니다.'); + return; + } - // 응답이 JSON이 아닐 수도 있으니 안전하게 - let data = {}; - try { data = await res.json(); } catch (e) { } + // ✅ JSON 응답인지 먼저 체크 (리다이렉트/HTML이면 여기서 차단) + const ct = (res.headers.get('content-type') || '').toLowerCase(); + if (!ct.includes('application/json')) { + // 보통 세션만료/권한필터/리다이렉트/에러페이지가 이 케이스 + setGlobalError('세션이 만료되었거나 서버가 JSON이 아닌 응답을 반환했습니다. 다시 시도/로그인 해주세요.'); + return; + } + + // JSON 파싱 (여기서 실패하면 서버가 JSON을 깨뜨린 것) + let data; + try { + data = await res.json(); + } catch (err) { + setGlobalError('서버 JSON 파싱 실패(응답 형식 오류).'); + return; + } + + // ✅ 422: 필드별 에러 + 전역 메시지 + if (res.status === 422 && data && data.errors) { + const globalMsg = data.message || '입력값을 확인해 주세요.'; + setGlobalError(globalMsg); - // ✅ 422: 필드별 에러 - if (res.status === 422 && data.errors) { Object.entries(data.errors).forEach(([field, msg]) => { const box = document.querySelector(`[data-error-for="${field}"]`); if (box) box.textContent = msg; @@ -56,16 +84,14 @@ return; } - // ✅ 그 외 에러: message를 전역 에러로 표시 - if (!res.ok || data.ok === false) { - const globalBox = document.querySelector(`[data-error-for="_global"]`); - if (globalBox) globalBox.textContent = data.message || '처리 중 오류가 발생했습니다.'; - else alert(data.message || '처리 중 오류가 발생했습니다.'); + // ✅ 성공은 "반드시 data.ok === true" 로만 인정 + if (!res.ok || !data || data.ok !== true) { + setGlobalError((data && data.message) ? data.message : '처리 중 오류가 발생했습니다.'); return; } // ✅ 성공 - alert('문의가 접수되었습니다.'); + alert(data.message || '문의가 접수되었습니다.'); form.reset(); }); });