From cdf8759aefc8c5792866a76622b75b4b48858032 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 24 May 2026 00:30:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(tarot):=20=EC=B9=B4=EB=93=9C=2078=EC=9E=A5?= =?UTF-8?q?=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20(=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=EC=A0=80=2022=20+=20=EB=A7=88=EC=9D=B4=EB=84=88=2056)?= =?UTF-8?q?=20(T8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/tarot/data/cards.js | 191 +++++++++++++++++++++++++++++ src/pages/tarot/data/cards.test.js | 61 +++++++++ 2 files changed, 252 insertions(+) create mode 100644 src/pages/tarot/data/cards.js create mode 100644 src/pages/tarot/data/cards.test.js diff --git a/src/pages/tarot/data/cards.js b/src/pages/tarot/data/cards.js new file mode 100644 index 0000000..c649e68 --- /dev/null +++ b/src/pages/tarot/data/cards.js @@ -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; +} diff --git a/src/pages/tarot/data/cards.test.js b/src/pages/tarot/data/cards.test.js new file mode 100644 index 0000000..6e07b1b --- /dev/null +++ b/src/pages/tarot/data/cards.test.js @@ -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('일·커리어'); + }); +});