validate([ 'oid' => ['required','string','max:64'], 'method' => ['required','in:card,vact,phone,wire'], 'card_kind' => ['nullable','in:general,exchange'], 'phone_mode' => ['nullable','in:prod,dev'], 'is_mobile' => ['nullable','boolean'], ]); $memNo = $this->currentMemNo($request); if ($memNo <= 0) abort(403); $out = $this->service->start( $data['oid'], $memNo, $data['method'], [ 'card_kind' => $data['card_kind'] ?? null, 'phone_mode' => $data['phone_mode'] ?? null, 'is_mobile' => (bool)($data['is_mobile'] ?? false), ] ); if (($out['type'] ?? '') === 'redirect') { return view('web.payments.danal.redirect', [ 'actionUrl' => $out['start']['actionUrl'], 'params' => $out['start']['params'], 'acceptCharset' => $out['start']['acceptCharset'] ?? 'EUC-KR', ]); } return view('web.payments.danal.result', $out); } // 카드 RETURNURL public function cardReturn(Request $request) { $token = (string)$request->query('a', ''); if ($token === '') abort(404); if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제실패', 'message' => '결제에 실패했습니다.', ]); } // 가상계좌 RETURNURL public function vactReturn(Request $request) { $token = (string)$request->query('a', ''); if ($token === '') abort(404); $out = $this->service->handleVactReturn($token, $request->all()); if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'issued') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '가상계좌 발급', 'message' => '가상계좌가 발급되었습니다. 입금 후 결제가 완료됩니다. 구매페이지로 이동합니다.', 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '처리실패', 'message' => '가상계좌 처리에 실패했습니다.', ]); } // 가상계좌 NOTIURL (반드시 OK) public function vactNoti(Request $request) { $this->service->handleVactNoti($request->all()); return response('OK', 200)->header('Content-Type', 'text/plain'); } public function wireReturn(Request $request) { $token = (string)$request->query('a', ''); if ($token === '') abort(404); // 🔥 예외 렌더러가 죽지 않도록 요청값 UTF-8 정리 $post = $this->forceUtf8Array($request->all()); $out = $this->service->handleWireReturn($token, $post); if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제실패', 'message' => '결제에 실패했습니다.', ]); } public function wireNoti(Request $request) { // 🔥 예외 렌더러가 죽지 않도록 요청값 UTF-8 정리 $post = $this->forceUtf8Array($request->all()); // NOTI는 성공/실패와 무관하게 OK만 주면 다날 재시도 종료 $this->service->handleWireNoti($post); return response('OK', 200)->header('Content-Type', 'text/plain'); } private function forceUtf8Array(array $arr): array { foreach ($arr as $k => $v) { if (is_array($v)) { $arr[$k] = $this->forceUtf8Array($v); continue; } if (!is_string($v) || $v === '') continue; // 이미 UTF-8이면 그대로 if (function_exists('mb_check_encoding') && mb_check_encoding($v, 'UTF-8')) continue; // 다날은 EUC-KR 가능성이 높음 → UTF-8로 변환(깨진 바이트 제거) $out = @iconv('EUC-KR', 'UTF-8//IGNORE', $v); if ($out === false) $out = ''; if (function_exists('mb_check_encoding') && !mb_check_encoding($out, 'UTF-8')) { $out = @iconv('UTF-8', 'UTF-8//IGNORE', $out) ?: ''; } $arr[$k] = $out; } return $arr; } // 휴대폰 TargetURL public function phoneReturn(Request $request) { $out = $this->service->handlePhoneReturn($request->all()); if (($out['ok'] ?? false) && ($out['status'] ?? '') === 'paid') { $attemptId = (int)($out['meta']['attempt_id'] ?? 0); return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제완료', 'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.', 'redirect' => url("/mypage/usage?attempt_id={$attemptId}"), ]); } return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'title' => '결제실패', 'message' => '결제에 실패했습니다.', ]); } // 휴대폰 BackURL(취소) public function phoneCancel(Request $request) { $out = $this->service->handlePhoneCancel($request->all()); return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'message' => '결제가 취소되었습니다.', 'title' => '결제취소', ]); } // 카드/가상계좌 CancelURL public function cancel(Request $request) { $token = (string)$request->query('a', ''); if ($token === '') abort(404); $out = $this->service->handleCancel($token); // 취소면: iframe 닫고 showMsg 실행 if (($out['meta']['code'] ?? '') === 'CANCEL') { return view('web.payments.danal.finish_top_action', [ 'action' => 'close_modal', 'message' => '결제가 취소되었습니다.', 'title' => '결제취소', ]); } return view('web.payments.danal.result', $out); } private function currentMemNo(Request $request): int { // 프로젝트에 맞게 연결해라: // 1) Auth::user()->mem_no 가 있으면 그걸 쓰고, // 2) local 테스트용은 request('mem_no') 허용 $u = $request->user(); if ($u && isset($u->mem_no)) return (int)$u->mem_no; if (app()->environment('local')) { return (int)$request->input('mem_no', 0); } return 0; } }