400 // * critical/alert/emergency => 500 // * info/notice/debug/default => 200 // - RedirectResponse|ResponseInterface로 엄격 정리 // ========================================================= namespace App\Controllers; use App\Libraries\AuthContext; use App\Traits\LogTrait; use CodeIgniter\Controller; use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\ResponseInterface; use Psr\Log\LoggerInterface; use App\Exceptions\FormValidationException; abstract class AbstractWebController extends Controller { use LogTrait; private array $_action_paths = []; private array $_viewDatas = []; private ?string $_title = null; protected $layouts = []; protected $service = null; // --- 초기화 및 DI --- public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger) { parent::initController($request, $response, $logger); helper('util'); } final protected function getAuthContext(): AuthContext { return service('myauth')->getAuthContext(); } protected function getTitle(): string { if ($this->_title === null) { $this->_title = lang("{$this->service->getClassPaths(false)}.title"); } return $this->_title; } // --- 경로 및 뷰 데이터 관리 --- final protected function addActionPaths(string $path): void { $this->_action_paths[] = $path; } final protected function getActionPaths($isArray = true, $delimeter = DIRECTORY_SEPARATOR): array|string { return $isArray ? $this->_action_paths : implode($delimeter, $this->_action_paths); } final protected function addViewDatas(string $key, mixed $value): void { $this->_viewDatas[$key] = $value; } final protected function getViewDatas(?string $key = null): mixed { if ($key === null) return $this->_viewDatas; return $this->_viewDatas[$key] ?? null; } // --- 공통 처리 로직 (Override 가능) --- protected function action_init_process(string $action, array $formDatas = []): void { $this->addViewDatas('action', $action); $this->addViewDatas('authContext', $this->getAuthContext()); $this->addViewDatas('classPath', $this->service->getClassPaths(false)); $this->addViewDatas('uri', $this->request->getUri()); } /** * action_redirect_process * ✅ AJAX 요청이면 RedirectResponse 대신 JSON으로 변환(방어) * * 상태코드 정책(고정): * - warning/error => 400 * - critical/alert/emergency => 500 * - info/notice/debug/default => 200 */ protected function action_redirect_process(string $type, string $message, ?string $redirect_url = null): RedirectResponse|ResponseInterface { $resolvedRedirect = $redirect_url ?? $this->getAuthContext()->popPreviousUrl() ?? implode(DIRECTORY_SEPARATOR, $this->getActionPaths()); if ($this->request->isAJAX()) { $error400 = ['warning', 'error']; $error500 = ['critical', 'alert', 'emergency']; if (in_array($type, $error400, true)) { log_message($type, $message); return $this->response->setStatusCode(400)->setJSON([ 'ok' => false, 'message' => $message, 'redirect' => $resolvedRedirect, ]); } if (in_array($type, $error500, true)) { log_message($type, $message); return $this->response->setStatusCode(500)->setJSON([ 'ok' => false, 'message' => $message, 'redirect' => $resolvedRedirect, ]); } return $this->response->setStatusCode(200)->setJSON([ 'ok' => true, 'message' => $message, 'redirect' => $resolvedRedirect, ]); } switch ($type) { case 'warning': case 'error': case 'critical': case 'alert': case 'emergency': log_message($type, $message); return redirect()->back()->withInput()->with('message', $message); case 'debug': case 'info': case 'notice': default: return redirect()->to($resolvedRedirect)->with('message', $message); } } /** * 뷰 렌더링 */ protected function action_render_process(string $view_file, array $viewDatas, ?string $template_path = null): string { helper(['form', 'utility']); $baseViewPath = trim($viewDatas['layout']['path'], '/'); if ($template_path) $baseViewPath .= '/' . trim($template_path, '/'); $viewName = $baseViewPath . '/' . ltrim($view_file, '/'); return view($viewName, [ 'viewDatas' => [ ...$viewDatas, 'forms' => [ 'attributes' => ['method' => 'post'], 'hiddens' => [], ], ], ]); } // ========================================================= // 공통화: runAction / okResponse / failResponse // ========================================================= protected function runAction(string $action, callable $core): mixed { try { return $core(); } catch (FormValidationException $e) { return $this->failResponse($action, $e); } catch (\Throwable $e) { return $this->failResponse($action, $e); } } protected function okResponse(string $message, array $payload = [], ?string $redirectUrl = null): RedirectResponse|ResponseInterface { if ($this->request->isAJAX()) { return $this->response->setStatusCode(200)->setJSON(array_merge( ['ok' => true, 'message' => $message], $payload )); } return $this->action_redirect_process('info', $message, $redirectUrl); } protected function failResponse(string $action, \Throwable $e, ?string $humanPrefix = null): RedirectResponse|ResponseInterface { if ($e instanceof FormValidationException) { if ($this->request->isAJAX()) { return $this->response->setStatusCode(422)->setJSON([ 'ok' => false, 'errors' => $e->errors, ]); } return $this->action_redirect_process('error', dev_exception($e->getMessage())); } if ($this->request->isAJAX()) { return $this->response->setStatusCode(500)->setJSON([ 'ok' => false, 'message' => static::class . '->' . $action . "에서 오류:" . $e->getMessage(), ]); } $msg = $humanPrefix ? ($humanPrefix . $e->getMessage()) : $e->getMessage(); return $this->action_redirect_process('error', dev_exception($msg)); } }