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