@blastlabs/utils 1.11.1 → 1.12.1

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 (151) hide show
  1. package/README.md +20 -13
  2. package/dist/components/auth/AuthGuard.d.ts +95 -0
  3. package/dist/components/auth/AuthGuard.d.ts.map +1 -0
  4. package/dist/components/auth/AuthGuard.js +103 -0
  5. package/dist/components/auth/index.d.ts +5 -0
  6. package/dist/components/auth/index.d.ts.map +1 -0
  7. package/dist/components/auth/index.js +4 -0
  8. package/dist/components/dev/ApiLogger.d.ts +2 -2
  9. package/dist/components/dev/ApiLogger.js +3 -3
  10. package/dist/components/dev/DevPanel.d.ts +1 -1
  11. package/dist/components/dev/DevPanel.js +2 -2
  12. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
  13. package/dist/components/dev/FormDevTools/FormDevTools.js +2 -2
  14. package/dist/components/dev/FormDevTools/index.d.ts +1 -1
  15. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
  16. package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
  17. package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
  18. package/dist/components/dev/IdSelector/IdSelector.js +60 -0
  19. package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
  20. package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
  21. package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
  22. package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
  23. package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
  24. package/dist/components/dev/IdSelector/LoginCard.js +16 -0
  25. package/dist/components/dev/IdSelector/index.d.ts +3 -0
  26. package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
  27. package/dist/components/dev/IdSelector/index.js +1 -0
  28. package/dist/components/dev/IdSelector/styles.d.ts +16 -0
  29. package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
  30. package/dist/components/dev/IdSelector/styles.js +66 -0
  31. package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
  32. package/dist/components/dev/WindowSizeDisplay.js +2 -2
  33. package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
  34. package/dist/components/dev/ZIndexDebugger.js +1 -1
  35. package/dist/components/dev/index.d.ts +2 -1
  36. package/dist/components/dev/index.d.ts.map +1 -1
  37. package/dist/components/index.d.ts +1 -0
  38. package/dist/components/index.d.ts.map +1 -1
  39. package/dist/components/index.js +1 -0
  40. package/dist/hooks/auth/__tests__/useAuth.test.d.ts +2 -0
  41. package/dist/hooks/auth/__tests__/useAuth.test.d.ts.map +1 -0
  42. package/dist/hooks/auth/__tests__/useAuth.test.js +139 -0
  43. package/dist/hooks/auth/index.d.ts +5 -0
  44. package/dist/hooks/auth/index.d.ts.map +1 -0
  45. package/dist/hooks/auth/index.js +4 -0
  46. package/dist/hooks/auth/useAuth.d.ts +275 -0
  47. package/dist/hooks/auth/useAuth.d.ts.map +1 -0
  48. package/dist/hooks/auth/useAuth.js +384 -0
  49. package/dist/hooks/event/index.d.ts +6 -0
  50. package/dist/hooks/event/index.d.ts.map +1 -0
  51. package/dist/hooks/event/index.js +5 -0
  52. package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
  53. package/dist/hooks/event/useEventListener.d.ts.map +1 -0
  54. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
  55. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
  56. package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
  57. package/dist/hooks/form/index.d.ts +5 -0
  58. package/dist/hooks/form/index.d.ts.map +1 -0
  59. package/dist/hooks/form/index.js +4 -0
  60. package/dist/hooks/form/useCRUDForm.d.ts +211 -0
  61. package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
  62. package/dist/hooks/form/useCRUDForm.js +287 -0
  63. package/dist/hooks/index.d.ts +10 -14
  64. package/dist/hooks/index.d.ts.map +1 -1
  65. package/dist/hooks/index.js +18 -19
  66. package/dist/hooks/performance/index.d.ts +7 -0
  67. package/dist/hooks/performance/index.d.ts.map +1 -0
  68. package/dist/hooks/performance/index.js +6 -0
  69. package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
  70. package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
  71. package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
  72. package/dist/hooks/state/index.d.ts +6 -0
  73. package/dist/hooks/state/index.d.ts.map +1 -0
  74. package/dist/hooks/state/index.js +5 -0
  75. package/dist/hooks/state/usePrevious.d.ts.map +1 -0
  76. package/dist/hooks/state/useToggle.d.ts.map +1 -0
  77. package/dist/hooks/storage/index.d.ts +7 -0
  78. package/dist/hooks/storage/index.d.ts.map +1 -0
  79. package/dist/hooks/storage/index.js +6 -0
  80. package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
  81. package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
  82. package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
  83. package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
  84. package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
  85. package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
  86. package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
  87. package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
  88. package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
  89. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
  90. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
  91. package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
  92. package/dist/hooks/time/index.d.ts +7 -0
  93. package/dist/hooks/time/index.d.ts.map +1 -0
  94. package/dist/hooks/time/index.js +6 -0
  95. package/dist/hooks/time/useCountdown.d.ts +116 -0
  96. package/dist/hooks/time/useCountdown.d.ts.map +1 -0
  97. package/dist/hooks/time/useCountdown.js +152 -0
  98. package/dist/hooks/time/useInterval.d.ts +40 -0
  99. package/dist/hooks/time/useInterval.d.ts.map +1 -0
  100. package/dist/hooks/time/useInterval.js +61 -0
  101. package/dist/hooks/time/useStopwatch.d.ts +142 -0
  102. package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
  103. package/dist/hooks/time/useStopwatch.js +179 -0
  104. package/dist/hooks/ui/index.d.ts +7 -0
  105. package/dist/hooks/ui/index.d.ts.map +1 -0
  106. package/dist/hooks/ui/index.js +6 -0
  107. package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
  108. package/dist/hooks/ui/useTabs.d.ts +33 -0
  109. package/dist/hooks/ui/useTabs.d.ts.map +1 -0
  110. package/dist/hooks/ui/useTabs.js +117 -0
  111. package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
  112. package/dist/index.js +1 -1
  113. package/package.json +14 -4
  114. package/dist/components/dev/IdSelector.d.ts.map +0 -1
  115. package/dist/components/dev/IdSelector.js +0 -129
  116. package/dist/hooks/useClickOutside.d.ts.map +0 -1
  117. package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
  118. package/dist/hooks/useDebounce.d.ts.map +0 -1
  119. package/dist/hooks/useEventListener.d.ts.map +0 -1
  120. package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
  121. package/dist/hooks/useLocalStorage.d.ts.map +0 -1
  122. package/dist/hooks/useMediaQuery.d.ts.map +0 -1
  123. package/dist/hooks/usePrevious.d.ts.map +0 -1
  124. package/dist/hooks/useSessionStorage.d.ts.map +0 -1
  125. package/dist/hooks/useThrottle.d.ts.map +0 -1
  126. package/dist/hooks/useToggle.d.ts.map +0 -1
  127. package/dist/hooks/useWindowSize.d.ts.map +0 -1
  128. /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
  129. /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
  130. /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
  131. /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
  132. /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
  133. /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
  134. /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
  135. /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
  136. /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
  137. /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
  138. /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
  139. /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
  140. /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
  141. /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
  142. /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
  143. /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
  144. /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
  145. /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
  146. /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
  147. /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
  148. /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
  149. /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
  150. /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
  151. /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInterval.d.ts","sourceRoot":"","sources":["../../../src/hooks/time/useInterval.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CA0BrE"}
