feat(tarot): Reading.jsx — 3장 스프레드 메인 (T15)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
142
src/pages/tarot/Reading.jsx
Normal file
142
src/pages/tarot/Reading.jsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import './Tarot.css';
|
||||||
|
import { TAROT_DECK, CATEGORIES, SPREADS } from './data/cards';
|
||||||
|
import { useTarotShuffle } from './hooks/useTarotShuffle';
|
||||||
|
import { useTarotReading } from './hooks/useTarotReading';
|
||||||
|
import CardGrid from './components/CardGrid';
|
||||||
|
import SpreadSlots from './components/SpreadSlots';
|
||||||
|
import InterpretationPanel from './components/InterpretationPanel';
|
||||||
|
|
||||||
|
export default function Reading() {
|
||||||
|
const [category, setCategory] = useState('일반');
|
||||||
|
const [question, setQuestion] = useState('');
|
||||||
|
const [spreadId, setSpreadId] = useState('three_card');
|
||||||
|
const [step, setStep] = useState(1); // 1: 입력, 2: 뽑기, 3: 해석
|
||||||
|
const [picks, setPicks] = useState([]);
|
||||||
|
const [focusIdx, setFocusIdx] = useState(null);
|
||||||
|
|
||||||
|
const spread = SPREADS[spreadId];
|
||||||
|
const { slice, reshuffle } = useTarotShuffle(TAROT_DECK, 16);
|
||||||
|
const { status, interpretation, runInterpretAndSave, error } = useTarotReading();
|
||||||
|
|
||||||
|
const startShuffle = () => {
|
||||||
|
reshuffle();
|
||||||
|
setPicks([]);
|
||||||
|
setFocusIdx(null);
|
||||||
|
setStep(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePick = (card) => {
|
||||||
|
if (picks.length >= spread.positions.length) return;
|
||||||
|
const idx = picks.length;
|
||||||
|
const pos = spread.positions[idx];
|
||||||
|
const next = [...picks, { card, position: pos.label, reversed: card.reversed }];
|
||||||
|
setPicks(next);
|
||||||
|
if (next.length === spread.positions.length) {
|
||||||
|
setFocusIdx(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInterpret = async () => {
|
||||||
|
try {
|
||||||
|
await runInterpretAndSave({
|
||||||
|
spread_type: spreadId, category,
|
||||||
|
question: question.trim() || null, picks,
|
||||||
|
});
|
||||||
|
setStep(3);
|
||||||
|
} catch { /* error는 hook state */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledIds = picks.map((p) => p.card.slug);
|
||||||
|
const focusCard = focusIdx !== null && picks[focusIdx] ? picks[focusIdx].card : null;
|
||||||
|
const focusCardId = focusCard?.slug;
|
||||||
|
const allPicked = picks.length === spread.positions.length;
|
||||||
|
const busy = status === 'interpreting' || status === 'saving';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tarot tarot-reading">
|
||||||
|
<aside className="tarot-reading__col">
|
||||||
|
<div className="tarot-reading__step-label">1. 질문</div>
|
||||||
|
<textarea
|
||||||
|
className="tarot-reading__textarea"
|
||||||
|
value={question}
|
||||||
|
onChange={(e) => setQuestion(e.target.value)}
|
||||||
|
placeholder="궁금한 것을 적어주세요"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="tarot-reading__step-label" style={{ marginTop: 16 }}>2. 카테고리</div>
|
||||||
|
<div className="tarot-reading__chips">
|
||||||
|
{CATEGORIES.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className={`tarot-chip ${category === c ? 'is-active' : ''}`}
|
||||||
|
onClick={() => setCategory(c)}
|
||||||
|
>{c}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tarot-reading__step-label" style={{ marginTop: 16 }}>3. 스프레드</div>
|
||||||
|
<div className="tarot-reading__radio-row">
|
||||||
|
<label>
|
||||||
|
<input type="radio" checked={spreadId === 'three_card'}
|
||||||
|
onChange={() => setSpreadId('three_card')} /> 3장 (과거/현재/미래)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="radio" checked={spreadId === 'one_card'}
|
||||||
|
onChange={() => setSpreadId('one_card')} /> 1장 (오늘의 카드)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="tarot-reading__primary" onClick={startShuffle}>
|
||||||
|
⊃ 카드 셔플하기
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{step >= 2 && allPicked && step < 3 && (
|
||||||
|
<button className="tarot-reading__primary" onClick={handleInterpret} disabled={busy}>
|
||||||
|
{busy ? '해석 중…' : 'AI 해석 시작'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 3 && (
|
||||||
|
<button className="tarot-reading__primary" onClick={() => { setStep(1); setPicks([]); setFocusIdx(null); }}>
|
||||||
|
새 리딩
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && <p style={{ color: '#f43f5e', marginTop: 12, fontSize: 13 }}>{error}</p>}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="tarot-reading__col">
|
||||||
|
{step < 2 && <p style={{ color: 'var(--tarot-text-dim)' }}>좌측에서 질문·카테고리·스프레드를 선택하고 셔플하세요.</p>}
|
||||||
|
{step === 2 && (
|
||||||
|
<>
|
||||||
|
{!allPicked && (
|
||||||
|
<>
|
||||||
|
<p style={{ color: 'var(--tarot-text-dim)', marginBottom: 16 }}>
|
||||||
|
카드 {picks.length + 1}/{spread.positions.length} — {spread.positions[picks.length].label}
|
||||||
|
</p>
|
||||||
|
<CardGrid slice={slice} onPick={handlePick} disabledIds={disabledIds} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SpreadSlots
|
||||||
|
spread={spread} picks={picks}
|
||||||
|
onCardClick={(idx) => setFocusIdx(idx)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{step === 3 && (
|
||||||
|
<SpreadSlots
|
||||||
|
spread={spread} picks={picks}
|
||||||
|
onCardClick={(idx) => setFocusIdx(idx)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<InterpretationPanel
|
||||||
|
interpretation={interpretation}
|
||||||
|
selectedCard={focusCard}
|
||||||
|
focusCardId={focusCardId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user