feat(phase2): 사주 AI 해석 무료화 — 결제 게이트 → 로그인 게이트
- page.tsx: hasPaid를 orders 'saju_detail' paid 조회 대신 로그인 여부(!!user)로 산출 - SajuAISection: 미로그인 시 "개편 준비 중" 안내를 /login?next= 유도 CTA로 교체 - analyze fetch가 429(일일 무료 횟수 초과)를 받으면 전용 에러 메시지 표시(재시도 버튼 숨김) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { usePathname, useSearchParams } from 'next/navigation';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
|
||||
@@ -187,6 +188,11 @@ export default function SajuAISection({
|
||||
currentUrl,
|
||||
engineData,
|
||||
}: SajuAISectionProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const search = searchParams.toString();
|
||||
const loginHref = `/login?next=${encodeURIComponent(`${pathname}${search ? `?${search}` : ''}`)}`;
|
||||
|
||||
// 저장된 해석이 mock 데이터면 재생성 필요
|
||||
const isMock = isMockInterpretation(savedInterpretation);
|
||||
const validSaved = savedInterpretation && !isMock ? savedInterpretation : null;
|
||||
@@ -196,6 +202,7 @@ export default function SajuAISection({
|
||||
);
|
||||
const [interpretation, setInterpretation] = useState(validSaved ?? '');
|
||||
const [openSections, setOpenSections] = useState<Set<number>>(new Set([0]));
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const called = useRef(false);
|
||||
|
||||
const sections = parseInterpretation(interpretation);
|
||||
@@ -221,13 +228,22 @@ export default function SajuAISection({
|
||||
setTimeout(() => {
|
||||
called.current = false;
|
||||
setStatus('loading');
|
||||
setErrorMessage(null);
|
||||
fetch('/api/saju/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(async r => {
|
||||
if (r.status === 429) return { __rateLimited: true };
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.__rateLimited) {
|
||||
setErrorMessage('오늘 무료 횟수를 모두 사용했습니다. 내일 다시 시도해주세요.');
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
if (data.interpretation && !isMockInterpretation(data.interpretation)) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
@@ -253,14 +269,23 @@ export default function SajuAISection({
|
||||
if (!hasPaid || validSaved || called.current) return;
|
||||
called.current = true;
|
||||
setStatus('loading');
|
||||
setErrorMessage(null);
|
||||
|
||||
fetch('/api/saju/analyze', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ saju: sajuData, daeun, daeunList, gender, engineData }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(async r => {
|
||||
if (r.status === 429) return { __rateLimited: true };
|
||||
return r.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.__rateLimited) {
|
||||
setErrorMessage('오늘 무료 횟수를 모두 사용했습니다. 내일 다시 시도해주세요.');
|
||||
setStatus('error');
|
||||
return;
|
||||
}
|
||||
if (data.interpretation) {
|
||||
setInterpretation(data.interpretation);
|
||||
setStatus('done');
|
||||
@@ -286,7 +311,7 @@ export default function SajuAISection({
|
||||
.catch(() => setStatus('error'));
|
||||
}, [hasPaid]);
|
||||
|
||||
// ── 미결제 ──────────────────────────────────────────────────────────
|
||||
// ── 미로그인 ────────────────────────────────────────────────────────
|
||||
if (!hasPaid) {
|
||||
return (
|
||||
<div className="bg-gradient-to-br from-[#04102b] via-[#0a1f5c] to-[#04102b] rounded-2xl border border-[#1a3a7a] p-7 text-center relative overflow-hidden">
|
||||
@@ -312,10 +337,13 @@ export default function SajuAISection({
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<a
|
||||
href={loginHref}
|
||||
className="inline-flex items-center gap-2 bg-white/10 hover:bg-white/20 text-blue-100/80 font-semibold px-7 py-3 rounded-xl transition-colors"
|
||||
>
|
||||
로그인하고 AI 상세 해석 무료로 받기
|
||||
</a>
|
||||
<p className="text-blue-200/40 text-xs mt-3">로그인 회원은 하루 1회 무료 · 저장된 해석은 언제든 다시 보기</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -343,13 +371,17 @@ export default function SajuAISection({
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div className="bg-white rounded-2xl border border-red-200 p-6 text-center">
|
||||
<p className="text-red-500 text-sm font-medium mb-3">AI 해석 생성에 실패했습니다.</p>
|
||||
<button
|
||||
onClick={() => { called.current = false; setStatus('idle'); }}
|
||||
className="text-xs text-blue-600 underline"
|
||||
>
|
||||
다시 시도하기
|
||||
</button>
|
||||
<p className="text-red-500 text-sm font-medium mb-3">
|
||||
{errorMessage ?? 'AI 해석 생성에 실패했습니다.'}
|
||||
</p>
|
||||
{!errorMessage && (
|
||||
<button
|
||||
onClick={() => { called.current = false; setStatus('idle'); }}
|
||||
className="text-xs text-blue-600 underline"
|
||||
>
|
||||
다시 시도하기
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user