giftcon_dev/app/Services/Danal/DanalAuthtelService.php
2026-01-26 12:59:59 +09:00

223 lines
7.1 KiB
PHP

<?php
namespace App\Services\Danal;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* Danal PASS(Authtel) 준비 서비스
*
* - prepare(): 다날 Start 페이지로 POST할 hidden fields를 구성
* - callTrans / makeFormFields / getRandom 등은 별도 Client로 분리하는 게 이상적이지만,
* 지금은 "작동 우선"으로 이 파일 하나로 정리할 수 있게 구성합니다.
*/
final class DanalAuthtelService
{
private string $serviceUrl;
private string $cpid;
private string $cppwd;
private string $charset;
private int $connectTimeout;
private int $timeout;
private bool $debug;
public function __construct()
{
$cfg = (array) config('danal.authtel', []);
$this->serviceUrl = (string) Arr::get($cfg, 'service_url', '');
$this->cpid = (string) Arr::get($cfg, 'cpid', '');
$this->cppwd = (string) Arr::get($cfg, 'cppwd', '');
$this->charset = (string) Arr::get($cfg, 'charset', 'UTF-8');
$this->connectTimeout = (int) Arr::get($cfg, 'connect_timeout', 5);
$this->timeout = (int) Arr::get($cfg, 'timeout', 30);
$this->debug = (bool) Arr::get($cfg, 'debug', false);
}
public function prepare(array $opt): array
{
// 필수 설정 체크
if ($this->cpid === '' || $this->cppwd === '' || $this->serviceUrl === '') {
return ['ok' => false, 'message' => 'DANAL 설정(CPID/CPPWD/SERVICE_URL)이 없습니다.'];
}
$targetUrl = (string) ($opt['targetUrl'] ?? '');
$backUrl = (string) ($opt['backUrl'] ?? '');
if ($targetUrl === '') return ['ok' => false, 'message' => 'targetUrl 누락'];
if ($backUrl === '') return ['ok' => false, 'message' => 'backUrl 누락'];
// 기본 타이틀
$cpTitle = (string) ($opt['cpTitle'] ?? request()->getHost());
// TransR
$trans = [
'TXTYPE' => 'ITEMSEND',
'SERVICE' => 'UAS',
'AUTHTYPE' => '36',
'CPID' => $this->cpid,
'CPPWD' => $this->cppwd,
'TARGETURL' => $targetUrl,
'CPTITLE' => $cpTitle,
];
// 다날 서버 통신
$res = $this->callTrans($trans);
if (($res['RETURNCODE'] ?? '') !== '0000') {
return [
'ok' => false,
'message' => $this->formatReturnMsg($res),
];
}
// CI3의 ByPassValue
$byPass = [
// CI의 GetBgColor(0~10 => "00"~"10") 느낌 유지
'BgColor' => $this->getBgColor($this->getRandom(0, 10)),
'BackURL' => $backUrl,
'IsCharSet' => $this->charset,
'ByBuffer' => 'This value bypass to CPCGI Page',
'ByAnyName' => 'AnyValue',
];
// Start.php로 보낼 hidden fields 구성
// - 여기서는 절대 e()/htmlspecialchars 같은 escape를 하지 마세요.
// - 프론트에서 input.value로 넣으면 브라우저가 처리합니다.
$fields = array_merge(
$this->makeFormFields($res, ['RETURNCODE', 'RETURNMSG']),
$this->makeFormFields($byPass)
);
return [
'ok' => true,
'txid' => $res['TID'] ?? $res['TXID'] ?? null,
'fields' => $fields,
];
}
/**
* 다날 서버 통신
* @return array<string,string>
*/
public function callTrans(array $reqData): array
{
// x-www-form-urlencoded
$body = http_build_query($reqData, '', '&', PHP_QUERY_RFC3986);
$ch = curl_init();
curl_setopt($ch, CURLOPT_POST, 1);
// SSL 검증은 끄지 마세요 (운영 보안)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
curl_setopt($ch, CURLOPT_URL, $this->serviceUrl);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/x-www-form-urlencoded; charset=' . $this->charset,
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$resStr = curl_exec($ch);
if (($errno = curl_errno($ch)) !== 0) {
$err = curl_error($ch) ?: 'curl error';
curl_close($ch);
return [
'RETURNCODE' => '-1',
'RETURNMSG' => 'NETWORK ERROR(' . $errno . ':' . $err . ')',
];
}
curl_close($ch);
// 다날 응답 파싱
return $this->str2data((string) $resStr);
}
/** @return array<string,string> */
private function str2data(string $str): array
{
$data = [];
foreach (explode('&', $str) as $line) {
$kv = explode('=', $line, 2);
if (count($kv) === 2) {
$data[$kv[0]] = $kv[1];
}
}
return $data;
}
/**
* hidden fields 만들기 (escape 하지 않음)
* @param array<string, mixed> $arr
* @param array<int, string> $excludeKeys
* @return array<string, string>
*/
private function makeFormFields(array $arr, array $excludeKeys = [], string $prefix = ''): array
{
$out = [];
$preLen = strlen(trim($prefix));
foreach ($arr as $key => $value) {
$key = (string) $key;
if ($key === '' || trim($key) === '') continue;
if (in_array($key, $excludeKeys, true)) continue;
if ($preLen > 0 && substr($key, 0, $preLen) !== $prefix) continue;
// 다날은 value에 urlencoded가 들어갈 수 있음
// 그대로 보내는 게 원칙이지만, 필요 시 여기서 정책적으로 urldecode/encode 조절 가능
$out[$key] = is_scalar($value) || $value === null ? (string) $value : json_encode($value, JSON_UNESCAPED_UNICODE);
}
return $out;
}
private function getBgColor($bgColor): string
{
$color = 0;
$i = (int) $bgColor;
if ($i > 0 && $i < 11) $color = $i;
return sprintf('%02d', $color);
}
private function getRandom(int $min, int $max): int
{
return random_int($min, $max);
}
/** @param array<string,string> $res */
private function formatReturnMsg(array $res): string
{
$msg = (string)($res['RETURNMSG'] ?? 'DANAL ERROR');
$code = (string)($res['RETURNCODE'] ?? 'NO_CODE');
return $msg . ' (' . $code . ')';
}
public function confirm(string $tid, int $confirmOption = 0, int $idenOption = 1): array
{
$req = [
'TXTYPE' => 'CONFIRM',
'TID' => $tid,
'CONFIRMOPTION' => $confirmOption,
'IDENOPTION' => $idenOption,
];
// CI 주석: CONFIRMOPTION=1이면 CPID/ORDERID 필수
if ($confirmOption === 1) {
$req['CPID'] = $this->cpid;
}
return $this->callTrans($req);
}
}