feat: 포트원 V2 결제 마이그레이션 + 법적 페이지 추가 (PG 심사용)
- 토스페이먼츠 SDK → 포트원 V2 (@portone/browser-sdk) 전환 - 4채널 결제수단 선택 UI: 카드(KPN)/카카오페이/네이버페이/토스페이 - 서버 결제 검증 API를 포트원 V2 조회 방식으로 변경 - 이용약관(/legal/terms), 개인정보처리방침(/legal/privacy), 환불정책(/legal/refund) 페이지 생성 - 푸터에 법적 페이지 링크 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,20 +15,12 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { paymentKey, orderId, amount } = body;
|
||||
const { paymentId } = body;
|
||||
|
||||
// ── 기본 파라미터 검증 ────────────────────────────────────
|
||||
if (!paymentKey || !orderId || amount === undefined) {
|
||||
if (!paymentId || typeof paymentId !== 'string' || paymentId.length > 200) {
|
||||
return NextResponse.json({ error: '필수 파라미터 누락' }, { status: 400 });
|
||||
}
|
||||
// 타입 강제 검증
|
||||
if (
|
||||
typeof paymentKey !== 'string' || paymentKey.length > 200 ||
|
||||
typeof orderId !== 'string' || orderId.length > 200 ||
|
||||
typeof amount !== 'number' || amount <= 0 || !Number.isInteger(amount)
|
||||
) {
|
||||
return NextResponse.json({ error: '잘못된 파라미터 형식' }, { status: 400 });
|
||||
}
|
||||
|
||||
// ── 로그인 사용자 확인 ────────────────────────────────────
|
||||
const supabase = await createClient();
|
||||
@@ -37,66 +29,73 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: '로그인이 필요합니다' }, { status: 401 });
|
||||
}
|
||||
|
||||
// ── DB에서 주문 확인 (금액 서버사이드 검증) ───────────────
|
||||
// ── DB에서 주문 확인 ──────────────────────────────────────
|
||||
const { data: order, error: orderFetchError } = await supabase
|
||||
.from('orders')
|
||||
.select('*')
|
||||
.eq('id', orderId)
|
||||
.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 });
|
||||
}
|
||||
// 서버 DB 금액과 비교 (클라이언트 금액 위조 방어)
|
||||
if (order.amount !== amount) {
|
||||
console.warn(`[Payment] 금액 불일치 orderId=${orderId} db=${order.amount} req=${amount} user=${user.id}`);
|
||||
return NextResponse.json({ error: '결제 금액이 올바르지 않습니다' }, { status: 400 });
|
||||
}
|
||||
if (order.status === 'paid') {
|
||||
return NextResponse.json({ error: '이미 처리된 주문입니다' }, { status: 400 });
|
||||
}
|
||||
|
||||
// ── 토스페이먼츠 서버 승인 ────────────────────────────────
|
||||
const secretKey = process.env.TOSS_SECRET_KEY;
|
||||
if (!secretKey) {
|
||||
console.error('[Payment] TOSS_SECRET_KEY 미설정');
|
||||
// ── 포트원 V2 결제 조회 API ───────────────────────────────
|
||||
const apiSecret = process.env.PORTONE_API_SECRET;
|
||||
if (!apiSecret) {
|
||||
console.error('[Payment] PORTONE_API_SECRET 미설정');
|
||||
return NextResponse.json({ error: '결제 서비스 설정 오류' }, { status: 500 });
|
||||
}
|
||||
if (!secretKey.startsWith('test_') && process.env.NODE_ENV === 'development') {
|
||||
console.warn('[Payment] WARNING: live Toss key detected in development!');
|
||||
}
|
||||
|
||||
const encoded = Buffer.from(`${secretKey}:`).toString('base64');
|
||||
const tossRes = await fetch('https://api.tosspayments.com/v1/payments/confirm', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${encoded}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ paymentKey, orderId, amount }),
|
||||
});
|
||||
const portoneRes = await fetch(
|
||||
`https://api.portone.io/payments/${encodeURIComponent(paymentId)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `PortOne ${apiSecret}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!tossRes.ok) {
|
||||
const err = await tossRes.json().catch(() => ({}));
|
||||
// 내부 에러 코드는 서버 로그에만 기록
|
||||
console.error(`[Payment] Toss 승인 실패 orderId=${orderId} code=${err.code} msg=${err.message}`);
|
||||
if (!portoneRes.ok) {
|
||||
const err = await portoneRes.json().catch(() => ({}));
|
||||
console.error(`[Payment] 포트원 조회 실패 paymentId=${paymentId} status=${portoneRes.status}`, err);
|
||||
return NextResponse.json(
|
||||
{ error: '결제 승인에 실패했습니다. 카드사 또는 고객센터에 문의해주세요.' },
|
||||
{ error: '결제 확인에 실패했습니다. 고객센터에 문의해주세요.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const tossData = await tossRes.json();
|
||||
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', orderId);
|
||||
.eq('id', paymentId);
|
||||
|
||||
if (updateError) {
|
||||
console.error('[Payment] Order update error:', updateError.message);
|
||||
@@ -104,14 +103,15 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// ── payments 레코드 생성 ──────────────────────────────────
|
||||
const pgPaymentId = paymentData.pgResponse?.pgTxId ?? paymentData.paymentId ?? paymentId;
|
||||
const { error: paymentError } = await supabase.from('payments').insert({
|
||||
user_id: order.user_id,
|
||||
order_id: orderId,
|
||||
order_id: paymentId,
|
||||
product_name: order.metadata?.product_name ?? order.product_id,
|
||||
amount: order.amount,
|
||||
status: 'paid',
|
||||
pg_provider: 'toss',
|
||||
pg_payment_key: paymentKey,
|
||||
pg_provider: 'portone_kcp',
|
||||
pg_payment_key: pgPaymentId,
|
||||
});
|
||||
|
||||
if (paymentError) {
|
||||
@@ -119,7 +119,15 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: '결제 내역 저장 실패' }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: true, data: tossData });
|
||||
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 });
|
||||
|
||||
@@ -55,6 +55,11 @@ export default function DashboardShell({ children }: { children: React.ReactNode
|
||||
<span>전화: 010-3907-1392</span>
|
||||
<span>이메일: bgg8988@gmail.com</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-3 mt-3">
|
||||
<a href="/legal/terms" className="text-slate-400 hover:text-[#1a56db] transition underline underline-offset-2">이용약관</a>
|
||||
<a href="/legal/privacy" className="text-slate-400 hover:text-[#1a56db] transition underline underline-offset-2">개인정보처리방침</a>
|
||||
<a href="/legal/refund" className="text-slate-400 hover:text-[#1a56db] transition underline underline-offset-2">환불 정책</a>
|
||||
</div>
|
||||
<p className="mt-2 text-slate-400">© 2025 쟁승메이드. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -4,6 +4,8 @@ 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;
|
||||
@@ -15,11 +17,14 @@ interface PaymentButtonProps {
|
||||
|
||||
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 handlePayment = async () => {
|
||||
const processPayment = async (channel: PaymentChannel) => {
|
||||
setShowMethodPicker(false);
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. 로그인 확인
|
||||
@@ -29,85 +34,171 @@ export default function PaymentButton({ productId, className, style, children, r
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 프로필 없으면 생성 (Google OAuth 등으로 트리거 미실행된 경우 대비)
|
||||
// 2. 프로필 없으면 생성
|
||||
await supabase.from('profiles').upsert({ id: user.id, email: user.email }, { onConflict: 'id' });
|
||||
|
||||
// 3. Supabase에 order 생성
|
||||
const orderId = crypto.randomUUID();
|
||||
const paymentId = `order_${Date.now()}_${crypto.randomUUID().slice(0, 8)}`;
|
||||
const { error: orderError } = await supabase
|
||||
.from('orders')
|
||||
.insert({
|
||||
id: orderId,
|
||||
id: paymentId,
|
||||
user_id: user.id,
|
||||
product_id: productId,
|
||||
amount: product.price,
|
||||
status: 'pending',
|
||||
metadata: { product_name: product.name },
|
||||
metadata: { product_name: product.name, pay_channel: channel.id },
|
||||
});
|
||||
|
||||
if (orderError) throw new Error('주문 생성 실패: ' + orderError.message);
|
||||
|
||||
// 4. 토스페이먼츠 결제창 호출
|
||||
// dev: NEXT_PUBLIC_TOSS_CLIENT_KEY=test_ck_* → 테스트 결제 (실제 청구 없음)
|
||||
// prod: NEXT_PUBLIC_TOSS_CLIENT_KEY=live_ck_* → 실결제 (Vercel 환경변수에 설정)
|
||||
const { loadTossPayments } = await import('@tosspayments/tosspayments-sdk');
|
||||
const clientKey = process.env.NEXT_PUBLIC_TOSS_CLIENT_KEY!;
|
||||
const tossPayments = await loadTossPayments(clientKey);
|
||||
// 4. 포트원 V2 결제 요청
|
||||
const storeId = process.env.NEXT_PUBLIC_PORTONE_STORE_ID!;
|
||||
|
||||
const payment = tossPayments.payment({
|
||||
customerKey: user.id,
|
||||
});
|
||||
|
||||
await payment.requestPayment({
|
||||
method: 'CARD',
|
||||
amount: {
|
||||
currency: 'KRW',
|
||||
value: product.price,
|
||||
},
|
||||
orderId,
|
||||
const response = await PortOne.requestPayment({
|
||||
storeId,
|
||||
channelKey: channel.channelKey,
|
||||
paymentId,
|
||||
orderName: product.name,
|
||||
successUrl: `${window.location.origin}/payment/success${returnUrl ? '?returnUrl=' + encodeURIComponent(returnUrl) : ''}`,
|
||||
failUrl: `${window.location.origin}/payment/fail`,
|
||||
customerEmail: user.email,
|
||||
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') {
|
||||
alert('결제 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
|
||||
console.error(err);
|
||||
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_TOSS_CLIENT_KEY?.startsWith('test_');
|
||||
const isTestMode = process.env.NEXT_PUBLIC_PORTONE_STORE_ID?.includes('test')
|
||||
|| process.env.NODE_ENV === 'development';
|
||||
|
||||
return (
|
||||
<div style={{ display: style ? 'block' : 'inline-block', position: 'relative' }}>
|
||||
<button
|
||||
onClick={handlePayment}
|
||||
disabled={loading}
|
||||
className={className}
|
||||
style={style}
|
||||
>
|
||||
{loading ? '결제 처리 중...' : children}
|
||||
</button>
|
||||
{/* dev/test 환경에서만 표시되는 배지 — 실수로 실결제 누르는 것 방지 */}
|
||||
{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 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>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
177
app/legal/privacy/page.tsx
Normal file
177
app/legal/privacy/page.tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '개인정보처리방침 | 쟁승메이드',
|
||||
description: '쟁승메이드 개인정보처리방침',
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">개인정보처리방침</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<p>
|
||||
쟁승메이드(이하 "회사")는 개인정보보호법 등 관련 법령을 준수하며,
|
||||
이용자의 개인정보를 보호하기 위해 다음과 같은 개인정보처리방침을 수립·공개합니다.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">1. 수집하는 개인정보 항목</h2>
|
||||
<p>회사는 서비스 제공을 위해 다음의 개인정보를 수집합니다.</p>
|
||||
<table className="w-full text-sm border border-slate-200 mt-3">
|
||||
<thead>
|
||||
<tr className="bg-slate-50">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">수집 시점</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">수집 항목</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">회원가입</td>
|
||||
<td className="border border-slate-200 px-3 py-2">이메일, 이름(닉네임)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">문의 접수</td>
|
||||
<td className="border border-slate-200 px-3 py-2">이름, 이메일, 연락처, 문의 내용</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">유료 결제</td>
|
||||
<td className="border border-slate-200 px-3 py-2">결제 수단 정보(PG사를 통해 처리, 회사 직접 저장하지 않음)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">사주 분석</td>
|
||||
<td className="border border-slate-200 px-3 py-2">생년월일, 출생시간, 성별</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">자동 수집</td>
|
||||
<td className="border border-slate-200 px-3 py-2">접속 IP, 브라우저 종류, 접속 일시, 쿠키</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">2. 개인정보의 수집·이용 목적</h2>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>서비스 제공 및 계약 이행: 문의 응대, 서비스 제공, 결제 처리</li>
|
||||
<li>회원 관리: 회원 식별, 본인 확인, 서비스 이용 내역 관리</li>
|
||||
<li>서비스 개선: 접속 통계, 이용 패턴 분석을 통한 서비스 개선</li>
|
||||
<li>마케팅·홍보: 신규 서비스 안내, 이벤트 정보 제공 (별도 동의 시)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">3. 개인정보의 보유 및 이용 기간</h2>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>회원 정보: 회원 탈퇴 시까지 (탈퇴 후 즉시 파기)</li>
|
||||
<li>문의 내역: 문의 처리 완료 후 3년 (전자상거래법)</li>
|
||||
<li>결제 기록: 5년 (전자상거래법)</li>
|
||||
<li>접속 로그: 3개월 (통신비밀보호법)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">4. 개인정보의 제3자 제공</h2>
|
||||
<p>
|
||||
회사는 이용자의 동의 없이 개인정보를 제3자에게 제공하지 않습니다.
|
||||
다만, 다음의 경우는 예외로 합니다.
|
||||
</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>이용자가 사전에 동의한 경우</li>
|
||||
<li>법령에 의해 요구되는 경우</li>
|
||||
<li>결제 처리를 위해 PG사(포트원, 한국결제네트웍스 등)에 필요 최소 정보 전달</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">5. 개인정보의 처리 위탁</h2>
|
||||
<table className="w-full text-sm border border-slate-200 mt-3">
|
||||
<thead>
|
||||
<tr className="bg-slate-50">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">수탁업체</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">위탁 업무</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">포트원(주)</td>
|
||||
<td className="border border-slate-200 px-3 py-2">전자결제 처리</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">Supabase Inc.</td>
|
||||
<td className="border border-slate-200 px-3 py-2">회원 인증 및 데이터 저장</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">Vercel Inc.</td>
|
||||
<td className="border border-slate-200 px-3 py-2">웹사이트 호스팅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">Resend Inc.</td>
|
||||
<td className="border border-slate-200 px-3 py-2">이메일 발송</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">6. 개인정보의 파기 절차 및 방법</h2>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>파기 절차: 보유 기간 경과 또는 처리 목적 달성 시 지체 없이 파기</li>
|
||||
<li>파기 방법: 전자적 파일은 복구 불가능한 방법으로 삭제, 종이 문서는 분쇄 또는 소각</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">7. 이용자의 권리</h2>
|
||||
<p>이용자는 언제든지 다음의 권리를 행사할 수 있습니다.</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>개인정보 열람 요구</li>
|
||||
<li>개인정보 정정·삭제 요구</li>
|
||||
<li>개인정보 처리 정지 요구</li>
|
||||
<li>회원 탈퇴 (마이페이지 또는 이메일 요청)</li>
|
||||
</ul>
|
||||
<p className="mt-2">
|
||||
위 요청은 이메일(bgg8988@gmail.com)로 접수하면 10일 이내에 처리합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">8. 쿠키(Cookie) 운용</h2>
|
||||
<p>
|
||||
회사는 서비스 이용 분석을 위해 Google Analytics를 사용하며, 이 과정에서 쿠키가 설치됩니다.
|
||||
이용자는 브라우저 설정을 통해 쿠키 저장을 거부할 수 있으나, 일부 서비스 이용이 제한될 수 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">9. 개인정보 보호책임자</h2>
|
||||
<ul className="list-none space-y-0.5">
|
||||
<li>성명: 박재오</li>
|
||||
<li>직위: 대표</li>
|
||||
<li>이메일: bgg8988@gmail.com</li>
|
||||
<li>전화: 010-3907-1392</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">10. 개인정보 침해 구제</h2>
|
||||
<p>개인정보 침해에 대한 신고·상담은 아래 기관에 문의하실 수 있습니다.</p>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>개인정보분쟁조정위원회: 1833-6972 (kopico.go.kr)</li>
|
||||
<li>개인정보침해신고센터: 118 (privacy.kisa.or.kr)</li>
|
||||
<li>대검찰청 사이버수사과: 1301 (spo.go.kr)</li>
|
||||
<li>경찰청 사이버수사국: 182 (ecrm.cyber.go.kr)</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-slate-200 pt-6 mt-10">
|
||||
<p className="text-slate-400 text-xs">
|
||||
시행일자: 2025년 4월 7일<br />
|
||||
상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
app/legal/refund/page.tsx
Normal file
148
app/legal/refund/page.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '환불 정책 | 쟁승메이드',
|
||||
description: '쟁승메이드 환불 및 취소 정책',
|
||||
};
|
||||
|
||||
export default function RefundPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">환불 정책</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<p>
|
||||
쟁승메이드(이하 "회사")는 공정하고 투명한 환불 정책을 운영합니다.
|
||||
서비스 유형에 따라 환불 조건이 다르므로 아래 내용을 확인해주세요.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">1. 디지털 콘텐츠 (즉시 제공 상품)</h2>
|
||||
<p className="font-medium text-slate-700">대상: 프롬프트 패키지, AI 사주 리포트, 로또 분석 등</p>
|
||||
<table className="w-full text-sm border border-slate-200 mt-3">
|
||||
<thead>
|
||||
<tr className="bg-slate-50">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">시점</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">환불 가능 여부</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">결제 후 다운로드/열람 전</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-600 font-medium">전액 환불</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">다운로드/열람 후 7일 이내</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-600 font-medium">서비스 하자 시 환불 (정상 제공 시 환불 불가)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">7일 경과</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-slate-400">환불 불가</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
* 전자상거래법 제17조에 따라, 디지털 콘텐츠는 이용 후 청약철회가 제한될 수 있습니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">2. 구독 서비스</h2>
|
||||
<p className="font-medium text-slate-700">대상: AI 자동화 키트 월 구독, 로또 월간 플랜, 주식 월 유지비 등</p>
|
||||
<table className="w-full text-sm border border-slate-200 mt-3">
|
||||
<thead>
|
||||
<tr className="bg-slate-50">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">시점</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">환불 가능 여부</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">결제 후 서비스 이용 전</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-600 font-medium">전액 환불</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">이용 시작 후 7일 이내</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-600 font-medium">이용일수 공제 후 환불</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">7일 경과</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-slate-400">당월 환불 불가, 다음 결제일 전 해지 가능</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
* 구독 해지는 마이페이지 또는 이메일(bgg8988@gmail.com)로 요청 가능합니다.
|
||||
해지 시 다음 결제일부터 과금이 중지되며, 당월 잔여 기간은 계속 이용 가능합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">3. 외주 개발 서비스</h2>
|
||||
<p className="font-medium text-slate-700">대상: 홈페이지 제작, 업무 자동화, 맞춤 개발 등</p>
|
||||
<table className="w-full text-sm border border-slate-200 mt-3">
|
||||
<thead>
|
||||
<tr className="bg-slate-50">
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">시점</th>
|
||||
<th className="border border-slate-200 px-3 py-2 text-left font-semibold">환불 가능 여부</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">착수 전 (선금 결제 후 작업 시작 전)</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-emerald-600 font-medium">선금 전액 환불</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">작업 진행 중</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-amber-600 font-medium">진행률에 따른 부분 환불 (협의)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="border border-slate-200 px-3 py-2">납품 완료 후</td>
|
||||
<td className="border border-slate-200 px-3 py-2 text-slate-400">환불 불가 (하자 보수는 별도)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="text-xs text-slate-400 mt-2">
|
||||
* 외주 개발은 선금 50% / 잔금 50% 구조이며, 계약서에 명시된 조건을 우선합니다.
|
||||
<br />
|
||||
* 회사 귀책으로 납기 지연 시 1일당 계약금의 1%를 차감합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">4. 환불 신청 방법</h2>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-4">
|
||||
<ol className="list-decimal pl-5 space-y-2">
|
||||
<li>이메일(<strong>bgg8988@gmail.com</strong>)로 환불 요청
|
||||
<ul className="list-disc pl-5 mt-1 text-xs text-slate-500">
|
||||
<li>제목: [환불 요청] 주문번호 또는 서비스명</li>
|
||||
<li>본문: 성함, 결제일, 환불 사유</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>담당자 확인 후 <strong>영업일 기준 3일 이내</strong> 환불 처리</li>
|
||||
<li>환불 금액은 원래 결제 수단으로 환급 (카드 취소는 카드사에 따라 3~7영업일 소요)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">5. 환불이 불가능한 경우</h2>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
<li>이용자의 단순 변심으로 디지털 콘텐츠를 이미 열람/다운로드한 경우</li>
|
||||
<li>서비스 이용 후 7일이 경과한 경우</li>
|
||||
<li>이용자의 과실로 서비스 이용이 불가능해진 경우</li>
|
||||
<li>관련 법령에 의해 환불이 제한되는 경우</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-slate-200 pt-6 mt-10">
|
||||
<p className="text-slate-400 text-xs">
|
||||
시행일자: 2025년 4월 7일<br />
|
||||
상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822<br />
|
||||
환불 문의: bgg8988@gmail.com · 010-3907-1392
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
138
app/legal/terms/page.tsx
Normal file
138
app/legal/terms/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Metadata } from 'next';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: '이용약관 | 쟁승메이드',
|
||||
description: '쟁승메이드 서비스 이용약관',
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-6 py-12">
|
||||
<h1 className="text-2xl font-extrabold text-[#04102b] mb-8">이용약관</h1>
|
||||
|
||||
<div className="prose prose-sm prose-slate max-w-none space-y-6 text-slate-600 leading-relaxed">
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제1조 (목적)</h2>
|
||||
<p>
|
||||
본 약관은 쟁승메이드(이하 "회사")가 운영하는 웹사이트(jaengseung-made.com)를 통해
|
||||
제공하는 서비스(이하 "서비스")의 이용 조건 및 절차, 회사와 이용자의 권리·의무 및
|
||||
책임사항을 규정함을 목적으로 합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제2조 (정의)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>"서비스"란 회사가 제공하는 웹 개발, AI 자동화, 프롬프트 엔지니어링, 사주 분석, 로또 번호 추천 등 각종 디지털 서비스를 말합니다.</li>
|
||||
<li>"이용자"란 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.</li>
|
||||
<li>"회원"이란 회사에 개인정보를 제공하여 회원 등록을 한 자로서, 회사의 서비스를 지속적으로 이용할 수 있는 자를 말합니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제3조 (약관의 효력 및 변경)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>본 약관은 서비스 화면에 게시하거나 기타의 방법으로 이용자에게 공지함으로써 효력이 발생합니다.</li>
|
||||
<li>회사는 관련 법령을 위반하지 않는 범위에서 본 약관을 변경할 수 있으며, 변경 시 적용일자 및 변경 사유를 명시하여 최소 7일 전 공지합니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제4조 (서비스의 제공 및 변경)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>회사는 다음과 같은 서비스를 제공합니다.
|
||||
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
||||
<li>홈페이지 제작 서비스</li>
|
||||
<li>업무 자동화 개발 서비스</li>
|
||||
<li>프롬프트 엔지니어링 서비스</li>
|
||||
<li>AI 자동화 키트 구독 서비스</li>
|
||||
<li>AI 사주 분석 서비스</li>
|
||||
<li>로또 번호 추천 서비스</li>
|
||||
<li>주식 자동 매매 프로그램</li>
|
||||
<li>기타 회사가 추가 개발하거나 제휴를 통해 제공하는 서비스</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>회사는 서비스의 내용을 변경하는 경우 변경 사항을 사전에 공지합니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제5조 (서비스의 중단)</h2>
|
||||
<p>
|
||||
회사는 컴퓨터 등 정보통신설비의 보수·점검, 교체 및 고장, 통신두절 또는 천재지변 등의 사유가
|
||||
발생한 경우 서비스의 제공을 일시적으로 중단할 수 있습니다. 이 경우 회사는 가능한 한 사전에 공지합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제6조 (회원가입)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>이용자는 회사가 정한 가입 양식에 따라 회원 정보를 기입한 후 본 약관에 동의하여 회원 가입을 신청합니다.</li>
|
||||
<li>회사는 전항에 따른 회원 가입 신청에 대해 원칙적으로 승낙합니다. 다만, 다음 각 호에 해당하는 경우 거부할 수 있습니다.
|
||||
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
||||
<li>가입 신청자가 본 약관에 의해 이전에 회원 자격을 상실한 적이 있는 경우</li>
|
||||
<li>허위 정보를 기재한 경우</li>
|
||||
<li>기타 회사가 정한 이용신청 요건이 미비된 경우</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제7조 (결제 및 요금)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>유료 서비스의 이용 요금은 해당 서비스 페이지에 명시된 금액을 따릅니다.</li>
|
||||
<li>결제는 신용카드, 간편결제(카카오페이, 네이버페이, 토스페이) 등 회사가 지원하는 수단으로 가능합니다.</li>
|
||||
<li>결제 완료 후에는 회사의 환불 정책에 따라 환불이 처리됩니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제8조 (이용자의 의무)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>이용자는 서비스 이용 시 관련 법령, 본 약관, 이용 안내 등을 준수하여야 합니다.</li>
|
||||
<li>이용자는 다음 행위를 하여서는 안 됩니다.
|
||||
<ul className="list-disc pl-5 mt-1 space-y-0.5">
|
||||
<li>타인의 정보를 도용하는 행위</li>
|
||||
<li>회사의 서비스를 이용하여 얻은 정보를 무단으로 복제, 배포하는 행위</li>
|
||||
<li>회사 및 제3자의 지적 재산권을 침해하는 행위</li>
|
||||
<li>서비스의 안정적 운영을 방해하는 행위</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제9조 (회사의 의무)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>회사는 관련 법령과 본 약관이 정하는 바에 따라 지속적이고 안정적으로 서비스를 제공하기 위해 노력합니다.</li>
|
||||
<li>회사는 이용자의 개인정보를 보호하기 위해 개인정보처리방침을 수립하고 이를 준수합니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제10조 (면책조항)</h2>
|
||||
<ol className="list-decimal pl-5 space-y-1">
|
||||
<li>회사는 천재지변 또는 이에 준하는 불가항력으로 서비스를 제공할 수 없는 경우에는 책임이 면제됩니다.</li>
|
||||
<li>AI 기반 서비스(사주 분석, 로또 추천 등)는 참고 목적으로 제공되며, 서비스 결과에 대한 정확성을 보증하지 않습니다.</li>
|
||||
<li>이용자의 귀책사유로 인한 서비스 이용 장애에 대하여 회사는 책임을 지지 않습니다.</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-slate-800 mt-8 mb-3">제11조 (분쟁 해결)</h2>
|
||||
<p>
|
||||
본 약관에 관한 분쟁은 대한민국 법률을 준거법으로 하며, 서울중앙지방법원을 관할법원으로 합니다.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="border-t border-slate-200 pt-6 mt-10">
|
||||
<p className="text-slate-400 text-xs">
|
||||
시행일자: 2025년 4월 7일<br />
|
||||
상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -17,10 +17,10 @@ function FailContent() {
|
||||
</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 === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'}
|
||||
{code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold text-[#04102b] mb-2">
|
||||
{code === 'PAY_PROCESS_CANCELED' ? '결제를 취소하셨습니다' : '결제에 실패했습니다'}
|
||||
{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">
|
||||
|
||||
@@ -1,82 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
|
||||
function SuccessContent() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [productName, setProductName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const paymentKey = params.get('paymentKey');
|
||||
const orderId = params.get('orderId');
|
||||
const amount = Number(params.get('amount'));
|
||||
const returnUrl = params.get('returnUrl');
|
||||
|
||||
if (!paymentKey || !orderId || !amount) {
|
||||
setStatus('error');
|
||||
setErrorMsg('잘못된 접근입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/payment/confirm', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paymentKey, orderId, amount }),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
setProductName(data.data?.orderName ?? '');
|
||||
setStatus('success');
|
||||
if (returnUrl) {
|
||||
router.replace(returnUrl);
|
||||
}
|
||||
} else {
|
||||
setStatus('error');
|
||||
setErrorMsg(data.error || '결제 승인에 실패했습니다.');
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setStatus('error');
|
||||
setErrorMsg('서버 오류가 발생했습니다. 결제 내역을 확인해주세요.');
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="text-center py-20 px-6">
|
||||
<div className="w-12 h-12 border-2 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||
<p className="text-slate-500 text-sm">결제를 확인하는 중...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="text-center py-20 px-6">
|
||||
<div className="w-16 h-16 rounded-full bg-red-50 border-2 border-red-200 flex items-center justify-center mx-auto mb-5">
|
||||
<svg className="w-8 h-8 text-red-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>
|
||||
<h2 className="text-xl font-bold text-[#04102b] mb-2">결제 처리 실패</h2>
|
||||
<p className="text-slate-500 text-sm mb-8">{errorMsg}</p>
|
||||
<div className="flex justify-center gap-3">
|
||||
<Link href="/mypage" className="inline-flex items-center gap-2 bg-white border border-[#dbe8ff] text-slate-600 px-5 py-2.5 rounded-xl font-semibold text-sm hover:bg-slate-50 transition">
|
||||
결제 내역 확인
|
||||
</Link>
|
||||
<Link href="/" className="inline-flex items-center gap-2 bg-[#1a56db] hover:bg-[#1e4fc2] text-white px-5 py-2.5 rounded-xl font-semibold text-sm transition">
|
||||
홈으로 →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const paymentId = params.get('paymentId');
|
||||
|
||||
return (
|
||||
<div className="text-center py-20 px-6">
|
||||
@@ -89,10 +19,10 @@ function SuccessContent() {
|
||||
결제 완료
|
||||
</div>
|
||||
<h2 className="text-2xl font-extrabold text-[#04102b] mb-2">결제가 완료되었습니다!</h2>
|
||||
{productName && (
|
||||
<p className="text-slate-500 text-sm mb-1">{productName}</p>
|
||||
{paymentId && (
|
||||
<p className="text-slate-400 text-xs mb-1">주문번호: {paymentId}</p>
|
||||
)}
|
||||
<p className="text-slate-400 text-sm mb-8">
|
||||
<p className="text-slate-500 text-sm mb-8">
|
||||
마이페이지에서 결제 내역과 서비스 이용 현황을 확인하세요.
|
||||
</p>
|
||||
<div className="flex justify-center gap-3 flex-wrap">
|
||||
|
||||
Reference in New Issue
Block a user