늘 그렇듯 쉬운 것부터 시작합니다. 일단 유저 입력값을 받는 Input 컴포넌트를 만들고, 어떤 스프레드로 카드를 뽑을지(=카드를 몇 장 뽑을지) 정하는 SpreadSelector를 만들었습니다.
입력창 내에서 줄바꿈을 할 수 있게 하기 위해 input태그 대신 textArea태그를 사용했습니다. 스타일링도 안하고 그냥 '입력만 가능하게' 간단하게 만들어둔 상태입니다.
import React, { useState } from 'react';
interface InputProps {
onFormSubmit: (inputValue: string) => void;
}
export default function Input({ onFormSubmit }: InputProps) {
const [inputValue, setInputValue] = useState('');
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputValue(e.target.value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setInputValue('');
onFormSubmit(inputValue);
};
return (
<div>
<h2>User Input</h2>
<form onSubmit={handleSubmit}>
<textarea
value={inputValue}
onChange={handleInputChange}
placeholder="아무 고민이나 적어보세요"
rows={5}
/>
<button type="submit">제출</button>
</form>
</div>
);
}
얼마나 간단하냐면 이렇게 글에다 복붙해도 괜찮을 정도임
이건 처음에는 싱글-트리플 전환하는 토글로 만들려고 했다가, 나중에 스프레드가 다양해졌을 때 어차피 드랍다운으로 바꿔야 할 것 같아서 처음부터 드랍다운으로 했습니다
interface SpreadSelectorProps {
cardCount: number;
onCardCountChange: (newCardCount: number) => void;
}
export default function SpreadSelector({
cardCount,
onCardCountChange,
}: SpreadSelectorProps) {
const handleCardCountChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
onCardCountChange(Number(e.target.value));
};
return (
<div>
<label htmlFor="card-count">어떤 스프레드로 뽑아볼까요?</label>
<select
id="card-count"
value={cardCount}
onChange={handleCardCountChange}
>
<option value={1}>싱글</option>
<option value={3}>트리플</option>
<option value={5}>파이브 카드 크로스</option>
<option value={10}>켈틱 크로스</option>
</select>
</div>
);
}
각 옵션마다 값이 정해져있고, 선택하면 App에서 cardCount 상태가 업데이트 됩니다. 진짜 완전 간단함
그리고 이것들을
App.tsx에 띄우면 요따구가 됩니다.
cardCount는 App에서 useState로 만들어준 상태입니다. 나중에 상태 관리 라이브러리를 쓰면 전역상태로 관리하겠지만 지금은 딱히 필요성을 느끼지 않아서 안 깔아뒀으니 App에서 간단하게 만들고 SpreadSelector로 내려줄겁니다.
Input의 onFormSubmit은 Input 내에 있는 form의 submit 버튼을 누르면 동작합니다. 여기에선 checkSubmittedForm이라는 App에서 정의한 함수를 실행시킵니다.
타로카드의 데이터 구조를 정하고 이걸 타입으로 만들어줍니다.
export interface TarotCard {
id: number;
name: {
en: string;
ko: string;
};
arcanaType: 'Major' | 'Minor';
suit: 'Cup' | 'Pentacle' | 'Sword' | 'Wand' | null;
number: number;
upright: {
keywords: string[];
description: string;
};
reversed: {
keywords: string[];
description: string;
};
}
이제 타로 덱 78장의 데이터를 만들어줍니다. GPT가 도와줬습니다. 매달 2만5천원씩 꼬박꼬박 바치고 있는데 이정도는 해줘야겠죠?
어쩌구저쩌구... 이렇게 TarotCard 객체들이 78개 들어있는 배열을 만들어줬습니다.
이제 랜덤한 카드를 뽑는 함수를 만들겁니다.
es-toolkit의 shuffle 함수를 이용해서, tarotDeck 배열을 섞어주고 그걸 cardCount값만큼 slice합니다.
export function drawRandomCards(cardCount: number) {
const shuffledDeck = shuffle([...tarotDeck]);
const result = shuffledDeck
.slice(0, cardCount);
return result;
}
이러면 중복을 걱정할 일도 없겠죠?
다만 카드를 한장씩 뽑는다라는 타로카드의 메커니즘에 조금 위배되긴 합니다. 이건 덱을 셔플한 후에 맨 위에서 n장 통째로 가져오는 거거든요. 타로점을 즐겨 치는 친구는 이걸 불경한 행위이며, 타로의 신에게 천벌받을 것이라고 말했지만, 천벌이 가능하면 늦게 오길 바라는 수 밖에 없을 것 같습니다. 왜냐면 한 장씩 뽑게 하면 비효율적이잖아요. 이게 훨씬 나음
뽑힌 카드에 정방향/역방향을 부여해줄겁니다.
function processCardDirection(card: TarotCard) {
const direction: '정방향' | '역방향' =
random(0, 1) > 0.5 ? '정방향' : '역방향';
const result = {
id: card.id,
name: card.name,
arcanaType: card.arcanaType,
suit: card.suit,
number: card.number,
direction,
keywords:
direction === '정방향' ? card.upright.keywords : card.reversed.keywords,
description:
direction === '정방향'
? card.upright.description
: card.reversed.description,
};
return result;
}
es-toolkit의 random 함수를 사용해서 0과 1 사이의 난수를 뽑고, 그 값이 0.5보다 크면 정방향, 작으면 역방향이 direction으로 정해집니다. 그 후 card객체에서 정보를 뽑아와 direction을 필드로 더한 새 객체를 만들어주는데, keywords와 description에는 원래 카드 정보에서 정방향/역방향 정보 중의 하나를 뽑아와서 지정해줄 겁니다.
이렇게 데이터에 정/역방향 부여와 그에 따른 정보 가공을 거친 후의 객체는 더 이상 TarotCard가 아닙니다. 타입으로 지정해줘야 나중에 이걸 써먹을 때 빨간줄이 안뜹니다.
export interface DrawnTarotCard {
id: number;
name: {
en: string;
ko: string;
};
arcanaType: 'Major' | 'Minor';
suit: 'Cup' | 'Pentacle' | 'Sword' | 'Wand' | null;
number: number;
direction: '정방향' | '역방향';
keywords: string[];
description: string;
}
일케 말입니다
export function drawRandomCards(cardCount: number) {
const shuffledDeck = shuffle([...tarotDeck]);
const result = shuffledDeck
.slice(0, cardCount)
.map((card) => processCardDirection(card));
return result;
}
이런식으로, 슬라이스해온 카드에 map 걸어서 뽑힌 카드들의 정/역방향을 정하고 데이터를 가공해줍니다.
와...개못생겼죠? 처음에는 다 그런 법입니다.
이렇게요
이제 유저 입력도 받을 수 있고, 정/역방향이 랜덤으로 정해진 무작위 카드를 스프레드에 정해진 수 만큼 뽑을 수 있으니 절반은 끝났습니다. 시작은 했다는 뜻입니다. 시작이 반이랬거든요.