feat(tarot): useTarotShuffle hook (Fisher-Yates + reversed 플래그) (T9)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
25
src/pages/tarot/hooks/useTarotShuffle.js
Normal file
25
src/pages/tarot/hooks/useTarotShuffle.js
Normal file
@@ -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 };
|
||||||
|
}
|
||||||
47
src/pages/tarot/hooks/useTarotShuffle.test.js
Normal file
47
src/pages/tarot/hooks/useTarotShuffle.test.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user