chore(phase0): PortOne 잔재 제거 — 계좌이체 단일 소스 확정, saju 결제 CTA 제거
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<div style={{ display: style ? 'block' : 'inline-block', position: 'relative' }}>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={loading}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{loading ? '결제 처리 중...' : children}
|
||||
</button>
|
||||
{isTestMode && (
|
||||
<span style={{
|
||||
position: 'absolute', top: -8, right: -8,
|
||||
background: '#f59e0b', color: '#fff',
|
||||
fontSize: 9, fontWeight: 800, letterSpacing: '0.05em',
|
||||
padding: '2px 6px', borderRadius: 4,
|
||||
pointerEvents: 'none', userSelect: 'none',
|
||||
}}>
|
||||
TEST
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 결제수단 선택 모달 */}
|
||||
{showMethodPicker && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
||||
onClick={() => setShowMethodPicker(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="bg-[#04102b] px-5 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-6 h-6 rounded-md bg-[#1a56db] flex items-center justify-center text-white font-bold text-[10px]">
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">결제수단 선택</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowMethodPicker(false)}
|
||||
className="text-white/60 hover:text-white transition text-lg leading-none"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<p className="text-slate-500 text-xs mb-3">
|
||||
{product.name} · {product.price.toLocaleString()}원
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{channels.map((channel) => (
|
||||
<button
|
||||
key={channel.id}
|
||||
onClick={() => processPayment(channel)}
|
||||
className="w-full flex items-center gap-3 px-4 py-3.5 rounded-xl border border-slate-200 hover:border-[#1a56db] hover:bg-blue-50/50 transition text-left group"
|
||||
>
|
||||
<span className="text-xl">{channel.icon}</span>
|
||||
<span className="text-sm font-semibold text-slate-700 group-hover:text-[#1a56db]">
|
||||
{channel.label}
|
||||
</span>
|
||||
<svg className="w-4 h-4 text-slate-300 group-hover:text-[#1a56db] ml-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
function FailContent() {
|
||||
const params = useSearchParams();
|
||||
const message = params.get('message') ?? '결제가 취소되었거나 실패했습니다.';
|
||||
const code = params.get('code') ?? '';
|
||||
|
||||
return (
|
||||
<div className="text-center py-20 px-6">
|
||||
<div className="w-16 h-16 rounded-full bg-slate-100 border-2 border-slate-200 flex items-center justify-center mx-auto mb-5">
|
||||
<svg className="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="inline-block bg-slate-100 border border-slate-200 text-slate-600 text-xs font-bold px-3 py-1 rounded-full mb-4">
|
||||
{code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold mb-2" style={{ color: 'var(--jsm-ink)' }}>
|
||||
{code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제를 취소하셨습니다' : '결제에 실패했습니다'}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-sm mb-8 max-w-xs mx-auto leading-relaxed">{message}</p>
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
<button
|
||||
onClick={() => window.history.back()}
|
||||
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-6 py-3 rounded-xl font-semibold text-sm shadow-lg shadow-blue-600/20 transition"
|
||||
>
|
||||
다시 시도하기
|
||||
</button>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 bg-white border border-[#dbe8ff] text-slate-600 px-6 py-3 rounded-xl font-semibold text-sm hover:bg-slate-50 transition"
|
||||
>
|
||||
홈으로
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaymentFailPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center px-6 py-16">
|
||||
<div className="w-full max-w-md bg-white rounded-2xl border border-[#dbe8ff] shadow-lg overflow-hidden">
|
||||
<div className="px-6 py-4 border-b" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-white font-bold text-xs" style={{ background: 'var(--jsm-accent)' }}>
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">쟁승메이드 결제</span>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={<div className="py-20 text-center text-slate-400 text-sm">로딩 중...</div>}>
|
||||
<FailContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
function SuccessContent() {
|
||||
const params = useSearchParams();
|
||||
const paymentId = params.get('paymentId');
|
||||
|
||||
return (
|
||||
<div className="text-center py-20 px-6">
|
||||
<div className="w-16 h-16 rounded-full bg-emerald-50 border-2 border-emerald-400 flex items-center justify-center mx-auto mb-5">
|
||||
<svg className="w-8 h-8 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="inline-block bg-emerald-50 border border-emerald-200 text-emerald-700 text-xs font-bold px-3 py-1 rounded-full mb-4">
|
||||
결제 완료
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold mb-2" style={{ color: 'var(--jsm-ink)' }}>결제가 완료되었습니다!</h2>
|
||||
{paymentId && (
|
||||
<p className="text-slate-400 text-xs mb-1">주문번호: {paymentId}</p>
|
||||
)}
|
||||
<p className="text-slate-500 text-sm mb-8">
|
||||
마이페이지에서 결제 내역과 서비스 이용 현황을 확인하세요.
|
||||
</p>
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
<Link
|
||||
href="/mypage?tab=payments"
|
||||
className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-6 py-3 rounded-xl font-semibold text-sm shadow-lg shadow-blue-600/20 transition"
|
||||
>
|
||||
결제 내역 확인 →
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-2 bg-white border border-[#dbe8ff] text-slate-600 px-6 py-3 rounded-xl font-semibold text-sm hover:bg-slate-50 transition"
|
||||
>
|
||||
홈으로
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PaymentSuccessPage() {
|
||||
return (
|
||||
<div className="min-h-full bg-[#f0f5ff] flex items-center justify-center px-6 py-16">
|
||||
<div className="w-full max-w-md bg-white rounded-2xl border border-[#dbe8ff] shadow-lg overflow-hidden">
|
||||
<div className="px-6 py-4 border-b" style={{ background: 'var(--jsm-navy)' }}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-lg flex items-center justify-center text-white font-bold text-xs" style={{ background: 'var(--jsm-accent)' }}>
|
||||
쟁
|
||||
</div>
|
||||
<span className="text-white font-bold text-sm">쟁승메이드 결제</span>
|
||||
</div>
|
||||
</div>
|
||||
<Suspense fallback={
|
||||
<div className="py-20 text-center">
|
||||
<div className="w-8 h-8 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto" />
|
||||
</div>
|
||||
}>
|
||||
<SuccessContent />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import PaymentButton from '@/app/components/PaymentButton';
|
||||
import { PRODUCTS } from '@/lib/products';
|
||||
|
||||
// DB products 테이블에 등록된 상품만 테스트 가능
|
||||
const TEST_PRODUCTS = [
|
||||
'saju_detail', // 1,000원
|
||||
];
|
||||
|
||||
export default function PaymentTestPage() {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto px-6 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-2">결제 테스트</h1>
|
||||
<p className="text-slate-500 text-sm">
|
||||
포트원 V2 테스트 모드 — 실제 청구되지 않습니다.
|
||||
</p>
|
||||
<div className="mt-3 bg-amber-50 border border-amber-200 text-amber-800 text-xs px-4 py-2.5 rounded-xl">
|
||||
이 페이지는 관리자 테스트 전용입니다. 배포 전 삭제하세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{TEST_PRODUCTS.map((id) => {
|
||||
const product = PRODUCTS[id];
|
||||
if (!product) return null;
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className="flex items-center justify-between bg-white border border-slate-200 rounded-xl px-5 py-4"
|
||||
>
|
||||
<div>
|
||||
<p className="font-semibold text-sm text-slate-800">{product.name}</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
{product.price.toLocaleString()}원
|
||||
{product.type === 'monthly' && ' / 월'}
|
||||
<span className="ml-2 text-slate-300">({id})</span>
|
||||
</p>
|
||||
</div>
|
||||
<PaymentButton
|
||||
productId={id}
|
||||
className="bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-5 py-2.5 rounded-xl text-sm font-bold transition shadow-lg shadow-blue-600/20"
|
||||
>
|
||||
결제 테스트
|
||||
</PaymentButton>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PaymentButton from '@/app/components/PaymentButton';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
const faqItems = [
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import PaymentButton from '@/app/components/PaymentButton';
|
||||
|
||||
interface BirthKey {
|
||||
birth_year: number;
|
||||
@@ -313,13 +312,10 @@ export default function SajuAISection({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<PaymentButton
|
||||
productId="saju_detail"
|
||||
className="inline-flex items-center gap-2 bg-amber-400 hover:bg-amber-300 text-[#04102b] font-bold px-7 py-3 rounded-xl transition-all"
|
||||
>
|
||||
AI 상세 해석 받기 — 1,000원
|
||||
</PaymentButton>
|
||||
<p className="text-blue-200/40 text-xs mt-3">결제 후 즉시 AI 분석 시작 · 로그인 필요</p>
|
||||
<p className="inline-flex items-center gap-2 bg-white/10 text-blue-100/80 font-semibold px-7 py-3 rounded-xl">
|
||||
AI 상세 해석은 서비스 개편 준비 중입니다
|
||||
</p>
|
||||
<p className="text-blue-200/40 text-xs mt-3">사주 서비스 개편(Phase 2)에서 무료 제공 예정</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user