diff --git a/app/api/payment/confirm/route.ts b/app/api/payment/confirm/route.ts index d013de0..e72da97 100644 --- a/app/api/payment/confirm/route.ts +++ b/app/api/payment/confirm/route.ts @@ -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 }); diff --git a/app/components/DashboardShell.tsx b/app/components/DashboardShell.tsx index 4bb3b5f..c12e959 100644 --- a/app/components/DashboardShell.tsx +++ b/app/components/DashboardShell.tsx @@ -55,6 +55,11 @@ export default function DashboardShell({ children }: { children: React.ReactNode 전화: 010-3907-1392 이메일: bgg8988@gmail.com +
+ 이용약관 + 개인정보처리방침 + 환불 정책 +

© 2025 쟁승메이드. All rights reserved.

diff --git a/app/components/PaymentButton.tsx b/app/components/PaymentButton.tsx index aeed4fc..37ebc6d 100644 --- a/app/components/PaymentButton.tsx +++ b/app/components/PaymentButton.tsx @@ -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 ( -
- - {/* dev/test 환경에서만 표시되는 배지 — 실수로 실결제 누르는 것 방지 */} - {isTestMode && ( - - TEST - + <> +
+ + {isTestMode && ( + + TEST + + )} +
+ + {/* 결제수단 선택 모달 */} + {showMethodPicker && ( +
setShowMethodPicker(false)} + > +
e.stopPropagation()} + > +
+
+
+ 쟁 +
+ 결제수단 선택 +
+ +
+ +
+

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

+
+ {channels.map((channel) => ( + + ))} +
+
+
+
)} -
+ ); } diff --git a/app/legal/privacy/page.tsx b/app/legal/privacy/page.tsx new file mode 100644 index 0000000..7b3b84b --- /dev/null +++ b/app/legal/privacy/page.tsx @@ -0,0 +1,177 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '개인정보처리방침 | 쟁승메이드', + description: '쟁승메이드 개인정보처리방침', +}; + +export default function PrivacyPage() { + return ( +
+

개인정보처리방침

+ +
+

+ 쟁승메이드(이하 "회사")는 개인정보보호법 등 관련 법령을 준수하며, + 이용자의 개인정보를 보호하기 위해 다음과 같은 개인정보처리방침을 수립·공개합니다. +

+ +
+

1. 수집하는 개인정보 항목

+

회사는 서비스 제공을 위해 다음의 개인정보를 수집합니다.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
수집 시점수집 항목
회원가입이메일, 이름(닉네임)
문의 접수이름, 이메일, 연락처, 문의 내용
유료 결제결제 수단 정보(PG사를 통해 처리, 회사 직접 저장하지 않음)
사주 분석생년월일, 출생시간, 성별
자동 수집접속 IP, 브라우저 종류, 접속 일시, 쿠키
+
+ +
+

2. 개인정보의 수집·이용 목적

+
    +
  • 서비스 제공 및 계약 이행: 문의 응대, 서비스 제공, 결제 처리
  • +
  • 회원 관리: 회원 식별, 본인 확인, 서비스 이용 내역 관리
  • +
  • 서비스 개선: 접속 통계, 이용 패턴 분석을 통한 서비스 개선
  • +
  • 마케팅·홍보: 신규 서비스 안내, 이벤트 정보 제공 (별도 동의 시)
  • +
+
+ +
+

3. 개인정보의 보유 및 이용 기간

+
    +
  • 회원 정보: 회원 탈퇴 시까지 (탈퇴 후 즉시 파기)
  • +
  • 문의 내역: 문의 처리 완료 후 3년 (전자상거래법)
  • +
  • 결제 기록: 5년 (전자상거래법)
  • +
  • 접속 로그: 3개월 (통신비밀보호법)
  • +
+
+ +
+

4. 개인정보의 제3자 제공

+

+ 회사는 이용자의 동의 없이 개인정보를 제3자에게 제공하지 않습니다. + 다만, 다음의 경우는 예외로 합니다. +

+
    +
  • 이용자가 사전에 동의한 경우
  • +
  • 법령에 의해 요구되는 경우
  • +
  • 결제 처리를 위해 PG사(포트원, 한국결제네트웍스 등)에 필요 최소 정보 전달
  • +
+
+ +
+

5. 개인정보의 처리 위탁

+ + + + + + + + + + + + + + + + + + + + + + + + + +
수탁업체위탁 업무
포트원(주)전자결제 처리
Supabase Inc.회원 인증 및 데이터 저장
Vercel Inc.웹사이트 호스팅
Resend Inc.이메일 발송
+
+ +
+

6. 개인정보의 파기 절차 및 방법

+
    +
  • 파기 절차: 보유 기간 경과 또는 처리 목적 달성 시 지체 없이 파기
  • +
  • 파기 방법: 전자적 파일은 복구 불가능한 방법으로 삭제, 종이 문서는 분쇄 또는 소각
  • +
+
+ +
+

7. 이용자의 권리

+

이용자는 언제든지 다음의 권리를 행사할 수 있습니다.

+
    +
  • 개인정보 열람 요구
  • +
  • 개인정보 정정·삭제 요구
  • +
  • 개인정보 처리 정지 요구
  • +
  • 회원 탈퇴 (마이페이지 또는 이메일 요청)
  • +
+

+ 위 요청은 이메일(bgg8988@gmail.com)로 접수하면 10일 이내에 처리합니다. +

+
+ +
+

8. 쿠키(Cookie) 운용

+

+ 회사는 서비스 이용 분석을 위해 Google Analytics를 사용하며, 이 과정에서 쿠키가 설치됩니다. + 이용자는 브라우저 설정을 통해 쿠키 저장을 거부할 수 있으나, 일부 서비스 이용이 제한될 수 있습니다. +

+
+ +
+

9. 개인정보 보호책임자

+
    +
  • 성명: 박재오
  • +
  • 직위: 대표
  • +
  • 이메일: bgg8988@gmail.com
  • +
  • 전화: 010-3907-1392
  • +
+
+ +
+

10. 개인정보 침해 구제

+

개인정보 침해에 대한 신고·상담은 아래 기관에 문의하실 수 있습니다.

+
    +
  • 개인정보분쟁조정위원회: 1833-6972 (kopico.go.kr)
  • +
  • 개인정보침해신고센터: 118 (privacy.kisa.or.kr)
  • +
  • 대검찰청 사이버수사과: 1301 (spo.go.kr)
  • +
  • 경찰청 사이버수사국: 182 (ecrm.cyber.go.kr)
  • +
+
+ +
+

+ 시행일자: 2025년 4월 7일
+ 상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822 +

+
+
+
+ ); +} diff --git a/app/legal/refund/page.tsx b/app/legal/refund/page.tsx new file mode 100644 index 0000000..a7429b3 --- /dev/null +++ b/app/legal/refund/page.tsx @@ -0,0 +1,148 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '환불 정책 | 쟁승메이드', + description: '쟁승메이드 환불 및 취소 정책', +}; + +export default function RefundPage() { + return ( +
+

환불 정책

+ +
+

+ 쟁승메이드(이하 "회사")는 공정하고 투명한 환불 정책을 운영합니다. + 서비스 유형에 따라 환불 조건이 다르므로 아래 내용을 확인해주세요. +

+ +
+

1. 디지털 콘텐츠 (즉시 제공 상품)

+

대상: 프롬프트 패키지, AI 사주 리포트, 로또 분석 등

+ + + + + + + + + + + + + + + + + + + + + +
시점환불 가능 여부
결제 후 다운로드/열람 전전액 환불
다운로드/열람 후 7일 이내서비스 하자 시 환불 (정상 제공 시 환불 불가)
7일 경과환불 불가
+

+ * 전자상거래법 제17조에 따라, 디지털 콘텐츠는 이용 후 청약철회가 제한될 수 있습니다. +

+
+ +
+

2. 구독 서비스

+

대상: AI 자동화 키트 월 구독, 로또 월간 플랜, 주식 월 유지비 등

+ + + + + + + + + + + + + + + + + + + + + +
시점환불 가능 여부
결제 후 서비스 이용 전전액 환불
이용 시작 후 7일 이내이용일수 공제 후 환불
7일 경과당월 환불 불가, 다음 결제일 전 해지 가능
+

+ * 구독 해지는 마이페이지 또는 이메일(bgg8988@gmail.com)로 요청 가능합니다. + 해지 시 다음 결제일부터 과금이 중지되며, 당월 잔여 기간은 계속 이용 가능합니다. +

+
+ +
+

3. 외주 개발 서비스

+

대상: 홈페이지 제작, 업무 자동화, 맞춤 개발 등

+ + + + + + + + + + + + + + + + + + + + + +
시점환불 가능 여부
착수 전 (선금 결제 후 작업 시작 전)선금 전액 환불
작업 진행 중진행률에 따른 부분 환불 (협의)
납품 완료 후환불 불가 (하자 보수는 별도)
+

+ * 외주 개발은 선금 50% / 잔금 50% 구조이며, 계약서에 명시된 조건을 우선합니다. +
+ * 회사 귀책으로 납기 지연 시 1일당 계약금의 1%를 차감합니다. +

+
+ +
+

4. 환불 신청 방법

+
+
    +
  1. 이메일(bgg8988@gmail.com)로 환불 요청 +
      +
    • 제목: [환불 요청] 주문번호 또는 서비스명
    • +
    • 본문: 성함, 결제일, 환불 사유
    • +
    +
  2. +
  3. 담당자 확인 후 영업일 기준 3일 이내 환불 처리
  4. +
  5. 환불 금액은 원래 결제 수단으로 환급 (카드 취소는 카드사에 따라 3~7영업일 소요)
  6. +
+
+
+ +
+

5. 환불이 불가능한 경우

+
    +
  • 이용자의 단순 변심으로 디지털 콘텐츠를 이미 열람/다운로드한 경우
  • +
  • 서비스 이용 후 7일이 경과한 경우
  • +
  • 이용자의 과실로 서비스 이용이 불가능해진 경우
  • +
  • 관련 법령에 의해 환불이 제한되는 경우
  • +
+
+ +
+

+ 시행일자: 2025년 4월 7일
+ 상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822
+ 환불 문의: bgg8988@gmail.com · 010-3907-1392 +

+
+
+
+ ); +} diff --git a/app/legal/terms/page.tsx b/app/legal/terms/page.tsx new file mode 100644 index 0000000..daf0343 --- /dev/null +++ b/app/legal/terms/page.tsx @@ -0,0 +1,138 @@ +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: '이용약관 | 쟁승메이드', + description: '쟁승메이드 서비스 이용약관', +}; + +export default function TermsPage() { + return ( +
+

이용약관

+ +
+
+

제1조 (목적)

+

+ 본 약관은 쟁승메이드(이하 "회사")가 운영하는 웹사이트(jaengseung-made.com)를 통해 + 제공하는 서비스(이하 "서비스")의 이용 조건 및 절차, 회사와 이용자의 권리·의무 및 + 책임사항을 규정함을 목적으로 합니다. +

+
+ +
+

제2조 (정의)

+
    +
  1. "서비스"란 회사가 제공하는 웹 개발, AI 자동화, 프롬프트 엔지니어링, 사주 분석, 로또 번호 추천 등 각종 디지털 서비스를 말합니다.
  2. +
  3. "이용자"란 본 약관에 따라 회사가 제공하는 서비스를 이용하는 회원 및 비회원을 말합니다.
  4. +
  5. "회원"이란 회사에 개인정보를 제공하여 회원 등록을 한 자로서, 회사의 서비스를 지속적으로 이용할 수 있는 자를 말합니다.
  6. +
+
+ +
+

제3조 (약관의 효력 및 변경)

+
    +
  1. 본 약관은 서비스 화면에 게시하거나 기타의 방법으로 이용자에게 공지함으로써 효력이 발생합니다.
  2. +
  3. 회사는 관련 법령을 위반하지 않는 범위에서 본 약관을 변경할 수 있으며, 변경 시 적용일자 및 변경 사유를 명시하여 최소 7일 전 공지합니다.
  4. +
+
+ +
+

제4조 (서비스의 제공 및 변경)

+
    +
  1. 회사는 다음과 같은 서비스를 제공합니다. +
      +
    • 홈페이지 제작 서비스
    • +
    • 업무 자동화 개발 서비스
    • +
    • 프롬프트 엔지니어링 서비스
    • +
    • AI 자동화 키트 구독 서비스
    • +
    • AI 사주 분석 서비스
    • +
    • 로또 번호 추천 서비스
    • +
    • 주식 자동 매매 프로그램
    • +
    • 기타 회사가 추가 개발하거나 제휴를 통해 제공하는 서비스
    • +
    +
  2. +
  3. 회사는 서비스의 내용을 변경하는 경우 변경 사항을 사전에 공지합니다.
  4. +
+
+ +
+

제5조 (서비스의 중단)

+

+ 회사는 컴퓨터 등 정보통신설비의 보수·점검, 교체 및 고장, 통신두절 또는 천재지변 등의 사유가 + 발생한 경우 서비스의 제공을 일시적으로 중단할 수 있습니다. 이 경우 회사는 가능한 한 사전에 공지합니다. +

+
+ +
+

제6조 (회원가입)

+
    +
  1. 이용자는 회사가 정한 가입 양식에 따라 회원 정보를 기입한 후 본 약관에 동의하여 회원 가입을 신청합니다.
  2. +
  3. 회사는 전항에 따른 회원 가입 신청에 대해 원칙적으로 승낙합니다. 다만, 다음 각 호에 해당하는 경우 거부할 수 있습니다. +
      +
    • 가입 신청자가 본 약관에 의해 이전에 회원 자격을 상실한 적이 있는 경우
    • +
    • 허위 정보를 기재한 경우
    • +
    • 기타 회사가 정한 이용신청 요건이 미비된 경우
    • +
    +
  4. +
+
+ +
+

제7조 (결제 및 요금)

+
    +
  1. 유료 서비스의 이용 요금은 해당 서비스 페이지에 명시된 금액을 따릅니다.
  2. +
  3. 결제는 신용카드, 간편결제(카카오페이, 네이버페이, 토스페이) 등 회사가 지원하는 수단으로 가능합니다.
  4. +
  5. 결제 완료 후에는 회사의 환불 정책에 따라 환불이 처리됩니다.
  6. +
+
+ +
+

제8조 (이용자의 의무)

+
    +
  1. 이용자는 서비스 이용 시 관련 법령, 본 약관, 이용 안내 등을 준수하여야 합니다.
  2. +
  3. 이용자는 다음 행위를 하여서는 안 됩니다. +
      +
    • 타인의 정보를 도용하는 행위
    • +
    • 회사의 서비스를 이용하여 얻은 정보를 무단으로 복제, 배포하는 행위
    • +
    • 회사 및 제3자의 지적 재산권을 침해하는 행위
    • +
    • 서비스의 안정적 운영을 방해하는 행위
    • +
    +
  4. +
+
+ +
+

제9조 (회사의 의무)

+
    +
  1. 회사는 관련 법령과 본 약관이 정하는 바에 따라 지속적이고 안정적으로 서비스를 제공하기 위해 노력합니다.
  2. +
  3. 회사는 이용자의 개인정보를 보호하기 위해 개인정보처리방침을 수립하고 이를 준수합니다.
  4. +
+
+ +
+

제10조 (면책조항)

+
    +
  1. 회사는 천재지변 또는 이에 준하는 불가항력으로 서비스를 제공할 수 없는 경우에는 책임이 면제됩니다.
  2. +
  3. AI 기반 서비스(사주 분석, 로또 추천 등)는 참고 목적으로 제공되며, 서비스 결과에 대한 정확성을 보증하지 않습니다.
  4. +
  5. 이용자의 귀책사유로 인한 서비스 이용 장애에 대하여 회사는 책임을 지지 않습니다.
  6. +
+
+ +
+

제11조 (분쟁 해결)

+

+ 본 약관에 관한 분쟁은 대한민국 법률을 준거법으로 하며, 서울중앙지방법원을 관할법원으로 합니다. +

+
+ +
+

+ 시행일자: 2025년 4월 7일
+ 상호: 쟁승메이드 · 대표: 박재오 · 사업자등록번호: 267-53-00822 +

+
+
+
+ ); +} diff --git a/app/payment/fail/page.tsx b/app/payment/fail/page.tsx index 0c7b519..b47ef19 100644 --- a/app/payment/fail/page.tsx +++ b/app/payment/fail/page.tsx @@ -17,10 +17,10 @@ function FailContent() {
- {code === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'} + {code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제 취소' : '결제 실패'}

- {code === 'PAY_PROCESS_CANCELED' ? '결제를 취소하셨습니다' : '결제에 실패했습니다'} + {code === 'USER_CANCEL' || code === 'PAY_PROCESS_CANCELED' ? '결제를 취소하셨습니다' : '결제에 실패했습니다'}

{message}

diff --git a/app/payment/success/page.tsx b/app/payment/success/page.tsx index 6a157cf..12f062a 100644 --- a/app/payment/success/page.tsx +++ b/app/payment/success/page.tsx @@ -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 ( -
-
-

결제를 확인하는 중...

-
- ); - } - - if (status === 'error') { - return ( -
-
- - - -
-

결제 처리 실패

-

{errorMsg}

-
- - 결제 내역 확인 - - - 홈으로 → - -
-
- ); - } + const paymentId = params.get('paymentId'); return (
@@ -89,10 +19,10 @@ function SuccessContent() { 결제 완료

결제가 완료되었습니다!

- {productName && ( -

{productName}

+ {paymentId && ( +

주문번호: {paymentId}

)} -

+

마이페이지에서 결제 내역과 서비스 이용 현황을 확인하세요.

diff --git a/lib/payment-channels.ts b/lib/payment-channels.ts new file mode 100644 index 0000000..0ab839d --- /dev/null +++ b/lib/payment-channels.ts @@ -0,0 +1,50 @@ +/** + * 포트원 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/package-lock.json b/package-lock.json index e768167..d74c05d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,9 +11,9 @@ "@anthropic-ai/sdk": "^0.79.0", "@google-analytics/data": "^5.2.1", "@google/generative-ai": "^0.24.1", + "@portone/browser-sdk": "^0.1.3", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.99.0", - "@tosspayments/tosspayments-sdk": "^2.6.0", "cheerio": "^1.2.0", "dotenv": "^17.3.1", "lunar-javascript": "^1.7.7", @@ -1358,6 +1358,11 @@ "node": ">=14" } }, + "node_modules/@portone/browser-sdk": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@portone/browser-sdk/-/browser-sdk-0.1.3.tgz", + "integrity": "sha512-GRmXb8gBLs/23CES1YRvzMggbL8+tFeH5WS9YSA6WUPu5/9kgRKKZLeC25N2raq3zVJd9OfDTeSf0tUr27vF1Q==" + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -1821,12 +1826,6 @@ "tailwindcss": "4.1.18" } }, - "node_modules/@tosspayments/tosspayments-sdk": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@tosspayments/tosspayments-sdk/-/tosspayments-sdk-2.6.0.tgz", - "integrity": "sha512-d/9tCj5d+Jbj312bWYXo9dtGNzQaWRxTKWmw6rTwuDgw5g4mrJWV9dP9qiN6/x9PYaphbYDjxGenguHoVQXTGA==", - "license": "MIT" - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", diff --git a/package.json b/package.json index 1867a1d..b191f01 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,9 @@ "@anthropic-ai/sdk": "^0.79.0", "@google-analytics/data": "^5.2.1", "@google/generative-ai": "^0.24.1", + "@portone/browser-sdk": "^0.1.3", "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.99.0", - "@tosspayments/tosspayments-sdk": "^2.6.0", "cheerio": "^1.2.0", "dotenv": "^17.3.1", "lunar-javascript": "^1.7.7",