diff --git a/app/api/payment/confirm/route.ts b/app/api/payment/confirm/route.ts deleted file mode 100644 index e72da97..0000000 --- a/app/api/payment/confirm/route.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; -import { createClient } from '@/lib/supabase/server'; -import { checkRateLimit, getClientIp } from '@/lib/security'; - -export async function POST(request: NextRequest) { - try { - // ── Rate Limit: IP당 1분 10회 (결제 재시도 남용 방지) ───── - const ip = getClientIp(request); - const rl = checkRateLimit(`payment:${ip}`, 60_000, 10); - if (!rl.allowed) { - return NextResponse.json( - { error: '요청이 너무 많습니다. 잠시 후 다시 시도해주세요.' }, - { status: 429 } - ); - } - - const body = await request.json(); - const { paymentId } = body; - - // ── 기본 파라미터 검증 ──────────────────────────────────── - if (!paymentId || typeof paymentId !== 'string' || paymentId.length > 200) { - return NextResponse.json({ error: '필수 파라미터 누락' }, { status: 400 }); - } - - // ── 로그인 사용자 확인 ──────────────────────────────────── - const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); - if (!user) { - return NextResponse.json({ error: '로그인이 필요합니다' }, { status: 401 }); - } - - // ── DB에서 주문 확인 ────────────────────────────────────── - const { data: order, error: orderFetchError } = await supabase - .from('orders') - .select('*') - .eq('id', paymentId) - .single(); - - if (orderFetchError || !order) { - return NextResponse.json({ error: '주문을 찾을 수 없습니다' }, { status: 404 }); - } - if (order.user_id !== user.id) { - return NextResponse.json({ error: '접근 권한이 없습니다' }, { status: 403 }); - } - if (order.status === 'paid') { - return NextResponse.json({ error: '이미 처리된 주문입니다' }, { status: 400 }); - } - - // ── 포트원 V2 결제 조회 API ─────────────────────────────── - const apiSecret = process.env.PORTONE_API_SECRET; - if (!apiSecret) { - console.error('[Payment] PORTONE_API_SECRET 미설정'); - return NextResponse.json({ error: '결제 서비스 설정 오류' }, { status: 500 }); - } - - const portoneRes = await fetch( - `https://api.portone.io/payments/${encodeURIComponent(paymentId)}`, - { - method: 'GET', - headers: { - Authorization: `PortOne ${apiSecret}`, - 'Content-Type': 'application/json', - }, - } - ); - - if (!portoneRes.ok) { - const err = await portoneRes.json().catch(() => ({})); - console.error(`[Payment] 포트원 조회 실패 paymentId=${paymentId} status=${portoneRes.status}`, err); - return NextResponse.json( - { error: '결제 확인에 실패했습니다. 고객센터에 문의해주세요.' }, - { status: 400 } - ); - } - - const paymentData = await portoneRes.json(); - - // ── 결제 상태 & 금액 검증 ───────────────────────────────── - if (paymentData.status !== 'PAID') { - console.warn(`[Payment] 미완료 결제 paymentId=${paymentId} status=${paymentData.status}`); - return NextResponse.json( - { error: '결제가 완료되지 않았습니다.' }, - { status: 400 } - ); - } - - // 서버 DB 금액과 포트원 결제 금액 비교 (위조 방어) - const paidAmount = paymentData.amount?.total; - if (paidAmount !== order.amount) { - console.warn(`[Payment] 금액 불일치 paymentId=${paymentId} db=${order.amount} paid=${paidAmount} user=${user.id}`); - return NextResponse.json({ error: '결제 금액이 올바르지 않습니다' }, { status: 400 }); - } - - // ── orders 상태 업데이트 ────────────────────────────────── - const { error: updateError } = await supabase - .from('orders') - .update({ status: 'paid' }) - .eq('id', paymentId); - - if (updateError) { - console.error('[Payment] Order update error:', updateError.message); - return NextResponse.json({ error: '주문 상태 업데이트 실패' }, { status: 500 }); - } - - // ── payments 레코드 생성 ────────────────────────────────── - const pgPaymentId = paymentData.pgResponse?.pgTxId ?? paymentData.paymentId ?? paymentId; - const { error: paymentError } = await supabase.from('payments').insert({ - user_id: order.user_id, - order_id: paymentId, - product_name: order.metadata?.product_name ?? order.product_id, - amount: order.amount, - status: 'paid', - pg_provider: 'portone_kcp', - pg_payment_key: pgPaymentId, - }); - - if (paymentError) { - console.error('[Payment] Payment insert error:', paymentError.message); - return NextResponse.json({ error: '결제 내역 저장 실패' }, { status: 500 }); - } - - return NextResponse.json({ - success: true, - data: { - paymentId, - orderName: paymentData.orderName, - amount: paidAmount, - status: paymentData.status, - }, - }); - } catch (error: unknown) { - console.error('[Payment] Unexpected error:', error); - return NextResponse.json({ error: '서버 오류가 발생했습니다' }, { status: 500 }); - } -} diff --git a/app/components/PaymentButton.tsx b/app/components/PaymentButton.tsx deleted file mode 100644 index 250f017..0000000 --- a/app/components/PaymentButton.tsx +++ /dev/null @@ -1,202 +0,0 @@ -'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 = crypto.randomUUID(); - 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 response = await PortOne.requestPayment({ - storeId: process.env.NEXT_PUBLIC_PORTONE_STORE_ID ?? '', - 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 - || process.env.NODE_ENV === 'development'; - - return ( - <> -
- {product.name} · {product.price.toLocaleString()}원 -
-{message}
-주문번호: {paymentId}
- )} -- 마이페이지에서 결제 내역과 서비스 이용 현황을 확인하세요. -
-- 포트원 V2 테스트 모드 — 실제 청구되지 않습니다. -
-{product.name}
-- {product.price.toLocaleString()}원 - {product.type === 'monthly' && ' / 월'} - ({id}) -
-결제 후 즉시 AI 분석 시작 · 로그인 필요
++ AI 상세 해석은 서비스 개편 준비 중입니다 +
+사주 서비스 개편(Phase 2)에서 무료 제공 예정
); diff --git a/lib/payment-channels.ts b/lib/payment-channels.ts deleted file mode 100644 index 0ab839d..0000000 --- a/lib/payment-channels.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 포트원 V2 결제 채널 설정 - * 포트원 admin에서 채널 추가 후 각 Channel Key를 .env.local에 설정 - */ - -export type PayMethod = 'CARD' | 'EASY_PAY'; - -export interface PaymentChannel { - id: string; - label: string; - icon: string; // SVG or emoji placeholder - channelKey: string; - payMethod: PayMethod; -} - -export const PAYMENT_CHANNELS: PaymentChannel[] = [ - { - id: 'card', - label: '신용/체크카드', - icon: '💳', - channelKey: process.env.NEXT_PUBLIC_PORTONE_CHANNEL_KPN ?? '', - payMethod: 'CARD', - }, - { - id: 'kakaopay', - label: '카카오페이', - icon: '🟡', - channelKey: process.env.NEXT_PUBLIC_PORTONE_CHANNEL_KAKAOPAY ?? '', - payMethod: 'EASY_PAY', - }, - { - id: 'naverpay', - label: '네이버페이', - icon: '🟢', - channelKey: process.env.NEXT_PUBLIC_PORTONE_CHANNEL_NAVERPAY ?? '', - payMethod: 'EASY_PAY', - }, - { - id: 'tosspay', - label: '토스페이', - icon: '🔵', - channelKey: process.env.NEXT_PUBLIC_PORTONE_CHANNEL_TOSSPAY ?? '', - payMethod: 'EASY_PAY', - }, -]; - -/** channelKey가 설정된 채널만 반환 */ -export function getActiveChannels(): PaymentChannel[] { - return PAYMENT_CHANNELS.filter((ch) => ch.channelKey.length > 0); -} diff --git a/lib/products.ts b/lib/products.ts deleted file mode 100644 index 61aa686..0000000 --- a/lib/products.ts +++ /dev/null @@ -1,143 +0,0 @@ -export interface Product { - id: string; - name: string; - price: number; - type: 'one_time' | 'monthly' | 'annual'; - description: string; -} - -export const PRODUCTS: Record