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