feat(tarot): 카드 78장 메타데이터 (메이저 22 + 마이너 56) (T8)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
191
src/pages/tarot/data/cards.js
Normal file
191
src/pages/tarot/data/cards.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const cardImage = (slug) => `/images/tarot/cards/${slug}.png`;
|
||||||
|
|
||||||
|
const MAJOR_ARCANA = [
|
||||||
|
{ id: 0, slug: 'the-fool', name: '바보', nameEn: 'The Fool', element: 'air',
|
||||||
|
keywords: ['새로운 시작','도약','순수','자유','무한한 가능성'],
|
||||||
|
reversedKeywords: ['무모함','경솔함','위험','방향 상실','준비 부족'],
|
||||||
|
meaningUpright: '미지의 세계로 내딛는 첫걸음. 계산보다 직관과 신뢰로 시작하는 시기. 위험은 있으나 그 자체가 성장의 통로.',
|
||||||
|
meaningReversed: '준비 없이 뛰어들어 위험을 자초하거나, 두려움으로 첫걸음을 미루는 상태.' },
|
||||||
|
{ id: 1, slug: 'the-magician', name: '마법사', nameEn: 'The Magician', element: 'air',
|
||||||
|
keywords: ['의지','창조','집중','실행력','자기 효능감'],
|
||||||
|
reversedKeywords: ['조작','자기 기만','산만함','잠재력 미발현'],
|
||||||
|
meaningUpright: '내가 가진 자원과 의지를 명확히 모아 현실로 옮길 수 있는 시기. 시작과 추진력이 일치한다.',
|
||||||
|
meaningReversed: '의도가 흐려지거나 능력을 잘못 사용해 자기 기만에 빠질 위험.' },
|
||||||
|
{ id: 2, slug: 'the-high-priestess', name: '여사제', nameEn: 'The High Priestess', element: 'water',
|
||||||
|
keywords: ['직관','내면의 지혜','비밀','잠재의식','신비'],
|
||||||
|
reversedKeywords: ['직관 무시','정보 단절','억압','표면적 판단'],
|
||||||
|
meaningUpright: '드러나지 않은 진실을 들여다볼 시기. 외부 답이 아닌 내면의 신호에 귀 기울일 때 길이 보인다.',
|
||||||
|
meaningReversed: '직관을 무시하거나 비밀이 노출되어 균형이 깨지는 상태.' },
|
||||||
|
{ id: 3, slug: 'the-empress', name: '여황제', nameEn: 'The Empress', element: 'earth',
|
||||||
|
keywords: ['풍요','창조성','어머니','자연','감각적 충만'],
|
||||||
|
reversedKeywords: ['창조 정체','과보호','의존','정서적 소진'],
|
||||||
|
meaningUpright: '풍요와 창조가 무르익는 시기. 보살핌·예술·자연과의 연결에서 에너지가 자라남.',
|
||||||
|
meaningReversed: '돌봄이 과해 자신을 잃거나, 창조 흐름이 정체된 상태.' },
|
||||||
|
{ id: 4, slug: 'the-emperor', name: '황제', nameEn: 'The Emperor', element: 'fire',
|
||||||
|
keywords: ['권위','구조','책임','통제','아버지'],
|
||||||
|
reversedKeywords: ['독선','경직','통제 욕구','권위 남용'],
|
||||||
|
meaningUpright: '질서와 책임을 세워 안정을 만드는 시기. 명확한 경계와 원칙이 힘이 된다.',
|
||||||
|
meaningReversed: '경직된 통제가 관계를 막거나, 권위 남용으로 신뢰가 깨질 위험.' },
|
||||||
|
{ id: 5, slug: 'the-hierophant', name: '교황', nameEn: 'The Hierophant', element: 'earth',
|
||||||
|
keywords: ['전통','가르침','믿음','제도','조언자'],
|
||||||
|
reversedKeywords: ['관습 거부','독학','권위 도전','형식주의'],
|
||||||
|
meaningUpright: '전통과 멘토의 지혜를 빌릴 때. 검증된 길과 가르침이 도움을 준다.',
|
||||||
|
meaningReversed: '관습이 답이 되지 않거나, 자기만의 길을 새로 찾고 싶은 시기.' },
|
||||||
|
{ id: 6, slug: 'the-lovers', name: '연인', nameEn: 'The Lovers', element: 'air',
|
||||||
|
keywords: ['사랑','선택','조화','가치관 일치','결합'],
|
||||||
|
reversedKeywords: ['관계 갈등','선택의 어려움','가치관 충돌','미성숙한 결정'],
|
||||||
|
meaningUpright: '깊은 결합과 가치관의 일치. 중요한 선택을 마음으로부터 내릴 때.',
|
||||||
|
meaningReversed: '두 길 사이에서 머뭇거리거나, 이미 내린 선택의 의구심이 커지는 시기.' },
|
||||||
|
{ id: 7, slug: 'the-chariot', name: '전차', nameEn: 'The Chariot', element: 'water',
|
||||||
|
keywords: ['의지','전진','승리','자기 통제','목표 추진'],
|
||||||
|
reversedKeywords: ['방향 상실','자기 통제 부족','과욕','지연'],
|
||||||
|
meaningUpright: '명확한 목표와 강한 의지로 전진하는 시기. 상반된 힘들을 조율해 추진력으로 바꾼다.',
|
||||||
|
meaningReversed: '방향이 흔들리거나 통제력을 잃어 진전이 멈춘 상태.' },
|
||||||
|
{ id: 8, slug: 'strength', name: '힘', nameEn: 'Strength', element: 'fire',
|
||||||
|
keywords: ['내면의 힘','용기','부드러운 통제','인내','자제'],
|
||||||
|
reversedKeywords: ['자신감 부족','감정 과잉','자제력 상실'],
|
||||||
|
meaningUpright: '강제가 아닌 부드러움으로 어려움을 다루는 시기. 진짜 힘은 자기 통제와 인내에서 나온다.',
|
||||||
|
meaningReversed: '감정에 휘말려 자제력을 잃거나, 자신감이 흔들리는 상태.' },
|
||||||
|
{ id: 9, slug: 'the-hermit', name: '은둔자', nameEn: 'The Hermit', element: 'earth',
|
||||||
|
keywords: ['성찰','고독','내면의 빛','지혜 추구','은둔'],
|
||||||
|
reversedKeywords: ['고립','회피','외로움','자기 폐쇄'],
|
||||||
|
meaningUpright: '바깥 소음에서 물러나 자기 안의 빛으로 길을 찾는 시기.',
|
||||||
|
meaningReversed: '회피·고립이 길어져 균형이 깨진 상태.' },
|
||||||
|
{ id: 10, slug: 'wheel-of-fortune', name: '운명의 수레바퀴', nameEn: 'Wheel of Fortune', element: 'fire',
|
||||||
|
keywords: ['전환점','순환','운명','기회','변화'],
|
||||||
|
reversedKeywords: ['악순환','정체','불운','통제력 상실'],
|
||||||
|
meaningUpright: '큰 흐름이 바뀌는 전환점. 받아들이고 흐름에 올라타면 새로운 국면이 열린다.',
|
||||||
|
meaningReversed: '순환의 하강기. 흐름을 거스르기보다 자세를 낮추고 견뎌야 할 시기.' },
|
||||||
|
{ id: 11, slug: 'justice', name: '정의', nameEn: 'Justice', element: 'air',
|
||||||
|
keywords: ['정의','균형','진실','책임','명료성'],
|
||||||
|
reversedKeywords: ['불공정','책임 회피','판단 왜곡'],
|
||||||
|
meaningUpright: '원인과 결과가 명확히 드러나는 시기. 진실에 기초한 결정이 길을 연다.',
|
||||||
|
meaningReversed: '책임을 외면하거나 한쪽 시각에 치우쳐 균형이 깨진 상태.' },
|
||||||
|
{ id: 12, slug: 'the-hanged-man', name: '매달린 사람', nameEn: 'The Hanged Man', element: 'water',
|
||||||
|
keywords: ['시야 전환','내려놓음','희생','수용','새로운 관점'],
|
||||||
|
reversedKeywords: ['고집','정체','희생 거부','시야의 닫힘'],
|
||||||
|
meaningUpright: '잠시 멈춰 시야를 뒤집어 보는 시기. 강제로 풀려 하지 말고 다른 각도를 받아들이자.',
|
||||||
|
meaningReversed: '내려놓아야 할 것을 붙들고 있어 정체가 길어지는 상태.' },
|
||||||
|
{ id: 13, slug: 'death', name: '죽음', nameEn: 'Death', element: 'water',
|
||||||
|
keywords: ['종결','변형','놓아주기','재탄생','전환'],
|
||||||
|
reversedKeywords: ['변화 저항','놓지 못함','정체','두려움'],
|
||||||
|
meaningUpright: '한 챕터가 닫히고 새로운 챕터가 열리는 결정적 전환. 끝맺음이 새 시작의 조건이다.',
|
||||||
|
meaningReversed: '끝나야 할 것을 붙들어 변화가 늦어지는 상태.' },
|
||||||
|
{ id: 14, slug: 'temperance', name: '절제', nameEn: 'Temperance', element: 'fire',
|
||||||
|
keywords: ['조화','중용','연금술적 결합','인내','치유'],
|
||||||
|
reversedKeywords: ['불균형','과잉','조급함','조화 상실'],
|
||||||
|
meaningUpright: '서로 다른 것들을 천천히 섞어 균형 잡힌 상태로 만드는 시기. 조급함보다 끈기.',
|
||||||
|
meaningReversed: '극단으로 치우치거나 조급함이 흐름을 깨는 상태.' },
|
||||||
|
{ id: 15, slug: 'the-devil', name: '악마', nameEn: 'The Devil', element: 'earth',
|
||||||
|
keywords: ['속박','집착','중독','물질주의','그림자'],
|
||||||
|
reversedKeywords: ['해방','구속에서 벗어남','자각','단절'],
|
||||||
|
meaningUpright: '스스로 묶어둔 사슬을 직시할 시기. 욕망·중독·집착이 시야를 가린다.',
|
||||||
|
meaningReversed: '구속이 풀리며 의식적인 해방이 가능한 상태.' },
|
||||||
|
{ id: 16, slug: 'the-tower', name: '탑', nameEn: 'The Tower', element: 'fire',
|
||||||
|
keywords: ['붕괴','갑작스러운 변화','각성','진실 노출'],
|
||||||
|
reversedKeywords: ['붕괴 회피','두려움','지연된 충격'],
|
||||||
|
meaningUpright: '거짓 기반 위의 구조가 갑자기 무너지는 시기. 충격은 크나 진실의 자리를 만든다.',
|
||||||
|
meaningReversed: '붕괴를 미루거나 외면해 더 큰 충격을 키울 수 있는 상태.' },
|
||||||
|
{ id: 17, slug: 'the-star', name: '별', nameEn: 'The Star', element: 'air',
|
||||||
|
keywords: ['희망','영감','치유','평온','신뢰'],
|
||||||
|
reversedKeywords: ['희망 상실','자기 의심','단절감'],
|
||||||
|
meaningUpright: '폭풍 뒤의 평온. 영감이 회복되고 길게 볼 힘이 돌아오는 시기.',
|
||||||
|
meaningReversed: '의심과 무력감으로 빛이 잘 보이지 않는 상태.' },
|
||||||
|
{ id: 18, slug: 'the-moon', name: '달', nameEn: 'The Moon', element: 'water',
|
||||||
|
keywords: ['직관','무의식','환영','불안','꿈'],
|
||||||
|
reversedKeywords: ['혼란 해소','진실 드러남','직관 회복'],
|
||||||
|
meaningUpright: '명확하지 않은 신호와 감정의 파도. 직관을 따르되 환상은 분별해야 하는 시기.',
|
||||||
|
meaningReversed: '안개가 걷히며 가려졌던 진실이 드러나는 상태.' },
|
||||||
|
{ id: 19, slug: 'the-sun', name: '태양', nameEn: 'The Sun', element: 'fire',
|
||||||
|
keywords: ['기쁨','성공','명료성','활력','진실'],
|
||||||
|
reversedKeywords: ['과신','단편적 기쁨','피상적 성공'],
|
||||||
|
meaningUpright: '명료하고 따뜻한 시기. 노력의 결실이 분명히 드러난다.',
|
||||||
|
meaningReversed: '겉만 환한 기쁨이거나, 자만으로 본질을 놓칠 위험.' },
|
||||||
|
{ id: 20, slug: 'judgement', name: '심판', nameEn: 'Judgement', element: 'fire',
|
||||||
|
keywords: ['각성','부름','재평가','부활','결단'],
|
||||||
|
reversedKeywords: ['자기 비판','부름 무시','과거에 묶임'],
|
||||||
|
meaningUpright: '오랜 흐름을 정산하고 새 부름에 응답하는 시기. 결단의 순간.',
|
||||||
|
meaningReversed: '과거의 비판이나 미련에 묶여 새 길로 나서지 못하는 상태.' },
|
||||||
|
{ id: 21, slug: 'the-world', name: '세계', nameEn: 'The World', element: 'earth',
|
||||||
|
keywords: ['완성','통합','성취','순환의 닫힘','전체성'],
|
||||||
|
reversedKeywords: ['미완성','마무리 지연','반복'],
|
||||||
|
meaningUpright: '한 사이클의 완성과 통합. 다음 시작을 위한 단단한 기반이 마련된다.',
|
||||||
|
meaningReversed: '마무리가 늦어지거나 반복으로 인해 다음 단계로 나아가지 못하는 상태.' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SUITS = [
|
||||||
|
{ suit: 'wands', element: 'fire', kr: '완드' },
|
||||||
|
{ suit: 'cups', element: 'water', kr: '컵' },
|
||||||
|
{ suit: 'swords', element: 'air', kr: '소드' },
|
||||||
|
{ suit: 'pentacles', element: 'earth', kr: '펜타클' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RANK_NAMES = ['에이스', '2', '3', '4', '5', '6', '7', '8', '9', '10', '시종', '기사', '여왕', '왕'];
|
||||||
|
const RANK_EN = ['Ace', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Page', 'Knight', 'Queen', 'King'];
|
||||||
|
const SUIT_NAMES_EN = { wands: 'Wands', cups: 'Cups', swords: 'Swords', pentacles: 'Pentacles' };
|
||||||
|
|
||||||
|
const SUIT_KEYWORDS = {
|
||||||
|
wands: { up: ['열정','창조','행동','의지'], down: ['고갈','지연','분열'], theme: '의지와 창조의 불꽃' },
|
||||||
|
cups: { up: ['감정','관계','직관','사랑'], down: ['감정 정체','상실','오해'], theme: '감정과 관계의 흐름' },
|
||||||
|
swords: { up: ['사고','갈등','명료성','진실'], down: ['혼란','과도한 사고','오해'], theme: '사고와 결단의 칼날' },
|
||||||
|
pentacles: { up: ['물질','일','안정','성취'], down: ['결핍','정체','집착'], theme: '물질과 일의 토대' },
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildMinor() {
|
||||||
|
const out = [];
|
||||||
|
let id = 22;
|
||||||
|
for (const { suit, element, kr } of SUITS) {
|
||||||
|
for (let rank = 1; rank <= 14; rank++) {
|
||||||
|
const krName = `${kr} ${RANK_NAMES[rank - 1]}`;
|
||||||
|
const enName = `${RANK_EN[rank - 1]} of ${SUIT_NAMES_EN[suit]}`;
|
||||||
|
const kw = SUIT_KEYWORDS[suit];
|
||||||
|
out.push({
|
||||||
|
id: id++,
|
||||||
|
slug: `${RANK_EN[rank - 1].toLowerCase()}-of-${suit}`,
|
||||||
|
name: krName,
|
||||||
|
nameEn: enName,
|
||||||
|
arcana: 'minor',
|
||||||
|
suit,
|
||||||
|
rank,
|
||||||
|
element,
|
||||||
|
keywords: [...kw.up, `${kr} ${rank}의 단계`],
|
||||||
|
reversedKeywords: [...kw.down, `${kr} ${rank} 정체`],
|
||||||
|
meaningUpright: `${kw.theme} — ${krName} 단계. ${kw.up.join(', ')} 의 흐름이 작동하는 시점.`,
|
||||||
|
meaningReversed: `${kr} 흐름의 정체 또는 왜곡. ${kw.down.join(', ')} 양상이 드러남.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TAROT_DECK = [
|
||||||
|
...MAJOR_ARCANA.map((c) => ({
|
||||||
|
...c,
|
||||||
|
arcana: 'major',
|
||||||
|
image: cardImage(c.slug),
|
||||||
|
})),
|
||||||
|
...buildMinor().map((c) => ({ ...c, image: cardImage(c.slug) })),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SPREADS = {
|
||||||
|
one_card: {
|
||||||
|
id: 'one_card',
|
||||||
|
name: '오늘의 카드',
|
||||||
|
positions: [{ idx: 0, label: '오늘' }],
|
||||||
|
},
|
||||||
|
three_card: {
|
||||||
|
id: 'three_card',
|
||||||
|
name: '3장 스프레드',
|
||||||
|
positions: [
|
||||||
|
{ idx: 0, label: '과거' },
|
||||||
|
{ idx: 1, label: '현재' },
|
||||||
|
{ idx: 2, label: '미래' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CATEGORIES = ['연애', '일·커리어', '관계', '재물', '건강', '일반'];
|
||||||
|
|
||||||
|
export function findCard(slug) {
|
||||||
|
return TAROT_DECK.find((c) => c.slug === slug) || null;
|
||||||
|
}
|
||||||
61
src/pages/tarot/data/cards.test.js
Normal file
61
src/pages/tarot/data/cards.test.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { TAROT_DECK, SPREADS, CATEGORIES } from './cards';
|
||||||
|
|
||||||
|
describe('TAROT_DECK', () => {
|
||||||
|
it('총 78장', () => {
|
||||||
|
expect(TAROT_DECK).toHaveLength(78);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('메이저 22장 + 마이너 56장', () => {
|
||||||
|
const majors = TAROT_DECK.filter((c) => c.arcana === 'major');
|
||||||
|
const minors = TAROT_DECK.filter((c) => c.arcana === 'minor');
|
||||||
|
expect(majors).toHaveLength(22);
|
||||||
|
expect(minors).toHaveLength(56);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('slug 중복 없음', () => {
|
||||||
|
const slugs = TAROT_DECK.map((c) => c.slug);
|
||||||
|
expect(new Set(slugs).size).toBe(slugs.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('모든 카드에 필수 필드 존재', () => {
|
||||||
|
for (const c of TAROT_DECK) {
|
||||||
|
expect(c.id).toBeTypeOf('number');
|
||||||
|
expect(c.slug).toBeTruthy();
|
||||||
|
expect(c.name).toBeTruthy();
|
||||||
|
expect(c.arcana).toMatch(/^(major|minor)$/);
|
||||||
|
expect(c.keywords).toBeInstanceOf(Array);
|
||||||
|
expect(c.keywords.length).toBeGreaterThan(0);
|
||||||
|
expect(c.reversedKeywords).toBeInstanceOf(Array);
|
||||||
|
expect(c.reversedKeywords.length).toBeGreaterThan(0);
|
||||||
|
expect(c.meaningUpright).toBeTruthy();
|
||||||
|
expect(c.meaningReversed).toBeTruthy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('마이너 카드는 suit, rank 필드를 가진다', () => {
|
||||||
|
const minors = TAROT_DECK.filter((c) => c.arcana === 'minor');
|
||||||
|
for (const c of minors) {
|
||||||
|
expect(c.suit).toMatch(/^(wands|cups|swords|pentacles)$/);
|
||||||
|
expect(c.rank).toBeTypeOf('number');
|
||||||
|
expect(c.rank).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(c.rank).toBeLessThanOrEqual(14);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SPREADS', () => {
|
||||||
|
it('one_card / three_card 정의 존재', () => {
|
||||||
|
expect(SPREADS.one_card.positions).toHaveLength(1);
|
||||||
|
expect(SPREADS.three_card.positions).toHaveLength(3);
|
||||||
|
expect(SPREADS.three_card.positions.map((p) => p.label)).toEqual(['과거', '현재', '미래']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CATEGORIES', () => {
|
||||||
|
it('6개 카테고리', () => {
|
||||||
|
expect(CATEGORIES).toHaveLength(6);
|
||||||
|
expect(CATEGORIES).toContain('연애');
|
||||||
|
expect(CATEGORIES).toContain('일·커리어');
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user