diff --git a/src/pages/tarot/hooks/useTarotShuffle.js b/src/pages/tarot/hooks/useTarotShuffle.js new file mode 100644 index 0000000..ff126a6 --- /dev/null +++ b/src/pages/tarot/hooks/useTarotShuffle.js @@ -0,0 +1,25 @@ +import { useCallback, useState } from 'react'; + +export function fisherYates(input) { + const a = [...input]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +function buildShuffle(deck, size) { + return fisherYates(deck).slice(0, size).map((c) => ({ + ...c, + reversed: Math.random() < 0.5, + })); +} + +export function useTarotShuffle(deck, size = 16) { + const [slice, setSlice] = useState(() => buildShuffle(deck, size)); + const reshuffle = useCallback(() => { + setSlice(buildShuffle(deck, size)); + }, [deck, size]); + return { slice, reshuffle }; +} diff --git a/src/pages/tarot/hooks/useTarotShuffle.test.js b/src/pages/tarot/hooks/useTarotShuffle.test.js new file mode 100644 index 0000000..8fcbc75 --- /dev/null +++ b/src/pages/tarot/hooks/useTarotShuffle.test.js @@ -0,0 +1,47 @@ +import { describe, it, expect } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useTarotShuffle, fisherYates } from './useTarotShuffle'; + +describe('fisherYates', () => { + it('원본 길이를 유지하고 모든 원소를 포함한다', () => { + const src = [1, 2, 3, 4, 5, 6, 7, 8]; + const out = fisherYates(src); + expect(out).toHaveLength(8); + expect([...out].sort()).toEqual(src); + }); + + it('새 배열을 반환한다 (mutation 없음)', () => { + const src = [1, 2, 3]; + const out = fisherYates(src); + expect(src).toEqual([1, 2, 3]); + expect(out).not.toBe(src); + }); +}); + +describe('useTarotShuffle', () => { + it('16장 슬라이스를 반환한다', () => { + const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` })); + const { result } = renderHook(() => useTarotShuffle(deck, 16)); + expect(result.current.slice).toHaveLength(16); + }); + + it('reshuffle 시 새 슬라이스를 만든다 (대부분 다름)', () => { + const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` })); + const { result } = renderHook(() => useTarotShuffle(deck, 16)); + const first = result.current.slice.map((c) => c.id); + act(() => result.current.reshuffle()); + const second = result.current.slice.map((c) => c.id); + // 무작위지만 16장 모두 같을 확률은 사실상 0 + const overlap = first.filter((id) => second.includes(id)).length; + expect(overlap).toBeLessThan(16); + }); + + it('reversed 플래그를 카드에 부여한다', () => { + const deck = Array.from({ length: 78 }, (_, i) => ({ id: i, slug: `c${i}` })); + const { result } = renderHook(() => useTarotShuffle(deck, 16)); + for (const card of result.current.slice) { + expect(card).toHaveProperty('reversed'); + expect(typeof card.reversed).toBe('boolean'); + } + }); +});