feat(tarot): TarotCard·CardGrid·SpreadSlots·InterpretationPanel 컴포넌트 (T11)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
21
src/pages/tarot/components/CardGrid.jsx
Normal file
21
src/pages/tarot/components/CardGrid.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import TarotCard from './TarotCard';
|
||||
|
||||
export default function CardGrid({ slice, onPick, disabledIds = [] }) {
|
||||
return (
|
||||
<div className="tarot-grid">
|
||||
{slice.map((card) => {
|
||||
const disabled = disabledIds.includes(card.slug);
|
||||
return (
|
||||
<TarotCard
|
||||
key={card.slug}
|
||||
card={card}
|
||||
faceDown
|
||||
clickable={!disabled}
|
||||
onClick={() => !disabled && onPick(card)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
src/pages/tarot/components/InterpretationPanel.jsx
Normal file
92
src/pages/tarot/components/InterpretationPanel.jsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
function ConfidenceBadge({ level }) {
|
||||
if (!level) return null;
|
||||
const cls = level === 'high' ? 'is-high' : level === 'low' ? 'is-low' : 'is-medium';
|
||||
const text = level === 'high' ? '높음' : level === 'low' ? '낮음' : '보통';
|
||||
return <span className={`tarot-confidence ${cls}`}>확신 {text}</span>;
|
||||
}
|
||||
|
||||
export default function InterpretationPanel({ interpretation, selectedCard, focusCardId }) {
|
||||
const [showEvidence, setShowEvidence] = useState(true);
|
||||
|
||||
if (!interpretation) {
|
||||
return (
|
||||
<aside className="tarot-panel tarot-panel--empty">
|
||||
<p>카드를 모두 뽑은 후 AI 해석을 시작하세요.</p>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
const cardDetail = focusCardId
|
||||
? (interpretation.cards || []).find((c) => c.card === focusCardId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<aside className="tarot-panel">
|
||||
{selectedCard && (
|
||||
<header className="tarot-panel__head">
|
||||
<h3 className="tarot-panel__title">{selectedCard.name}</h3>
|
||||
<p className="tarot-panel__sub">{selectedCard.nameEn}</p>
|
||||
<div className="tarot-panel__chips">
|
||||
{(selectedCard.keywords || []).slice(0, 4).map((k) => (
|
||||
<span key={k} className="tarot-chip">{k}</span>
|
||||
))}
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
{cardDetail && (
|
||||
<section className="tarot-panel__section">
|
||||
<h4>이 위치의 해석</h4>
|
||||
<p>{cardDetail.interpretation}</p>
|
||||
<p className="tarot-panel__advice">💡 {cardDetail.advice}</p>
|
||||
<button
|
||||
type="button"
|
||||
className="tarot-panel__toggle"
|
||||
onClick={() => setShowEvidence((v) => !v)}
|
||||
>
|
||||
{showEvidence ? '근거 접기' : '근거 펼치기'}
|
||||
</button>
|
||||
{showEvidence && cardDetail.evidence && (
|
||||
<dl className="tarot-evidence">
|
||||
<dt>카드 의미</dt>
|
||||
<dd>{cardDetail.evidence.card_meaning_used}</dd>
|
||||
<dt>위치 결합</dt>
|
||||
<dd>{cardDetail.evidence.position_logic}</dd>
|
||||
<dt>카테고리 관점</dt>
|
||||
<dd>{cardDetail.evidence.category_lens}</dd>
|
||||
</dl>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="tarot-panel__section">
|
||||
<h4>종합 해석 <ConfidenceBadge level={interpretation.confidence} /></h4>
|
||||
<p>{interpretation.summary}</p>
|
||||
<p className="tarot-panel__advice">💡 {interpretation.advice}</p>
|
||||
{interpretation.warning && (
|
||||
<p className="tarot-panel__warning">⚠️ {interpretation.warning}</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{(interpretation.interactions || []).length > 0 && (
|
||||
<section className="tarot-panel__section">
|
||||
<h4>카드 상호작용</h4>
|
||||
<ul className="tarot-interactions">
|
||||
{interpretation.interactions.map((it, i) => (
|
||||
<li key={i}>
|
||||
<span className={`tarot-interaction-type tarot-interaction-type--${it.type}`}>
|
||||
{it.type === 'synergy' ? '시너지' : it.type === 'conflict' ? '충돌' : '전환'}
|
||||
</span>
|
||||
{' '}
|
||||
<strong>{(it.between || []).join(' ↔ ')}</strong>
|
||||
<p>{it.explanation}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
27
src/pages/tarot/components/SpreadSlots.jsx
Normal file
27
src/pages/tarot/components/SpreadSlots.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import TarotCard from './TarotCard';
|
||||
|
||||
export default function SpreadSlots({ spread, picks, onCardClick }) {
|
||||
return (
|
||||
<div className="tarot-slots">
|
||||
{spread.positions.map((pos) => {
|
||||
const pick = picks[pos.idx];
|
||||
return (
|
||||
<div key={pos.idx} className="tarot-slots__cell">
|
||||
<div className="tarot-slots__label">{pos.label}</div>
|
||||
{pick ? (
|
||||
<TarotCard
|
||||
card={pick.card}
|
||||
reversed={pick.reversed}
|
||||
clickable
|
||||
onClick={() => onCardClick(pos.idx)}
|
||||
/>
|
||||
) : (
|
||||
<div className="tarot-slots__empty">_</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
src/pages/tarot/components/TarotCard.jsx
Normal file
50
src/pages/tarot/components/TarotCard.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function TarotCard({
|
||||
card, reversed = false, size = 'md', faceDown = false,
|
||||
clickable = false, onClick, label,
|
||||
}) {
|
||||
const sizeClass = size === 'sm' ? 'tarot-card--sm' : size === 'lg' ? 'tarot-card--lg' : 'tarot-card--md';
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (!clickable) return;
|
||||
onClick?.(card, e);
|
||||
};
|
||||
|
||||
if (faceDown || !card) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`tarot-card tarot-card--back ${sizeClass} ${clickable ? 'is-clickable' : ''}`}
|
||||
onClick={handleClick}
|
||||
aria-label={label || '카드 뒷면'}
|
||||
disabled={!clickable}
|
||||
>
|
||||
<img src="/images/tarot/card_back.svg" alt="" draggable={false} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const styleClass = reversed ? 'tarot-card--reversed' : '';
|
||||
const onImgError = (e) => { e.currentTarget.style.display = 'none'; };
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={`tarot-card tarot-card--face ${sizeClass} ${styleClass} ${clickable ? 'is-clickable' : ''}`}
|
||||
onClick={handleClick}
|
||||
aria-label={`${card.name}${reversed ? ' 역방향' : ''}`}
|
||||
disabled={!clickable}
|
||||
>
|
||||
<div className="tarot-card__inner">
|
||||
<img src={card.image} alt="" onError={onImgError} draggable={false} />
|
||||
<div className="tarot-card__fallback">
|
||||
<div className="tarot-card__symbol">{card.arcana === 'major' ? '✦' : '◆'}</div>
|
||||
<div className="tarot-card__name">{card.name}</div>
|
||||
<div className="tarot-card__name-en">{card.nameEn}</div>
|
||||
</div>
|
||||
</div>
|
||||
{label && <div className="tarot-card__label">{label}</div>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user