Files
web-page/src/pages/tarot/Reading.jsx
gahusb 423304dce3 feat(tarot): 시안 기반 UI 재구성 — 랜딩 좌→우 그라데이션 + Reading 테이블 배경
랜딩(tarot_main_landing_page.png 참고):
- hero overlay를 full-screen dark에서 좌→우 그라데이션으로 변경
- 좌측만 어둡게 (텍스트 가독), 우측은 영상 선명히 노출

Reading(tarot_card_select_page.png 참고):
- tarot_table.png 배경 fixed (보라 신비 톤 + vignette)
- 상단 step indicator (질문 & 설정 → 카드 선택 → 해석)
- 패널 backdrop-filter blur + 금색 보더로 시안 느낌 강화
- 하단 남은 카드 row 미리보기 (12장)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 01:12:04 +09:00

202 lines
7.0 KiB
JavaScript

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 TarotCard from './components/TarotCard';
import CardGrid from './components/CardGrid';
import SpreadSlots from './components/SpreadSlots';
import InterpretationPanel from './components/InterpretationPanel';
const STEPS = [
{ id: 1, label: '질문 & 설정' },
{ id: 2, label: '카드 선택' },
{ id: 3, label: '해석' },
];
function StepIndicator({ current }) {
return (
<div className="tarot-reading-steps">
{STEPS.map((s, i) => (
<React.Fragment key={s.id}>
<div className={`tarot-reading-steps__item ${s.id < current ? 'is-done' : ''} ${s.id === current ? 'is-active' : ''}`}>
<span className="tarot-reading-steps__dot">{s.id < current ? '✓' : s.id}</span>
<span>{s.label}</span>
</div>
{i < STEPS.length - 1 && <div className="tarot-reading-steps__sep" />}
</React.Fragment>
))}
</div>
);
}
export default function Reading() {
const [category, setCategory] = useState('일반');
const [question, setQuestion] = useState('');
const [spreadId, setSpreadId] = useState('three_card');
const [step, setStep] = useState(1);
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 restart = () => {
setStep(1); setPicks([]); setFocusIdx(null);
};
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';
const remaining = slice.filter((c) => !disabledIds.includes(c.slug));
return (
<div className="tarot tarot-reading-page">
<StepIndicator current={step} />
<div className="tarot-reading">
<aside className="tarot-reading__col">
<div className="tarot-reading__step-label">질문</div>
<textarea
className="tarot-reading__textarea"
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="질문을 입력하세요"
/>
<div className="tarot-reading__step-label" style={{ marginTop: 16 }}>카테고리</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 }}>스프레드 선택</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>
{step === 1 && (
<button className="tarot-reading__primary" onClick={startShuffle}>
카드 셔플하기
</button>
)}
{step === 2 && !allPicked && (
<button className="tarot-reading__primary" onClick={() => reshuffle()}>
다시 셔플
</button>
)}
{step === 2 && allPicked && (
<button className="tarot-reading__primary" onClick={handleInterpret} disabled={busy}>
{busy ? '해석 중…' : 'AI 해석 시작'}
</button>
)}
{step === 3 && (
<button className="tarot-reading__primary" onClick={restart}>
리딩
</button>
)}
{error && <p style={{ color: '#f43f5e', marginTop: 12, fontSize: 13 }}>{error}</p>}
</aside>
<div className="tarot-reading__col tarot-reading__center">
{step < 2 && (
<p style={{ color: 'var(--tarot-text-dim)', textAlign: 'center', padding: '60px 20px' }}>
좌측에서 질문·카테고리·스프레드를 선택하고 셔플하세요.
</p>
)}
{step === 2 && (
<>
{!allPicked ? (
<>
<p style={{ color: 'var(--tarot-text-dim)', marginBottom: 20, letterSpacing: 1 }}>
카드 {picks.length + 1}/{spread.positions.length} <span style={{ color: 'var(--tarot-gold)' }}>{spread.positions[picks.length].label}</span>
</p>
<CardGrid slice={slice} onPick={handlePick} disabledIds={disabledIds} />
{picks.length > 0 && (
<SpreadSlots
spread={spread} picks={picks}
onCardClick={(idx) => setFocusIdx(idx)}
/>
)}
</>
) : (
<SpreadSlots
spread={spread} picks={picks}
onCardClick={(idx) => setFocusIdx(idx)}
/>
)}
</>
)}
{step === 3 && (
<SpreadSlots
spread={spread} picks={picks}
onCardClick={(idx) => setFocusIdx(idx)}
/>
)}
{(step === 2 || step === 3) && remaining.length > 0 && allPicked && (
<div className="tarot-reading__preview-row">
{remaining.slice(0, 12).map((c) => (
<TarotCard key={c.slug} card={c} faceDown size="sm" />
))}
</div>
)}
</div>
<InterpretationPanel
interpretation={interpretation}
selectedCard={focusCard}
focusCardId={focusCardId}
/>
</div>
</div>
);
}