@@ -0,0 +1,61 @@
1
+ import { useEffect, useRef } from 'react';
2
+ /**
3
+ * 일정 시간 간격으로 콜백을 실행하는 hook
4
+ * interval을 자동으로 관리하고 컴포넌트 언마운트 시 정리합니다.
5
+ *
6
+ * @param callback - 일정 간격으로 실행할 함수
7
+ * @param delay - 실행 간격 (밀리초)
8
+ *
9
+ * @example
10
+ * // 1초마다 카운터 증가
11
+ * function TimerComponent() {
12
+ * const [count, setCount] = useState(0);
13
+ *
14
+ * useInterval(() => {
15
+ * setCount(c => c + 1);
16
+ * }, 1000);
17
+ *
18
+ * return <div>Count: {count}</div>;
19
+ * }
20
+ *
21
+ * @example
22
+ * // delay 변경으로 interval 속도 조절
23
+ * function AdjustableTimer() {
24
+ * const [count, setCount] = useState(0);
25
+ * const [delay, setDelay] = useState(1000);
26
+ *
27
+ * useInterval(() => {
28
+ * setCount(c => c + 1);
29
+ * }, delay);
30
+ *
31
+ * return (
32
+ * <div>
33
+ * <div>Count: {count}</div>
34
+ * <button onClick={() => setDelay(500)}>Speed up</button>
35
+ * <button onClick={() => setDelay(2000)}>Slow down</button>
36
+ * </div>
37
+ * );
38
+ * }
39
+ */
40
+ export function useInterval(callback, delay) {
41
+ const intervalRef = useRef(null);
42
+ const stopInterval = () => {
43
+ if (intervalRef.current) {
44
+ clearInterval(intervalRef.current);
45
+ }
46
+ };
47
+ const startInterval = (nextDelay) => {
48
+ stopInterval();
49
+ intervalRef.current = setInterval(() => {
50
+ callback();
51
+ }, nextDelay);
52
+ };
53
+ useEffect(() => {
54
+ startInterval(delay);
55
+ }, [delay]);
56
+ useEffect(() => {
57
+ return () => {
58
+ stopInterval();
59
+ };
60
+ }, []);
61
+ }
@@ -0,0 +1,142 @@
1
+ export interface UseStopwatchOptions {
2
+ /** 업데이트 간격 (밀리초, 기본값: 10ms) */
3
+ interval?: number;
4
+ /** 자동 시작 여부 (기본값: false) */
5
+ autoStart?: boolean;
6
+ }
7
+ export interface UseStopwatchReturn {
8
+ /** 경과 시간 (밀리초) */
9
+ elapsed: number;
10
+ /** 경과 시간 (초) */
11
+ elapsedSeconds: number;
12
+ /** 스톱워치 시작 */
13
+ start: () => void;
14
+ /** 스톱워치 일시정지 */
15
+ pause: () => void;
16
+ /** 스톱워치 재개 */
17
+ resume: () => void;
18
+ /** 스톱워치 리셋 */
19
+ reset: () => void;
20
+ /** 스톱워치 실행 중 여부 */
21
+ isRunning: boolean;
22
+ }
23
+ /**
24
+ * 스톱워치 hook
25
+ *
26
+ * 경과 시간을 측정하는 스톱워치를 제공합니다.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * // 기본 사용
31
+ * function Stopwatch() {
32
+ * const { elapsed, elapsedSeconds, start, pause, reset, isRunning } = useStopwatch();
33
+ *
34
+ * return (
35
+ * <div>
36
+ * <div>경과 시간: {elapsedSeconds.toFixed(2)}초</div>
37
+ * <div>밀리초: {elapsed}ms</div>
38
+ * <button onClick={start} disabled={isRunning}>시작</button>
39
+ * <button onClick={pause} disabled={!isRunning}>일시정지</button>
40
+ * <button onClick={reset}>리셋</button>
41
+ * </div>
42
+ * );
43
+ * }
44
+ * ```
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * // 포맷팅된 시간 표시
49
+ * function FormattedStopwatch() {
50
+ * const { elapsed } = useStopwatch({ autoStart: true });
51
+ *
52
+ * const formatTime = (ms: number) => {
53
+ * const seconds = Math.floor(ms / 1000);
54
+ * const minutes = Math.floor(seconds / 60);
55
+ * const hours = Math.floor(minutes / 60);
56
+ *
57
+ * const displaySeconds = seconds % 60;
58
+ * const displayMinutes = minutes % 60;
59
+ * const displayMs = ms % 1000;
60
+ *
61
+ * return `${hours.toString().padStart(2, '0')}:${displayMinutes
62
+ * .toString()
63
+ * .padStart(2, '0')}:${displaySeconds.toString().padStart(2, '0')}.${Math.floor(
64
+ * displayMs / 10
65
+ * )
66
+ * .toString()
67
+ * .padStart(2, '0')}`;
68
+ * };
69
+ *
70
+ * return <div>{formatTime(elapsed)}</div>;
71
+ * }
72
+ * ```
73
+ *
74
+ * @example
75
+ * ```tsx
76
+ * // 랩 타임 기록
77
+ * function LapTimer() {
78
+ * const { elapsed, start, pause, reset, isRunning } = useStopwatch();
79
+ * const [laps, setLaps] = useState<number[]>([]);
80
+ *
81
+ * const handleLap = () => {
82
+ * setLaps((prev) => [...prev, elapsed]);
83
+ * };
84
+ *
85
+ * const handleReset = () => {
86
+ * reset();
87
+ * setLaps([]);
88
+ * };
89
+ *
90
+ * return (
91
+ * <div>
92
+ * <div>경과 시간: {(elapsed / 1000).toFixed(2)}초</div>
93
+ * <button onClick={isRunning ? pause : start}>
94
+ * {isRunning ? '일시정지' : '시작'}
95
+ * </button>
96
+ * <button onClick={handleLap} disabled={!isRunning}>
97
+ * 랩
98
+ * </button>
99
+ * <button onClick={handleReset}>리셋</button>
100
+ *
101
+ * <div>
102
+ * <h3>랩 타임</h3>
103
+ * {laps.map((lap, index) => (
104
+ * <div key={index}>
105
+ * Lap {index + 1}: {(lap / 1000).toFixed(2)}초
106
+ * </div>
107
+ * ))}
108
+ * </div>
109
+ * </div>
110
+ * );
111
+ * }
112
+ * ```
113
+ *
114
+ * @example
115
+ * ```tsx
116
+ * // 성능 측정
117
+ * function PerformanceTracker() {
118
+ * const { elapsed, start, pause, reset } = useStopwatch();
119
+ *
120
+ * const handleTaskStart = () => {
121
+ * reset(); // 이전 측정 리셋
122
+ * start(); // 새 측정 시작
123
+ * // 작업 수행...
124
+ * };
125
+ *
126
+ * const handleTaskEnd = () => {
127
+ * pause();
128
+ * console.log(`작업 완료 시간: ${elapsed}ms`);
129
+ * };
130
+ *
131
+ * return (
132
+ * <div>
133
+ * <button onClick={handleTaskStart}>작업 시작</button>
134
+ * <button onClick={handleTaskEnd}>작업 완료</button>
135
+ * <div>측정 시간: {elapsed}ms</div>
136
+ * </div>
137
+ * );
138
+ * }
139
+ * ```
140
+ */
141
+ export declare function useStopwatch(options?: UseStopwatchOptions): UseStopwatchReturn;
142
+ //# sourceMappingURL=useStopwatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStopwatch.d.ts","sourceRoot":"","sources":["../../../src/hooks/time/useStopwatch.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,mBAAmB;IAClC,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,kBAAkB;IACjC,kBAAkB;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc;IACd,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,gBAAgB;IAChB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,cAAc;IACd,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,cAAc;IACd,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,mBAAmB;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqHG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,kBAAkB,CAqElF"}
@@ -0,0 +1,179 @@
1
+ import { useState, useCallback, useRef, useEffect } from 'react';
2
+ /**
3
+ * 스톱워치 hook
4
+ *
5
+ * 경과 시간을 측정하는 스톱워치를 제공합니다.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // 기본 사용
10
+ * function Stopwatch() {
11
+ * const { elapsed, elapsedSeconds, start, pause, reset, isRunning } = useStopwatch();
12
+ *
13
+ * return (
14
+ * <div>
15
+ * <div>경과 시간: {elapsedSeconds.toFixed(2)}초</div>
16
+ * <div>밀리초: {elapsed}ms</div>
17
+ * <button onClick={start} disabled={isRunning}>시작</button>
18
+ * <button onClick={pause} disabled={!isRunning}>일시정지</button>
19
+ * <button onClick={reset}>리셋</button>
20
+ * </div>
21
+ * );
22
+ * }
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * // 포맷팅된 시간 표시
28
+ * function FormattedStopwatch() {
29
+ * const { elapsed } = useStopwatch({ autoStart: true });
30
+ *
31
+ * const formatTime = (ms: number) => {
32
+ * const seconds = Math.floor(ms / 1000);
33
+ * const minutes = Math.floor(seconds / 60);
34
+ * const hours = Math.floor(minutes / 60);
35
+ *
36
+ * const displaySeconds = seconds % 60;
37
+ * const displayMinutes = minutes % 60;
38
+ * const displayMs = ms % 1000;
39
+ *
40
+ * return `${hours.toString().padStart(2, '0')}:${displayMinutes
41
+ * .toString()
42
+ * .padStart(2, '0')}:${displaySeconds.toString().padStart(2, '0')}.${Math.floor(
43
+ * displayMs / 10
44
+ * )
45
+ * .toString()
46
+ * .padStart(2, '0')}`;
47
+ * };
48
+ *
49
+ * return <div>{formatTime(elapsed)}</div>;
50
+ * }
51
+ * ```
52
+ *
53
+ * @example
54
+ * ```tsx
55
+ * // 랩 타임 기록
56
+ * function LapTimer() {
57
+ * const { elapsed, start, pause, reset, isRunning } = useStopwatch();
58
+ * const [laps, setLaps] = useState<number[]>([]);
59
+ *
60
+ * const handleLap = () => {
61
+ * setLaps((prev) => [...prev, elapsed]);
62
+ * };
63
+ *
64
+ * const handleReset = () => {
65
+ * reset();
66
+ * setLaps([]);
67
+ * };
68
+ *
69
+ * return (
70
+ * <div>
71
+ * <div>경과 시간: {(elapsed / 1000).toFixed(2)}초</div>
72
+ * <button onClick={isRunning ? pause : start}>
73
+ * {isRunning ? '일시정지' : '시작'}
74
+ * </button>
75
+ * <button onClick={handleLap} disabled={!isRunning}>
76
+ * 랩
77
+ * </button>
78
+ * <button onClick={handleReset}>리셋</button>
79
+ *
80
+ * <div>
81
+ * <h3>랩 타임</h3>
82
+ * {laps.map((lap, index) => (
83
+ * <div key={index}>
84
+ * Lap {index + 1}: {(lap / 1000).toFixed(2)}초
85
+ * </div>
86
+ * ))}
87
+ * </div>
88
+ * </div>
89
+ * );
90
+ * }
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```tsx
95
+ * // 성능 측정
96
+ * function PerformanceTracker() {
97
+ * const { elapsed, start, pause, reset } = useStopwatch();
98
+ *
99
+ * const handleTaskStart = () => {
100
+ * reset(); // 이전 측정 리셋
101
+ * start(); // 새 측정 시작
102
+ * // 작업 수행...
103
+ * };
104
+ *
105
+ * const handleTaskEnd = () => {
106
+ * pause();
107
+ * console.log(`작업 완료 시간: ${elapsed}ms`);
108
+ * };
109
+ *
110
+ * return (
111
+ * <div>
112
+ * <button onClick={handleTaskStart}>작업 시작</button>
113
+ * <button onClick={handleTaskEnd}>작업 완료</button>
114
+ * <div>측정 시간: {elapsed}ms</div>
115
+ * </div>
116
+ * );
117
+ * }
118
+ * ```
119
+ */
120
+ export function useStopwatch(options = {}) {
121
+ const { interval = 10, autoStart = false } = options;
122
+ const [elapsed, setElapsed] = useState(0);
123
+ const [isRunning, setIsRunning] = useState(autoStart);
124
+ const intervalRef = useRef(null);
125
+ const startTimeRef = useRef(null);
126
+ const previousElapsedRef = useRef(0);
127
+ const clear = useCallback(() => {
128
+ if (intervalRef.current) {
129
+ clearInterval(intervalRef.current);
130
+ intervalRef.current = null;
131
+ }
132
+ }, []);
133
+ const start = useCallback(() => {
134
+ if (!isRunning) {
135
+ startTimeRef.current = Date.now() - previousElapsedRef.current;
136
+ setIsRunning(true);
137
+ }
138
+ }, [isRunning]);
139
+ const pause = useCallback(() => {
140
+ clear();
141
+ setIsRunning(false);
142
+ previousElapsedRef.current = elapsed;
143
+ }, [clear, elapsed]);
144
+ const resume = useCallback(() => {
145
+ if (!isRunning) {
146
+ startTimeRef.current = Date.now() - previousElapsedRef.current;
147
+ setIsRunning(true);
148
+ }
149
+ }, [isRunning]);
150
+ const reset = useCallback(() => {
151
+ clear();
152
+ setElapsed(0);
153
+ setIsRunning(false);
154
+ startTimeRef.current = null;
155
+ previousElapsedRef.current = 0;
156
+ }, [clear]);
157
+ useEffect(() => {
158
+ if (isRunning) {
159
+ if (startTimeRef.current === null) {
160
+ startTimeRef.current = Date.now() - previousElapsedRef.current;
161
+ }
162
+ intervalRef.current = setInterval(() => {
163
+ if (startTimeRef.current !== null) {
164
+ setElapsed(Date.now() - startTimeRef.current);
165
+ }
166
+ }, interval);
167
+ return () => clear();
168
+ }
169
+ }, [isRunning, interval, clear]);
170
+ return {
171
+ elapsed,
172
+ elapsedSeconds: elapsed / 1000,
173
+ start,
174
+ pause,
175
+ resume,
176
+ reset,
177
+ isRunning,
178
+ };
179
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * UI/UX Hooks
3
+ */
4
+ export * from './useMediaQuery';
5
+ export * from './useWindowSize';
6
+ export * from './useTabs';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * UI/UX Hooks
3
+ */
4
+ export * from './useMediaQuery';
5
+ export * from './useWindowSize';
6
+ export * from './useTabs';
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMediaQuery.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/useMediaQuery.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA8CpD;AAED;;GAEG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
@@ -0,0 +1,33 @@
1
+ type UseTabsOptions = {
2
+ items: Array<{
3
+ id: string;
4
+ }>;
5
+ defaultTab?: string;
6
+ syncWithUrl?: boolean;
7
+ urlParamName?: string;
8
+ };
9
+ type UseTabsReturn = {
10
+ selectedTab: string;
11
+ selectedIndex: number;
12
+ setSelectedTab: (tabId: string) => void;
13
+ setSelectedIndex: (index: number) => void;
14
+ getTabId: (index: number) => string | undefined;
15
+ getTabIndex: (tabId: string) => number;
16
+ };
17
+ /**
18
+ * 탭 상태 관리 훅
19
+ * URL 동기화 및 상태 관리 로직을 제공
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const { selectedTab, setSelectedTab, selectedIndex } = useTabs({
24
+ * items: tabs,
25
+ * defaultTab: 'tab1',
26
+ * syncWithUrl: true,
27
+ * urlParamName: 'tab'
28
+ * });
29
+ * ```
30
+ */
31
+ export declare function useTabs({ items, defaultTab, syncWithUrl, urlParamName, }: UseTabsOptions): UseTabsReturn;
32
+ export {};
33
+ //# sourceMappingURL=useTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useTabs.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/useTabs.ts"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAChD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,UAAU,EACV,WAAmB,EACnB,YAAoB,GACrB,EAAE,cAAc,GAAG,aAAa,CA4GhC"}
@@ -0,0 +1,117 @@
1
+ 'use client';
2
+ import { useState, useEffect, useMemo, useCallback } from 'react';
3
+ /**
4
+ * 탭 상태 관리 훅
5
+ * URL 동기화 및 상태 관리 로직을 제공
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * const { selectedTab, setSelectedTab, selectedIndex } = useTabs({
10
+ * items: tabs,
11
+ * defaultTab: 'tab1',
12
+ * syncWithUrl: true,
13
+ * urlParamName: 'tab'
14
+ * });
15
+ * ```
16
+ */
17
+ export function useTabs({ items, defaultTab, syncWithUrl = false, urlParamName = 'tab', }) {
18
+ // URL에서 탭 ID 가져오기
19
+ const getTabFromUrl = useCallback(() => {
20
+ if (!syncWithUrl || typeof window === 'undefined') {
21
+ return undefined;
22
+ }
23
+ const params = new URLSearchParams(window.location.search);
24
+ return params.get(urlParamName) || undefined;
25
+ }, [syncWithUrl, urlParamName]);
26
+ // 초기 선택된 탭 결정 (우선순위: URL > defaultTab > 첫 번째)
27
+ const initialSelectedTab = useMemo(() => {
28
+ if (syncWithUrl) {
29
+ const urlTab = getTabFromUrl();
30
+ if (urlTab)
31
+ return urlTab;
32
+ }
33
+ return defaultTab || items[0]?.id || '';
34
+ }, [syncWithUrl, defaultTab, items, getTabFromUrl]);
35
+ const [selectedTab, setSelectedTabState] = useState(initialSelectedTab);
36
+ // 선택된 탭의 인덱스
37
+ const selectedIndex = useMemo(() => {
38
+ const index = items.findIndex((item) => item.id === selectedTab);
39
+ return index >= 0 ? index : 0;
40
+ }, [items, selectedTab]);
41
+ // URL 업데이트 함수
42
+ const updateUrl = (tabId) => {
43
+ if (!syncWithUrl || typeof window === 'undefined')
44
+ return;
45
+ const url = new URL(window.location.href);
46
+ if (tabId === defaultTab) {
47
+ // 기본 탭이면 URL에서 제거 (깔끔한 URL 유지)
48
+ url.searchParams.delete(urlParamName);
49
+ }
50
+ else {
51
+ url.searchParams.set(urlParamName, tabId);
52
+ }
53
+ window.history.pushState({}, '', url.toString());
54
+ };
55
+ // 탭 ID로 선택
56
+ const setSelectedTab = (tabId) => {
57
+ setSelectedTabState(tabId);
58
+ if (syncWithUrl) {
59
+ updateUrl(tabId);
60
+ }
61
+ };
62
+ // 인덱스로 선택
63
+ const setSelectedIndex = (index) => {
64
+ const tabId = items[index]?.id;
65
+ if (tabId) {
66
+ setSelectedTab(tabId);
67
+ }
68
+ };
69
+ // 인덱스로 탭 ID 가져오기
70
+ const getTabId = (index) => {
71
+ return items[index]?.id;
72
+ };
73
+ // 탭 ID로 인덱스 가져오기
74
+ const getTabIndex = (tabId) => {
75
+ return items.findIndex((item) => item.id === tabId);
76
+ };
77
+ // 브라우저 뒤로가기/앞으로가기 지원
78
+ useEffect(() => {
79
+ if (!syncWithUrl || typeof window === 'undefined')
80
+ return;
81
+ const handlePopState = () => {
82
+ const urlTab = getTabFromUrl();
83
+ if (urlTab) {
84
+ setSelectedTabState(urlTab);
85
+ }
86
+ else if (defaultTab) {
87
+ setSelectedTabState(defaultTab);
88
+ }
89
+ else if (items[0]?.id) {
90
+ setSelectedTabState(items[0].id);
91
+ }
92
+ };
93
+ window.addEventListener('popstate', handlePopState);
94
+ return () => {
95
+ window.removeEventListener('popstate', handlePopState);
96
+ };
97
+ }, [syncWithUrl, defaultTab, items, getTabFromUrl]);
98
+ // URL 초기 동기화
99
+ useEffect(() => {
100
+ if (!syncWithUrl || typeof window === 'undefined')
101
+ return;
102
+ const urlTab = getTabFromUrl();
103
+ if (urlTab && urlTab !== selectedTab) {
104
+ // URL과 상태가 다를 때만 업데이트 (초기 로드 시)
105
+ setSelectedTabState(urlTab);
106
+ }
107
+ // eslint-disable-next-line react-hooks/exhaustive-deps
108
+ }, [syncWithUrl, urlParamName]); // 초기 마운트 시에만 실행
109
+ return {
110
+ selectedTab,
111
+ selectedIndex,
112
+ setSelectedTab,
113
+ setSelectedIndex,
114
+ getTabId,
115
+ getTabIndex,
116
+ };
117
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useWindowSize.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/useWindowSize.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kBAAkB;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,aAAa,IAAI,UAAU,CAoC1C"}
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ export * from './string';
8
8
  export * from './number';
9
9
  // Mock data utilities
10
10
  export * from './mock';
11
- // React Hooks (import separately: 'goodchuck-utils/hooks')
11
+ // React Hooks (import separately: '@blastlabs/utils/hooks')
12
12
  // Note: Hooks are not exported from main entry to avoid React dependency for non-React users
13
13
  // export * from './hooks';
14
14
  // Array utilities (placeholder for future)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blastlabs/utils",
3
- "version": "1.11.1",
3
+ "version": "1.12.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "scripts": {
11
11
  "prepare": "npm run build:tsc",
12
12
  "prepublishOnly": "npm run test:run && npm run clean && npm run build:tsc",
13
- "build": "npm run build",
13
+ "build": "npm run clean && npm run build:tsc",
14
14
  "build:tsc": "tsc",
15
15
  "clean": "rm -rf dist",
16
16
  "dev": "tsc --watch",
@@ -62,15 +62,22 @@
62
62
  "dayjs": "^1.11.19"
63
63
  },
64
64
  "devDependencies": {
65
+ "@testing-library/react": "^16.3.1",
66
+ "@testing-library/user-event": "^14.6.1",
65
67
  "@types/node": "^25.0.6",
66
- "typescript": "^5.9.3",
67
68
  "@types/react": "^19.2.8",
68
69
  "@vitest/ui": "^4.0.16",
70
+ "happy-dom": "^20.1.0",
71
+ "react": "^19.2.3",
72
+ "react-dom": "^19.2.3",
73
+ "react-hook-form": "^7.71.1",
74
+ "typescript": "^5.9.3",
69
75
  "vitest": "^4.0.16"
70
76
  },
71
77
  "peerDependencies": {
72
78
  "react": ">=16.8.0",
73
- "react-dom": ">=16.8.0"
79
+ "react-dom": ">=16.8.0",
80
+ "react-hook-form": ">=7.0.0"
74
81
  },
75
82
  "peerDependenciesMeta": {
76
83
  "react": {
@@ -78,6 +85,9 @@
78
85
  },
79
86
  "react-dom": {
80
87
  "optional": true
88
+ },
89
+ "react-hook-form": {
90
+ "optional": true
81
91
  }
82
92
  },
83
93
  "publishConfig": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"IdSelector.d.ts","sourceRoot":"","sources":["../../../src/components/dev/IdSelector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkC,MAAM,OAAO,CAAC;AAEvD,KAAK,SAAS,GAAG;IACf,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,qBAuH3D"}