@blastlabs/utils 1.11.1 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/dev/ApiLogger.d.ts +1 -1
- package/dist/components/dev/ApiLogger.js +2 -2
- package/dist/components/dev/DevPanel.d.ts +1 -1
- package/dist/components/dev/DevPanel.js +2 -2
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
- package/dist/components/dev/FormDevTools/FormDevTools.js +2 -2
- package/dist/components/dev/FormDevTools/index.d.ts +1 -1
- package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
- package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
- package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/IdSelector.js +60 -0
- package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
- package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
- package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
- package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/LoginCard.js +16 -0
- package/dist/components/dev/IdSelector/index.d.ts +3 -0
- package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/index.js +1 -0
- package/dist/components/dev/IdSelector/styles.d.ts +16 -0
- package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
- package/dist/components/dev/IdSelector/styles.js +66 -0
- package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
- package/dist/components/dev/WindowSizeDisplay.js +2 -2
- package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
- package/dist/components/dev/ZIndexDebugger.js +1 -1
- package/dist/components/dev/index.d.ts +2 -1
- package/dist/components/dev/index.d.ts.map +1 -1
- package/dist/hooks/event/index.d.ts +6 -0
- package/dist/hooks/event/index.d.ts.map +1 -0
- package/dist/hooks/event/index.js +5 -0
- package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/event/useEventListener.d.ts.map +1 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
- package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
- package/dist/hooks/form/index.d.ts +5 -0
- package/dist/hooks/form/index.d.ts.map +1 -0
- package/dist/hooks/form/index.js +4 -0
- package/dist/hooks/form/useCRUDForm.d.ts +232 -0
- package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
- package/dist/hooks/form/useCRUDForm.js +287 -0
- package/dist/hooks/index.d.ts +9 -14
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +16 -19
- package/dist/hooks/performance/index.d.ts +7 -0
- package/dist/hooks/performance/index.d.ts.map +1 -0
- package/dist/hooks/performance/index.js +6 -0
- package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
- package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
- package/dist/hooks/state/index.d.ts +6 -0
- package/dist/hooks/state/index.d.ts.map +1 -0
- package/dist/hooks/state/index.js +5 -0
- package/dist/hooks/state/usePrevious.d.ts.map +1 -0
- package/dist/hooks/state/useToggle.d.ts.map +1 -0
- package/dist/hooks/storage/index.d.ts +7 -0
- package/dist/hooks/storage/index.d.ts.map +1 -0
- package/dist/hooks/storage/index.js +6 -0
- package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
- package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
- package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
- package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
- package/dist/hooks/time/index.d.ts +7 -0
- package/dist/hooks/time/index.d.ts.map +1 -0
- package/dist/hooks/time/index.js +6 -0
- package/dist/hooks/time/useCountdown.d.ts +116 -0
- package/dist/hooks/time/useCountdown.d.ts.map +1 -0
- package/dist/hooks/time/useCountdown.js +152 -0
- package/dist/hooks/time/useInterval.d.ts +40 -0
- package/dist/hooks/time/useInterval.d.ts.map +1 -0
- package/dist/hooks/time/useInterval.js +61 -0
- package/dist/hooks/time/useStopwatch.d.ts +142 -0
- package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
- package/dist/hooks/time/useStopwatch.js +179 -0
- package/dist/hooks/ui/index.d.ts +6 -0
- package/dist/hooks/ui/index.d.ts.map +1 -0
- package/dist/hooks/ui/index.js +5 -0
- package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
- package/package.json +14 -4
- package/dist/components/dev/IdSelector.d.ts.map +0 -1
- package/dist/components/dev/IdSelector.js +0 -129
- package/dist/hooks/useClickOutside.d.ts.map +0 -1
- package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
- package/dist/hooks/useDebounce.d.ts.map +0 -1
- package/dist/hooks/useEventListener.d.ts.map +0 -1
- package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
- package/dist/hooks/useLocalStorage.d.ts.map +0 -1
- package/dist/hooks/useMediaQuery.d.ts.map +0 -1
- package/dist/hooks/usePrevious.d.ts.map +0 -1
- package/dist/hooks/useSessionStorage.d.ts.map +0 -1
- package/dist/hooks/useThrottle.d.ts.map +0 -1
- package/dist/hooks/useToggle.d.ts.map +0 -1
- package/dist/hooks/useWindowSize.d.ts.map +0 -1
- /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
- /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
- /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
- /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
- /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
- /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
- /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
- /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
- /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
- /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
- /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
- /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
- /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
- /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
- /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
- /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
- /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
- /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
- /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
- /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
- /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
- /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
- /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
- /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCopyToClipboard.d.ts","sourceRoot":"","sources":["../../../src/hooks/storage/useCopyToClipboard.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,8BAA8B;IAC9B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oBAAoB;IACpB,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACzC,sBAAsB;IACtB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAgB,kBAAkB,IAAI,qBAAqB,CA0B1D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocalStorage.d.ts","sourceRoot":"","sources":["../../../src/hooks/storage/useLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,GACd,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CA0F9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionStorage.d.ts","sourceRoot":"","sources":["../../../src/hooks/storage/useSessionStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,GACd,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAoF9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCountdown.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/time/__tests__/useCountdown.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { useCountdown } from '../useCountdown';
|
|
4
|
+
describe('useCountdown', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
it('should initialize with the given time', () => {
|
|
12
|
+
const { result } = renderHook(() => useCountdown(60));
|
|
13
|
+
expect(result.current.timeLeft).toBe(60);
|
|
14
|
+
expect(result.current.isRunning).toBe(false);
|
|
15
|
+
expect(result.current.isCompleted).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it('should start countdown when start is called', () => {
|
|
18
|
+
const { result } = renderHook(() => useCountdown(5));
|
|
19
|
+
act(() => {
|
|
20
|
+
result.current.start();
|
|
21
|
+
});
|
|
22
|
+
expect(result.current.isRunning).toBe(true);
|
|
23
|
+
act(() => {
|
|
24
|
+
vi.advanceTimersByTime(1000);
|
|
25
|
+
});
|
|
26
|
+
expect(result.current.timeLeft).toBe(4);
|
|
27
|
+
});
|
|
28
|
+
it('should pause countdown', () => {
|
|
29
|
+
const { result } = renderHook(() => useCountdown(10));
|
|
30
|
+
act(() => {
|
|
31
|
+
result.current.start();
|
|
32
|
+
});
|
|
33
|
+
act(() => {
|
|
34
|
+
vi.advanceTimersByTime(3000);
|
|
35
|
+
});
|
|
36
|
+
expect(result.current.timeLeft).toBe(7);
|
|
37
|
+
act(() => {
|
|
38
|
+
result.current.pause();
|
|
39
|
+
});
|
|
40
|
+
expect(result.current.isRunning).toBe(false);
|
|
41
|
+
act(() => {
|
|
42
|
+
vi.advanceTimersByTime(2000);
|
|
43
|
+
});
|
|
44
|
+
// 일시정지 후에는 시간이 변하지 않아야 함
|
|
45
|
+
expect(result.current.timeLeft).toBe(7);
|
|
46
|
+
});
|
|
47
|
+
it('should resume countdown', () => {
|
|
48
|
+
const { result } = renderHook(() => useCountdown(10));
|
|
49
|
+
act(() => {
|
|
50
|
+
result.current.start();
|
|
51
|
+
});
|
|
52
|
+
act(() => {
|
|
53
|
+
vi.advanceTimersByTime(3000);
|
|
54
|
+
});
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.pause();
|
|
57
|
+
});
|
|
58
|
+
expect(result.current.timeLeft).toBe(7);
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.resume();
|
|
61
|
+
});
|
|
62
|
+
expect(result.current.isRunning).toBe(true);
|
|
63
|
+
act(() => {
|
|
64
|
+
vi.advanceTimersByTime(2000);
|
|
65
|
+
});
|
|
66
|
+
expect(result.current.timeLeft).toBe(5);
|
|
67
|
+
});
|
|
68
|
+
it('should reset countdown to initial time', () => {
|
|
69
|
+
const { result } = renderHook(() => useCountdown(10));
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.start();
|
|
72
|
+
});
|
|
73
|
+
act(() => {
|
|
74
|
+
vi.advanceTimersByTime(5000);
|
|
75
|
+
});
|
|
76
|
+
expect(result.current.timeLeft).toBe(5);
|
|
77
|
+
act(() => {
|
|
78
|
+
result.current.reset();
|
|
79
|
+
});
|
|
80
|
+
expect(result.current.timeLeft).toBe(10);
|
|
81
|
+
expect(result.current.isRunning).toBe(false);
|
|
82
|
+
expect(result.current.isCompleted).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
it('should reset countdown to new time', () => {
|
|
85
|
+
const { result } = renderHook(() => useCountdown(10));
|
|
86
|
+
act(() => {
|
|
87
|
+
result.current.start();
|
|
88
|
+
});
|
|
89
|
+
act(() => {
|
|
90
|
+
vi.advanceTimersByTime(5000);
|
|
91
|
+
});
|
|
92
|
+
act(() => {
|
|
93
|
+
result.current.reset(20);
|
|
94
|
+
});
|
|
95
|
+
expect(result.current.timeLeft).toBe(20);
|
|
96
|
+
});
|
|
97
|
+
it('should call onComplete when countdown reaches 0', () => {
|
|
98
|
+
const onComplete = vi.fn();
|
|
99
|
+
const { result } = renderHook(() => useCountdown(3, { onComplete }));
|
|
100
|
+
act(() => {
|
|
101
|
+
result.current.start();
|
|
102
|
+
});
|
|
103
|
+
act(() => {
|
|
104
|
+
vi.advanceTimersByTime(3000);
|
|
105
|
+
});
|
|
106
|
+
expect(result.current.timeLeft).toBe(0);
|
|
107
|
+
expect(result.current.isCompleted).toBe(true);
|
|
108
|
+
expect(result.current.isRunning).toBe(false);
|
|
109
|
+
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
it('should auto start when autoStart is true', () => {
|
|
112
|
+
const { result } = renderHook(() => useCountdown(5, { autoStart: true }));
|
|
113
|
+
expect(result.current.isRunning).toBe(true);
|
|
114
|
+
act(() => {
|
|
115
|
+
vi.advanceTimersByTime(2000);
|
|
116
|
+
});
|
|
117
|
+
expect(result.current.timeLeft).toBe(3);
|
|
118
|
+
});
|
|
119
|
+
it('should use custom interval', () => {
|
|
120
|
+
const { result } = renderHook(() => useCountdown(5, { interval: 500 }));
|
|
121
|
+
act(() => {
|
|
122
|
+
result.current.start();
|
|
123
|
+
});
|
|
124
|
+
act(() => {
|
|
125
|
+
vi.advanceTimersByTime(500);
|
|
126
|
+
});
|
|
127
|
+
expect(result.current.timeLeft).toBe(4);
|
|
128
|
+
act(() => {
|
|
129
|
+
vi.advanceTimersByTime(500);
|
|
130
|
+
});
|
|
131
|
+
expect(result.current.timeLeft).toBe(3);
|
|
132
|
+
});
|
|
133
|
+
it('should restart from initial time when start is called after completion', () => {
|
|
134
|
+
const { result } = renderHook(() => useCountdown(3));
|
|
135
|
+
act(() => {
|
|
136
|
+
result.current.start();
|
|
137
|
+
});
|
|
138
|
+
act(() => {
|
|
139
|
+
vi.advanceTimersByTime(3000);
|
|
140
|
+
});
|
|
141
|
+
expect(result.current.isCompleted).toBe(true);
|
|
142
|
+
expect(result.current.timeLeft).toBe(0);
|
|
143
|
+
act(() => {
|
|
144
|
+
result.current.start();
|
|
145
|
+
});
|
|
146
|
+
expect(result.current.timeLeft).toBe(3);
|
|
147
|
+
expect(result.current.isCompleted).toBe(false);
|
|
148
|
+
expect(result.current.isRunning).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInterval.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/time/__tests__/useInterval.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { useInterval } from "../useInterval";
|
|
4
|
+
describe('useInterval 단위 테스트', () => {
|
|
5
|
+
const mockAlert = vi.fn();
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
window.alert = mockAlert;
|
|
8
|
+
});
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
vi.useFakeTimers();
|
|
11
|
+
mockAlert.mockClear();
|
|
12
|
+
});
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
vi.clearAllTimers();
|
|
15
|
+
vi.runOnlyPendingTimers();
|
|
16
|
+
vi.useRealTimers();
|
|
17
|
+
});
|
|
18
|
+
it('callback 함수를 설정한 delay 마다 실행시킨다', () => {
|
|
19
|
+
renderHook(() => useInterval(() => {
|
|
20
|
+
window.alert('호출!');
|
|
21
|
+
}, 1000));
|
|
22
|
+
vi.advanceTimersByTime(200);
|
|
23
|
+
expect(mockAlert).not.toHaveBeenCalled();
|
|
24
|
+
vi.advanceTimersByTime(800);
|
|
25
|
+
expect(mockAlert).toHaveBeenCalledWith('호출!');
|
|
26
|
+
vi.advanceTimersByTime(1000);
|
|
27
|
+
expect(mockAlert).toHaveBeenCalledTimes(2);
|
|
28
|
+
});
|
|
29
|
+
it('delay가 변경되면 반복 주기를 변경한다', () => {
|
|
30
|
+
const { rerender } = renderHook(({ delay }) => useInterval(() => {
|
|
31
|
+
window.alert('호출!');
|
|
32
|
+
}, delay), { initialProps: { delay: 500 } });
|
|
33
|
+
vi.advanceTimersByTime(1000);
|
|
34
|
+
expect(mockAlert).toHaveBeenCalledTimes(2);
|
|
35
|
+
rerender({ delay: 200 });
|
|
36
|
+
vi.advanceTimersByTime(1000);
|
|
37
|
+
expect(mockAlert).toHaveBeenCalledTimes(7);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStopwatch.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/time/__tests__/useStopwatch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { useStopwatch } from '../useStopwatch';
|
|
4
|
+
describe('useStopwatch', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
});
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
it('should initialize with 0 elapsed time', () => {
|
|
12
|
+
const { result } = renderHook(() => useStopwatch());
|
|
13
|
+
expect(result.current.elapsed).toBe(0);
|
|
14
|
+
expect(result.current.elapsedSeconds).toBe(0);
|
|
15
|
+
expect(result.current.isRunning).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it('should start stopwatch', () => {
|
|
18
|
+
const { result } = renderHook(() => useStopwatch());
|
|
19
|
+
act(() => {
|
|
20
|
+
result.current.start();
|
|
21
|
+
});
|
|
22
|
+
expect(result.current.isRunning).toBe(true);
|
|
23
|
+
act(() => {
|
|
24
|
+
vi.advanceTimersByTime(1000);
|
|
25
|
+
});
|
|
26
|
+
expect(result.current.elapsed).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
it('should pause stopwatch', () => {
|
|
29
|
+
const { result } = renderHook(() => useStopwatch());
|
|
30
|
+
act(() => {
|
|
31
|
+
result.current.start();
|
|
32
|
+
});
|
|
33
|
+
act(() => {
|
|
34
|
+
vi.advanceTimersByTime(1000);
|
|
35
|
+
});
|
|
36
|
+
const elapsedBeforePause = result.current.elapsed;
|
|
37
|
+
act(() => {
|
|
38
|
+
result.current.pause();
|
|
39
|
+
});
|
|
40
|
+
expect(result.current.isRunning).toBe(false);
|
|
41
|
+
act(() => {
|
|
42
|
+
vi.advanceTimersByTime(1000);
|
|
43
|
+
});
|
|
44
|
+
// 일시정지 후에는 시간이 변하지 않아야 함
|
|
45
|
+
expect(result.current.elapsed).toBe(elapsedBeforePause);
|
|
46
|
+
});
|
|
47
|
+
it('should resume stopwatch', () => {
|
|
48
|
+
const { result } = renderHook(() => useStopwatch());
|
|
49
|
+
act(() => {
|
|
50
|
+
result.current.start();
|
|
51
|
+
});
|
|
52
|
+
act(() => {
|
|
53
|
+
vi.advanceTimersByTime(1000);
|
|
54
|
+
});
|
|
55
|
+
act(() => {
|
|
56
|
+
result.current.pause();
|
|
57
|
+
});
|
|
58
|
+
const elapsedBeforeResume = result.current.elapsed;
|
|
59
|
+
act(() => {
|
|
60
|
+
result.current.resume();
|
|
61
|
+
});
|
|
62
|
+
expect(result.current.isRunning).toBe(true);
|
|
63
|
+
act(() => {
|
|
64
|
+
vi.advanceTimersByTime(1000);
|
|
65
|
+
});
|
|
66
|
+
expect(result.current.elapsed).toBeGreaterThan(elapsedBeforeResume);
|
|
67
|
+
});
|
|
68
|
+
it('should reset stopwatch', () => {
|
|
69
|
+
const { result } = renderHook(() => useStopwatch());
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.start();
|
|
72
|
+
});
|
|
73
|
+
act(() => {
|
|
74
|
+
vi.advanceTimersByTime(2000);
|
|
75
|
+
});
|
|
76
|
+
expect(result.current.elapsed).toBeGreaterThan(0);
|
|
77
|
+
act(() => {
|
|
78
|
+
result.current.reset();
|
|
79
|
+
});
|
|
80
|
+
expect(result.current.elapsed).toBe(0);
|
|
81
|
+
expect(result.current.isRunning).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it('should auto start when autoStart is true', () => {
|
|
84
|
+
const { result } = renderHook(() => useStopwatch({ autoStart: true }));
|
|
85
|
+
expect(result.current.isRunning).toBe(true);
|
|
86
|
+
act(() => {
|
|
87
|
+
vi.advanceTimersByTime(1000);
|
|
88
|
+
});
|
|
89
|
+
expect(result.current.elapsed).toBeGreaterThan(0);
|
|
90
|
+
});
|
|
91
|
+
it('should calculate elapsedSeconds correctly', () => {
|
|
92
|
+
const { result } = renderHook(() => useStopwatch());
|
|
93
|
+
act(() => {
|
|
94
|
+
result.current.start();
|
|
95
|
+
});
|
|
96
|
+
act(() => {
|
|
97
|
+
vi.advanceTimersByTime(2500);
|
|
98
|
+
});
|
|
99
|
+
expect(result.current.elapsedSeconds).toBeCloseTo(2.5, 1);
|
|
100
|
+
});
|
|
101
|
+
it('should use custom interval', () => {
|
|
102
|
+
const { result } = renderHook(() => useStopwatch({ interval: 100 }));
|
|
103
|
+
act(() => {
|
|
104
|
+
result.current.start();
|
|
105
|
+
});
|
|
106
|
+
const initialElapsed = result.current.elapsed;
|
|
107
|
+
act(() => {
|
|
108
|
+
vi.advanceTimersByTime(100);
|
|
109
|
+
});
|
|
110
|
+
expect(result.current.elapsed).toBeGreaterThan(initialElapsed);
|
|
111
|
+
});
|
|
112
|
+
it('should accumulate time across pause and resume', () => {
|
|
113
|
+
const { result } = renderHook(() => useStopwatch());
|
|
114
|
+
act(() => {
|
|
115
|
+
result.current.start();
|
|
116
|
+
});
|
|
117
|
+
act(() => {
|
|
118
|
+
vi.advanceTimersByTime(1000);
|
|
119
|
+
});
|
|
120
|
+
act(() => {
|
|
121
|
+
result.current.pause();
|
|
122
|
+
});
|
|
123
|
+
const elapsed1 = result.current.elapsed;
|
|
124
|
+
act(() => {
|
|
125
|
+
result.current.resume();
|
|
126
|
+
});
|
|
127
|
+
act(() => {
|
|
128
|
+
vi.advanceTimersByTime(1000);
|
|
129
|
+
});
|
|
130
|
+
expect(result.current.elapsed).toBeGreaterThan(elapsed1);
|
|
131
|
+
});
|
|
132
|
+
it('should not change elapsed time when paused', () => {
|
|
133
|
+
const { result } = renderHook(() => useStopwatch());
|
|
134
|
+
act(() => {
|
|
135
|
+
result.current.start();
|
|
136
|
+
});
|
|
137
|
+
act(() => {
|
|
138
|
+
vi.advanceTimersByTime(1000);
|
|
139
|
+
});
|
|
140
|
+
act(() => {
|
|
141
|
+
result.current.pause();
|
|
142
|
+
});
|
|
143
|
+
const pausedElapsed = result.current.elapsed;
|
|
144
|
+
act(() => {
|
|
145
|
+
vi.advanceTimersByTime(5000);
|
|
146
|
+
});
|
|
147
|
+
expect(result.current.elapsed).toBe(pausedElapsed);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/time/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export interface UseCountdownOptions {
|
|
2
|
+
/** 카운트다운 완료 시 호출되는 콜백 */
|
|
3
|
+
onComplete?: () => void;
|
|
4
|
+
/** 카운트다운 간격 (밀리초, 기본값: 1000ms) */
|
|
5
|
+
interval?: number;
|
|
6
|
+
/** 자동 시작 여부 (기본값: false) */
|
|
7
|
+
autoStart?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface UseCountdownReturn {
|
|
10
|
+
/** 남은 시간 (초) */
|
|
11
|
+
timeLeft: number;
|
|
12
|
+
/** 카운트다운 시작 */
|
|
13
|
+
start: () => void;
|
|
14
|
+
/** 카운트다운 일시정지 */
|
|
15
|
+
pause: () => void;
|
|
16
|
+
/** 카운트다운 재개 */
|
|
17
|
+
resume: () => void;
|
|
18
|
+
/** 카운트다운 리셋 */
|
|
19
|
+
reset: (newTime?: number) => void;
|
|
20
|
+
/** 카운트다운 실행 중 여부 */
|
|
21
|
+
isRunning: boolean;
|
|
22
|
+
/** 카운트다운 완료 여부 */
|
|
23
|
+
isCompleted: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 카운트다운 타이머 hook
|
|
27
|
+
*
|
|
28
|
+
* 지정된 시간부터 0까지 카운트다운하는 타이머를 제공합니다.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* // 기본 사용 - 60초 카운트다운
|
|
33
|
+
* function Timer() {
|
|
34
|
+
* const { timeLeft, start, pause, reset, isRunning } = useCountdown(60);
|
|
35
|
+
*
|
|
36
|
+
* return (
|
|
37
|
+
* <div>
|
|
38
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
39
|
+
* <button onClick={start} disabled={isRunning}>시작</button>
|
|
40
|
+
* <button onClick={pause} disabled={!isRunning}>일시정지</button>
|
|
41
|
+
* <button onClick={() => reset()}>리셋</button>
|
|
42
|
+
* </div>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* // 완료 콜백과 함께 사용
|
|
50
|
+
* function Timer() {
|
|
51
|
+
* const { timeLeft, start, isCompleted } = useCountdown(10, {
|
|
52
|
+
* onComplete: () => {
|
|
53
|
+
* alert('타이머 종료!');
|
|
54
|
+
* },
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* return (
|
|
58
|
+
* <div>
|
|
59
|
+
* {isCompleted ? (
|
|
60
|
+
* <div>완료!</div>
|
|
61
|
+
* ) : (
|
|
62
|
+
* <div>
|
|
63
|
+
* <div>{timeLeft}초</div>
|
|
64
|
+
* <button onClick={start}>시작</button>
|
|
65
|
+
* </div>
|
|
66
|
+
* )}
|
|
67
|
+
* </div>
|
|
68
|
+
* );
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* // 자동 시작
|
|
75
|
+
* function AutoTimer() {
|
|
76
|
+
* const { timeLeft, pause, resume, isRunning } = useCountdown(30, {
|
|
77
|
+
* autoStart: true,
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* return (
|
|
81
|
+
* <div>
|
|
82
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
83
|
+
* {isRunning ? (
|
|
84
|
+
* <button onClick={pause}>일시정지</button>
|
|
85
|
+
* ) : (
|
|
86
|
+
* <button onClick={resume}>재개</button>
|
|
87
|
+
* )}
|
|
88
|
+
* </div>
|
|
89
|
+
* );
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```tsx
|
|
95
|
+
* // 동적 시간 변경
|
|
96
|
+
* function DynamicTimer() {
|
|
97
|
+
* const [duration, setDuration] = useState(60);
|
|
98
|
+
* const { timeLeft, start, reset } = useCountdown(duration);
|
|
99
|
+
*
|
|
100
|
+
* return (
|
|
101
|
+
* <div>
|
|
102
|
+
* <input
|
|
103
|
+
* type="number"
|
|
104
|
+
* value={duration}
|
|
105
|
+
* onChange={(e) => setDuration(Number(e.target.value))}
|
|
106
|
+
* />
|
|
107
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
108
|
+
* <button onClick={start}>시작</button>
|
|
109
|
+
* <button onClick={() => reset(duration)}>리셋</button>
|
|
110
|
+
* </div>
|
|
111
|
+
* );
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export declare function useCountdown(initialTime: number, options?: UseCountdownOptions): UseCountdownReturn;
|
|
116
|
+
//# sourceMappingURL=useCountdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCountdown.d.ts","sourceRoot":"","sources":["../../../src/hooks/time/useCountdown.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,yBAAyB;IACzB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,gBAAgB;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe;IACf,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,iBAAiB;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,eAAe;IACf,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,eAAe;IACf,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,oBAAoB;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,kBAAkB;IAClB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyFG;AACH,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAqEpB"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 카운트다운 타이머 hook
|
|
4
|
+
*
|
|
5
|
+
* 지정된 시간부터 0까지 카운트다운하는 타이머를 제공합니다.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // 기본 사용 - 60초 카운트다운
|
|
10
|
+
* function Timer() {
|
|
11
|
+
* const { timeLeft, start, pause, reset, isRunning } = useCountdown(60);
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <div>
|
|
15
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
16
|
+
* <button onClick={start} disabled={isRunning}>시작</button>
|
|
17
|
+
* <button onClick={pause} disabled={!isRunning}>일시정지</button>
|
|
18
|
+
* <button onClick={() => reset()}>리셋</button>
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // 완료 콜백과 함께 사용
|
|
27
|
+
* function Timer() {
|
|
28
|
+
* const { timeLeft, start, isCompleted } = useCountdown(10, {
|
|
29
|
+
* onComplete: () => {
|
|
30
|
+
* alert('타이머 종료!');
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* return (
|
|
35
|
+
* <div>
|
|
36
|
+
* {isCompleted ? (
|
|
37
|
+
* <div>완료!</div>
|
|
38
|
+
* ) : (
|
|
39
|
+
* <div>
|
|
40
|
+
* <div>{timeLeft}초</div>
|
|
41
|
+
* <button onClick={start}>시작</button>
|
|
42
|
+
* </div>
|
|
43
|
+
* )}
|
|
44
|
+
* </div>
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* // 자동 시작
|
|
52
|
+
* function AutoTimer() {
|
|
53
|
+
* const { timeLeft, pause, resume, isRunning } = useCountdown(30, {
|
|
54
|
+
* autoStart: true,
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* return (
|
|
58
|
+
* <div>
|
|
59
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
60
|
+
* {isRunning ? (
|
|
61
|
+
* <button onClick={pause}>일시정지</button>
|
|
62
|
+
* ) : (
|
|
63
|
+
* <button onClick={resume}>재개</button>
|
|
64
|
+
* )}
|
|
65
|
+
* </div>
|
|
66
|
+
* );
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* // 동적 시간 변경
|
|
73
|
+
* function DynamicTimer() {
|
|
74
|
+
* const [duration, setDuration] = useState(60);
|
|
75
|
+
* const { timeLeft, start, reset } = useCountdown(duration);
|
|
76
|
+
*
|
|
77
|
+
* return (
|
|
78
|
+
* <div>
|
|
79
|
+
* <input
|
|
80
|
+
* type="number"
|
|
81
|
+
* value={duration}
|
|
82
|
+
* onChange={(e) => setDuration(Number(e.target.value))}
|
|
83
|
+
* />
|
|
84
|
+
* <div>남은 시간: {timeLeft}초</div>
|
|
85
|
+
* <button onClick={start}>시작</button>
|
|
86
|
+
* <button onClick={() => reset(duration)}>리셋</button>
|
|
87
|
+
* </div>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function useCountdown(initialTime, options = {}) {
|
|
93
|
+
const { onComplete, interval = 1000, autoStart = false } = options;
|
|
94
|
+
const [timeLeft, setTimeLeft] = useState(initialTime);
|
|
95
|
+
const [isRunning, setIsRunning] = useState(autoStart);
|
|
96
|
+
const [isCompleted, setIsCompleted] = useState(false);
|
|
97
|
+
const intervalRef = useRef(null);
|
|
98
|
+
const initialTimeRef = useRef(initialTime);
|
|
99
|
+
const clear = useCallback(() => {
|
|
100
|
+
if (intervalRef.current) {
|
|
101
|
+
clearInterval(intervalRef.current);
|
|
102
|
+
intervalRef.current = null;
|
|
103
|
+
}
|
|
104
|
+
}, []);
|
|
105
|
+
const start = useCallback(() => {
|
|
106
|
+
if (isCompleted) {
|
|
107
|
+
setTimeLeft(initialTimeRef.current);
|
|
108
|
+
setIsCompleted(false);
|
|
109
|
+
}
|
|
110
|
+
setIsRunning(true);
|
|
111
|
+
}, [isCompleted]);
|
|
112
|
+
const pause = useCallback(() => {
|
|
113
|
+
clear();
|
|
114
|
+
setIsRunning(false);
|
|
115
|
+
}, [clear]);
|
|
116
|
+
const resume = useCallback(() => {
|
|
117
|
+
setIsRunning(true);
|
|
118
|
+
}, []);
|
|
119
|
+
const reset = useCallback((newTime) => {
|
|
120
|
+
clear();
|
|
121
|
+
const resetTime = newTime !== undefined ? newTime : initialTimeRef.current;
|
|
122
|
+
initialTimeRef.current = resetTime;
|
|
123
|
+
setTimeLeft(resetTime);
|
|
124
|
+
setIsRunning(false);
|
|
125
|
+
setIsCompleted(false);
|
|
126
|
+
}, [clear]);
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (isRunning && timeLeft > 0) {
|
|
129
|
+
intervalRef.current = setInterval(() => {
|
|
130
|
+
setTimeLeft((prev) => {
|
|
131
|
+
if (prev <= 1) {
|
|
132
|
+
setIsRunning(false);
|
|
133
|
+
setIsCompleted(true);
|
|
134
|
+
onComplete?.();
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
return prev - 1;
|
|
138
|
+
});
|
|
139
|
+
}, interval);
|
|
140
|
+
return () => clear();
|
|
141
|
+
}
|
|
142
|
+
}, [isRunning, timeLeft, interval, onComplete, clear]);
|
|
143
|
+
return {
|
|
144
|
+
timeLeft,
|
|
145
|
+
start,
|
|
146
|
+
pause,
|
|
147
|
+
resume,
|
|
148
|
+
reset,
|
|
149
|
+
isRunning,
|
|
150
|
+
isCompleted,
|
|
151
|
+
};
|
|
152
|
+
}
|