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