fix(ecommerce,board,tosspayments,pay_kginicis,pay_nhnkcp,pay_nicepayments,core,basic): 공개 이슈 잔여 결함 5건 해소 — 상점 주소 SSoT·게시판 계약/안내/상한/색인
공개 이슈(gnuboard/g7) 17건 전수 대조에서 "진짜 누락"으로 판정된 5건을 해소한다.
— 서버가 만들어 내보내는 상점 주소가 각 지점에서 `/shop` 리터럴이나
`"/{$routePath}/..."` 조립으로 흩어져 있었다. 기본값이 아닌 상점에서는 그 주소가
존재하지 않는 화면을 가리키는데, 서버는 문자열을 만들어 내려보냈을 뿐이라 예외도
404 로그도 남지 않는다 — 링크만 조용히 죽는다. `ShopPathResolver` 를 단일 해석
지점으로 두고 주문 완료 이동·비회원 조회 안내·알림 메일·통합검색·상품문의·SEO
캐시·사이트맵 전 지점을 경유시켰다. 같은 값에 종속된 PG 4종 리다이렉트는
`{shopBase}` 자리표시자로, 편집기 상태 scope 는 선택 세그먼트 토큰 `/*?`
(engine-v1.58.0)로 해소했다. 기설치본은 업그레이드 스텝이 백필한다.
M-02 — 답변 깊이 상한이 config 는 1~10 인데 컬럼 comment 만 1~5 로 남아,
comment 에서 자동 생성되는 계약 문서 4곳이 실제보다 좁은 상한을 공개하고 있었다.
L-01 — 관리자 게시글 폼에 첨부 안내가 아예 없었고, 대신 사장된 고정 문구
("최대 5개 파일, 각 10MB")가 남아 있었다. 게시판마다 다른 실제 설정값을 렌더한다.
— 목록 1페이지가 공지 전량을 상한 없이 읽던 것에 안전상한을 두고(구조 무변경,
limit 부가만), 조회수 정렬 목록 술어를 덮는 복합 색인을 신설했다.
브라우저 실측 검수에서 발견한 사전 결함 1건(사용자 게시판 목록이 URL 정렬
파라미터를 데이터소스로 전달하지 않던 문제)도 함께 고쳤다. 폴백을 빈 문자열로 둬
URL 에 정렬이 없을 때 게시판 설정을 덮어쓰지 않게 했다.
This commit is contained in:
1 parent
3715be5e51
commit
f9c4c025cb
138 files changed
+3907
-361
No files matched your search
+73
-71
@@ -6,6 +6,7 @@ namespace Plugins\Sirsoft\PayKginicis\Controllers;
|
||||
|
||||
use App\Services\PluginSettingsService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Modules\Sirsoft\Ecommerce\Enums\PaymentStatusEnum;
|
||||
use Modules\Sirsoft\Ecommerce\Exceptions\PaymentAmountMismatchException;
|
||||
@@ -19,6 +20,7 @@ use Plugins\Sirsoft\PayKginicis\Concerns\SerializesPaymentCallbacks;
|
||||
use Plugins\Sirsoft\PayKginicis\Concerns\ValidatesCbtOrderContext;
|
||||
use Plugins\Sirsoft\PayKginicis\Http\Requests\MobileCallbackRequest;
|
||||
use Plugins\Sirsoft\PayKginicis\Services\KgInicisApiService;
|
||||
use Plugins\Sirsoft\PayKginicis\Support\ShopRedirectUrl;
|
||||
|
||||
/**
|
||||
* KG 이니시스 모바일 결제 콜백 컨트롤러
|
||||
@@ -90,23 +92,23 @@ class MobileCallbackController
|
||||
* handle
|
||||
*
|
||||
* @param MobileCallbackRequest $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @return RedirectResponse
|
||||
*/
|
||||
public function handle(MobileCallbackRequest $request): \Illuminate\Http\RedirectResponse
|
||||
public function handle(MobileCallbackRequest $request): RedirectResponse
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$selectedEasyPayMethod = $this->resolveSelectedEasyPayMethod($request);
|
||||
|
||||
$pStatus = $validated['P_STATUS'];
|
||||
$pStatus = $validated['P_STATUS'];
|
||||
// KG 이니시스 모바일 메뉴얼(STEP 2) 표준 응답에는 P_OID 가 없으므로 P_NEXT_URL 쿼리스트링의
|
||||
// orderId 를 fallback 으로 사용한다. 일부 PG 환경에서 P_OID 를 echo 하면 우선 채택.
|
||||
$moid = $validated['P_OID'] ?? $request->query('orderId') ?? null;
|
||||
$pAmt = $validated['P_AMT'] ?? null;
|
||||
$moid = $validated['P_OID'] ?? $request->query('orderId') ?? null;
|
||||
$pAmt = $validated['P_AMT'] ?? null;
|
||||
|
||||
Log::info('KG Inicis mobile: callback received', [
|
||||
'order_id' => $moid,
|
||||
'P_STATUS' => $pStatus,
|
||||
'idc_name' => $validated['idc_name'] ?? null,
|
||||
'order_id' => $moid,
|
||||
'P_STATUS' => $pStatus,
|
||||
'idc_name' => $validated['idc_name'] ?? null,
|
||||
'P_REQ_URL' => $validated['P_REQ_URL'] ?? null,
|
||||
'input_keys' => array_keys($request->all()),
|
||||
'query_keys' => array_keys($request->query()),
|
||||
@@ -127,9 +129,9 @@ class MobileCallbackController
|
||||
$isUserCancel = $this->isUserCancelMessage($pMesg);
|
||||
|
||||
Log::info('KG Inicis mobile: auth not success', [
|
||||
'P_OID' => $moid,
|
||||
'P_STATUS' => $pStatus,
|
||||
'P_RMESG1' => $pMesg,
|
||||
'P_OID' => $moid,
|
||||
'P_STATUS' => $pStatus,
|
||||
'P_RMESG1' => $pMesg,
|
||||
'is_user_cancel' => $isUserCancel,
|
||||
]);
|
||||
|
||||
@@ -139,21 +141,21 @@ class MobileCallbackController
|
||||
}
|
||||
|
||||
return redirect($this->resolveFailUrl([
|
||||
'error' => $pStatus,
|
||||
'error' => $pStatus,
|
||||
'message' => $pMesg,
|
||||
'orderId' => $moid,
|
||||
]));
|
||||
}
|
||||
|
||||
$pTid = $validated['P_TID'] ?? null;
|
||||
$pTid = $validated['P_TID'] ?? null;
|
||||
$idcName = $validated['idc_name'] ?? null;
|
||||
$reqUrl = $validated['P_REQ_URL'] ?? null;
|
||||
$reqUrl = $validated['P_REQ_URL'] ?? null;
|
||||
|
||||
if (! $pTid || ! $idcName || ! $reqUrl) {
|
||||
Log::error('KG Inicis mobile: missing required fields', [
|
||||
'P_OID' => $moid,
|
||||
'P_OID' => $moid,
|
||||
'idc_name' => $idcName,
|
||||
'P_TID' => $pTid,
|
||||
'P_TID' => $pTid,
|
||||
'P_REQ_URL' => $reqUrl,
|
||||
]);
|
||||
|
||||
@@ -163,7 +165,7 @@ class MobileCallbackController
|
||||
// P_REQ_URL 화이트리스트 검증 (모바일 IDC URL, SSRF 방어)
|
||||
if (! $this->apiService->isValidIdcAuthUrl($idcName, $reqUrl)) {
|
||||
Log::error('KG Inicis mobile: P_REQ_URL not in whitelist (possible SSRF attempt)', [
|
||||
'P_OID' => $moid,
|
||||
'P_OID' => $moid,
|
||||
'idc_name' => $idcName,
|
||||
'received' => $reqUrl,
|
||||
]);
|
||||
@@ -196,7 +198,7 @@ class MobileCallbackController
|
||||
|
||||
if ($resultStatus !== '00') {
|
||||
Log::warning('KG Inicis mobile: server approve failed', [
|
||||
'P_OID' => $moid,
|
||||
'P_OID' => $moid,
|
||||
'P_STATUS' => $resultStatus,
|
||||
'P_RMESG1' => $result['P_RMESG1'] ?? '',
|
||||
]);
|
||||
@@ -204,15 +206,15 @@ class MobileCallbackController
|
||||
$this->orderService->failPayment($order, $resultStatus, $result['P_RMESG1'] ?? '');
|
||||
|
||||
return redirect($this->resolveFailUrl([
|
||||
'error' => $resultStatus,
|
||||
'error' => $resultStatus,
|
||||
'message' => $result['P_RMESG1'] ?? '',
|
||||
'orderId' => $moid,
|
||||
]));
|
||||
}
|
||||
|
||||
$tid = $result['P_TID'] ?? $pTid;
|
||||
$tid = $result['P_TID'] ?? $pTid;
|
||||
$totPrice = (int) ($result['P_AMT'] ?? $pAmt ?? 0);
|
||||
$payType = (string) ($result['P_TYPE'] ?? '');
|
||||
$payType = (string) ($result['P_TYPE'] ?? '');
|
||||
|
||||
// 가상계좌: completePayment 없이 발급 정보만 저장 (입금 통보 시점에 completePayment)
|
||||
if (strcasecmp($payType, 'VBank') === 0) {
|
||||
@@ -258,24 +260,24 @@ class MobileCallbackController
|
||||
]);
|
||||
|
||||
$this->orderService->completePayment($order, [
|
||||
'transaction_id' => $tid,
|
||||
'card_approval_number' => $result['P_APPL_NUM'] ?? null,
|
||||
'card_number_masked' => $result['P_CARD_NUM'] ?? null,
|
||||
'card_name' => $result['P_CARD_ISSUER_NAME'] ?? null,
|
||||
'transaction_id' => $tid,
|
||||
'card_approval_number' => $result['P_APPL_NUM'] ?? null,
|
||||
'card_number_masked' => $result['P_CARD_NUM'] ?? null,
|
||||
'card_name' => $result['P_CARD_ISSUER_NAME'] ?? null,
|
||||
'card_installment_months' => (int) ($result['P_CARD_QUOTA'] ?? 0),
|
||||
'is_interest_free' => false,
|
||||
'embedded_pg_provider' => $embeddedPgProvider,
|
||||
'receipt_url' => null,
|
||||
'payment_meta' => array_merge([
|
||||
'result_code' => $resultStatus,
|
||||
'pay_method' => $payType ?: null,
|
||||
'auth_date' => $result['P_AUTH_DT'] ?? null,
|
||||
'mid' => $this->apiService->getMid(),
|
||||
'is_test_mode' => $this->apiService->isTestMode(),
|
||||
'is_interest_free' => false,
|
||||
'embedded_pg_provider' => $embeddedPgProvider,
|
||||
'receipt_url' => null,
|
||||
'payment_meta' => array_merge([
|
||||
'result_code' => $resultStatus,
|
||||
'pay_method' => $payType ?: null,
|
||||
'auth_date' => $result['P_AUTH_DT'] ?? null,
|
||||
'mid' => $this->apiService->getMid(),
|
||||
'is_test_mode' => $this->apiService->isTestMode(),
|
||||
'pg_response_sanitized' => true,
|
||||
'pg_raw_response' => $this->sanitizePgResponse($result, self::MOBILE_APPROVE_RESPONSE_KEYS),
|
||||
], $this->buildEasyPayPaymentMeta($selectedEasyPayMethod)),
|
||||
'payment_device' => 'mobile',
|
||||
'payment_device' => 'mobile',
|
||||
], $totPrice);
|
||||
|
||||
$order->payment()->update(['pg_provider' => 'kginicis']);
|
||||
@@ -285,9 +287,9 @@ class MobileCallbackController
|
||||
|
||||
} catch (PaymentAmountMismatchException $e) {
|
||||
Log::error('KG Inicis mobile: amount mismatch', [
|
||||
'P_OID' => $moid,
|
||||
'P_OID' => $moid,
|
||||
'expected' => $e->getExpectedAmount(),
|
||||
'actual' => $e->getActualAmount(),
|
||||
'actual' => $e->getActualAmount(),
|
||||
]);
|
||||
|
||||
$this->autoCancelIfApproved($approvedTid, $moid, $approvedTotPrice, 'amount_mismatch');
|
||||
@@ -316,7 +318,7 @@ class MobileCallbackController
|
||||
$this->autoCancelIfApproved($approvedTid, $moid, $approvedTotPrice, 'approve_failed');
|
||||
|
||||
return redirect($this->resolveFailUrl([
|
||||
'error' => 'approve_failed',
|
||||
'error' => 'approve_failed',
|
||||
'message' => $e->getMessage(),
|
||||
'orderId' => $moid,
|
||||
]));
|
||||
@@ -350,24 +352,24 @@ class MobileCallbackController
|
||||
$tid,
|
||||
'Card',
|
||||
null,
|
||||
'auto-cancel: ' . $reason,
|
||||
'auto-cancel: '.$reason,
|
||||
$totPrice > 0 ? $totPrice : null,
|
||||
);
|
||||
|
||||
Log::warning('KG Inicis mobile: auto-cancel after post-approve failure', [
|
||||
'tid' => $tid,
|
||||
'P_OID' => $moid,
|
||||
'amount' => $totPrice,
|
||||
'reason' => $reason,
|
||||
'tid' => $tid,
|
||||
'P_OID' => $moid,
|
||||
'amount' => $totPrice,
|
||||
'reason' => $reason,
|
||||
'pg_result' => $result,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('KG Inicis mobile: auto-cancel FAILED — manual reconciliation required', [
|
||||
'tid' => $tid,
|
||||
'P_OID' => $moid,
|
||||
'tid' => $tid,
|
||||
'P_OID' => $moid,
|
||||
'amount' => $totPrice,
|
||||
'reason' => $reason,
|
||||
'error' => $e->getMessage(),
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -387,38 +389,38 @@ class MobileCallbackController
|
||||
|
||||
if ($vactDate && strlen((string) $vactDate) === 8) {
|
||||
try {
|
||||
$vbankDueAt = Carbon::createFromFormat('YmdHis', $vactDate . $vactTime);
|
||||
$vbankDueAt = Carbon::createFromFormat('YmdHis', $vactDate.$vactTime);
|
||||
} catch (\Exception) {
|
||||
$vbankDueAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
$order->payment()->update(array_filter([
|
||||
'pg_provider' => 'kginicis',
|
||||
'payment_status' => PaymentStatusEnum::WAITING_DEPOSIT,
|
||||
'transaction_id' => $tid ?: null,
|
||||
'vbank_code' => $result['P_VACT_BANK_CODE'] ?? $result['P_FN_CD1'] ?? null,
|
||||
'vbank_name' => $result['P_VACT_BANK_NAME'] ?? $result['P_FN_NM'] ?? null,
|
||||
'vbank_number' => $result['P_VACT_NUM'] ?? null,
|
||||
'vbank_holder' => $result['P_VACT_NAME'] ?? $result['P_RVACTNM'] ?? null,
|
||||
'vbank_due_at' => $vbankDueAt,
|
||||
'pg_provider' => 'kginicis',
|
||||
'payment_status' => PaymentStatusEnum::WAITING_DEPOSIT,
|
||||
'transaction_id' => $tid ?: null,
|
||||
'vbank_code' => $result['P_VACT_BANK_CODE'] ?? $result['P_FN_CD1'] ?? null,
|
||||
'vbank_name' => $result['P_VACT_BANK_NAME'] ?? $result['P_FN_NM'] ?? null,
|
||||
'vbank_number' => $result['P_VACT_NUM'] ?? null,
|
||||
'vbank_holder' => $result['P_VACT_NAME'] ?? $result['P_RVACTNM'] ?? null,
|
||||
'vbank_due_at' => $vbankDueAt,
|
||||
'vbank_issued_at' => now(),
|
||||
'payment_device' => 'mobile',
|
||||
'payment_meta' => [
|
||||
'result_code' => $result['P_STATUS'] ?? '00',
|
||||
'pay_method' => 'VBank',
|
||||
'auth_date' => $result['P_AUTH_DT'] ?? null,
|
||||
'mid' => $this->apiService->getMid(),
|
||||
'is_test_mode' => $this->apiService->isTestMode(),
|
||||
'payment_device' => 'mobile',
|
||||
'payment_meta' => [
|
||||
'result_code' => $result['P_STATUS'] ?? '00',
|
||||
'pay_method' => 'VBank',
|
||||
'auth_date' => $result['P_AUTH_DT'] ?? null,
|
||||
'mid' => $this->apiService->getMid(),
|
||||
'is_test_mode' => $this->apiService->isTestMode(),
|
||||
'pg_response_sanitized' => true,
|
||||
'pg_raw_response' => $this->sanitizePgResponse($result, self::MOBILE_VBANK_ISSUE_RESPONSE_KEYS),
|
||||
],
|
||||
], fn ($v) => $v !== null));
|
||||
|
||||
Log::info('KG Inicis mobile: vbank account issued', [
|
||||
'P_OID' => $order->order_number,
|
||||
'P_TID' => $tid,
|
||||
'vbank_name' => $result['P_VACT_BANK_NAME'] ?? $result['P_FN_NM'] ?? null,
|
||||
'P_OID' => $order->order_number,
|
||||
'P_TID' => $tid,
|
||||
'vbank_name' => $result['P_VACT_BANK_NAME'] ?? $result['P_FN_NM'] ?? null,
|
||||
'vbank_number' => $result['P_VACT_NUM'] ?? null,
|
||||
'vbank_due_at' => $vbankDueAt?->toDateTimeString(),
|
||||
]);
|
||||
@@ -427,15 +429,15 @@ class MobileCallbackController
|
||||
private function resolveSuccessUrl(string $orderId): string
|
||||
{
|
||||
$settings = $this->pluginSettingsService->get(self::PLUGIN_IDENTIFIER) ?? [];
|
||||
$urlTemplate = $settings['redirect_success_url'] ?? '/shop/orders/{orderId}/complete';
|
||||
$urlTemplate = $settings['redirect_success_url'] ?? ShopRedirectUrl::DEFAULT_SUCCESS_URL;
|
||||
|
||||
return $this->absolutize(str_replace('{orderId}', $orderId, $urlTemplate));
|
||||
return $this->absolutize(ShopRedirectUrl::resolve($urlTemplate, ['{orderId}' => $orderId]));
|
||||
}
|
||||
|
||||
private function resolveFailUrl(array $queryParams = []): string
|
||||
{
|
||||
$settings = $this->pluginSettingsService->get(self::PLUGIN_IDENTIFIER) ?? [];
|
||||
$baseUrl = $this->absolutize($settings['redirect_fail_url'] ?? '/shop/checkout');
|
||||
$baseUrl = $this->absolutize(ShopRedirectUrl::resolve($settings['redirect_fail_url'] ?? ShopRedirectUrl::DEFAULT_FAIL_URL));
|
||||
|
||||
if (empty($queryParams)) {
|
||||
return $baseUrl;
|
||||
@@ -444,7 +446,7 @@ class MobileCallbackController
|
||||
$query = http_build_query(array_filter($queryParams, fn ($v) => $v !== null && $v !== ''));
|
||||
$separator = str_contains($baseUrl, '?') ? '&' : '?';
|
||||
|
||||
return $baseUrl . $separator . $query;
|
||||
return $baseUrl.$separator.$query;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -483,8 +485,8 @@ class MobileCallbackController
|
||||
}
|
||||
|
||||
$base = rtrim((string) config('app.url'), '/');
|
||||
$path = $url === '' ? '/' : ($url[0] === '/' ? $url : '/' . $url);
|
||||
$path = $url === '' ? '/' : ($url[0] === '/' ? $url : '/'.$url);
|
||||
|
||||
return $base . $path;
|
||||
return $base.$path;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user