결제 계좌이체
This commit is contained in:
parent
20d6ea2732
commit
fb0cec13ef
35
app/Http/Controllers/Web/Mypage/UsageController.php
Normal file
35
app/Http/Controllers/Web/Mypage/UsageController.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web\Mypage;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Mypage\UsageService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
final class UsageController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UsageService $service,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /mypage/usage?attempt_id=...
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
// legacy.auth가 있지만, 결제 플로우 안전장치로 한 번 더
|
||||||
|
if ((bool)session('_sess._login_') !== true) {
|
||||||
|
return redirect()->route('web.auth.login'); // 프로젝트 로그인 라우트에 맞춰 조정
|
||||||
|
}
|
||||||
|
|
||||||
|
$memNo = (int)session('_sess._mno', 0);
|
||||||
|
if ($memNo <= 0) abort(403);
|
||||||
|
|
||||||
|
$attemptId = $request->query('attempt_id');
|
||||||
|
$attemptId = is_numeric($attemptId) ? (int)$attemptId : null;
|
||||||
|
|
||||||
|
$data = $this->service->buildPageData($attemptId, $memNo);
|
||||||
|
|
||||||
|
return view('web.mypage.usage.index', $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,7 +17,7 @@ final class DanalController extends Controller
|
|||||||
{
|
{
|
||||||
$data = $request->validate([
|
$data = $request->validate([
|
||||||
'oid' => ['required','string','max:64'],
|
'oid' => ['required','string','max:64'],
|
||||||
'method' => ['required','in:card,vact,phone'],
|
'method' => ['required','in:card,vact,phone,wire'],
|
||||||
'card_kind' => ['nullable','in:general,exchange'],
|
'card_kind' => ['nullable','in:general,exchange'],
|
||||||
'phone_mode' => ['nullable','in:prod,dev'],
|
'phone_mode' => ['nullable','in:prod,dev'],
|
||||||
'is_mobile' => ['nullable','boolean'],
|
'is_mobile' => ['nullable','boolean'],
|
||||||
@ -108,6 +108,68 @@ final class DanalController extends Controller
|
|||||||
return response('OK', 200)->header('Content-Type', 'text/plain');
|
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
|
// 휴대폰 TargetURL
|
||||||
public function phoneReturn(Request $request)
|
public function phoneReturn(Request $request)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -22,6 +22,19 @@ final class DanalConfig
|
|||||||
return $cfg;
|
return $cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function wiretransfer(): array
|
||||||
|
{
|
||||||
|
$cfg = config('danal.wiretransfer');
|
||||||
|
if (empty($cfg['cpid'])) throw new \RuntimeException('DANAL_AUTHVACT_CPID empty');
|
||||||
|
if (!is_string($cfg['key']) || !preg_match('/^[0-9a-fA-F]{64}$/', $cfg['key'])) {
|
||||||
|
throw new \RuntimeException('DANAL_AUTHVACT_KEY must be 64 hex chars');
|
||||||
|
}
|
||||||
|
if (!is_string($cfg['iv']) || !preg_match('/^[0-9a-fA-F]{32}$/', $cfg['iv'])) {
|
||||||
|
throw new \RuntimeException('DANAL_AUTHVACT_IV must be 32 hex chars');
|
||||||
|
}
|
||||||
|
return $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
public function phone(string $mode): array
|
public function phone(string $mode): array
|
||||||
{
|
{
|
||||||
$base = config('danal.phone');
|
$base = config('danal.phone');
|
||||||
|
|||||||
117
app/Providers/Danal/Gateways/WireGateway.php
Normal file
117
app/Providers/Danal/Gateways/WireGateway.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers\Danal\Gateways;
|
||||||
|
|
||||||
|
use App\Models\Payments\GcPinOrder;
|
||||||
|
use App\Providers\Danal\Clients\DanalCpcgiClient;
|
||||||
|
use App\Providers\Danal\DanalConfig;
|
||||||
|
use App\Support\Danal\EucKr;
|
||||||
|
|
||||||
|
final class WireGateway
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DanalConfig $cfg,
|
||||||
|
private readonly DanalCpcgiClient $client,
|
||||||
|
private readonly EucKr $euc,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function auth(GcPinOrder $order, string $attemptToken, bool $isMobile, array $buyer): array
|
||||||
|
{
|
||||||
|
$c = $this->cfg->wiretransfer();
|
||||||
|
$userAgent = $isMobile ? 'MW' : 'PC';
|
||||||
|
|
||||||
|
// wire 라우트 네이밍은 web.php 그대로 사용
|
||||||
|
$returnUrl = route('wire.return', ['a' => $attemptToken], true);
|
||||||
|
$cancelUrl = route('web.payments.danal.cancel', ['a' => $attemptToken], true);
|
||||||
|
$notiUrl = route('wire.noti', [], true);
|
||||||
|
|
||||||
|
$req = [
|
||||||
|
'TXTYPE' => 'AUTH',
|
||||||
|
'SERVICETYPE' => 'WIRETRANSFER',
|
||||||
|
|
||||||
|
'ORDERID' => $order->oid,
|
||||||
|
'ITEMNAME' => $this->safeItemName($this->orderTitle($order)),
|
||||||
|
'AMOUNT' => (string)$order->pay_money,
|
||||||
|
|
||||||
|
'USERNAME' => (string)($buyer['user_name'] ?? ''),
|
||||||
|
'USERID' => (string)($buyer['user_id'] ?? (string)$order->mem_no),
|
||||||
|
'USEREMAIL' => (string)($buyer['user_email'] ?? ''),
|
||||||
|
'USERPHONE' => (string)($buyer['user_phone'] ?? ''),
|
||||||
|
'USERAGENT' => $userAgent,
|
||||||
|
|
||||||
|
'RETURNURL' => $returnUrl,
|
||||||
|
'CANCELURL' => $cancelUrl,
|
||||||
|
|
||||||
|
'ISNOTI' => (string)($c['is_noti'] ?? 'Y'),
|
||||||
|
'NOTIURL' => $notiUrl,
|
||||||
|
|
||||||
|
// NOTI에서 token 추출(권장)
|
||||||
|
'BYPASSVALUE' => 'AT=' . $attemptToken,
|
||||||
|
];
|
||||||
|
|
||||||
|
\Log::info('danal_wire_auth_req', [
|
||||||
|
'cpid' => $c['cpid'] ?? null,
|
||||||
|
'key_len' => isset($c['key']) ? strlen((string)$c['key']) : null,
|
||||||
|
'iv_len' => isset($c['iv']) ? strlen((string)$c['iv']) : null,
|
||||||
|
'req' => $req,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$res = $this->client->call((string)$c['tx_url'], (string)$c['cpid'], $req, (string)$c['key'], (string)$c['iv']);
|
||||||
|
|
||||||
|
$params = ['STARTPARAMS' => (string)($res['STARTPARAMS'] ?? '')];
|
||||||
|
if (!empty($c['ci_url'])) $params['CIURL'] = (string)$c['ci_url'];
|
||||||
|
if (!empty($c['color'])) $params['COLOR'] = (string)$c['color'];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'req' => $req,
|
||||||
|
'res' => $res,
|
||||||
|
'start' => [
|
||||||
|
'actionUrl' => (string)($res['STARTURL'] ?? ''),
|
||||||
|
'params' => $params,
|
||||||
|
'acceptCharset' => 'EUC-KR',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decryptReturn(string $returnParams): array
|
||||||
|
{
|
||||||
|
$c = $this->cfg->wiretransfer();
|
||||||
|
return $this->client->decryptReturnParams($returnParams, (string)$c['key'], (string)$c['iv']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bill(GcPinOrder $order, string $tid): array
|
||||||
|
{
|
||||||
|
$c = $this->cfg->wiretransfer();
|
||||||
|
|
||||||
|
$req = [
|
||||||
|
'TXTYPE' => 'BILL',
|
||||||
|
'SERVICETYPE' => 'WIRETRANSFER',
|
||||||
|
'TID' => $tid,
|
||||||
|
// AUTH 금액과 반드시 동일
|
||||||
|
'AMOUNT' => (string)$order->pay_money,
|
||||||
|
];
|
||||||
|
|
||||||
|
$res = $this->client->call((string)$c['tx_url'], (string)$c['cpid'], $req, (string)$c['key'], (string)$c['iv']);
|
||||||
|
return ['req' => $req, 'res' => $res];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decryptNotiData(string $data): array
|
||||||
|
{
|
||||||
|
$c = $this->cfg->wiretransfer();
|
||||||
|
return $this->client->decryptReturnParams($data, (string)$c['key'], (string)$c['iv']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function orderTitle(GcPinOrder $order): string
|
||||||
|
{
|
||||||
|
$items = $order->items()->limit(2)->get();
|
||||||
|
if ($items->count() === 0) return '상품권';
|
||||||
|
if ($items->count() === 1) return (string)$items[0]->item_name;
|
||||||
|
return (string)$items[0]->item_name . ' 외';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function safeItemName(string $s): string
|
||||||
|
{
|
||||||
|
$s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s);
|
||||||
|
return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,4 +26,12 @@ class MemInfoRepository
|
|||||||
->where('mem_no', $memNo)
|
->where('mem_no', $memNo)
|
||||||
->first();
|
->first();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findForWirePay(int $memNo): ?MemInfo
|
||||||
|
{
|
||||||
|
return MemInfo::query()
|
||||||
|
->select(['mem_no','name','email','cell_phone'])
|
||||||
|
->where('mem_no', $memNo)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
app/Repositories/Mypage/UsageRepository.php
Normal file
82
app/Repositories/Mypage/UsageRepository.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repositories\Mypage;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class UsageRepository
|
||||||
|
{
|
||||||
|
public function findAttemptWithOrder(int $attemptId): ?object
|
||||||
|
{
|
||||||
|
return DB::table('gc_payment_attempts as a')
|
||||||
|
->leftJoin('gc_pin_order as o', 'o.id', '=', 'a.order_id')
|
||||||
|
->select([
|
||||||
|
'a.id as attempt_id',
|
||||||
|
'a.provider as attempt_provider',
|
||||||
|
'a.oid as attempt_oid',
|
||||||
|
'a.mem_no as attempt_mem_no',
|
||||||
|
'a.order_id as attempt_order_id',
|
||||||
|
'a.pay_method as attempt_pay_method',
|
||||||
|
'a.status as attempt_status',
|
||||||
|
'a.pg_tid as attempt_pg_tid',
|
||||||
|
'a.return_code as attempt_return_code',
|
||||||
|
'a.return_msg as attempt_return_msg',
|
||||||
|
'a.request_payload as attempt_request_payload',
|
||||||
|
'a.response_payload as attempt_response_payload',
|
||||||
|
'a.return_payload as attempt_return_payload',
|
||||||
|
'a.noti_payload as attempt_noti_payload',
|
||||||
|
'a.created_at as attempt_created_at',
|
||||||
|
'a.updated_at as attempt_updated_at',
|
||||||
|
|
||||||
|
'o.id as order_id',
|
||||||
|
'o.oid as order_oid',
|
||||||
|
'o.mem_no as order_mem_no',
|
||||||
|
'o.stat_pay as order_stat_pay',
|
||||||
|
'o.provider as order_provider',
|
||||||
|
'o.pay_method as order_pay_method',
|
||||||
|
'o.pg_tid as order_pg_tid',
|
||||||
|
'o.ret_code as order_ret_code',
|
||||||
|
'o.ret_msg as order_ret_msg',
|
||||||
|
'o.subtotal_amount as order_subtotal_amount',
|
||||||
|
'o.fee_amount as order_fee_amount',
|
||||||
|
'o.pg_fee_amount as order_pg_fee_amount',
|
||||||
|
'o.pay_money as order_pay_money',
|
||||||
|
'o.pay_data as order_pay_data',
|
||||||
|
'o.ret_data as order_ret_data',
|
||||||
|
'o.created_at as order_created_at',
|
||||||
|
'o.updated_at as order_updated_at',
|
||||||
|
])
|
||||||
|
->where('a.id', $attemptId)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrderItems(int $orderId)
|
||||||
|
{
|
||||||
|
return DB::table('gc_pin_order_items')
|
||||||
|
->where('order_id', $orderId)
|
||||||
|
->orderBy('id', 'asc')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countAssignedPins(int $orderId): int
|
||||||
|
{
|
||||||
|
return (int) DB::table('gc_pins')
|
||||||
|
->where('order_id', $orderId)
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAssignedPinsStatusSummary(int $orderId): array
|
||||||
|
{
|
||||||
|
$rows = DB::table('gc_pins')
|
||||||
|
->selectRaw('status, COUNT(*) as cnt')
|
||||||
|
->where('order_id', $orderId)
|
||||||
|
->groupBy('status')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$out = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$out[(string)$r->status] = (int)$r->cnt;
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
}
|
||||||
236
app/Services/Mypage/UsageService.php
Normal file
236
app/Services/Mypage/UsageService.php
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Mypage;
|
||||||
|
|
||||||
|
use App\Repositories\Mypage\UsageRepository;
|
||||||
|
|
||||||
|
final class UsageService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly UsageRepository $repo,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function buildPageData(?int $attemptId, int $sessionMemNo): array
|
||||||
|
{
|
||||||
|
// attempt_id 없으면 "구매내역(리스트) 준비중" 모드로 렌더
|
||||||
|
if (!$attemptId || $attemptId <= 0) {
|
||||||
|
return [
|
||||||
|
'mode' => 'empty',
|
||||||
|
'pageTitle' => '구매내역',
|
||||||
|
'pageDesc' => '결제 완료 후 핀 확인/발급/매입을 진행할 수 있습니다.',
|
||||||
|
'mypageActive' => 'usage',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = $this->repo->findAttemptWithOrder($attemptId);
|
||||||
|
if (!$row) abort(404);
|
||||||
|
|
||||||
|
// 소유자 검증 (존재 여부 숨김)
|
||||||
|
$attemptMem = (int)($row->attempt_mem_no ?? 0);
|
||||||
|
$orderMem = (int)($row->order_mem_no ?? 0);
|
||||||
|
|
||||||
|
if ($attemptMem !== $sessionMemNo || ($orderMem > 0 && $orderMem !== $sessionMemNo)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderId = (int)($row->order_id ?? 0);
|
||||||
|
if ($orderId <= 0) abort(404);
|
||||||
|
|
||||||
|
$items = $this->repo->getOrderItems($orderId);
|
||||||
|
|
||||||
|
$requiredQty = 0;
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$requiredQty += (int)($it->qty ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedPinsCount = $this->repo->countAssignedPins($orderId);
|
||||||
|
$pinsSummary = $this->repo->getAssignedPinsStatusSummary($orderId);
|
||||||
|
|
||||||
|
$attemptStatus = (string)($row->attempt_status ?? '');
|
||||||
|
$orderStatPay = (string)($row->order_stat_pay ?? '');
|
||||||
|
|
||||||
|
$stepKey = $this->resolveStepKey($attemptStatus, $orderStatPay, $requiredQty, $assignedPinsCount);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'mode' => 'detail',
|
||||||
|
'pageTitle' => '구매내역',
|
||||||
|
'pageDesc' => '결제 상태 및 핀 발급/매입 진행을 확인합니다.',
|
||||||
|
'mypageActive' => 'usage',
|
||||||
|
|
||||||
|
'attempt' => $this->attemptViewModel($row),
|
||||||
|
'order' => $this->orderViewModel($row),
|
||||||
|
'items' => $this->itemsViewModel($items),
|
||||||
|
|
||||||
|
'requiredQty' => $requiredQty,
|
||||||
|
'assignedPinsCount' => $assignedPinsCount,
|
||||||
|
'pinsSummary' => $pinsSummary,
|
||||||
|
|
||||||
|
'stepKey' => $stepKey,
|
||||||
|
'steps' => $this->steps(),
|
||||||
|
'vactInfo' => $this->extractVactInfo($row),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveStepKey(string $attemptStatus, string $orderStatPay, int $requiredQty, int $assignedPinsCount): string
|
||||||
|
{
|
||||||
|
// 취소/실패 우선
|
||||||
|
if (in_array($orderStatPay, ['c','f'], true) || in_array($attemptStatus, ['cancelled','failed'], true)) {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 가상계좌 입금대기
|
||||||
|
if ($orderStatPay === 'w' || $attemptStatus === 'issued') {
|
||||||
|
return 'deposit_wait';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결제완료
|
||||||
|
if ($orderStatPay === 'p' || $attemptStatus === 'paid') {
|
||||||
|
if ($requiredQty > 0 && $assignedPinsCount >= $requiredQty) return 'pin_done';
|
||||||
|
return 'pin_check';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결제 진행 중/확인 중
|
||||||
|
if (in_array($attemptStatus, ['ready','redirected','auth_ok'], true) || $orderStatPay === 'ready') {
|
||||||
|
return 'pay_processing';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'pay_processing';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function steps(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['key' => 'deposit_wait', 'label' => '입금대기'],
|
||||||
|
['key' => 'pin_check', 'label' => '결제완료'],
|
||||||
|
['key' => 'pin_verify', 'label' => '핀발급 확인'],
|
||||||
|
['key' => 'pin_done', 'label' => '핀발급완료'],
|
||||||
|
['key' => 'buyback', 'label' => '매입진행'],
|
||||||
|
['key' => 'settlement', 'label' => '정산완료'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function attemptViewModel(object $row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int)$row->attempt_id,
|
||||||
|
'provider' => (string)$row->attempt_provider,
|
||||||
|
'oid' => (string)$row->attempt_oid,
|
||||||
|
'pay_method' => (string)$row->attempt_pay_method,
|
||||||
|
'status' => (string)$row->attempt_status,
|
||||||
|
'pg_tid' => (string)($row->attempt_pg_tid ?? ''),
|
||||||
|
'return_code' => (string)($row->attempt_return_code ?? ''),
|
||||||
|
'return_msg' => (string)($row->attempt_return_msg ?? ''),
|
||||||
|
'payloads' => [
|
||||||
|
'request' => $this->jsonDecodeOrRaw($row->attempt_request_payload ?? null),
|
||||||
|
'response' => $this->jsonDecodeOrRaw($row->attempt_response_payload ?? null),
|
||||||
|
'return' => $this->jsonDecodeOrRaw($row->attempt_return_payload ?? null),
|
||||||
|
'noti' => $this->jsonDecodeOrRaw($row->attempt_noti_payload ?? null),
|
||||||
|
],
|
||||||
|
'created_at' => (string)($row->attempt_created_at ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function orderViewModel(object $row): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int)$row->order_id,
|
||||||
|
'oid' => (string)$row->order_oid,
|
||||||
|
'mem_no' => (int)$row->order_mem_no,
|
||||||
|
'stat_pay' => (string)$row->order_stat_pay,
|
||||||
|
'provider' => (string)($row->order_provider ?? ''),
|
||||||
|
'pay_method' => (string)($row->order_pay_method ?? ''),
|
||||||
|
'pg_tid' => (string)($row->order_pg_tid ?? ''),
|
||||||
|
'ret_code' => (string)($row->order_ret_code ?? ''),
|
||||||
|
'ret_msg' => (string)($row->order_ret_msg ?? ''),
|
||||||
|
'amounts' => [
|
||||||
|
'subtotal' => (int)($row->order_subtotal_amount ?? 0),
|
||||||
|
'fee' => (int)($row->order_fee_amount ?? 0),
|
||||||
|
'pg_fee' => (int)($row->order_pg_fee_amount ?? 0),
|
||||||
|
'pay_money' => (int)($row->order_pay_money ?? 0),
|
||||||
|
],
|
||||||
|
'pay_data' => $this->jsonDecodeOrRaw($row->order_pay_data ?? null),
|
||||||
|
'ret_data' => $this->jsonDecodeOrRaw($row->order_ret_data ?? null),
|
||||||
|
'created_at' => (string)($row->order_created_at ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function itemsViewModel($items): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ($items as $it) {
|
||||||
|
$out[] = [
|
||||||
|
'name' => (string)($it->item_name ?? ''),
|
||||||
|
'code' => (string)($it->item_code ?? ''),
|
||||||
|
'qty' => (int)($it->qty ?? 0),
|
||||||
|
'unit' => (int)($it->unit_pay_price ?? 0),
|
||||||
|
'total'=> (int)($it->line_total ?? 0),
|
||||||
|
'meta' => $this->jsonDecodeOrRaw($it->meta ?? null),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractVactInfo(object $row): array
|
||||||
|
{
|
||||||
|
// 어떤 키로 들어오든 "있으면 보여주기" 수준의 안전한 추출
|
||||||
|
$candidates = [
|
||||||
|
$this->jsonDecodeOrArray($row->order_pay_data ?? null),
|
||||||
|
$this->jsonDecodeOrArray($row->order_ret_data ?? null),
|
||||||
|
$this->jsonDecodeOrArray($row->attempt_return_payload ?? null),
|
||||||
|
$this->jsonDecodeOrArray($row->attempt_noti_payload ?? null),
|
||||||
|
];
|
||||||
|
|
||||||
|
$pick = function(array $a, array $keys) {
|
||||||
|
foreach ($keys as $k) {
|
||||||
|
if (array_key_exists($k, $a) && $a[$k] !== '' && $a[$k] !== null) return $a[$k];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
$info = [
|
||||||
|
'bank' => null,
|
||||||
|
'account' => null,
|
||||||
|
'holder' => null,
|
||||||
|
'amount' => null,
|
||||||
|
'expire_at' => null,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($candidates as $a) {
|
||||||
|
if (!$a) continue;
|
||||||
|
$info['bank'] ??= $pick($a, ['bank', 'vbank', 'Vbank', 'BankName']);
|
||||||
|
$info['account'] ??= $pick($a, ['account', 'vaccount', 'Vaccount', 'AccountNo']);
|
||||||
|
$info['holder'] ??= $pick($a, ['holder', 'depositor', 'Depositor', 'AccountHolder']);
|
||||||
|
$info['amount'] ??= $pick($a, ['amount', 'Amt', 'pay_money', 'PayMoney']);
|
||||||
|
$info['expire_at'] ??= $pick($a, ['expire_at', 'Vdate', 'vdate', 'ExpireDate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 다 null이면 빈 배열로 처리
|
||||||
|
$hasAny = false;
|
||||||
|
foreach ($info as $v) { if ($v !== null) { $hasAny = true; break; } }
|
||||||
|
return $hasAny ? $info : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function jsonDecodeOrArray($v): array
|
||||||
|
{
|
||||||
|
if ($v === null) return [];
|
||||||
|
if (is_array($v)) return $v;
|
||||||
|
$s = trim((string)$v);
|
||||||
|
if ($s === '') return [];
|
||||||
|
$decoded = json_decode($s, true);
|
||||||
|
return is_array($decoded) ? $decoded : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function jsonDecodeOrRaw($v)
|
||||||
|
{
|
||||||
|
if ($v === null) return null;
|
||||||
|
if (is_array($v)) return $v;
|
||||||
|
$s = trim((string)$v);
|
||||||
|
if ($s === '') return null;
|
||||||
|
|
||||||
|
$decoded = json_decode($s, true);
|
||||||
|
if (json_last_error() === JSON_ERROR_NONE) return $decoded;
|
||||||
|
|
||||||
|
// JSON이 아니라면 원문 그대로(운영 디버그용)
|
||||||
|
return $s;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -227,6 +227,7 @@ final class OrderCheckoutService
|
|||||||
'CREDIT_CARD' => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
|
'CREDIT_CARD' => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
|
||||||
'CREDIT_CARD_REFUND' => ['card', ['card_kind'=>'exchange', 'is_mobile'=>$isMobile]],
|
'CREDIT_CARD_REFUND' => ['card', ['card_kind'=>'exchange', 'is_mobile'=>$isMobile]],
|
||||||
'VACT' => ['vact', ['is_mobile'=>$isMobile]],
|
'VACT' => ['vact', ['is_mobile'=>$isMobile]],
|
||||||
|
'WIRETRANSFER' => ['wire', ['is_mobile'=>$isMobile]],
|
||||||
default => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
|
default => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,308 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Payments\Danal\Clients;
|
|
||||||
|
|
||||||
use App\Models\GcPaymentAttempt;
|
|
||||||
use Illuminate\Support\Facades\Crypt;
|
|
||||||
use Symfony\Component\Process\Process;
|
|
||||||
|
|
||||||
final class DanalTeleditClient
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_cell() -> SClient ITEMSEND2
|
|
||||||
* return: redirect info for Start.php
|
|
||||||
*/
|
|
||||||
public function ready(GcPaymentAttempt $attempt, string $token, array $order): array
|
|
||||||
{
|
|
||||||
$binPath = (string)config('danal.phone.bin_path', '');
|
|
||||||
if ($binPath === '') throw new \RuntimeException('DANAL_PHONE_BIN_PATH missing');
|
|
||||||
|
|
||||||
$cpid = (string)config('danal.phone.cpid', '');
|
|
||||||
$pwd = (string)config('danal.phone.pwd', '');
|
|
||||||
if ($cpid === '' || $pwd === '') throw new \RuntimeException('DANAL_PHONE_CPID/PWD missing');
|
|
||||||
|
|
||||||
// CI3 상수(_PG_ITEMCODE_DANAL) 역할: item_code 필요
|
|
||||||
$itemCode = (string)config('danal.phone.item_code', '');
|
|
||||||
if ($itemCode === '') throw new \RuntimeException('config(danal.phone.item_code) missing (DANAL_PHONE_ITEMCODE 필요)');
|
|
||||||
|
|
||||||
$oid = (string)($order['oid'] ?? '');
|
|
||||||
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? 0);
|
|
||||||
$itemName = (string)($order['itemName'] ?? $order['pin_name'] ?? '상품권');
|
|
||||||
$memNo = (string)($order['mem_no'] ?? $attempt->mem_no ?? '');
|
|
||||||
|
|
||||||
$cellCorp = (string)($order['cell_corp'] ?? '');
|
|
||||||
$dstAddr = preg_replace('/\D+/', '', (string)($order['cell_phone'] ?? ''));
|
|
||||||
|
|
||||||
$carrier = $this->mapCarrier($cellCorp);
|
|
||||||
|
|
||||||
$trans = [
|
|
||||||
'Command' => 'ITEMSEND2',
|
|
||||||
'SERVICE' => 'TELEDIT',
|
|
||||||
'ItemCount' => '1',
|
|
||||||
'OUTPUTOPTION' => 'DEFAULT',
|
|
||||||
|
|
||||||
'ID' => $cpid,
|
|
||||||
'PWD' => $pwd,
|
|
||||||
|
|
||||||
'SUBCP' => '',
|
|
||||||
'USERID' => $memNo,
|
|
||||||
'ORDERID' => $oid,
|
|
||||||
'IsPreOtbill' => 'N',
|
|
||||||
'IsSubscript' => 'N',
|
|
||||||
|
|
||||||
'ItemInfo' => $this->makeItemInfo($amount, $itemCode, $itemName),
|
|
||||||
];
|
|
||||||
|
|
||||||
$attempt->request_payload = $trans;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
$res = $this->runClient($binPath, 'SClient', $trans);
|
|
||||||
|
|
||||||
$attempt->response_payload = $res;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
if (($res['Result'] ?? '') !== '0') {
|
|
||||||
$msg = (string)($res['ErrMsg'] ?? '휴대폰 결제 모듈 실패');
|
|
||||||
throw new \RuntimeException("Teledit ITEMSEND2 failed: {$msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// CPCGI로 POST 전달되는 bypass
|
|
||||||
$byPass = [
|
|
||||||
'BgColor' => '00',
|
|
||||||
'TargetURL' => route('web.payments.danal.phone.return', [], true),
|
|
||||||
'BackURL' => route('web.payments.danal.phone.cancel', [], true),
|
|
||||||
'IsUseCI' => 'N',
|
|
||||||
'CIURL' => url('/img/main/top_logo.png'),
|
|
||||||
'Email' => (string)($order['email'] ?? ''),
|
|
||||||
'IsCharSet' => '',
|
|
||||||
// 기존 macro->encrypt("DanaLTelediT", "oid/amount/mem_no") 구조 유지 + token 추가
|
|
||||||
'Order' => Crypt::encryptString($oid . '/' . $amount . '/' . $memNo . '/' . $token),
|
|
||||||
];
|
|
||||||
|
|
||||||
$isMobile = (bool)($order['is_mobile'] ?? false);
|
|
||||||
$actionUrl = $isMobile
|
|
||||||
? 'https://ui.teledit.com/Danal/Teledit/Mobile/Start.php'
|
|
||||||
: 'https://ui.teledit.com/Danal/Teledit/Web/Start.php';
|
|
||||||
|
|
||||||
// CI3처럼 Result/ErrMsg 제외하고 Res를 폼에 다 넣음
|
|
||||||
$params = [];
|
|
||||||
foreach ($res as $k => $v) {
|
|
||||||
if ($k === 'Result' || $k === 'ErrMsg') continue;
|
|
||||||
$params[$k] = $v;
|
|
||||||
}
|
|
||||||
foreach ($byPass as $k => $v) $params[$k] = $v;
|
|
||||||
|
|
||||||
// 추가 Hidden
|
|
||||||
$params['CPName'] = (string)($order['cp_name'] ?? '핀포유');
|
|
||||||
$params['ItemName'] = $itemName;
|
|
||||||
$params['ItemAmt'] = (string)$amount;
|
|
||||||
$params['IsPreOtbill'] = (string)$trans['IsPreOtbill'];
|
|
||||||
$params['IsSubscript'] = (string)$trans['IsSubscript'];
|
|
||||||
$params['IsCarrier'] = $carrier; // SKT/KTF/LGT
|
|
||||||
$params['IsDstAddr'] = $dstAddr; // 010xxxxxxxx
|
|
||||||
|
|
||||||
return [
|
|
||||||
'actionUrl' => $actionUrl,
|
|
||||||
'params' => $params,
|
|
||||||
'acceptCharset' => 'EUC-KR',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_cell_return()
|
|
||||||
* - Order 복호화 -> NCONFIRM -> NBILL
|
|
||||||
* - 성공 시 onPaid(oid, tid, amount, payload)
|
|
||||||
*/
|
|
||||||
public function confirmAndBillOnReturn(array $post, array $order, callable $onPaid): array
|
|
||||||
{
|
|
||||||
$binPath = (string)config('danal.phone.bin_path', '');
|
|
||||||
if ($binPath === '') throw new \RuntimeException('DANAL_PHONE_BIN_PATH missing');
|
|
||||||
|
|
||||||
$serverInfo = (string)($post['ServerInfo'] ?? '');
|
|
||||||
if ($serverInfo === '') return $this->fail('E990', 'ServerInfo 누락');
|
|
||||||
|
|
||||||
$encOrder = (string)($post['Order'] ?? '');
|
|
||||||
if ($encOrder === '') return $this->fail('E991', 'Order 누락');
|
|
||||||
|
|
||||||
[$oid, $amount, $memNo, $token] = $this->decryptOrder($encOrder);
|
|
||||||
|
|
||||||
// (보안) 휴대폰번호 변조 체크: order에 cell_phone이 있으면 비교
|
|
||||||
$dst = preg_replace('/\D+/', '', (string)($post['IsDstAddr'] ?? ''));
|
|
||||||
$expected = preg_replace('/\D+/', '', (string)($order['cell_phone'] ?? ''));
|
|
||||||
if ($expected !== '' && $dst !== '' && $dst !== $expected) {
|
|
||||||
return $this->fail('E992', '휴대폰번호 변조 의심');
|
|
||||||
}
|
|
||||||
|
|
||||||
// NCONFIRM
|
|
||||||
$nConfirm = [
|
|
||||||
'Command' => 'NCONFIRM',
|
|
||||||
'OUTPUTOPTION' => 'DEFAULT',
|
|
||||||
'ServerInfo' => $serverInfo,
|
|
||||||
'IFVERSION' => 'V1.1.2',
|
|
||||||
'ConfirmOption' => '0',
|
|
||||||
];
|
|
||||||
$res1 = $this->runClient($binPath, 'SClient', $nConfirm);
|
|
||||||
if (($res1['Result'] ?? '') !== '0') {
|
|
||||||
return $this->fail((string)($res1['Result'] ?? 'E993'), (string)($res1['ErrMsg'] ?? 'NCONFIRM 실패'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// NBILL
|
|
||||||
$nBill = [
|
|
||||||
'Command' => 'NBILL',
|
|
||||||
'OUTPUTOPTION' => 'DEFAULT',
|
|
||||||
'ServerInfo' => $serverInfo,
|
|
||||||
'IFVERSION' => 'V1.1.2',
|
|
||||||
'BillOption' => '0',
|
|
||||||
];
|
|
||||||
$res2 = $this->runClient($binPath, 'SClient', $nBill);
|
|
||||||
if (($res2['Result'] ?? '') !== '0') {
|
|
||||||
return $this->fail((string)($res2['Result'] ?? 'E994'), (string)($res2['ErrMsg'] ?? 'NBILL 실패'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 성공: CI3처럼 Res(=NCONFIRM 결과)에서 TID/AMOUNT/ORDERID 사용
|
|
||||||
$tid = (string)($res1['TID'] ?? '');
|
|
||||||
$paidAmount = (int)($res1['AMOUNT'] ?? $amount);
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'Res' => $res1,
|
|
||||||
'Res2' => $res2,
|
|
||||||
'Post' => $post,
|
|
||||||
];
|
|
||||||
|
|
||||||
$onPaid($oid, $tid, $paidAmount, $payload);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => '결제가 완료되었습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
'meta' => [
|
|
||||||
'oid' => $oid,
|
|
||||||
'tid' => $tid,
|
|
||||||
'amount' => $paidAmount,
|
|
||||||
'code' => (string)($res2['Result'] ?? '0'),
|
|
||||||
'msg' => (string)($res2['ErrMsg'] ?? ''),
|
|
||||||
'ret_data' => $payload,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_cell_cancel()
|
|
||||||
*/
|
|
||||||
public function handleCancel(array $post, callable $onCancel): array
|
|
||||||
{
|
|
||||||
$encOrder = (string)($post['Order'] ?? '');
|
|
||||||
if ($encOrder === '') return $this->fail('C990', 'Order 누락');
|
|
||||||
|
|
||||||
[$oid, $amount, $memNo, $token] = $this->decryptOrder($encOrder);
|
|
||||||
|
|
||||||
$onCancel($oid, [
|
|
||||||
'Post' => $post,
|
|
||||||
'oid' => $oid,
|
|
||||||
'amount' => $amount,
|
|
||||||
'mem_no' => $memNo,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => 'cancel',
|
|
||||||
'message' => '사용자 결제 취소',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function decryptOrder(string $encOrder): array
|
|
||||||
{
|
|
||||||
$plain = Crypt::decryptString($encOrder);
|
|
||||||
$parts = explode('/', $plain);
|
|
||||||
|
|
||||||
$oid = (string)($parts[0] ?? '');
|
|
||||||
$amount = (int)($parts[1] ?? 0);
|
|
||||||
$memNo = (string)($parts[2] ?? '');
|
|
||||||
$token = (string)($parts[3] ?? '');
|
|
||||||
|
|
||||||
if ($oid === '' || $amount <= 0 || $memNo === '' || $token === '') {
|
|
||||||
throw new \RuntimeException('Invalid Order payload');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [$oid, $amount, $memNo, $token];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function runClient(string $binPath, string $bin, array $params): array
|
|
||||||
{
|
|
||||||
$arg = $this->makeParam($params);
|
|
||||||
|
|
||||||
$proc = new Process([$binPath . '/' . $bin, $arg]);
|
|
||||||
$proc->setTimeout((int)config('danal.authtel.timeout', 30));
|
|
||||||
$proc->run();
|
|
||||||
|
|
||||||
$out = $proc->getOutput();
|
|
||||||
if ($out === '' && !$proc->isSuccessful()) {
|
|
||||||
$err = $proc->getErrorOutput();
|
|
||||||
throw new \RuntimeException("Teledit {$bin} failed: {$err}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->parseOutput($out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseOutput(string $out): array
|
|
||||||
{
|
|
||||||
// CI3 Parsor(): 라인들을 &로 붙이고 key=value 파싱 + urldecode
|
|
||||||
$out = str_replace("\r", '', $out);
|
|
||||||
$lines = array_filter(array_map('trim', explode("\n", $out)), fn($v) => $v !== '');
|
|
||||||
$in = implode('&', $lines);
|
|
||||||
|
|
||||||
$map = [];
|
|
||||||
foreach (explode('&', $in) as $tok) {
|
|
||||||
$tok = trim($tok);
|
|
||||||
if ($tok === '') continue;
|
|
||||||
|
|
||||||
$tmp = explode('=', $tok, 2);
|
|
||||||
$name = trim($tmp[0] ?? '');
|
|
||||||
$value = trim($tmp[1] ?? '');
|
|
||||||
|
|
||||||
if ($name === '') continue;
|
|
||||||
$map[$name] = urldecode($value);
|
|
||||||
}
|
|
||||||
return $map;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeParam(array $arr): string
|
|
||||||
{
|
|
||||||
// CI3 MakeParam(): key=value;key=value...
|
|
||||||
$parts = [];
|
|
||||||
foreach ($arr as $k => $v) {
|
|
||||||
$parts[] = $k . '=' . $v;
|
|
||||||
}
|
|
||||||
return implode(';', $parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function makeItemInfo(int $amt, string $code, string $name): string
|
|
||||||
{
|
|
||||||
// CI3 MakeItemInfo(): substr(code,0,1)|amt|1|code|name
|
|
||||||
return substr($code, 0, 1) . '|' . $amt . '|1|' . $code . '|' . $name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function mapCarrier(string $corp): string
|
|
||||||
{
|
|
||||||
// CI3: 01=SKT, 02=KTF, 03=LGT
|
|
||||||
return match ($corp) {
|
|
||||||
'01' => 'SKT',
|
|
||||||
'02' => 'KTF',
|
|
||||||
'03' => 'LGT',
|
|
||||||
default => '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fail(string $code, string $msg): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'status' => 'fail',
|
|
||||||
'message' => $msg ?: '결제에 실패했습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
'meta' => [
|
|
||||||
'code' => $code,
|
|
||||||
'msg' => $msg,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,278 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Payments\Danal;
|
|
||||||
|
|
||||||
use App\Models\GcPaymentAttempt;
|
|
||||||
use App\Services\Payments\Danal\Gateways\DanalCardGateway;
|
|
||||||
use App\Services\Payments\Danal\Gateways\DanalVactGateway;
|
|
||||||
use App\Services\Payments\Danal\Clients\DanalTeleditClient;
|
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
final class DanalPaymentService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly DanalCardGateway $card,
|
|
||||||
private readonly DanalVactGateway $vact,
|
|
||||||
private readonly DanalTeleditClient $teledit,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 결제 시작: card/vact/phone 분기
|
|
||||||
*/
|
|
||||||
public function start(int $memNo, array $data): array
|
|
||||||
{
|
|
||||||
$oid = (string)$data['oid'];
|
|
||||||
$payMethod = (string)$data['pay_method'];
|
|
||||||
|
|
||||||
// TODO: 여기서 CI3처럼 주문정보를 DB에서 조회해서 변조 방지해야 함
|
|
||||||
// - 필요한 값: amount, itemName, stat_pay(w), mid(환금성 판단), mem_no 일치 등
|
|
||||||
// - 현재는 최소값만 받되, 반드시 네 주문 테이블에 맞게 연결할 것.
|
|
||||||
$order = $this->loadOrderForPaymentOrFail($oid, $memNo);
|
|
||||||
|
|
||||||
return DB::transaction(function () use ($memNo, $data, $order, $payMethod, $oid) {
|
|
||||||
[$attempt, $token] = $this->upsertAttempt($memNo, $order, $data);
|
|
||||||
|
|
||||||
if ($payMethod === 'card') {
|
|
||||||
$kind = $data['card_kind'] ?? 'general'; // general|exchange
|
|
||||||
$start = $this->card->auth($attempt, $token, $order, $kind);
|
|
||||||
} elseif ($payMethod === 'vact') {
|
|
||||||
$kind = $data['vact_kind'] ?? 'a'; // a|v (기본 a)
|
|
||||||
$start = $this->vact->auth($attempt, $token, $order, $kind);
|
|
||||||
} else { // phone
|
|
||||||
$start = $this->teledit->ready($attempt, $token, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
$attempt->status = 'redirected';
|
|
||||||
$attempt->redirected_at = now();
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
return $start;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleCardReturn(string $oid, string $token, array $all, array $post): array
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($oid, $token, $post) {
|
|
||||||
$attempt = $this->findAttemptOrFail('card', $oid, $token);
|
|
||||||
$order = $this->loadOrderForPaymentOrFail($attempt->oid, (int)$attempt->mem_no);
|
|
||||||
|
|
||||||
$ret = $this->card->billOnReturn($attempt, $token, $order, $post);
|
|
||||||
|
|
||||||
return $ret;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleVactReturn(string $token, array $post): array
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($token, $post) {
|
|
||||||
$attempt = $this->findAttemptByTokenOrFail('vact', $token);
|
|
||||||
$order = $this->loadOrderForPaymentOrFail($attempt->oid, (int)$attempt->mem_no);
|
|
||||||
|
|
||||||
return $this->vact->issueVaccountOnReturn($attempt, $token, $order, $post);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handleVactNoti(array $post): void
|
|
||||||
{
|
|
||||||
// NOTI는 idempotent하게: 이미 paid면 그냥 OK
|
|
||||||
DB::transaction(function () use ($post) {
|
|
||||||
$this->vact->handleNoti($post, function (string $oid, string $tid, int $amount, array $payload) {
|
|
||||||
// TODO: 여기서 주문 paid 처리 + 장부 처리
|
|
||||||
// $this->markOrderPaid($oid, $tid, $amount, $payload);
|
|
||||||
|
|
||||||
// attempt도 paid로
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')->where('oid',$oid)->where('pay_method','vact')
|
|
||||||
->lockForUpdate()->first();
|
|
||||||
|
|
||||||
if ($attempt) {
|
|
||||||
if ($attempt->status !== 'paid') {
|
|
||||||
$attempt->status = 'paid';
|
|
||||||
$attempt->pg_tid = $tid ?: $attempt->pg_tid;
|
|
||||||
$attempt->amount = $amount ?: $attempt->amount;
|
|
||||||
$attempt->noti_payload = $payload;
|
|
||||||
$attempt->noti_at = now();
|
|
||||||
$attempt->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handlePhoneReturn(array $post): array
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($post) {
|
|
||||||
|
|
||||||
// Order 복호화로 oid/mem_no/token 확보
|
|
||||||
[$oid, $amount, $memNo, $token] = $this->teledit->decryptOrder((string)($post['Order'] ?? ''));
|
|
||||||
|
|
||||||
$order = $this->loadOrderForPaymentOrFail($oid, (int)$memNo);
|
|
||||||
|
|
||||||
return $this->teledit->confirmAndBillOnReturn($post, $order, function (string $oid, string $tid, int $amount, array $payload) {
|
|
||||||
// TODO: 주문 paid 처리 + 장부 처리
|
|
||||||
// $this->markOrderPaid($oid, $tid, $amount, $payload);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')->where('oid',$oid)->where('pay_method','phone')
|
|
||||||
->lockForUpdate()->first();
|
|
||||||
|
|
||||||
if ($attempt) {
|
|
||||||
$attempt->status = 'paid';
|
|
||||||
$attempt->pg_tid = $tid;
|
|
||||||
$attempt->returned_at = now();
|
|
||||||
$attempt->return_payload = $payload;
|
|
||||||
$attempt->save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function handlePhoneCancel(array $post): array
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($post) {
|
|
||||||
|
|
||||||
return $this->teledit->handleCancel($post, function (string $oid, array $payload) {
|
|
||||||
// TODO: 주문 cancel 처리
|
|
||||||
// $this->cancelOrder($oid, 'C999', '사용자 결제 취소', $payload);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')->where('oid',$oid)->where('pay_method','phone')
|
|
||||||
->lockForUpdate()->first();
|
|
||||||
|
|
||||||
if ($attempt) {
|
|
||||||
$attempt->status = 'cancelled';
|
|
||||||
$attempt->return_payload = $payload;
|
|
||||||
$attempt->returned_at = now();
|
|
||||||
$attempt->save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function handleCancel(string $oid, string $token): array
|
|
||||||
{
|
|
||||||
return DB::transaction(function () use ($oid, $token) {
|
|
||||||
$attempt = $this->findAttemptAnyMethodOrFail($oid, $token);
|
|
||||||
|
|
||||||
// TODO: 주문 cancel 처리
|
|
||||||
// $this->cancelOrder($oid, 'USER_CANCEL', '사용자 결제 취소', []);
|
|
||||||
|
|
||||||
$attempt->status = 'cancelled';
|
|
||||||
$attempt->returned_at = now();
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => 'cancel',
|
|
||||||
'message' => '구매를 취소했습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function upsertAttempt(int $memNo, array $order, array $data): array
|
|
||||||
{
|
|
||||||
$token = bin2hex(random_bytes(32));
|
|
||||||
$tokenHash = hash('sha256', $token);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')
|
|
||||||
->where('oid', (string)$order['oid'])
|
|
||||||
->where('pay_method', (string)$data['pay_method'])
|
|
||||||
->lockForUpdate()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$attempt) {
|
|
||||||
$attempt = new GcPaymentAttempt();
|
|
||||||
$attempt->provider = 'danal';
|
|
||||||
$attempt->oid = (string)$order['oid'];
|
|
||||||
$attempt->mem_no = $memNo;
|
|
||||||
$attempt->pay_method = (string)$data['pay_method'];
|
|
||||||
$attempt->amount = (int)$order['amount'];
|
|
||||||
$attempt->currency = 'KRW';
|
|
||||||
$attempt->ready_at = now();
|
|
||||||
}
|
|
||||||
|
|
||||||
$attempt->status = 'ready';
|
|
||||||
$attempt->token_hash = $tokenHash;
|
|
||||||
$attempt->card_kind = $data['card_kind'] ?? null;
|
|
||||||
$attempt->vact_kind = $data['vact_kind'] ?? null;
|
|
||||||
$attempt->user_agent = request()->userAgent();
|
|
||||||
$attempt->user_ip = inet_pton(request()->ip() ?: '127.0.0.1');
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
return [$attempt, $token];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findAttemptOrFail(string $method, string $oid, string $token): GcPaymentAttempt
|
|
||||||
{
|
|
||||||
$tokenHash = hash('sha256', $token);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')
|
|
||||||
->where('oid',$oid)
|
|
||||||
->where('pay_method',$method)
|
|
||||||
->lockForUpdate()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$attempt) abort(404);
|
|
||||||
if (!hash_equals((string)$attempt->token_hash, $tokenHash)) abort(403);
|
|
||||||
|
|
||||||
return $attempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findAttemptByTokenOrFail(string $method, string $token): GcPaymentAttempt
|
|
||||||
{
|
|
||||||
$tokenHash = hash('sha256', $token);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')
|
|
||||||
->where('pay_method',$method)
|
|
||||||
->where('token_hash',$tokenHash)
|
|
||||||
->lockForUpdate()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$attempt) abort(404);
|
|
||||||
|
|
||||||
return $attempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findAttemptAnyMethodOrFail(string $oid, string $token): GcPaymentAttempt
|
|
||||||
{
|
|
||||||
$tokenHash = hash('sha256', $token);
|
|
||||||
|
|
||||||
$attempt = GcPaymentAttempt::query()
|
|
||||||
->where('provider','danal')
|
|
||||||
->where('oid',$oid)
|
|
||||||
->where('token_hash',$tokenHash)
|
|
||||||
->lockForUpdate()
|
|
||||||
->first();
|
|
||||||
|
|
||||||
if (!$attempt) abort(404);
|
|
||||||
return $attempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: 네 주문테이블에 맞게 구현해야 하는 핵심 함수
|
|
||||||
* - CI3 Product.php payment_danal_ready()에서 하던 검증(oid/mem_no/stat_pay/amount 등)
|
|
||||||
*/
|
|
||||||
private function loadOrderForPaymentOrFail(string $oid, int $memNo): array
|
|
||||||
{
|
|
||||||
// ✅ 여기만 너 DB 구조에 맞게 바꾸면 나머지 다날 연동은 그대로 감
|
|
||||||
// 예시 형태:
|
|
||||||
// $row = DB::table('gc_orders')->where('oid',$oid)->first();
|
|
||||||
// if (!$row) abort(404);
|
|
||||||
// if ((int)$row->mem_no !== $memNo) abort(403);
|
|
||||||
// if ($row->stat_pay !== 'w') abort(409);
|
|
||||||
// return ['oid'=>$row->oid,'amount'=>(int)$row->pay_money,'itemName'=>$row->pin_name,'mid'=>$row->mid];
|
|
||||||
|
|
||||||
return [
|
|
||||||
'oid' => $oid,
|
|
||||||
'amount' => (int) request('amount', 0), // 임시
|
|
||||||
'itemName' => (string) request('itemName', '상품권'),
|
|
||||||
'mid' => (string) request('mid', ''),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,243 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Payments\Danal\Gateways;
|
|
||||||
|
|
||||||
use App\Models\GcPaymentAttempt;
|
|
||||||
use App\Support\DanalAes256CbcHex;
|
|
||||||
|
|
||||||
final class DanalCardGateway
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly DanalAes256CbcHex $aes,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_card() -> AUTH
|
|
||||||
* return: redirect form info
|
|
||||||
*/
|
|
||||||
public function auth(GcPaymentAttempt $attempt, string $token, array $order, string $cardKind = 'general'): array
|
|
||||||
{
|
|
||||||
$cfg = config("danal.card.$cardKind");
|
|
||||||
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
||||||
throw new \RuntimeException("Danal card config missing: $cardKind");
|
|
||||||
}
|
|
||||||
|
|
||||||
$oid = (string)($order['oid'] ?? '');
|
|
||||||
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? 0);
|
|
||||||
$itemName = (string)($order['itemName'] ?? $order['pin_name'] ?? '상품권');
|
|
||||||
|
|
||||||
$isMobile = (bool)($order['is_mobile'] ?? false);
|
|
||||||
$userAgent = $isMobile ? 'WM' : 'PC';
|
|
||||||
|
|
||||||
// Return/Cancel: token을 query로 붙여서 세션 의존 제거
|
|
||||||
$returnUrl = route('web.payments.danal.card.return', ['o' => $oid, 't' => $token], true);
|
|
||||||
$cancelUrl = route('web.payments.danal.cancel', ['o' => $oid, 't' => $token], true);
|
|
||||||
|
|
||||||
// BYPASSVALUE는 '&' 금지(다날 주의) → ; 구분
|
|
||||||
$bypass = "TOKEN={$token};OID={$oid};AMOUNT={$amount};";
|
|
||||||
|
|
||||||
$req = [
|
|
||||||
'SUBCPID' => '',
|
|
||||||
'AMOUNT' => (string)$amount,
|
|
||||||
'CURRENCY' => '410',
|
|
||||||
'ITEMNAME' => $this->toEucKr($itemName),
|
|
||||||
'USERAGENT' => $userAgent,
|
|
||||||
'ORDERID' => $oid,
|
|
||||||
'OFFERPERIOD' => '',
|
|
||||||
|
|
||||||
'USERNAME' => '',
|
|
||||||
'USERID' => (string)($order['mem_no'] ?? ''),
|
|
||||||
'USEREMAIL' => '',
|
|
||||||
|
|
||||||
'CANCELURL' => $cancelUrl,
|
|
||||||
'RETURNURL' => $returnUrl,
|
|
||||||
|
|
||||||
'TXTYPE' => 'AUTH',
|
|
||||||
'SERVICETYPE' => 'DANALCARD',
|
|
||||||
'ISNOTI' => 'N',
|
|
||||||
'BYPASSVALUE' => $bypass,
|
|
||||||
];
|
|
||||||
|
|
||||||
$attempt->card_kind = $cardKind;
|
|
||||||
$attempt->request_payload = $req;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
$res = $this->postCpcgi(
|
|
||||||
(string)$cfg['url'],
|
|
||||||
(string)$cfg['cpid'],
|
|
||||||
$req,
|
|
||||||
(string)$cfg['key'],
|
|
||||||
(string)$cfg['iv']
|
|
||||||
);
|
|
||||||
|
|
||||||
$attempt->response_payload = $res;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
if (($res['RETURNCODE'] ?? '') !== '0000') {
|
|
||||||
$msg = (string)($res['RETURNMSG'] ?? '모듈 호출 실패');
|
|
||||||
throw new \RuntimeException("Danal card AUTH failed: {$res['RETURNCODE']} {$msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'actionUrl' => (string)$res['STARTURL'],
|
|
||||||
'params' => [
|
|
||||||
'STARTPARAMS' => (string)$res['STARTPARAMS'],
|
|
||||||
],
|
|
||||||
'acceptCharset' => 'EUC-KR',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_card_return() -> RETURN decrypt -> BILL
|
|
||||||
* return: result view payload
|
|
||||||
*/
|
|
||||||
public function billOnReturn(GcPaymentAttempt $attempt, string $token, array $order, array $post): array
|
|
||||||
{
|
|
||||||
$cardKind = $attempt->card_kind ?: 'general';
|
|
||||||
$cfg = config("danal.card.$cardKind");
|
|
||||||
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
||||||
throw new \RuntimeException("Danal card config missing: $cardKind");
|
|
||||||
}
|
|
||||||
|
|
||||||
$oid = (string)($order['oid'] ?? $attempt->oid);
|
|
||||||
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? $attempt->amount ?? 0);
|
|
||||||
|
|
||||||
$returnParams = (string)($post['RETURNPARAMS'] ?? '');
|
|
||||||
if ($returnParams === '') {
|
|
||||||
return $this->fail('C990', 'RETURNPARAMS 누락');
|
|
||||||
}
|
|
||||||
|
|
||||||
// RETURNPARAMS 복호화
|
|
||||||
$retStr = $this->aes->decrypt($returnParams, (string)$cfg['key'], (string)$cfg['iv']);
|
|
||||||
$retMap = $this->aes->parseQuery($retStr);
|
|
||||||
|
|
||||||
// 주문번호 일치 검증
|
|
||||||
if (($retMap['ORDERID'] ?? '') !== $oid) {
|
|
||||||
return $this->fail('C991', '주문번호 불일치');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 인증 결과 확인
|
|
||||||
$retCode = (string)($retMap['RETURNCODE'] ?? '');
|
|
||||||
$retMsg = (string)($retMap['RETURNMSG'] ?? '');
|
|
||||||
|
|
||||||
if ($retCode !== '0000') {
|
|
||||||
return $this->fail($retCode ?: 'C992', $retMsg ?: '카드 인증 실패');
|
|
||||||
}
|
|
||||||
|
|
||||||
// BILL 요청
|
|
||||||
$billReq = [
|
|
||||||
'TID' => (string)($retMap['TID'] ?? ''),
|
|
||||||
'AMOUNT' => (string)$amount,
|
|
||||||
'TXTYPE' => 'BILL',
|
|
||||||
'SERVICETYPE' => 'DANALCARD',
|
|
||||||
];
|
|
||||||
|
|
||||||
$res = $this->postCpcgi(
|
|
||||||
(string)$cfg['url'],
|
|
||||||
(string)$cfg['cpid'],
|
|
||||||
$billReq,
|
|
||||||
(string)$cfg['key'],
|
|
||||||
(string)$cfg['iv']
|
|
||||||
);
|
|
||||||
|
|
||||||
$res = $this->eucKrArrayToUtf8($res);
|
|
||||||
|
|
||||||
if (($res['RETURNCODE'] ?? '') === '0000') {
|
|
||||||
$attempt->status = 'paid';
|
|
||||||
$attempt->pg_tid = (string)($res['TID'] ?? '');
|
|
||||||
$attempt->return_code = (string)($res['RETURNCODE'] ?? '');
|
|
||||||
$attempt->return_msg = (string)($res['RETURNMSG'] ?? '');
|
|
||||||
$attempt->returned_at = now();
|
|
||||||
$attempt->return_payload = ['ret' => $retMap, 'bill' => $res];
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => '결제가 완료되었습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
// 필요하면 아래를 서비스에서 order_complete에 사용
|
|
||||||
'meta' => [
|
|
||||||
'oid' => $oid,
|
|
||||||
'tid' => (string)($res['TID'] ?? ''),
|
|
||||||
'amount' => (int)($res['AMOUNT'] ?? $amount),
|
|
||||||
'code' => (string)($res['RETURNCODE'] ?? ''),
|
|
||||||
'msg' => (string)($res['RETURNMSG'] ?? ''),
|
|
||||||
'ret_data' => $res,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$msg = (string)($res['RETURNMSG'] ?? '카드 결제 실패');
|
|
||||||
return $this->fail((string)($res['RETURNCODE'] ?? 'C993'), $msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function postCpcgi(string $url, string $outerCpid, array $reqData, string $hexKey, string $hexIv): array
|
|
||||||
{
|
|
||||||
// CI3 CallCredit/CallVAccount 규칙 그대로:
|
|
||||||
// data2str -> AES -> base64 -> urlencode -> CPID=...&DATA=...
|
|
||||||
$plain = $this->aes->buildQuery($reqData);
|
|
||||||
$enc = $this->aes->encrypt($plain, $hexKey, $hexIv);
|
|
||||||
$payload = 'CPID=' . $outerCpid . '&DATA=' . urlencode($enc);
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int)config('danal.authtel.connect_timeout', 5));
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, (int)config('danal.authtel.timeout', 30));
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-type:application/x-www-form-urlencoded; charset=euc-kr"]);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
||||||
|
|
||||||
$resStr = curl_exec($ch);
|
|
||||||
|
|
||||||
if (($errno = curl_errno($ch)) !== 0) {
|
|
||||||
$err = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
return [
|
|
||||||
'RETURNCODE' => 'E_NETWORK',
|
|
||||||
'RETURNMSG' => "NETWORK({$errno}:{$err})",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$resMap = $this->aes->parseQuery((string)$resStr);
|
|
||||||
if (isset($resMap['DATA'])) {
|
|
||||||
$dec = $this->aes->decrypt((string)$resMap['DATA'], $hexKey, $hexIv);
|
|
||||||
$resMap = $this->aes->parseQuery($dec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $resMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function toEucKr(string $s): string
|
|
||||||
{
|
|
||||||
if ($s === '') return '';
|
|
||||||
$out = @iconv('UTF-8', 'EUC-KR//IGNORE', $s);
|
|
||||||
return $out === false ? $s : $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function eucKrArrayToUtf8(array $arr): array
|
|
||||||
{
|
|
||||||
foreach ($arr as $k => $v) {
|
|
||||||
if (!is_string($v) || $v === '') continue;
|
|
||||||
$u = @iconv('EUC-KR', 'UTF-8//IGNORE', $v);
|
|
||||||
if ($u !== false) $arr[$k] = $u;
|
|
||||||
}
|
|
||||||
return $arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fail(string $code, string $msg): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'status' => 'fail',
|
|
||||||
'message' => $msg ?: '결제에 실패했습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
'meta' => [
|
|
||||||
'code' => $code,
|
|
||||||
'msg' => $msg,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,277 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Services\Payments\Danal\Gateways;
|
|
||||||
|
|
||||||
use App\Models\GcPaymentAttempt;
|
|
||||||
use App\Support\DanalAes256CbcHex;
|
|
||||||
|
|
||||||
final class DanalVactGateway
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly DanalAes256CbcHex $aes,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_vact() -> AUTH
|
|
||||||
*/
|
|
||||||
public function auth(GcPaymentAttempt $attempt, string $token, array $order, string $vactKind = 'a'): array
|
|
||||||
{
|
|
||||||
// 현재 config는 vact만 있으니 a(무통장) 기준 구현
|
|
||||||
// v(인증계좌/WIRETRANSFER)는 config 확장 시 추가
|
|
||||||
if ($vactKind !== 'a') {
|
|
||||||
throw new \RuntimeException("Vact kind not supported yet: {$vactKind}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$cfg = config('danal.vact');
|
|
||||||
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
||||||
throw new \RuntimeException("Danal vact config missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
$oid = (string)($order['oid'] ?? '');
|
|
||||||
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? 0);
|
|
||||||
$itemName = (string)($order['itemName'] ?? $order['pin_name'] ?? '상품권');
|
|
||||||
|
|
||||||
$isMobile = (bool)($order['is_mobile'] ?? false);
|
|
||||||
$userAgent = $isMobile ? 'MW' : 'PC';
|
|
||||||
|
|
||||||
$holder = (string)($cfg['holder'] ?? '핀포유');
|
|
||||||
|
|
||||||
$returnUrl = route('web.payments.danal.vact.return', ['t' => $token], true);
|
|
||||||
$cancelUrl = route('web.payments.danal.cancel', ['o' => $oid, 't' => $token], true);
|
|
||||||
$notiUrl = route('web.payments.danal.vact.noti', [], true);
|
|
||||||
|
|
||||||
// BYPASSVALUE는 ; 구분 (token 포함)
|
|
||||||
$memNo = (string)($order['mem_no'] ?? $attempt->mem_no ?? '');
|
|
||||||
$bypass = "PAYMETHOD=VACT;MEMNO={$memNo};AMOUNT={$amount};TOKEN={$token};";
|
|
||||||
|
|
||||||
$req = [
|
|
||||||
// CI3처럼 CPID를 내부 DATA에도 넣는다
|
|
||||||
'CPID' => (string)$cfg['cpid'],
|
|
||||||
'SUBCPID' => '',
|
|
||||||
'ACCOUNTHOLDER' => $this->toEucKr($holder),
|
|
||||||
'EXPIREDATE' => now()->addHours(23)->format('Ymd'),
|
|
||||||
|
|
||||||
'ORDERID' => $oid,
|
|
||||||
'ITEMNAME' => $this->toEucKr($itemName),
|
|
||||||
'AMOUNT' => (string)$amount,
|
|
||||||
'ISCASHRECEIPTUI' => 'N',
|
|
||||||
|
|
||||||
'USERNAME' => $this->toEucKr((string)($order['user_name'] ?? '')),
|
|
||||||
'USERID' => (string)($order['user_id'] ?? $order['mem_no'] ?? ''),
|
|
||||||
'USEREMAIL' => (string)($order['user_email'] ?? ''),
|
|
||||||
'USERPHONE' => '',
|
|
||||||
'USERAGENT' => $userAgent,
|
|
||||||
|
|
||||||
'TXTYPE' => 'AUTH',
|
|
||||||
'SERVICETYPE' => 'DANALVACCOUNT',
|
|
||||||
|
|
||||||
'RETURNURL' => $returnUrl,
|
|
||||||
'NOTIURL' => $notiUrl,
|
|
||||||
'CANCELURL' => $cancelUrl,
|
|
||||||
'BYPASSVALUE' => $bypass,
|
|
||||||
];
|
|
||||||
|
|
||||||
$attempt->vact_kind = 'a';
|
|
||||||
$attempt->request_payload = $req;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
$res = $this->postCpcgi(
|
|
||||||
(string)$cfg['url'],
|
|
||||||
(string)$cfg['cpid'],
|
|
||||||
$req,
|
|
||||||
(string)$cfg['key'],
|
|
||||||
(string)$cfg['iv']
|
|
||||||
);
|
|
||||||
|
|
||||||
$attempt->response_payload = $res;
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
if (($res['RETURNCODE'] ?? '') !== '0000') {
|
|
||||||
$msg = (string)($res['RETURNMSG'] ?? '모듈 호출 실패');
|
|
||||||
throw new \RuntimeException("Danal vact AUTH failed: {$res['RETURNCODE']} {$msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'actionUrl' => (string)$res['STARTURL'],
|
|
||||||
'params' => [
|
|
||||||
'STARTPARAMS' => (string)$res['STARTPARAMS'],
|
|
||||||
],
|
|
||||||
'acceptCharset' => 'EUC-KR',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* CI3: payment_danal_vact_return() -> RETURN decrypt -> ISSUEVACCOUNT
|
|
||||||
*/
|
|
||||||
public function issueVaccountOnReturn(GcPaymentAttempt $attempt, string $token, array $order, array $post): array
|
|
||||||
{
|
|
||||||
$cfg = config('danal.vact');
|
|
||||||
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
||||||
throw new \RuntimeException("Danal vact config missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
$returnParams = (string)($post['RETURNPARAMS'] ?? '');
|
|
||||||
if ($returnParams === '') {
|
|
||||||
return $this->fail('C990', 'RETURNPARAMS 누락');
|
|
||||||
}
|
|
||||||
|
|
||||||
$retStr = $this->aes->decrypt($returnParams, (string)$cfg['key'], (string)$cfg['iv']);
|
|
||||||
$retMap = $this->aes->parseQuery($retStr);
|
|
||||||
|
|
||||||
$oid = (string)($retMap['ORDERID'] ?? '');
|
|
||||||
if ($oid === '' || $oid !== (string)$attempt->oid) {
|
|
||||||
return $this->fail('C991', '주문번호 불일치');
|
|
||||||
}
|
|
||||||
|
|
||||||
$retCode = (string)($retMap['RETURNCODE'] ?? '');
|
|
||||||
$retMsg = (string)($retMap['RETURNMSG'] ?? '');
|
|
||||||
|
|
||||||
if ($retCode !== '0000') {
|
|
||||||
return $this->fail($retCode ?: 'C992', $retMsg ?: '가상계좌 인증 실패');
|
|
||||||
}
|
|
||||||
|
|
||||||
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? $attempt->amount ?? 0);
|
|
||||||
|
|
||||||
$issueReq = [
|
|
||||||
'CPID' => (string)$cfg['cpid'],
|
|
||||||
'TID' => (string)($retMap['TID'] ?? ''),
|
|
||||||
'AMOUNT' => (string)$amount,
|
|
||||||
'TXTYPE' => 'ISSUEVACCOUNT',
|
|
||||||
'SERVICETYPE' => 'DANALVACCOUNT',
|
|
||||||
];
|
|
||||||
|
|
||||||
$res = $this->postCpcgi(
|
|
||||||
(string)$cfg['url'],
|
|
||||||
(string)$cfg['cpid'],
|
|
||||||
$issueReq,
|
|
||||||
(string)$cfg['key'],
|
|
||||||
(string)$cfg['iv']
|
|
||||||
);
|
|
||||||
|
|
||||||
$res = $this->eucKrArrayToUtf8($res);
|
|
||||||
|
|
||||||
if (($res['RETURNCODE'] ?? '') === '0000') {
|
|
||||||
$attempt->status = 'issued';
|
|
||||||
$attempt->pg_tid = (string)($res['TID'] ?? $retMap['TID'] ?? '');
|
|
||||||
$attempt->returned_at = now();
|
|
||||||
$attempt->return_payload = ['ret' => $retMap, 'issue' => $res];
|
|
||||||
$attempt->save();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => '가상계좌가 발급되었습니다. 마이페이지에서 확인해주세요.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
'meta' => [
|
|
||||||
'oid' => $oid,
|
|
||||||
'tid' => (string)($res['TID'] ?? ''),
|
|
||||||
'amount' => $amount,
|
|
||||||
'code' => (string)($res['RETURNCODE'] ?? ''),
|
|
||||||
'msg' => (string)($res['RETURNMSG'] ?? ''),
|
|
||||||
'ret_data' => $res,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->fail((string)($res['RETURNCODE'] ?? 'C993'), (string)($res['RETURNMSG'] ?? '가상계좌 발급 실패'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* NOTI 수신 처리 (서버->서버)
|
|
||||||
* - post에 DATA가 오면 복호화해서 맵으로 변환
|
|
||||||
* - 성공이면 onPaid 콜백 실행
|
|
||||||
*/
|
|
||||||
public function handleNoti(array $post, callable $onPaid): void
|
|
||||||
{
|
|
||||||
$cfg = config('danal.vact');
|
|
||||||
if (!$cfg || empty($cfg['key']) || empty($cfg['iv'])) {
|
|
||||||
throw new \RuntimeException("Danal vact config missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
$map = $post;
|
|
||||||
|
|
||||||
// NOTI가 CPID/DATA 형태로 오면 DATA 복호화
|
|
||||||
if (isset($post['DATA']) && is_string($post['DATA']) && $post['DATA'] !== '') {
|
|
||||||
$dec = $this->aes->decrypt((string)$post['DATA'], (string)$cfg['key'], (string)$cfg['iv']);
|
|
||||||
$map = $this->aes->parseQuery($dec);
|
|
||||||
}
|
|
||||||
|
|
||||||
$oid = (string)($map['ORDERID'] ?? '');
|
|
||||||
$tid = (string)($map['TID'] ?? '');
|
|
||||||
$amount = (int)($map['AMOUNT'] ?? 0);
|
|
||||||
|
|
||||||
// 성공 판단은 최소한으로: RETURNCODE=0000 + 필수키 존재
|
|
||||||
$code = (string)($map['RETURNCODE'] ?? '');
|
|
||||||
if ($oid === '' || $tid === '' || $amount <= 0) return;
|
|
||||||
if ($code !== '' && $code !== '0000') return;
|
|
||||||
|
|
||||||
$onPaid($oid, $tid, $amount, $map);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function postCpcgi(string $url, string $outerCpid, array $reqData, string $hexKey, string $hexIv): array
|
|
||||||
{
|
|
||||||
$plain = $this->aes->buildQuery($reqData);
|
|
||||||
$enc = $this->aes->encrypt($plain, $hexKey, $hexIv);
|
|
||||||
$payload = 'CPID=' . $outerCpid . '&DATA=' . urlencode($enc);
|
|
||||||
|
|
||||||
$ch = curl_init();
|
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
|
||||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
|
||||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int)config('danal.authtel.connect_timeout', 5));
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, (int)config('danal.authtel.timeout', 30));
|
|
||||||
curl_setopt($ch, CURLOPT_URL, $url);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-type:application/x-www-form-urlencoded; charset=euc-kr"]);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
||||||
|
|
||||||
$resStr = curl_exec($ch);
|
|
||||||
|
|
||||||
if (($errno = curl_errno($ch)) !== 0) {
|
|
||||||
$err = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
return [
|
|
||||||
'RETURNCODE' => 'E_NETWORK',
|
|
||||||
'RETURNMSG' => "NETWORK({$errno}:{$err})",
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
$resMap = $this->aes->parseQuery((string)$resStr);
|
|
||||||
if (isset($resMap['DATA'])) {
|
|
||||||
$dec = $this->aes->decrypt((string)$resMap['DATA'], $hexKey, $hexIv);
|
|
||||||
$resMap = $this->aes->parseQuery($dec);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $resMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function toEucKr(string $s): string
|
|
||||||
{
|
|
||||||
if ($s === '') return '';
|
|
||||||
$out = @iconv('UTF-8', 'EUC-KR//IGNORE', $s);
|
|
||||||
return $out === false ? $s : $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function eucKrArrayToUtf8(array $arr): array
|
|
||||||
{
|
|
||||||
foreach ($arr as $k => $v) {
|
|
||||||
if (!is_string($v) || $v === '') continue;
|
|
||||||
$u = @iconv('EUC-KR', 'UTF-8//IGNORE', $v);
|
|
||||||
if ($u !== false) $arr[$k] = $u;
|
|
||||||
}
|
|
||||||
return $arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function fail(string $code, string $msg): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'status' => 'fail',
|
|
||||||
'message' => $msg ?: '결제에 실패했습니다.',
|
|
||||||
'redirectUrl' => url('/mypage/usage'),
|
|
||||||
'meta' => [
|
|
||||||
'code' => $code,
|
|
||||||
'msg' => $msg,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,10 +4,13 @@ namespace App\Services\Payments;
|
|||||||
|
|
||||||
use App\Models\Payments\GcPinOrder;
|
use App\Models\Payments\GcPinOrder;
|
||||||
use App\Repositories\Payments\GcPinOrderRepository;
|
use App\Repositories\Payments\GcPinOrderRepository;
|
||||||
|
use App\Repositories\Member\MemInfoRepository;
|
||||||
use App\Repositories\Payments\GcPaymentAttemptRepository;
|
use App\Repositories\Payments\GcPaymentAttemptRepository;
|
||||||
use App\Providers\Danal\Gateways\CardGateway;
|
use App\Providers\Danal\Gateways\CardGateway;
|
||||||
use App\Providers\Danal\Gateways\VactGateway;
|
use App\Providers\Danal\Gateways\VactGateway;
|
||||||
use App\Providers\Danal\Gateways\PhoneGateway;
|
use App\Providers\Danal\Gateways\PhoneGateway;
|
||||||
|
use App\Providers\Danal\Gateways\WireGateway;
|
||||||
|
use App\Support\LegacyCrypto\CiSeedCrypto;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
final class PaymentService
|
final class PaymentService
|
||||||
@ -15,8 +18,13 @@ final class PaymentService
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly GcPinOrderRepository $orders,
|
private readonly GcPinOrderRepository $orders,
|
||||||
private readonly GcPaymentAttemptRepository $attempts,
|
private readonly GcPaymentAttemptRepository $attempts,
|
||||||
|
|
||||||
|
private readonly MemInfoRepository $members,
|
||||||
|
private readonly CiSeedCrypto $seed,
|
||||||
|
|
||||||
private readonly CardGateway $card,
|
private readonly CardGateway $card,
|
||||||
private readonly VactGateway $vact,
|
private readonly VactGateway $vact,
|
||||||
|
private readonly WireGateway $wire,
|
||||||
private readonly PhoneGateway $phone,
|
private readonly PhoneGateway $phone,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -70,7 +78,35 @@ final class PaymentService
|
|||||||
$this->attempts->markRedirected($attempt, $out['req'], $out['res']);
|
$this->attempts->markRedirected($attempt, $out['req'], $out['res']);
|
||||||
return $this->ensureStart($out, $meta);
|
return $this->ensureStart($out, $meta);
|
||||||
|
|
||||||
} elseif ($method === 'phone') {
|
} elseif ($method === 'wire') {
|
||||||
|
$mem = $this->members->findForWirePay($memNo);
|
||||||
|
if (!$mem) return $this->fail('404', '회원정보를 찾을 수 없습니다.');
|
||||||
|
|
||||||
|
$memberName = trim((string)($mem->name ?? '')) ?: '고객';
|
||||||
|
$memberEmail = trim((string)($mem->email ?? ''));
|
||||||
|
if ($memberEmail === '') return $this->fail('EMAIL_REQUIRED', '계좌이체 결제를 위해 이메일 정보가 필요합니다.');
|
||||||
|
|
||||||
|
$memberPhoneDigits = '';
|
||||||
|
$rawPhoneEnc = (string)($mem->cell_phone ?? '');
|
||||||
|
if ($rawPhoneEnc !== '') {
|
||||||
|
try {
|
||||||
|
$plainPhone = (string)$this->seed->decrypt($rawPhoneEnc);
|
||||||
|
$memberPhoneDigits = preg_replace('/\D+/', '', $plainPhone) ?: '';
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$memberPhoneDigits = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = $this->wire->auth($order, $token, $isMobile, [
|
||||||
|
'user_name' => $memberName,
|
||||||
|
'user_id' => (string)$memNo,
|
||||||
|
'user_email' => $memberEmail,
|
||||||
|
'user_phone' => $memberPhoneDigits,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->attempts->markRedirected($attempt, $out['req'], $out['res']);
|
||||||
|
return $this->ensureStart($out, $meta);
|
||||||
|
}elseif ($method === 'phone') {
|
||||||
$mode = $opt['phone_mode'] ?? 'prod'; // prod|dev
|
$mode = $opt['phone_mode'] ?? 'prod'; // prod|dev
|
||||||
|
|
||||||
$out = $this->phone->ready($order, $token, $mode, $isMobile, [
|
$out = $this->phone->ready($order, $token, $mode, $isMobile, [
|
||||||
@ -290,6 +326,121 @@ final class PaymentService
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function handleWireReturn(string $attemptToken, array $post): array
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($attemptToken, $post) {
|
||||||
|
|
||||||
|
$attempt = $this->attempts->findByTokenForUpdate('wire', $attemptToken);
|
||||||
|
if (!$attempt) return $this->fail('404', '시도를 찾을 수 없습니다.');
|
||||||
|
|
||||||
|
$order = $this->orders->findByOidForUpdate((string)$attempt->oid);
|
||||||
|
if (!$order) return $this->fail('404', '주문을 찾을 수 없습니다.');
|
||||||
|
|
||||||
|
$returnParams = (string)($post['RETURNPARAMS'] ?? '');
|
||||||
|
if ($returnParams === '') {
|
||||||
|
$this->attempts->markFailed($attempt, 'RET_PARAM', 'RETURNPARAMS 누락', ['post'=>$post]);
|
||||||
|
$this->orders->markFailed($order, 'RET_PARAM', 'RETURNPARAMS 누락', ['post'=>$post]);
|
||||||
|
return $this->fail('RET_PARAM', 'RETURNPARAMS 누락');
|
||||||
|
}
|
||||||
|
|
||||||
|
$retMap = $this->wire->decryptReturn($returnParams);
|
||||||
|
|
||||||
|
if (($retMap['ORDERID'] ?? '') !== $order->oid) {
|
||||||
|
$this->attempts->markFailed($attempt, 'OID_MISMATCH', '주문번호 불일치', ['ret'=>$retMap]);
|
||||||
|
$this->orders->markFailed($order, 'OID_MISMATCH', '주문번호 불일치', ['ret'=>$retMap]);
|
||||||
|
return $this->fail('OID_MISMATCH', '주문번호 불일치');
|
||||||
|
}
|
||||||
|
|
||||||
|
$retCode = (string)($retMap['RETURNCODE'] ?? '');
|
||||||
|
$retMsg = (string)($retMap['RETURNMSG'] ?? '');
|
||||||
|
|
||||||
|
if ($retCode !== '0000') {
|
||||||
|
$this->attempts->markFailed($attempt, $retCode ?: 'AUTH_FAIL', $retMsg ?: '계좌 인증 실패', ['ret'=>$retMap]);
|
||||||
|
$this->orders->markFailed($order, $retCode ?: 'AUTH_FAIL', $retMsg ?: '계좌 인증 실패', ['ret'=>$retMap]);
|
||||||
|
return $this->fail($retCode ?: 'AUTH_FAIL', $retMsg ?: '계좌 인증 실패');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tid = (string)($retMap['TID'] ?? '');
|
||||||
|
if ($tid === '') {
|
||||||
|
$this->attempts->markFailed($attempt, 'NO_TID', 'TID 누락', ['ret'=>$retMap]);
|
||||||
|
$this->orders->markFailed($order, 'NO_TID', 'TID 누락', ['ret'=>$retMap]);
|
||||||
|
return $this->fail('NO_TID', 'TID 누락');
|
||||||
|
}
|
||||||
|
|
||||||
|
$bill = $this->wire->bill($order, $tid);
|
||||||
|
$billCode = (string)($bill['res']['RETURNCODE'] ?? '');
|
||||||
|
$billMsg = (string)($bill['res']['RETURNMSG'] ?? '');
|
||||||
|
|
||||||
|
if ($billCode === '0000') {
|
||||||
|
$payload = ['wire_return'=>$retMap, 'wire_bill'=>$bill['res']];
|
||||||
|
$this->attempts->markReturned($attempt, $payload, (string)($bill['res']['TID'] ?? $tid), $billCode, $billMsg, 'paid');
|
||||||
|
$this->orders->markPaid($order, (string)($bill['res']['TID'] ?? $tid), $billCode, $billMsg, $payload);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => true,
|
||||||
|
'status' => 'paid',
|
||||||
|
'meta' => ['attempt_id' => (int)$attempt->id, 'tid' => $tid],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attempts->markFailed($attempt, $billCode ?: 'BILL_FAIL', $billMsg ?: '출금요청 실패', ['ret'=>$retMap,'bill'=>$bill]);
|
||||||
|
$this->orders->markFailed($order, $billCode ?: 'BILL_FAIL', $billMsg ?: '출금요청 실패', ['ret'=>$retMap,'bill'=>$bill]);
|
||||||
|
return $this->fail($billCode ?: 'BILL_FAIL', $billMsg ?: '출금요청 실패');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleWireNoti(array $post): void
|
||||||
|
{
|
||||||
|
DB::transaction(function () use ($post) {
|
||||||
|
|
||||||
|
// DATA가 오면 복호화해서 map으로
|
||||||
|
$map = $post;
|
||||||
|
if (!empty($post['DATA']) && is_string($post['DATA'])) {
|
||||||
|
try {
|
||||||
|
$map = $this->wire->decryptNotiData((string)$post['DATA']);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$oid = (string)($map['ORDERID'] ?? '');
|
||||||
|
$tid = (string)($map['TID'] ?? '');
|
||||||
|
$amount = (int)($map['AMOUNT'] ?? 0);
|
||||||
|
$code = (string)($map['RETURNCODE'] ?? '');
|
||||||
|
|
||||||
|
if ($oid === '' || $tid === '' || $amount <= 0) return;
|
||||||
|
if ($code !== '' && $code !== '0000') return;
|
||||||
|
|
||||||
|
$order = $this->orders->findByOidForUpdate($oid);
|
||||||
|
if (!$order) return;
|
||||||
|
|
||||||
|
if ((int)$order->pay_money !== $amount) return;
|
||||||
|
|
||||||
|
$payload = ['wire_noti' => $map];
|
||||||
|
$this->orders->markPaid($order, $tid, '0000', 'NOTI', $payload);
|
||||||
|
|
||||||
|
$token = $this->extractAttemptTokenFromBypass((string)($map['BYPASSVALUE'] ?? ''));
|
||||||
|
if ($token !== '') {
|
||||||
|
$attempt = $this->attempts->findByTokenForUpdate('wire', $token);
|
||||||
|
if ($attempt) $this->attempts->markNotiPaid($attempt, $map, $tid, $amount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$row = \App\Models\Payments\GcPaymentAttempt::query()
|
||||||
|
->where('provider','danal')->where('oid',$oid)->where('pay_method','wire')
|
||||||
|
->lockForUpdate()->first();
|
||||||
|
|
||||||
|
if ($row) $this->attempts->markNotiPaid($row, $map, $tid, $amount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractAttemptTokenFromBypass(string $bypass): string
|
||||||
|
{
|
||||||
|
if ($bypass === '') return '';
|
||||||
|
if (preg_match('/(?:^|[;\s])AT=([0-9a-f]{64})/i', $bypass, $m)) return (string)$m[1];
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
/** 휴대폰 RETURN(TargetURL) -> NCONFIRM/NBILL -> paid */
|
/** 휴대폰 RETURN(TargetURL) -> NCONFIRM/NBILL -> paid */
|
||||||
public function handlePhoneReturn(array $post): array
|
public function handlePhoneReturn(array $post): array
|
||||||
{
|
{
|
||||||
|
|||||||
@ -34,6 +34,8 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
'pay/danal/vact/noti', #다날가상계좌
|
'pay/danal/vact/noti', #다날가상계좌
|
||||||
'pay/danal/phone/return', #다날휴대폰 결제
|
'pay/danal/phone/return', #다날휴대폰 결제
|
||||||
'pay/danal/phone/cancel', #다날휴대폰 결제취소
|
'pay/danal/phone/cancel', #다날휴대폰 결제취소
|
||||||
|
'pay/danal/wire/return', #다날계좌이체
|
||||||
|
'pay/danal/wire/noti', #다날계좌이체
|
||||||
'pay/danal/cancel',
|
'pay/danal/cancel',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -59,6 +59,19 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
//계좌이체
|
||||||
|
'wiretransfer' => [
|
||||||
|
'tx_url' => 'https://tx-wiretransfer.danalpay.com/bank/', // 메뉴얼:contentReference[oaicite:5]{index=5}
|
||||||
|
'cpid' => env('DANAL_AUTHVACT_CPID'),
|
||||||
|
'key' => env('DANAL_AUTHVACT_KEY'),
|
||||||
|
'iv' => env('DANAL_AUTHVACT_IV'),
|
||||||
|
|
||||||
|
// 선택(결제창 UI)
|
||||||
|
'ci_url' => env('DANAL_WIRE_CIURL', ''), // 89x34 권장:contentReference[oaicite:6]{index=6}
|
||||||
|
'color' => env('DANAL_WIRE_COLOR', '00'),
|
||||||
|
'is_noti'=> env('DANAL_WIRE_ISNOTI', 'Y'),
|
||||||
|
],
|
||||||
|
|
||||||
// 공통 네트워크 옵션
|
// 공통 네트워크 옵션
|
||||||
'http' => [
|
'http' => [
|
||||||
'connect_timeout' => (int)env('DANAL_CONNECT_TIMEOUT', 5),
|
'connect_timeout' => (int)env('DANAL_CONNECT_TIMEOUT', 5),
|
||||||
|
|||||||
@ -1,30 +1,185 @@
|
|||||||
@php
|
|
||||||
$pageTitle = '이용내역';
|
|
||||||
$pageDesc = '구매 → 결제 → 발송 상태를 확인할 수 있어요.';
|
|
||||||
|
|
||||||
$breadcrumbs = [
|
|
||||||
['label' => '홈', 'url' => url('/')],
|
|
||||||
['label' => '마이페이지', 'url' => url('/mypage/info')],
|
|
||||||
['label' => '이용내역', 'url' => url()->current()],
|
|
||||||
];
|
|
||||||
|
|
||||||
$mypageActive = 'usage';
|
|
||||||
@endphp
|
|
||||||
|
|
||||||
@extends('web.layouts.subpage')
|
@extends('web.layouts.subpage')
|
||||||
|
|
||||||
@section('title', '이용내역 | PIN FOR YOU')
|
@php
|
||||||
@section('meta_description', 'PIN FOR YOU 마이페이지 이용내역 입니다. 구매/결제/발송 내역을 확인하세요.')
|
// 탭 활성화용
|
||||||
@section('canonical', url('/mypage/usage'))
|
$mypageActive = $mypageActive ?? 'usage';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@section('title', $pageTitle ?? '구매내역')
|
||||||
|
|
||||||
@section('subcontent')
|
@section('subcontent')
|
||||||
<div class="mypage-usage-page">
|
<div class="mypage-usage">
|
||||||
@include('web.partials.content-head', [
|
|
||||||
'title' => '이용내역',
|
|
||||||
'desc' => '기간별로 내역을 확인할 수 있습니다.'
|
|
||||||
])
|
|
||||||
|
|
||||||
{{-- TODO: 내용 추후 구현 --}}
|
@if(($mode ?? 'empty') === 'empty')
|
||||||
@include('web.partials.mypage-quick-actions')
|
<div class="notice-box">
|
||||||
|
<p>결제 완료 후 이 페이지에서 핀 확인/발급/매입을 진행할 수 있습니다.</p>
|
||||||
|
<p class="muted">결제 완료 페이지에서 자동으로 이동되며, attempt_id가 없으면 상세 정보를 표시할 수 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
|
||||||
|
{{-- 상태 스텝 --}}
|
||||||
|
<div class="usage-steps">
|
||||||
|
@foreach(($steps ?? []) as $st)
|
||||||
|
@php
|
||||||
|
$active = false;
|
||||||
|
$done = false;
|
||||||
|
|
||||||
|
// 표시 규칙(간단): 현재 stepKey 기준으로 active 표시
|
||||||
|
$active = (($stepKey ?? '') === $st['key']);
|
||||||
|
|
||||||
|
// 완료표시(선택): deposit_wait 이전/이후 같은 세밀한 건 다음 단계에서 고도화 가능
|
||||||
|
// 여기서는 "active 이전"을 done으로 찍지 않고, 필요하면 확장
|
||||||
|
@endphp
|
||||||
|
<div class="step {{ $active ? 'is-active' : '' }}">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<div class="label">{{ $st['label'] }}</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 결제/주문 요약 --}}
|
||||||
|
<div class="usage-card">
|
||||||
|
<h3 class="card-title">주문/결제 정보</h3>
|
||||||
|
|
||||||
|
<div class="grid">
|
||||||
|
<div><span class="k">주문번호</span> <span class="v">{{ $order['oid'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">결제수단</span> <span class="v">{{ $order['pay_method'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">결제상태</span> <span class="v">{{ $order['stat_pay'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">PG TID</span> <span class="v">{{ $order['pg_tid'] ?? ($attempt['pg_tid'] ?? '-') }}</span></div>
|
||||||
|
<div><span class="k">결제일시</span> <span class="v">{{ $order['created_at'] ?? '-' }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="amounts">
|
||||||
|
<div><span class="k">상품금액</span> <span class="v">{{ number_format($order['amounts']['subtotal'] ?? 0) }}원</span></div>
|
||||||
|
<div><span class="k">고객수수료</span> <span class="v">{{ number_format($order['amounts']['fee'] ?? 0) }}원</span></div>
|
||||||
|
<div class="sum"><span class="k">결제금액</span> <span class="v">{{ number_format($order['amounts']['pay_money'] ?? 0) }}원</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(!empty($vactInfo))
|
||||||
|
<div class="vact-box">
|
||||||
|
<h4>가상계좌 안내</h4>
|
||||||
|
<div class="grid">
|
||||||
|
<div><span class="k">은행</span> <span class="v">{{ $vactInfo['bank'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">계좌번호</span> <span class="v">{{ $vactInfo['account'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">예금주</span> <span class="v">{{ $vactInfo['holder'] ?? '-' }}</span></div>
|
||||||
|
<div><span class="k">입금금액</span> <span class="v">{{ is_numeric($vactInfo['amount'] ?? null) ? number_format((int)$vactInfo['amount']).'원' : ($vactInfo['amount'] ?? '-') }}</span></div>
|
||||||
|
<div><span class="k">만료</span> <span class="v">{{ $vactInfo['expire_at'] ?? '-' }}</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 아이템 --}}
|
||||||
|
<div class="usage-card">
|
||||||
|
<h3 class="card-title">구매 상품</h3>
|
||||||
|
<div class="items">
|
||||||
|
@foreach(($items ?? []) as $it)
|
||||||
|
<div class="item-row">
|
||||||
|
<div class="name">{{ $it['name'] }}</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="pill">수량 {{ $it['qty'] }}</span>
|
||||||
|
<span class="pill">{{ number_format($it['unit'] ?? 0) }}원</span>
|
||||||
|
<span class="pill pill--strong">합계 {{ number_format($it['total'] ?? 0) }}원</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 핀 발급 상태/액션 (결제완료에서만 활성) --}}
|
||||||
|
<div class="usage-card">
|
||||||
|
<h3 class="card-title">핀 발급/전달</h3>
|
||||||
|
|
||||||
|
<div class="pin-status">
|
||||||
|
<div><span class="k">필요 핀 수량</span> <span class="v">{{ (int)($requiredQty ?? 0) }}개</span></div>
|
||||||
|
<div><span class="k">할당된 핀</span> <span class="v">{{ (int)($assignedPinsCount ?? 0) }}개</span></div>
|
||||||
|
@if(!empty($pinsSummary))
|
||||||
|
<div class="summary">
|
||||||
|
@foreach($pinsSummary as $st => $cnt)
|
||||||
|
<span class="pill">{{ $st }}: {{ $cnt }}</span>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@php
|
||||||
|
$isPaid = (($order['stat_pay'] ?? '') === 'p') || (($attempt['status'] ?? '') === 'paid');
|
||||||
|
$isVactWait = (($order['stat_pay'] ?? '') === 'w') || (($attempt['status'] ?? '') === 'issued');
|
||||||
|
$isFailed = in_array(($order['stat_pay'] ?? ''), ['c','f'], true) || in_array(($attempt['status'] ?? ''), ['cancelled','failed'], true);
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
@if($isVactWait)
|
||||||
|
<div class="notice-box">
|
||||||
|
<p>가상계좌 입금 확인 후 핀 발급을 진행할 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
@elseif($isFailed)
|
||||||
|
<div class="notice-box">
|
||||||
|
<p>결제가 취소/실패 상태입니다. 결제 정보를 확인해 주세요.</p>
|
||||||
|
</div>
|
||||||
|
@elseif($isPaid)
|
||||||
|
<div class="action-grid">
|
||||||
|
<button type="button" class="act-btn" onclick="uiUsage.todo('SMS 발송은 다음 단계에서 연결할게요.')">핀번호 SMS 발송</button>
|
||||||
|
<button type="button" class="act-btn act-btn--primary" onclick="uiUsage.todo('웹 핀 노출/재고할당 로직을 다음 단계에서 연결할게요.')">핀번호 바로 확인</button>
|
||||||
|
<button type="button" class="act-btn" onclick="uiUsage.todo('매입 테이블 설계 후 바로 붙일게요.')">핀번호 되팔기(매입)</button>
|
||||||
|
</div>
|
||||||
|
<p class="muted small">※ 사고 방지를 위해 핀 노출/발송/매입은 모두 서버 검증 후 처리됩니다.</p>
|
||||||
|
@else
|
||||||
|
<div class="notice-box">
|
||||||
|
<p>결제 진행 중이거나 확인 중입니다. 잠시 후 다시 확인해 주세요.</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- 디버그(접기) --}}
|
||||||
|
<details class="usage-debug">
|
||||||
|
<summary>결제 상세(디버그)</summary>
|
||||||
|
<pre>{{ json_encode(['attempt'=>$attempt,'order'=>$order], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES) }}</pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mypage-usage { display:flex; flex-direction:column; gap:14px; }
|
||||||
|
.usage-card { border:1px solid rgba(0,0,0,.08); border-radius:14px; padding:16px; background:#fff; }
|
||||||
|
.card-title { font-size:16px; margin:0 0 10px 0; }
|
||||||
|
.grid { display:grid; grid-template-columns:1fr 1fr; gap:8px 14px; }
|
||||||
|
.k { color:rgba(0,0,0,.55); margin-right:8px; }
|
||||||
|
.v { font-weight:600; }
|
||||||
|
.amounts { margin-top:12px; display:grid; grid-template-columns:1fr 1fr; gap:8px 14px; }
|
||||||
|
.amounts .sum { grid-column: span 2; padding-top:8px; border-top:1px dashed rgba(0,0,0,.12); }
|
||||||
|
.items { display:flex; flex-direction:column; gap:10px; }
|
||||||
|
.item-row { padding:12px; border:1px solid rgba(0,0,0,.08); border-radius:12px; }
|
||||||
|
.item-row .name { font-weight:700; margin-bottom:6px; }
|
||||||
|
.pill { display:inline-flex; padding:4px 8px; border-radius:999px; font-size:12px; border:1px solid rgba(0,0,0,.12); margin-right:6px; }
|
||||||
|
.pill--strong { font-weight:700; }
|
||||||
|
.muted { color:rgba(0,0,0,.55); }
|
||||||
|
.small { font-size:12px; }
|
||||||
|
.notice-box { padding:12px; border-radius:12px; background:rgba(0,0,0,.04); }
|
||||||
|
.vact-box { margin-top:14px; padding:12px; border-radius:12px; background:rgba(0,0,0,.03); border:1px solid rgba(0,0,0,.08); }
|
||||||
|
.pin-status { display:flex; gap:14px; flex-wrap:wrap; align-items:center; margin-bottom:12px; }
|
||||||
|
.pin-status .summary { width:100%; margin-top:6px; }
|
||||||
|
.action-grid { display:grid; grid-template-columns:1fr; gap:8px; }
|
||||||
|
@media (min-width: 720px){ .action-grid { grid-template-columns:1fr 1fr 1fr; } }
|
||||||
|
.act-btn { padding:12px 10px; border-radius:12px; border:1px solid rgba(0,0,0,.14); background:#fff; cursor:pointer; }
|
||||||
|
.act-btn--primary { font-weight:800; }
|
||||||
|
.usage-steps { display:grid; grid-template-columns: repeat(6, 1fr); gap:8px; }
|
||||||
|
.usage-steps .step { padding:10px 8px; border-radius:12px; border:1px solid rgba(0,0,0,.08); text-align:center; font-size:12px; background:#fff; }
|
||||||
|
.usage-steps .step.is-active { border-color:rgba(0,0,0,.25); box-shadow:0 1px 0 rgba(0,0,0,.06); font-weight:800; }
|
||||||
|
.usage-steps .dot { width:8px; height:8px; border-radius:50%; margin:0 auto 6px auto; background:rgba(0,0,0,.25); }
|
||||||
|
.usage-debug { border:1px dashed rgba(0,0,0,.18); border-radius:12px; padding:10px 12px; background:#fff; }
|
||||||
|
.usage-debug pre { margin:10px 0 0 0; max-height:240px; overflow:auto; font-size:12px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.uiUsage = {
|
||||||
|
todo(msg){
|
||||||
|
if (typeof showMsg === 'function') {
|
||||||
|
showMsg(msg, { type:'alert', title:'안내' });
|
||||||
|
} else {
|
||||||
|
alert(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use App\Http\Controllers\Web\Payment\DanalController;
|
|||||||
use App\Http\Controllers\Web\Payment\DanalDemoController;
|
use App\Http\Controllers\Web\Payment\DanalDemoController;
|
||||||
use App\Http\Controllers\Web\Order\OrderCheckoutController;
|
use App\Http\Controllers\Web\Order\OrderCheckoutController;
|
||||||
use App\Http\Controllers\Web\Mypage\MypageQnaController;
|
use App\Http\Controllers\Web\Mypage\MypageQnaController;
|
||||||
|
use App\Http\Controllers\Web\Mypage\UsageController;
|
||||||
|
|
||||||
Route::view('/', 'web.home')->name('web.home');
|
Route::view('/', 'web.home')->name('web.home');
|
||||||
|
|
||||||
@ -64,7 +64,8 @@ Route::prefix('mypage')->name('web.mypage.')
|
|||||||
Route::post('info/marketing/update', [InfoGateController::class, 'marketingUpdate'])->name('info.marketing.update');
|
Route::post('info/marketing/update', [InfoGateController::class, 'marketingUpdate'])->name('info.marketing.update');
|
||||||
Route::post('info/withdraw', [InfoGateController::class, 'withdraw'])->name('info.withdraw');
|
Route::post('info/withdraw', [InfoGateController::class, 'withdraw'])->name('info.withdraw');
|
||||||
|
|
||||||
Route::view('usage', 'web.mypage.usage.index')->name('usage.index');
|
Route::get('usage', [UsageController::class, 'index'])->name('usage.index');
|
||||||
|
|
||||||
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
Route::view('exchange', 'web.mypage.exchange.index')->name('exchange.index');
|
||||||
Route::get('qna', [MypageQnaController::class, 'index'])->name('qna.index');
|
Route::get('qna', [MypageQnaController::class, 'index'])->name('qna.index');
|
||||||
Route::get('qna/{seq}', [MypageQnaController::class, 'show'])->whereNumber('seq')->name('qna.show');
|
Route::get('qna/{seq}', [MypageQnaController::class, 'show'])->whereNumber('seq')->name('qna.show');
|
||||||
@ -79,6 +80,8 @@ Route::prefix('pay/danal')->group(function () {
|
|||||||
|
|
||||||
Route::match(['GET','POST'], '/phone/return', [DanalController::class, 'phoneReturn'])->name('web.payments.danal.phone.return');
|
Route::match(['GET','POST'], '/phone/return', [DanalController::class, 'phoneReturn'])->name('web.payments.danal.phone.return');
|
||||||
Route::match(['GET','POST'], '/phone/cancel', [DanalController::class, 'phoneCancel'])->name('web.payments.danal.phone.cancel');
|
Route::match(['GET','POST'], '/phone/cancel', [DanalController::class, 'phoneCancel'])->name('web.payments.danal.phone.cancel');
|
||||||
|
Route::post('wire/return', [DanalController::class, 'wireReturn'])->name('wire.return');
|
||||||
|
Route::post('wire/noti', [DanalController::class, 'wireNoti'])->name('wire.noti');
|
||||||
|
|
||||||
Route::match(['GET','POST'], '/cancel', [DanalController::class, 'cancel'])->name('web.payments.danal.cancel');
|
Route::match(['GET','POST'], '/cancel', [DanalController::class, 'cancel'])->name('web.payments.danal.cancel');
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user