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', ''), ]; } }