'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { createClient } from '@/lib/supabase/client'; import { PRODUCTS } from '@/lib/products'; import { getActiveChannels, type PaymentChannel } from '@/lib/payment-channels'; import PortOne from '@portone/browser-sdk/v2'; interface PaymentButtonProps { productId: string; className?: string; style?: React.CSSProperties; children: React.ReactNode; returnUrl?: string; } export default function PaymentButton({ productId, className, style, children, returnUrl }: PaymentButtonProps) { const [loading, setLoading] = useState(false); const [showMethodPicker, setShowMethodPicker] = useState(false); const router = useRouter(); const supabase = createClient(); const product = PRODUCTS[productId]; const channels = getActiveChannels(); const processPayment = async (channel: PaymentChannel) => { setShowMethodPicker(false); setLoading(true); try { // 1. 로그인 확인 const { data: { user } } = await supabase.auth.getUser(); if (!user) { router.push('/login?next=' + encodeURIComponent(window.location.pathname)); return; } // 2. 프로필 없으면 생성 await supabase.from('profiles').upsert({ id: user.id, email: user.email }, { onConflict: 'id' }); // 3. Supabase에 order 생성 const paymentId = `order_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`; const { error: orderError } = await supabase .from('orders') .insert({ id: paymentId, user_id: user.id, product_id: productId, amount: product.price, status: 'pending', metadata: { product_name: product.name, pay_channel: channel.id }, }); if (orderError) throw new Error('주문 생성 실패: ' + orderError.message); // 4. 포트원 V2 결제 요청 const storeId = process.env.NEXT_PUBLIC_PORTONE_STORE_ID!; const response = await PortOne.requestPayment({ storeId, channelKey: channel.channelKey, paymentId, orderName: product.name, totalAmount: product.price, currency: 'CURRENCY_KRW', payMethod: channel.payMethod, customer: { email: user.email ?? undefined, }, }); // 5. 결제 결과 처리 if (!response || response.code != null) { if (response?.code === 'FAILURE_TYPE_PG' || response?.message?.includes('cancel')) { return; } throw new Error(response?.message ?? '결제 요청 실패'); } // 6. 서버에서 결제 검증 const confirmRes = await fetch('/api/payment/confirm', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentId }), }); const confirmData = await confirmRes.json(); if (!confirmRes.ok || !confirmData.success) { throw new Error(confirmData.error || '결제 검증에 실패했습니다.'); } // 7. 결제 성공 if (returnUrl) { router.push(returnUrl); } else { router.push(`/payment/success?paymentId=${paymentId}`); } } catch (err: unknown) { const error = err as { code?: string; message?: string }; if (error?.code === 'USER_CANCEL' || error?.message?.includes('cancel')) { return; } alert('결제 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.'); console.error(err); } finally { setLoading(false); } }; const handleClick = () => { if (channels.length === 0) { alert('결제 서비스가 준비 중입니다.'); return; } // 채널이 1개면 바로 결제, 여러 개면 선택 UI if (channels.length === 1) { processPayment(channels[0]); } else { setShowMethodPicker(true); } }; if (!product) return null; const isTestMode = process.env.NEXT_PUBLIC_PORTONE_STORE_ID?.includes('test') || process.env.NODE_ENV === 'development'; return ( <>
{isTestMode && ( TEST )}
{/* 결제수단 선택 모달 */} {showMethodPicker && (
setShowMethodPicker(false)} >
e.stopPropagation()} >
결제수단 선택

{product.name} · {product.price.toLocaleString()}원

{channels.map((channel) => ( ))}
)} ); }