giftcon_dev/app/Http/Controllers/Web/Payment/DanalController.php
2026-02-24 13:07:23 +09:00

244 lines
8.3 KiB
PHP

<?php
namespace App\Http\Controllers\Web\Payment;
use App\Http\Controllers\Controller;
use App\Services\Payments\PaymentService;
use Illuminate\Http\Request;
final class DanalController extends Controller
{
public function __construct(
private readonly PaymentService $service,
) {}
// 결제 시작(POST 권장)
public function start(Request $request)
{
$data = $request->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);
$redirect = url("/mypage/usage?attempt_id={$attemptId}");
return view('web.payments.danal.finish_top_action', [
'action' => 'close_modal',
'title' => '결제완료',
'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.',
'redirect' => url($redirect),
]);
}
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);
$redirect = url("/mypage/usage?attempt_id={$attemptId}");
return view('web.payments.danal.finish_top_action', [
'action' => 'close_modal',
'title' => '가상계좌 발급',
'message' => '가상계좌가 발급되었습니다. 입금 후 결제가 완료됩니다. 구매페이지로 이동합니다.',
'redirect' => url($redirect),
]);
}
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);
$redirect = url("/mypage/usage?attempt_id={$attemptId}");
return view('web.payments.danal.finish_top_action', [
'action' => 'close_modal',
'title' => '결제완료',
'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.',
'redirect' => url($redirect),
]);
}
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);
$redirect = url("/mypage/usage?attempt_id={$attemptId}");
return view('web.payments.danal.finish_top_action', [
'action' => 'close_modal',
'title' => '결제완료',
'message' => '결제가 완료되었습니다. 구매페이지로 이동합니다.',
'redirect' => url($redirect),
]);
}
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;
}
}