@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.
Files changed (128) hide show
  1. package/dist/components/dev/ApiLogger.d.ts +1 -1
  2. package/dist/components/dev/ApiLogger.js +2 -2
  3. package/dist/components/dev/DevPanel.d.ts +1 -1
  4. package/dist/components/dev/DevPanel.js +2 -2
  5. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
  6. package/dist/components/dev/FormDevTools/FormDevTools.js +2 -2
  7. package/dist/components/dev/FormDevTools/index.d.ts +1 -1
  8. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
  9. package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
  10. package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
  11. package/dist/components/dev/IdSelector/IdSelector.js +60 -0
  12. package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
  13. package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
  14. package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
  15. package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
  16. package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
  17. package/dist/components/dev/IdSelector/LoginCard.js +16 -0
  18. package/dist/components/dev/IdSelector/index.d.ts +3 -0
  19. package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
  20. package/dist/components/dev/IdSelector/index.js +1 -0
  21. package/dist/components/dev/IdSelector/styles.d.ts +16 -0
  22. package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
  23. package/dist/components/dev/IdSelector/styles.js +66 -0
  24. package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
  25. package/dist/components/dev/WindowSizeDisplay.js +2 -2
  26. package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
  27. package/dist/components/dev/ZIndexDebugger.js +1 -1
  28. package/dist/components/dev/index.d.ts +2 -1
  29. package/dist/components/dev/index.d.ts.map +1 -1
  30. package/dist/hooks/event/index.d.ts +6 -0
  31. package/dist/hooks/event/index.d.ts.map +1 -0
  32. package/dist/hooks/event/index.js +5 -0
  33. package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
  34. package/dist/hooks/event/useEventListener.d.ts.map +1 -0
  35. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
  36. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
  37. package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
  38. package/dist/hooks/form/index.d.ts +5 -0
  39. package/dist/hooks/form/index.d.ts.map +1 -0
  40. package/dist/hooks/form/index.js +4 -0
  41. package/dist/hooks/form/useCRUDForm.d.ts +232 -0
  42. package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
  43. package/dist/hooks/form/useCRUDForm.js +287 -0
  44. package/dist/hooks/index.d.ts +9 -14
  45. package/dist/hooks/index.d.ts.map +1 -1
  46. package/dist/hooks/index.js +16 -19
  47. package/dist/hooks/performance/index.d.ts +7 -0
  48. package/dist/hooks/performance/index.d.ts.map +1 -0
  49. package/dist/hooks/performance/index.js +6 -0
  50. package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
  51. package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
  52. package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
  53. package/dist/hooks/state/index.d.ts +6 -0
  54. package/dist/hooks/state/index.d.ts.map +1 -0
  55. package/dist/hooks/state/index.js +5 -0
  56. package/dist/hooks/state/usePrevious.d.ts.map +1 -0
  57. package/dist/hooks/state/useToggle.d.ts.map +1 -0
  58. package/dist/hooks/storage/index.d.ts +7 -0
  59. package/dist/hooks/storage/index.d.ts.map +1 -0
  60. package/dist/hooks/storage/index.js +6 -0
  61. package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
  62. package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
  63. package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
  64. package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
  65. package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
  66. package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
  67. package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
  68. package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
  69. package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
  70. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
  71. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
  72. package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
  73. package/dist/hooks/time/index.d.ts +7 -0
  74. package/dist/hooks/time/index.d.ts.map +1 -0
  75. package/dist/hooks/time/index.js +6 -0
  76. package/dist/hooks/time/useCountdown.d.ts +116 -0
  77. package/dist/hooks/time/useCountdown.d.ts.map +1 -0
  78. package/dist/hooks/time/useCountdown.js +152 -0
  79. package/dist/hooks/time/useInterval.d.ts +40 -0
  80. package/dist/hooks/time/useInterval.d.ts.map +1 -0
  81. package/dist/hooks/time/useInterval.js +61 -0
  82. package/dist/hooks/time/useStopwatch.d.ts +142 -0
  83. package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
  84. package/dist/hooks/time/useStopwatch.js +179 -0
  85. package/dist/hooks/ui/index.d.ts +6 -0
  86. package/dist/hooks/ui/index.d.ts.map +1 -0
  87. package/dist/hooks/ui/index.js +5 -0
  88. package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
  89. package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
  90. package/package.json +14 -4
  91. package/dist/components/dev/IdSelector.d.ts.map +0 -1
  92. package/dist/components/dev/IdSelector.js +0 -129
  93. package/dist/hooks/useClickOutside.d.ts.map +0 -1
  94. package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
  95. package/dist/hooks/useDebounce.d.ts.map +0 -1
  96. package/dist/hooks/useEventListener.d.ts.map +0 -1
  97. package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
  98. package/dist/hooks/useLocalStorage.d.ts.map +0 -1
  99. package/dist/hooks/useMediaQuery.d.ts.map +0 -1
  100. package/dist/hooks/usePrevious.d.ts.map +0 -1
  101. package/dist/hooks/useSessionStorage.d.ts.map +0 -1
  102. package/dist/hooks/useThrottle.d.ts.map +0 -1
  103. package/dist/hooks/useToggle.d.ts.map +0 -1
  104. package/dist/hooks/useWindowSize.d.ts.map +0 -1
  105. /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
  106. /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
  107. /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
  108. /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
  109. /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
  110. /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
  111. /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
  112. /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
  113. /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
  114. /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
  115. /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
  116. /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
  117. /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
  118. /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
  119. /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
  120. /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
  121. /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
  122. /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
  123. /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
  124. /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
  125. /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
  126. /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
  127. /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
  128. /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Storage Hooks
3
+ */
4
+ export * from './useLocalStorage';
5
+ export * from './useSessionStorage';
6
+ export * from './useCopyToClipboard';
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useCountdown.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useInterval.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useStopwatch.test.d.ts.map
@@ -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,7 @@
1
+ /**
2
+ * Time-related Hooks
3
+ */
4
+ export * from './useInterval';
5
+ export * from './useCountdown';
6
+ export * from './useStopwatch';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -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,6 @@
1
+ /**
2
+ * Time-related Hooks
3
+ */
4
+ export * from './useInterval';
5
+ export * from './useCountdown';
6
+ export * from './useStopwatch';
@@ -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
+ }