/** * 결제 — 상세 리포트 구매 (polar.sh, PRD §3.9.3) * mock 모드(POLAR_MOCK)에서는 모의 결제 완료 버튼으로 webhook 호출. * Phase D-3: MD3 디자인 토큰 + 공용 컴포넌트(PillButton, InfoBanner, MatIcon) 적용 */ import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { http } from '../lib/api'; import PillButton from '../components/PillButton'; import InfoBanner from '../components/InfoBanner'; interface CheckoutResp { checkoutUrl?: string; orderId: string; mock: boolean; historyId: number; alreadyPaid?: boolean; } const Container = styled.div` padding-top: 16px; `; const Card = styled.div` background: var(--md3-surface); border-radius: 16px; padding: 24px 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06); border: 1px solid var(--md3-outline-variant); text-align: center; `; const Price = styled.div` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 36px; font-weight: 800; color: var(--md3-primary); margin: 12px 0; `; const Desc = styled.p` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 13px; color: var(--md3-on-surface-variant); margin: 0; `; const PageTitle = styled.h2` font-family: 'Inter', 'Pretendard', system-ui, sans-serif; font-size: 20px; font-weight: 700; color: var(--md3-on-surface); margin: 0 0 16px; `; const PaymentButton = styled(PillButton)` && { width: 100%; margin-top: 8px; } `; const MockButton = styled(PillButton)` && { width: 100%; margin-top: 8px; background: var(--md3-tertiary) !important; color: var(--md3-on-tertiary) !important; &:hover:not(:disabled) { background: var(--md3-tertiary-container) !important; } &:active:not(:disabled) { background: var(--md3-tertiary-container) !important; } &:disabled { background: var(--md3-outline-variant) !important; color: var(--md3-on-surface-variant) !important; } } `; const Mt16 = styled.div` margin-top: 16px; `; export default function Checkout() { const { round } = useParams(); const nav = useNavigate(); const [order, setOrder] = useState(null); const [busy, setBusy] = useState(false); const createOrder = async () => { setBusy(true); try { const res = await http.post('/api/payment/checkout', { round: Number(round) }); setOrder(res); if (res.alreadyPaid) { nav(`/history/${round}`); return; } if (!res.mock && res.checkoutUrl) { window.location.href = res.checkoutUrl; } } catch (err: any) { alert(err?.message ?? '결제 주문 생성에 실패했습니다.'); } finally { setBusy(false); } }; const mockPay = async () => { if (!order) return; setBusy(true); try { const payload = { type: 'order.paid', data: { id: order.orderId, amount: 15000, metadata: { historyId: String(order.historyId) } }, }; const res = await fetch('/api/payment/webhook', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); if (res.ok) { nav(`/history/${round}`); } else { alert('모의 결제 처리에 실패했습니다.'); } } finally { setBusy(false); } }; return ( 상세 리포트 구매 {round}회차 검사 · 상세 프로파일 ₩15,000 강점/약점 상세 분석, 지표별 해설, 전문가 권고사항 포함 {!order && ( {busy ? '주문 생성 중…' : '결제하기'} )} {order?.mock && ( {busy ? '처리 중…' : '모의 결제 완료 (개발용)'} )} {order?.alreadyPaid && ( 이미 결제된 내역입니다. nav(`/history/${round}`)}>상세 보기 )} {order && !order.mock && !order.checkoutUrl && ( 결제 URL을 받지 못했습니다. 다시 시도해주세요. )} ); }