@blastlabs/utils 1.11.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/README.md +267 -0
- package/dist/components/dev/ApiLogger.d.ts +136 -0
- package/dist/components/dev/ApiLogger.d.ts.map +1 -0
- package/dist/components/dev/ApiLogger.js +408 -0
- package/dist/components/dev/DevPanel.d.ts +32 -0
- package/dist/components/dev/DevPanel.d.ts.map +1 -0
- package/dist/components/dev/DevPanel.js +196 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts +136 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/FormDevTools.js +442 -0
- package/dist/components/dev/FormDevTools/index.d.ts +3 -0
- package/dist/components/dev/FormDevTools/index.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/index.js +1 -0
- package/dist/components/dev/FormDevTools/styles.d.ts +45 -0
- package/dist/components/dev/FormDevTools/styles.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/styles.js +197 -0
- package/dist/components/dev/IdSelector.d.ts +50 -0
- package/dist/components/dev/IdSelector.d.ts.map +1 -0
- package/dist/components/dev/IdSelector.js +129 -0
- package/dist/components/dev/WindowSizeDisplay.d.ts +44 -0
- package/dist/components/dev/WindowSizeDisplay.d.ts.map +1 -0
- package/dist/components/dev/WindowSizeDisplay.js +74 -0
- package/dist/components/dev/ZIndexDebugger.d.ts +32 -0
- package/dist/components/dev/ZIndexDebugger.d.ts.map +1 -0
- package/dist/components/dev/ZIndexDebugger.js +184 -0
- package/dist/components/dev/index.d.ts +15 -0
- package/dist/components/dev/index.d.ts.map +1 -0
- package/dist/components/dev/index.js +12 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +7 -0
- package/dist/date/index.d.ts +64 -0
- package/dist/date/index.d.ts.map +1 -0
- package/dist/date/index.js +92 -0
- package/dist/date/index.test.d.ts +2 -0
- package/dist/date/index.test.d.ts.map +1 -0
- package/dist/date/index.test.js +166 -0
- package/dist/form/__tests__/formatter.test.d.ts +2 -0
- package/dist/form/__tests__/formatter.test.d.ts.map +1 -0
- package/dist/form/__tests__/formatter.test.js +74 -0
- package/dist/form/__tests__/helpers.test.d.ts +2 -0
- package/dist/form/__tests__/helpers.test.d.ts.map +1 -0
- package/dist/form/__tests__/helpers.test.js +42 -0
- package/dist/form/__tests__/validation.test.d.ts +2 -0
- package/dist/form/__tests__/validation.test.d.ts.map +1 -0
- package/dist/form/__tests__/validation.test.js +67 -0
- package/dist/form/formatter.d.ts +34 -0
- package/dist/form/formatter.d.ts.map +1 -0
- package/dist/form/formatter.js +76 -0
- package/dist/form/helpers.d.ts +16 -0
- package/dist/form/helpers.d.ts.map +1 -0
- package/dist/form/helpers.js +34 -0
- package/dist/form/index.d.ts +9 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +11 -0
- package/dist/form/validation.d.ts +33 -0
- package/dist/form/validation.d.ts.map +1 -0
- package/dist/form/validation.js +56 -0
- package/dist/hooks/index.d.ts +19 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +23 -0
- package/dist/hooks/useClickOutside.d.ts +49 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.js +94 -0
- package/dist/hooks/useCopyToClipboard.d.ts +67 -0
- package/dist/hooks/useCopyToClipboard.d.ts.map +1 -0
- package/dist/hooks/useCopyToClipboard.js +79 -0
- package/dist/hooks/useDebounce.d.ts +47 -0
- package/dist/hooks/useDebounce.d.ts.map +1 -0
- package/dist/hooks/useDebounce.js +60 -0
- package/dist/hooks/useEventListener.d.ts +79 -0
- package/dist/hooks/useEventListener.d.ts.map +1 -0
- package/dist/hooks/useEventListener.js +33 -0
- package/dist/hooks/useIntersectionObserver.d.ts +109 -0
- package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
- package/dist/hooks/useIntersectionObserver.js +128 -0
- package/dist/hooks/useLocalStorage.d.ts +19 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.js +91 -0
- package/dist/hooks/useMediaQuery.d.ts +56 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +104 -0
- package/dist/hooks/usePrevious.d.ts +58 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +67 -0
- package/dist/hooks/useSessionStorage.d.ts +19 -0
- package/dist/hooks/useSessionStorage.d.ts.map +1 -0
- package/dist/hooks/useSessionStorage.js +85 -0
- package/dist/hooks/useThrottle.d.ts +57 -0
- package/dist/hooks/useThrottle.d.ts.map +1 -0
- package/dist/hooks/useThrottle.js +80 -0
- package/dist/hooks/useToggle.d.ts +49 -0
- package/dist/hooks/useToggle.d.ts.map +1 -0
- package/dist/hooks/useToggle.js +56 -0
- package/dist/hooks/useWindowSize.d.ts +58 -0
- package/dist/hooks/useWindowSize.d.ts.map +1 -0
- package/dist/hooks/useWindowSize.js +79 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/mock/form.d.ts +41 -0
- package/dist/mock/form.d.ts.map +1 -0
- package/dist/mock/form.js +195 -0
- package/dist/mock/generators.d.ts +112 -0
- package/dist/mock/generators.d.ts.map +1 -0
- package/dist/mock/generators.js +195 -0
- package/dist/mock/index.d.ts +8 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +9 -0
- package/dist/number/format.d.ts +116 -0
- package/dist/number/format.d.ts.map +1 -0
- package/dist/number/format.js +165 -0
- package/dist/number/index.d.ts +7 -0
- package/dist/number/index.d.ts.map +1 -0
- package/dist/number/index.js +7 -0
- package/dist/string/__tests__/case.test.d.ts +2 -0
- package/dist/string/__tests__/case.test.d.ts.map +1 -0
- package/dist/string/__tests__/case.test.js +61 -0
- package/dist/string/__tests__/manipulation.test.d.ts +2 -0
- package/dist/string/__tests__/manipulation.test.d.ts.map +1 -0
- package/dist/string/__tests__/manipulation.test.js +109 -0
- package/dist/string/__tests__/validation.test.d.ts +2 -0
- package/dist/string/__tests__/validation.test.d.ts.map +1 -0
- package/dist/string/__tests__/validation.test.js +101 -0
- package/dist/string/case.d.ts +42 -0
- package/dist/string/case.d.ts.map +1 -0
- package/dist/string/case.js +71 -0
- package/dist/string/index.d.ts +9 -0
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +11 -0
- package/dist/string/manipulation.d.ts +61 -0
- package/dist/string/manipulation.d.ts.map +1 -0
- package/dist/string/manipulation.js +106 -0
- package/dist/string/validation.d.ts +79 -0
- package/dist/string/validation.d.ts.map +1 -0
- package/dist/string/validation.js +115 -0
- package/package.json +86 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 미디어 쿼리 매칭 여부를 반환하는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param query - CSS 미디어 쿼리 문자열
|
|
5
|
+
* @returns 미디어 쿼리 매칭 여부
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 모바일 체크
|
|
9
|
+
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // 태블릿 체크
|
|
13
|
+
* const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // 데스크톱 체크
|
|
17
|
+
* const isDesktop = useMediaQuery('(min-width: 1025px)');
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 다크모드 체크
|
|
21
|
+
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 가로 모드 체크
|
|
25
|
+
* const isLandscape = useMediaQuery('(orientation: landscape)');
|
|
26
|
+
*/
|
|
27
|
+
export declare function useMediaQuery(query: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 일반적인 브레이크포인트를 위한 헬퍼 hook들
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* 모바일 디바이스 체크 (768px 이하)
|
|
33
|
+
* @example
|
|
34
|
+
* const isMobile = useIsMobile();
|
|
35
|
+
* if (isMobile) return <MobileView />;
|
|
36
|
+
*/
|
|
37
|
+
export declare function useIsMobile(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* 태블릿 디바이스 체크 (769px ~ 1024px)
|
|
40
|
+
* @example
|
|
41
|
+
* const isTablet = useIsTablet();
|
|
42
|
+
*/
|
|
43
|
+
export declare function useIsTablet(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 데스크톱 디바이스 체크 (1025px 이상)
|
|
46
|
+
* @example
|
|
47
|
+
* const isDesktop = useIsDesktop();
|
|
48
|
+
*/
|
|
49
|
+
export declare function useIsDesktop(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* 다크모드 체크
|
|
52
|
+
* @example
|
|
53
|
+
* const isDarkMode = useIsDarkMode();
|
|
54
|
+
*/
|
|
55
|
+
export declare function useIsDarkMode(): boolean;
|
|
56
|
+
//# sourceMappingURL=useMediaQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMediaQuery.d.ts","sourceRoot":"","sources":["../../src/hooks/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,104 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 미디어 쿼리 매칭 여부를 반환하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param query - CSS 미디어 쿼리 문자열
|
|
6
|
+
* @returns 미디어 쿼리 매칭 여부
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 모바일 체크
|
|
10
|
+
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 태블릿 체크
|
|
14
|
+
* const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // 데스크톱 체크
|
|
18
|
+
* const isDesktop = useMediaQuery('(min-width: 1025px)');
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // 다크모드 체크
|
|
22
|
+
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 가로 모드 체크
|
|
26
|
+
* const isLandscape = useMediaQuery('(orientation: landscape)');
|
|
27
|
+
*/
|
|
28
|
+
export function useMediaQuery(query) {
|
|
29
|
+
// SSR 안전성을 위한 초기값 설정
|
|
30
|
+
const getMatches = (query) => {
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
return window.matchMedia(query).matches;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
const [matches, setMatches] = useState(getMatches(query));
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (typeof window === 'undefined') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const mediaQuery = window.matchMedia(query);
|
|
42
|
+
// 초기 상태 설정
|
|
43
|
+
setMatches(mediaQuery.matches);
|
|
44
|
+
// 미디어 쿼리 변경 감지 (최신 API)
|
|
45
|
+
const handleChange = (event) => {
|
|
46
|
+
setMatches(event.matches);
|
|
47
|
+
};
|
|
48
|
+
// 이벤트 리스너 등록
|
|
49
|
+
// addEventListener를 지원하는 브라우저
|
|
50
|
+
if (mediaQuery.addEventListener) {
|
|
51
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// 구형 브라우저 지원 (deprecated)
|
|
55
|
+
mediaQuery.addListener(handleChange);
|
|
56
|
+
}
|
|
57
|
+
// 클린업
|
|
58
|
+
return () => {
|
|
59
|
+
if (mediaQuery.removeEventListener) {
|
|
60
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
mediaQuery.removeListener(handleChange);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [query]);
|
|
67
|
+
return matches;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 일반적인 브레이크포인트를 위한 헬퍼 hook들
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* 모바일 디바이스 체크 (768px 이하)
|
|
74
|
+
* @example
|
|
75
|
+
* const isMobile = useIsMobile();
|
|
76
|
+
* if (isMobile) return <MobileView />;
|
|
77
|
+
*/
|
|
78
|
+
export function useIsMobile() {
|
|
79
|
+
return useMediaQuery('(max-width: 768px)');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 태블릿 디바이스 체크 (769px ~ 1024px)
|
|
83
|
+
* @example
|
|
84
|
+
* const isTablet = useIsTablet();
|
|
85
|
+
*/
|
|
86
|
+
export function useIsTablet() {
|
|
87
|
+
return useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 데스크톱 디바이스 체크 (1025px 이상)
|
|
91
|
+
* @example
|
|
92
|
+
* const isDesktop = useIsDesktop();
|
|
93
|
+
*/
|
|
94
|
+
export function useIsDesktop() {
|
|
95
|
+
return useMediaQuery('(min-width: 1025px)');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 다크모드 체크
|
|
99
|
+
* @example
|
|
100
|
+
* const isDarkMode = useIsDarkMode();
|
|
101
|
+
*/
|
|
102
|
+
export function useIsDarkMode() {
|
|
103
|
+
return useMediaQuery('(prefers-color-scheme: dark)');
|
|
104
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 이전 렌더링의 값을 저장하는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param value - 추적할 값
|
|
5
|
+
* @returns 이전 렌더링의 값 (첫 렌더링에서는 undefined)
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 값 변경 감지
|
|
9
|
+
* function Counter() {
|
|
10
|
+
* const [count, setCount] = useState(0);
|
|
11
|
+
* const prevCount = usePrevious(count);
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <div>
|
|
15
|
+
* <p>Current: {count}</p>
|
|
16
|
+
* <p>Previous: {prevCount}</p>
|
|
17
|
+
* <p>Changed: {count !== prevCount ? 'Yes' : 'No'}</p>
|
|
18
|
+
* <button onClick={() => setCount(count + 1)}>Increment</button>
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 증가/감소 방향 표시
|
|
25
|
+
* function PriceDisplay({ price }: { price: number }) {
|
|
26
|
+
* const prevPrice = usePrevious(price);
|
|
27
|
+
*
|
|
28
|
+
* const trend = prevPrice === undefined
|
|
29
|
+
* ? null
|
|
30
|
+
* : price > prevPrice
|
|
31
|
+
* ? '📈 Up'
|
|
32
|
+
* : price < prevPrice
|
|
33
|
+
* ? '📉 Down'
|
|
34
|
+
* : '➡️ Same';
|
|
35
|
+
*
|
|
36
|
+
* return (
|
|
37
|
+
* <div>
|
|
38
|
+
* <span>${price}</span>
|
|
39
|
+
* {trend && <span>{trend}</span>}
|
|
40
|
+
* </div>
|
|
41
|
+
* );
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // 애니메이션 방향 결정
|
|
46
|
+
* function AnimatedList({ items }: { items: string[] }) {
|
|
47
|
+
* const prevItems = usePrevious(items);
|
|
48
|
+
* const isAdding = prevItems && items.length > prevItems.length;
|
|
49
|
+
*
|
|
50
|
+
* return (
|
|
51
|
+
* <ul className={isAdding ? 'slide-in' : 'slide-out'}>
|
|
52
|
+
* {items.map(item => <li key={item}>{item}</li>)}
|
|
53
|
+
* </ul>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export declare function usePrevious<T>(value: T): T | undefined;
|
|
58
|
+
//# sourceMappingURL=usePrevious.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePrevious.d.ts","sourceRoot":"","sources":["../../src/hooks/usePrevious.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,SAAS,CAWtD"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useRef, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 이전 렌더링의 값을 저장하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param value - 추적할 값
|
|
6
|
+
* @returns 이전 렌더링의 값 (첫 렌더링에서는 undefined)
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 값 변경 감지
|
|
10
|
+
* function Counter() {
|
|
11
|
+
* const [count, setCount] = useState(0);
|
|
12
|
+
* const prevCount = usePrevious(count);
|
|
13
|
+
*
|
|
14
|
+
* return (
|
|
15
|
+
* <div>
|
|
16
|
+
* <p>Current: {count}</p>
|
|
17
|
+
* <p>Previous: {prevCount}</p>
|
|
18
|
+
* <p>Changed: {count !== prevCount ? 'Yes' : 'No'}</p>
|
|
19
|
+
* <button onClick={() => setCount(count + 1)}>Increment</button>
|
|
20
|
+
* </div>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 증가/감소 방향 표시
|
|
26
|
+
* function PriceDisplay({ price }: { price: number }) {
|
|
27
|
+
* const prevPrice = usePrevious(price);
|
|
28
|
+
*
|
|
29
|
+
* const trend = prevPrice === undefined
|
|
30
|
+
* ? null
|
|
31
|
+
* : price > prevPrice
|
|
32
|
+
* ? '📈 Up'
|
|
33
|
+
* : price < prevPrice
|
|
34
|
+
* ? '📉 Down'
|
|
35
|
+
* : '➡️ Same';
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <div>
|
|
39
|
+
* <span>${price}</span>
|
|
40
|
+
* {trend && <span>{trend}</span>}
|
|
41
|
+
* </div>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // 애니메이션 방향 결정
|
|
47
|
+
* function AnimatedList({ items }: { items: string[] }) {
|
|
48
|
+
* const prevItems = usePrevious(items);
|
|
49
|
+
* const isAdding = prevItems && items.length > prevItems.length;
|
|
50
|
+
*
|
|
51
|
+
* return (
|
|
52
|
+
* <ul className={isAdding ? 'slide-in' : 'slide-out'}>
|
|
53
|
+
* {items.map(item => <li key={item}>{item}</li>)}
|
|
54
|
+
* </ul>
|
|
55
|
+
* );
|
|
56
|
+
* }
|
|
57
|
+
*/
|
|
58
|
+
export function usePrevious(value) {
|
|
59
|
+
// ref는 리렌더링 간에 값을 유지
|
|
60
|
+
const ref = useRef(undefined);
|
|
61
|
+
// 렌더링 후에 현재 값을 ref에 저장
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
ref.current = value;
|
|
64
|
+
}, [value]);
|
|
65
|
+
// 현재 렌더링에서는 이전 값을 반환
|
|
66
|
+
return ref.current;
|
|
67
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* sessionStorage와 동기화되는 state hook
|
|
4
|
+
*
|
|
5
|
+
* @param key - sessionStorage 키
|
|
6
|
+
* @param initialValue - 초기값
|
|
7
|
+
* @returns [storedValue, setValue, removeValue]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [token, setToken, removeToken] = useSessionStorage('auth-token', '');
|
|
11
|
+
*
|
|
12
|
+
* // 값 설정
|
|
13
|
+
* setToken('abc123');
|
|
14
|
+
*
|
|
15
|
+
* // 값 제거
|
|
16
|
+
* removeToken();
|
|
17
|
+
*/
|
|
18
|
+
export declare function useSessionStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>, () => void];
|
|
19
|
+
//# sourceMappingURL=useSessionStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionStorage.d.ts","sourceRoot":"","sources":["../../src/hooks/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,85 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* sessionStorage와 동기화되는 state hook
|
|
4
|
+
*
|
|
5
|
+
* @param key - sessionStorage 키
|
|
6
|
+
* @param initialValue - 초기값
|
|
7
|
+
* @returns [storedValue, setValue, removeValue]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [token, setToken, removeToken] = useSessionStorage('auth-token', '');
|
|
11
|
+
*
|
|
12
|
+
* // 값 설정
|
|
13
|
+
* setToken('abc123');
|
|
14
|
+
*
|
|
15
|
+
* // 값 제거
|
|
16
|
+
* removeToken();
|
|
17
|
+
*/
|
|
18
|
+
export function useSessionStorage(key, initialValue) {
|
|
19
|
+
// SSR 안전성 체크
|
|
20
|
+
const isBrowser = typeof window !== 'undefined';
|
|
21
|
+
// 초기값을 sessionStorage에서 가져오기
|
|
22
|
+
const readValue = useCallback(() => {
|
|
23
|
+
if (!isBrowser) {
|
|
24
|
+
return initialValue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const item = window.sessionStorage.getItem(key);
|
|
28
|
+
return item ? JSON.parse(item) : initialValue;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn(`Error reading sessionStorage key "${key}":`, error);
|
|
32
|
+
return initialValue;
|
|
33
|
+
}
|
|
34
|
+
}, [initialValue, key, isBrowser]);
|
|
35
|
+
const [storedValue, setStoredValue] = useState(readValue);
|
|
36
|
+
// 값 설정 함수
|
|
37
|
+
const setValue = useCallback((value) => {
|
|
38
|
+
if (!isBrowser) {
|
|
39
|
+
console.warn(`Tried setting sessionStorage key "${key}" even though environment is not a client`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// useState와 동일하게 함수형 업데이트 지원
|
|
44
|
+
const newValue = value instanceof Function ? value(storedValue) : value;
|
|
45
|
+
// sessionStorage에 저장
|
|
46
|
+
window.sessionStorage.setItem(key, JSON.stringify(newValue));
|
|
47
|
+
// state 업데이트
|
|
48
|
+
setStoredValue(newValue);
|
|
49
|
+
// storage event 발생
|
|
50
|
+
window.dispatchEvent(new Event('session-storage'));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`Error setting sessionStorage key "${key}":`, error);
|
|
54
|
+
}
|
|
55
|
+
}, [key, storedValue, isBrowser]);
|
|
56
|
+
// 값 제거 함수
|
|
57
|
+
const removeValue = useCallback(() => {
|
|
58
|
+
if (!isBrowser) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
window.sessionStorage.removeItem(key);
|
|
63
|
+
setStoredValue(initialValue);
|
|
64
|
+
window.dispatchEvent(new Event('session-storage'));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Error removing sessionStorage key "${key}":`, error);
|
|
68
|
+
}
|
|
69
|
+
}, [key, initialValue, isBrowser]);
|
|
70
|
+
// 같은 페이지 내의 변경사항 감지
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isBrowser) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const handleStorageChange = () => {
|
|
76
|
+
setStoredValue(readValue());
|
|
77
|
+
};
|
|
78
|
+
// 같은 페이지 내의 변경사항
|
|
79
|
+
window.addEventListener('session-storage', handleStorageChange);
|
|
80
|
+
return () => {
|
|
81
|
+
window.removeEventListener('session-storage', handleStorageChange);
|
|
82
|
+
};
|
|
83
|
+
}, [key, readValue, isBrowser]);
|
|
84
|
+
return [storedValue, setValue, removeValue];
|
|
85
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 값의 업데이트를 일정 시간 간격으로 제한하는 hook
|
|
3
|
+
* debounce와 달리 일정 간격마다 최신 값을 업데이트합니다.
|
|
4
|
+
*
|
|
5
|
+
* @param value - throttle할 값
|
|
6
|
+
* @param interval - 업데이트 간격 (밀리초, 기본값: 500ms)
|
|
7
|
+
* @returns throttle된 값
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 스크롤 위치 추적 최적화
|
|
11
|
+
* function ScrollTracker() {
|
|
12
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
13
|
+
* const throttledScrollY = useThrottle(scrollY, 200);
|
|
14
|
+
*
|
|
15
|
+
* useEffect(() => {
|
|
16
|
+
* const handleScroll = () => setScrollY(window.scrollY);
|
|
17
|
+
* window.addEventListener('scroll', handleScroll);
|
|
18
|
+
* return () => window.removeEventListener('scroll', handleScroll);
|
|
19
|
+
* }, []);
|
|
20
|
+
*
|
|
21
|
+
* return <div>Scroll position: {throttledScrollY}px</div>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 무한 스크롤
|
|
26
|
+
* function InfiniteScroll() {
|
|
27
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
28
|
+
* const throttledScrollY = useThrottle(scrollY, 300);
|
|
29
|
+
*
|
|
30
|
+
* useEffect(() => {
|
|
31
|
+
* const bottom = document.documentElement.scrollHeight - window.innerHeight;
|
|
32
|
+
* if (throttledScrollY >= bottom - 100) {
|
|
33
|
+
* loadMoreData();
|
|
34
|
+
* }
|
|
35
|
+
* }, [throttledScrollY]);
|
|
36
|
+
*
|
|
37
|
+
* return <div>...</div>;
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // 검색 입력 (실시간 검색에 적합)
|
|
42
|
+
* function LiveSearch() {
|
|
43
|
+
* const [query, setQuery] = useState('');
|
|
44
|
+
* const throttledQuery = useThrottle(query, 500);
|
|
45
|
+
*
|
|
46
|
+
* useEffect(() => {
|
|
47
|
+
* // 500ms마다 최대 한 번씩만 API 호출
|
|
48
|
+
* if (throttledQuery) {
|
|
49
|
+
* searchAPI(throttledQuery);
|
|
50
|
+
* }
|
|
51
|
+
* }, [throttledQuery]);
|
|
52
|
+
*
|
|
53
|
+
* return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
export declare function useThrottle<T>(value: T, interval?: number): T;
|
|
57
|
+
//# sourceMappingURL=useThrottle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useThrottle.d.ts","sourceRoot":"","sources":["../../src/hooks/useThrottle.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,QAAQ,GAAE,MAAY,GAAG,CAAC,CA0BlE"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 값의 업데이트를 일정 시간 간격으로 제한하는 hook
|
|
4
|
+
* debounce와 달리 일정 간격마다 최신 값을 업데이트합니다.
|
|
5
|
+
*
|
|
6
|
+
* @param value - throttle할 값
|
|
7
|
+
* @param interval - 업데이트 간격 (밀리초, 기본값: 500ms)
|
|
8
|
+
* @returns throttle된 값
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // 스크롤 위치 추적 최적화
|
|
12
|
+
* function ScrollTracker() {
|
|
13
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
14
|
+
* const throttledScrollY = useThrottle(scrollY, 200);
|
|
15
|
+
*
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* const handleScroll = () => setScrollY(window.scrollY);
|
|
18
|
+
* window.addEventListener('scroll', handleScroll);
|
|
19
|
+
* return () => window.removeEventListener('scroll', handleScroll);
|
|
20
|
+
* }, []);
|
|
21
|
+
*
|
|
22
|
+
* return <div>Scroll position: {throttledScrollY}px</div>;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* // 무한 스크롤
|
|
27
|
+
* function InfiniteScroll() {
|
|
28
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
29
|
+
* const throttledScrollY = useThrottle(scrollY, 300);
|
|
30
|
+
*
|
|
31
|
+
* useEffect(() => {
|
|
32
|
+
* const bottom = document.documentElement.scrollHeight - window.innerHeight;
|
|
33
|
+
* if (throttledScrollY >= bottom - 100) {
|
|
34
|
+
* loadMoreData();
|
|
35
|
+
* }
|
|
36
|
+
* }, [throttledScrollY]);
|
|
37
|
+
*
|
|
38
|
+
* return <div>...</div>;
|
|
39
|
+
* }
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // 검색 입력 (실시간 검색에 적합)
|
|
43
|
+
* function LiveSearch() {
|
|
44
|
+
* const [query, setQuery] = useState('');
|
|
45
|
+
* const throttledQuery = useThrottle(query, 500);
|
|
46
|
+
*
|
|
47
|
+
* useEffect(() => {
|
|
48
|
+
* // 500ms마다 최대 한 번씩만 API 호출
|
|
49
|
+
* if (throttledQuery) {
|
|
50
|
+
* searchAPI(throttledQuery);
|
|
51
|
+
* }
|
|
52
|
+
* }, [throttledQuery]);
|
|
53
|
+
*
|
|
54
|
+
* return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
export function useThrottle(value, interval = 500) {
|
|
58
|
+
const [throttledValue, setThrottledValue] = useState(value);
|
|
59
|
+
const lastExecuted = useRef(Date.now());
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
// 마지막 실행으로부터 경과된 시간
|
|
62
|
+
const timeSinceLastExecution = Date.now() - lastExecuted.current;
|
|
63
|
+
if (timeSinceLastExecution >= interval) {
|
|
64
|
+
// 간격이 지났으면 즉시 업데이트
|
|
65
|
+
setThrottledValue(value);
|
|
66
|
+
lastExecuted.current = Date.now();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// 아직 간격이 안 지났으면 남은 시간 후에 업데이트
|
|
70
|
+
const timer = setTimeout(() => {
|
|
71
|
+
setThrottledValue(value);
|
|
72
|
+
lastExecuted.current = Date.now();
|
|
73
|
+
}, interval - timeSinceLastExecution);
|
|
74
|
+
return () => {
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}, [value, interval]);
|
|
79
|
+
return throttledValue;
|
|
80
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* boolean 상태를 쉽게 토글할 수 있는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param initialValue - 초기값 (기본값: false)
|
|
5
|
+
* @returns [현재 값, 토글 함수, 값 설정 함수]
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 기본 사용
|
|
9
|
+
* function Modal() {
|
|
10
|
+
* const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
|
|
11
|
+
*
|
|
12
|
+
* return (
|
|
13
|
+
* <>
|
|
14
|
+
* <button onClick={toggleOpen}>Toggle Modal</button>
|
|
15
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
16
|
+
* <button onClick={() => setIsOpen(false)}>Close Modal</button>
|
|
17
|
+
* {isOpen && <div>Modal Content</div>}
|
|
18
|
+
* </>
|
|
19
|
+
* );
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // 다크모드 토글
|
|
24
|
+
* function ThemeToggle() {
|
|
25
|
+
* const [isDark, toggleTheme] = useToggle(false);
|
|
26
|
+
*
|
|
27
|
+
* return (
|
|
28
|
+
* <button onClick={toggleTheme}>
|
|
29
|
+
* {isDark ? '🌙 Dark' : '☀️ Light'}
|
|
30
|
+
* </button>
|
|
31
|
+
* );
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // 메뉴 열기/닫기
|
|
36
|
+
* function Sidebar() {
|
|
37
|
+
* const [isExpanded, toggleExpanded] = useToggle(true);
|
|
38
|
+
*
|
|
39
|
+
* return (
|
|
40
|
+
* <aside className={isExpanded ? 'expanded' : 'collapsed'}>
|
|
41
|
+
* <button onClick={toggleExpanded}>
|
|
42
|
+
* {isExpanded ? '◀' : '▶'}
|
|
43
|
+
* </button>
|
|
44
|
+
* </aside>
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
export declare function useToggle(initialValue?: boolean): [boolean, () => void, (value: boolean) => void];
|
|
49
|
+
//# sourceMappingURL=useToggle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useToggle.d.ts","sourceRoot":"","sources":["../../src/hooks/useToggle.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,wBAAgB,SAAS,CACvB,YAAY,GAAE,OAAe,GAC5B,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,CASjD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* boolean 상태를 쉽게 토글할 수 있는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param initialValue - 초기값 (기본값: false)
|
|
6
|
+
* @returns [현재 값, 토글 함수, 값 설정 함수]
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 기본 사용
|
|
10
|
+
* function Modal() {
|
|
11
|
+
* const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
|
|
12
|
+
*
|
|
13
|
+
* return (
|
|
14
|
+
* <>
|
|
15
|
+
* <button onClick={toggleOpen}>Toggle Modal</button>
|
|
16
|
+
* <button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
17
|
+
* <button onClick={() => setIsOpen(false)}>Close Modal</button>
|
|
18
|
+
* {isOpen && <div>Modal Content</div>}
|
|
19
|
+
* </>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 다크모드 토글
|
|
25
|
+
* function ThemeToggle() {
|
|
26
|
+
* const [isDark, toggleTheme] = useToggle(false);
|
|
27
|
+
*
|
|
28
|
+
* return (
|
|
29
|
+
* <button onClick={toggleTheme}>
|
|
30
|
+
* {isDark ? '🌙 Dark' : '☀️ Light'}
|
|
31
|
+
* </button>
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // 메뉴 열기/닫기
|
|
37
|
+
* function Sidebar() {
|
|
38
|
+
* const [isExpanded, toggleExpanded] = useToggle(true);
|
|
39
|
+
*
|
|
40
|
+
* return (
|
|
41
|
+
* <aside className={isExpanded ? 'expanded' : 'collapsed'}>
|
|
42
|
+
* <button onClick={toggleExpanded}>
|
|
43
|
+
* {isExpanded ? '◀' : '▶'}
|
|
44
|
+
* </button>
|
|
45
|
+
* </aside>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
*/
|
|
49
|
+
export function useToggle(initialValue = false) {
|
|
50
|
+
const [value, setValue] = useState(initialValue);
|
|
51
|
+
// 토글 함수는 리렌더링 시에도 동일한 참조를 유지
|
|
52
|
+
const toggle = useCallback(() => {
|
|
53
|
+
setValue((prev) => !prev);
|
|
54
|
+
}, []);
|
|
55
|
+
return [value, toggle, setValue];
|
|
56
|
+
}
|