products->getActiveSkuWithProduct($skuId); if (!$sku) return $this->fail('SKU_NOT_FOUND', '권종 정보를 찾을 수 없습니다.'); // allowed_payments 체크 $allowed = json_decode($sku->allowed_payments ?? '[]', true); $allowed = is_array($allowed) ? array_map('intval', $allowed) : []; if (!in_array((int)$payId, $allowed, true)) { return $this->fail('PAY_NOT_ALLOWED', '선택한 결제수단은 사용 불가합니다.'); } // qty 정책 $minQty = (int)($sku->min_buy_qty ?? 1); $maxQty = (int)($sku->max_buy_qty ?? 0); $maxAmount = (int)($sku->max_buy_amount ?? 0); if ($qty < $minQty) $qty = $minQty; if ($maxQty > 0 && $qty > $maxQty) { return $this->fail('QTY_MAX', '최대 구매 수량을 초과했습니다.'); } $unit = (int)($sku->final_price ?? 0); if ($unit <= 0) return $this->fail('PRICE_INVALID', '판매가 정보가 올바르지 않습니다.'); $subtotal = $unit * $qty; if ($maxAmount > 0 && $subtotal > $maxAmount) { return $this->fail('AMOUNT_MAX', '1회 최대 결제 금액을 초과했습니다.'); } $pm = $this->payMethods->findActiveById($payId); if (!$pm) return $this->fail('PAY_NOT_FOUND', '결제수단 정보를 찾을 수 없습니다.'); // 수수료(정수연산, 2자리 소수율) $customerFee = $this->calcFee($subtotal, (string)$pm->customer_fee_rate); $payMoney = $subtotal + $customerFee; $pgFee = $this->calcFee($payMoney, (string)$pm->pg_fee_rate); // 주문 생성 $oid = 'GC' . now()->format('YmdHis') . Str::upper(Str::random(6)); $order = GcPinOrder::create([ 'oid' => $oid, 'mem_no' => $memNo, 'order_type' => 'self', 'stat_pay' => 'ready', 'stat_tax' => 'taxfree', 'subtotal_amount' => $subtotal, 'fee_amount' => $customerFee, 'pg_fee_amount' => $pgFee, 'discount_amount' => 0, 'pay_money' => $payMoney, 'provider' => 'danal', 'ordered_at' => now(), 'pay_data' => [ 'payment_method' => [ 'id' => (int)$pm->id, 'code' => (string)$pm->code, 'name' => (string)$pm->name, 'display_name' => (string)$pm->display_name, 'customer_fee_rate' => (string)$pm->customer_fee_rate, 'pg_fee_rate' => (string)$pm->pg_fee_rate, ], 'calc' => [ 'unit_price' => $unit, 'qty' => $qty, 'subtotal' => $subtotal, 'customer_fee' => $customerFee, 'pay_money' => $payMoney, 'pg_fee' => $pgFee, ], ], ]); GcPinOrderItem::create([ 'order_id' => $order->id, 'tbl_pointer' => null, 'pin_seq' => null, 'item_name' => (string)$sku->name, 'item_code' => (string)($sku->sku_code ?? $sku->id), 'qty' => $qty, 'unit_price' => (int)($sku->face_value ?? $unit), 'unit_pay_price' => $unit, 'line_subtotal' => $subtotal, 'line_fee' => $customerFee, 'line_total' => $payMoney, 'meta' => [ 'product_id' => (int)$sku->product_id, 'product_name' => (string)$sku->product_name, 'sku_id' => (int)$sku->id, ], ]); // pay_id -> 결제모듈 옵션 매핑 [$method, $opt] = $this->mapPayMethod((string)$pm->code, $request); if ($method === 'phone') { $m = $this->members->findForPhonePay($memNo) ?? $this->members->findByMemNo($memNo); if (!$m) { return [ 'ok' => false, 'message' => '회원 정보를 찾을 수 없습니다.', 'redirect' => url('/'), ]; } $phoneEnc = (string)($m->cell_phone ?? ''); // 복호화 가능한 암호문 컬럼 $corpCode = (string)($m->cell_corp ?? ''); // 01~06 if ($phoneEnc === '') { return [ 'ok' => false, 'message' => '휴대폰 번호가 등록되어 있지 않습니다. 휴대폰 번호 등록 후 이용해 주세요.', 'redirect' => url('/mypage/info'), ]; } $seed = app(CiSeedCrypto::class); $phonePlain = (string)$seed->decrypt($phoneEnc); $digits = preg_replace('/\D+/', '', $phonePlain); if ($digits === '' || strlen($digits) < 10 || strlen($digits) > 11) { return [ 'ok' => false, 'message' => '회원 휴대폰 번호 정보가 올바르지 않습니다. 고객센터에 문의해 주세요.', 'redirect' => url('/'), ]; } $map = [ '01' => 'SKT', '02' => 'KTF', '03' => 'LGT', '04' => 'MVNO','05' => 'MVNO','06' => 'MVNO', ]; $corpCode = str_pad(preg_replace('/\D+/', '', $corpCode), 2, '0', STR_PAD_LEFT); $carrier = $map[$corpCode] ?? ''; if ($carrier === '') { return [ 'ok' => false, 'message' => '회원 통신사 정보가 올바르지 않습니다. 고객센터에 문의해 주세요.', 'redirect' => url('/'), ]; } $opt['member_phone_digits'] = $digits; $opt['member_carrier'] = $carrier; } // 결제 시작 (PG redirect 정보 생성) $out = $this->payment->start($order->oid, $memNo, $method, $opt); if (($out['type'] ?? '') === 'redirect') { return [ 'ok' => true, 'view' => 'autosubmit', 'action' => $out['start']['actionUrl'], 'fields' => $out['start']['params'], 'acceptCharset' => $out['start']['acceptCharset'] ?? 'EUC-KR', 'meta' => $out['meta'] ?? [], 'token' => $out['meta']['token'] ?? '', 'oid' => $out['meta']['oid'] ?? $order->oid, 'method' => $out['meta']['method'] ?? $method, ]; } // 실패 return [ 'ok' => false, 'code' => $out['meta']['code'] ?? 'PG_FAIL', 'message' => $out['message'] ?? '결제 시작 실패', 'redirect' => url("/product/detail/" . (int)$sku->product_id), ]; }); } private function mapPayMethod(string $code, Request $request): array { $ua = strtolower((string)$request->userAgent()); $isMobile = str_contains($ua, 'mobile') || str_contains($ua, 'android') || str_contains($ua, 'iphone'); // 휴대폰 dev/prod 분리: 운영=prod, 그 외=dev 기본 $phoneMode = (string)config('danal.phone.default_mode', 'prod'); // 개발환경에서만 ?phone_mode=dev|prod 로 override 허용(원하면) if (!app()->environment('production')) { $q = (string)$request->query('phone_mode', ''); if ($q === 'prod' || $q === 'dev') $phoneMode = $q; } return match ($code) { 'MOBILE' => ['phone', ['phone_mode'=>$phoneMode, 'is_mobile'=>$isMobile]], 'CREDIT_CARD' => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]], 'CREDIT_CARD_REFUND' => ['card', ['card_kind'=>'exchange', 'is_mobile'=>$isMobile]], 'VACT' => ['vact', ['is_mobile'=>$isMobile]], 'WIRETRANSFER' => ['wire', ['is_mobile'=>$isMobile]], default => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]], }; } // rateStr: "5.00" (%), 2자리 소수 -> 정수 계산 private function calcFee(int $amount, string $rateStr): int { $rate = (float)$rateStr; // 5.00 $bp = (int) round($rate * 100); // 500 (basis points of percent) // amount * bp / 10000 return (int) ceil(($amount * $bp) / 10000); } private function fail(string $code, string $message): array { return ['ok'=>false, 'code'=>$code, 'message'=>$message]; } }