goodchuck-utils 1.4.2 → 1.6.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 +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 +75 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/FormDevTools.js +218 -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 +187 -0
- package/dist/components/dev/FormDevTools.d.ts +76 -0
- package/dist/components/dev/FormDevTools.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools.js +399 -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/hooks/index.d.ts +8 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +11 -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/usePrevious.d.ts +58 -0
- package/dist/hooks/usePrevious.d.ts.map +1 -0
- package/dist/hooks/usePrevious.js +67 -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/package.json +23 -2
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCopyToClipboard의 반환 타입
|
|
3
|
+
*/
|
|
4
|
+
export interface CopyToClipboardResult {
|
|
5
|
+
/** 복사된 값 (null이면 아직 복사 안됨) */
|
|
6
|
+
copiedText: string | null;
|
|
7
|
+
/** 클립보드에 복사하는 함수 */
|
|
8
|
+
copy: (text: string) => Promise<boolean>;
|
|
9
|
+
/** 복사 상태를 초기화하는 함수 */
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 클립보드에 텍스트를 복사하는 hook
|
|
14
|
+
*
|
|
15
|
+
* @returns {CopyToClipboardResult} 복사된 텍스트, 복사 함수, 리셋 함수
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // 기본 사용
|
|
19
|
+
* function CopyButton() {
|
|
20
|
+
* const { copiedText, copy } = useCopyToClipboard();
|
|
21
|
+
*
|
|
22
|
+
* const handleCopy = () => {
|
|
23
|
+
* copy('Hello, World!');
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <button onClick={handleCopy}>
|
|
28
|
+
* {copiedText ? 'Copied!' : 'Copy'}
|
|
29
|
+
* </button>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // 코드 블록 복사
|
|
35
|
+
* function CodeBlock({ code }: { code: string }) {
|
|
36
|
+
* const { copiedText, copy } = useCopyToClipboard();
|
|
37
|
+
* const isCopied = copiedText === code;
|
|
38
|
+
*
|
|
39
|
+
* return (
|
|
40
|
+
* <div>
|
|
41
|
+
* <pre>{code}</pre>
|
|
42
|
+
* <button onClick={() => copy(code)}>
|
|
43
|
+
* {isCopied ? '✓ Copied' : 'Copy Code'}
|
|
44
|
+
* </button>
|
|
45
|
+
* </div>
|
|
46
|
+
* );
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // 에러 처리
|
|
51
|
+
* function ShareLink({ url }: { url: string }) {
|
|
52
|
+
* const { copy } = useCopyToClipboard();
|
|
53
|
+
*
|
|
54
|
+
* const handleShare = async () => {
|
|
55
|
+
* const success = await copy(url);
|
|
56
|
+
* if (success) {
|
|
57
|
+
* alert('Link copied!');
|
|
58
|
+
* } else {
|
|
59
|
+
* alert('Failed to copy');
|
|
60
|
+
* }
|
|
61
|
+
* };
|
|
62
|
+
*
|
|
63
|
+
* return <button onClick={handleShare}>Share</button>;
|
|
64
|
+
* }
|
|
65
|
+
*/
|
|
66
|
+
export declare function useCopyToClipboard(): CopyToClipboardResult;
|
|
67
|
+
//# sourceMappingURL=useCopyToClipboard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCopyToClipboard.d.ts","sourceRoot":"","sources":["../../src/hooks/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,79 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 클립보드에 텍스트를 복사하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @returns {CopyToClipboardResult} 복사된 텍스트, 복사 함수, 리셋 함수
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 기본 사용
|
|
9
|
+
* function CopyButton() {
|
|
10
|
+
* const { copiedText, copy } = useCopyToClipboard();
|
|
11
|
+
*
|
|
12
|
+
* const handleCopy = () => {
|
|
13
|
+
* copy('Hello, World!');
|
|
14
|
+
* };
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <button onClick={handleCopy}>
|
|
18
|
+
* {copiedText ? 'Copied!' : 'Copy'}
|
|
19
|
+
* </button>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 코드 블록 복사
|
|
25
|
+
* function CodeBlock({ code }: { code: string }) {
|
|
26
|
+
* const { copiedText, copy } = useCopyToClipboard();
|
|
27
|
+
* const isCopied = copiedText === code;
|
|
28
|
+
*
|
|
29
|
+
* return (
|
|
30
|
+
* <div>
|
|
31
|
+
* <pre>{code}</pre>
|
|
32
|
+
* <button onClick={() => copy(code)}>
|
|
33
|
+
* {isCopied ? '✓ Copied' : 'Copy Code'}
|
|
34
|
+
* </button>
|
|
35
|
+
* </div>
|
|
36
|
+
* );
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 에러 처리
|
|
41
|
+
* function ShareLink({ url }: { url: string }) {
|
|
42
|
+
* const { copy } = useCopyToClipboard();
|
|
43
|
+
*
|
|
44
|
+
* const handleShare = async () => {
|
|
45
|
+
* const success = await copy(url);
|
|
46
|
+
* if (success) {
|
|
47
|
+
* alert('Link copied!');
|
|
48
|
+
* } else {
|
|
49
|
+
* alert('Failed to copy');
|
|
50
|
+
* }
|
|
51
|
+
* };
|
|
52
|
+
*
|
|
53
|
+
* return <button onClick={handleShare}>Share</button>;
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
export function useCopyToClipboard() {
|
|
57
|
+
const [copiedText, setCopiedText] = useState(null);
|
|
58
|
+
const copy = useCallback(async (text) => {
|
|
59
|
+
// Clipboard API 지원 확인
|
|
60
|
+
if (!navigator?.clipboard) {
|
|
61
|
+
console.warn('Clipboard API not supported');
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await navigator.clipboard.writeText(text);
|
|
66
|
+
setCopiedText(text);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('Copy failed', error);
|
|
71
|
+
setCopiedText(null);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}, []);
|
|
75
|
+
const reset = useCallback(() => {
|
|
76
|
+
setCopiedText(null);
|
|
77
|
+
}, []);
|
|
78
|
+
return { copiedText, copy, reset };
|
|
79
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 값의 업데이트를 지연시키는 hook
|
|
3
|
+
* 사용자 입력이 멈춘 후 일정 시간이 지나면 값을 업데이트합니다.
|
|
4
|
+
*
|
|
5
|
+
* @param value - debounce할 값
|
|
6
|
+
* @param delay - 지연 시간 (밀리초, 기본값: 500ms)
|
|
7
|
+
* @returns debounce된 값
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 검색 입력 최적화
|
|
11
|
+
* function SearchComponent() {
|
|
12
|
+
* const [searchTerm, setSearchTerm] = useState('');
|
|
13
|
+
* const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
14
|
+
*
|
|
15
|
+
* useEffect(() => {
|
|
16
|
+
* if (debouncedSearchTerm) {
|
|
17
|
+
* // API 호출은 사용자가 타이핑을 멈춘 후 500ms 뒤에 실행
|
|
18
|
+
* searchAPI(debouncedSearchTerm);
|
|
19
|
+
* }
|
|
20
|
+
* }, [debouncedSearchTerm]);
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <input
|
|
24
|
+
* value={searchTerm}
|
|
25
|
+
* onChange={(e) => setSearchTerm(e.target.value)}
|
|
26
|
+
* placeholder="Search..."
|
|
27
|
+
* />
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // 윈도우 리사이즈 최적화
|
|
33
|
+
* function ResponsiveComponent() {
|
|
34
|
+
* const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
|
35
|
+
* const debouncedWidth = useDebounce(windowWidth, 200);
|
|
36
|
+
*
|
|
37
|
+
* useEffect(() => {
|
|
38
|
+
* const handleResize = () => setWindowWidth(window.innerWidth);
|
|
39
|
+
* window.addEventListener('resize', handleResize);
|
|
40
|
+
* return () => window.removeEventListener('resize', handleResize);
|
|
41
|
+
* }, []);
|
|
42
|
+
*
|
|
43
|
+
* return <div>Debounced width: {debouncedWidth}px</div>;
|
|
44
|
+
* }
|
|
45
|
+
*/
|
|
46
|
+
export declare function useDebounce<T>(value: T, delay?: number): T;
|
|
47
|
+
//# sourceMappingURL=useDebounce.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useDebounce.d.ts","sourceRoot":"","sources":["../../src/hooks/useDebounce.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAE,MAAY,GAAG,CAAC,CAgB/D"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 값의 업데이트를 지연시키는 hook
|
|
4
|
+
* 사용자 입력이 멈춘 후 일정 시간이 지나면 값을 업데이트합니다.
|
|
5
|
+
*
|
|
6
|
+
* @param value - debounce할 값
|
|
7
|
+
* @param delay - 지연 시간 (밀리초, 기본값: 500ms)
|
|
8
|
+
* @returns debounce된 값
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // 검색 입력 최적화
|
|
12
|
+
* function SearchComponent() {
|
|
13
|
+
* const [searchTerm, setSearchTerm] = useState('');
|
|
14
|
+
* const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
|
15
|
+
*
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* if (debouncedSearchTerm) {
|
|
18
|
+
* // API 호출은 사용자가 타이핑을 멈춘 후 500ms 뒤에 실행
|
|
19
|
+
* searchAPI(debouncedSearchTerm);
|
|
20
|
+
* }
|
|
21
|
+
* }, [debouncedSearchTerm]);
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <input
|
|
25
|
+
* value={searchTerm}
|
|
26
|
+
* onChange={(e) => setSearchTerm(e.target.value)}
|
|
27
|
+
* placeholder="Search..."
|
|
28
|
+
* />
|
|
29
|
+
* );
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // 윈도우 리사이즈 최적화
|
|
34
|
+
* function ResponsiveComponent() {
|
|
35
|
+
* const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
|
36
|
+
* const debouncedWidth = useDebounce(windowWidth, 200);
|
|
37
|
+
*
|
|
38
|
+
* useEffect(() => {
|
|
39
|
+
* const handleResize = () => setWindowWidth(window.innerWidth);
|
|
40
|
+
* window.addEventListener('resize', handleResize);
|
|
41
|
+
* return () => window.removeEventListener('resize', handleResize);
|
|
42
|
+
* }, []);
|
|
43
|
+
*
|
|
44
|
+
* return <div>Debounced width: {debouncedWidth}px</div>;
|
|
45
|
+
* }
|
|
46
|
+
*/
|
|
47
|
+
export function useDebounce(value, delay = 500) {
|
|
48
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
// delay 후에 값을 업데이트하는 타이머 설정
|
|
51
|
+
const handler = setTimeout(() => {
|
|
52
|
+
setDebouncedValue(value);
|
|
53
|
+
}, delay);
|
|
54
|
+
// value나 delay가 변경되면 이전 타이머를 클리어
|
|
55
|
+
return () => {
|
|
56
|
+
clearTimeout(handler);
|
|
57
|
+
};
|
|
58
|
+
}, [value, delay]);
|
|
59
|
+
return debouncedValue;
|
|
60
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* DOM 이벤트 리스너를 안전하게 추가/제거하는 hook
|
|
4
|
+
* 클린업이 자동으로 처리되며, SSR 환경에서도 안전합니다.
|
|
5
|
+
*
|
|
6
|
+
* @param eventName - 이벤트 이름 (예: 'click', 'scroll', 'keydown')
|
|
7
|
+
* @param handler - 이벤트 핸들러 함수
|
|
8
|
+
* @param element - 이벤트를 등록할 요소 (기본값: window)
|
|
9
|
+
* @param options - addEventListener 옵션
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Window 이벤트 리스너
|
|
13
|
+
* function ScrollIndicator() {
|
|
14
|
+
* const [scrollY, setScrollY] = useState(0);
|
|
15
|
+
*
|
|
16
|
+
* useEventListener('scroll', () => {
|
|
17
|
+
* setScrollY(window.scrollY);
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* return <div>Scroll position: {scrollY}px</div>;
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 특정 요소에 이벤트 리스너
|
|
25
|
+
* function CustomButton() {
|
|
26
|
+
* const buttonRef = useRef<HTMLButtonElement>(null);
|
|
27
|
+
*
|
|
28
|
+
* useEventListener('click', () => {
|
|
29
|
+
* console.log('Button clicked!');
|
|
30
|
+
* }, buttonRef);
|
|
31
|
+
*
|
|
32
|
+
* return <button ref={buttonRef}>Click me</button>;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Document 이벤트 리스너
|
|
37
|
+
* function KeyboardShortcuts() {
|
|
38
|
+
* useEventListener('keydown', (e) => {
|
|
39
|
+
* if (e.key === 'Escape') {
|
|
40
|
+
* console.log('ESC pressed');
|
|
41
|
+
* }
|
|
42
|
+
* }, document);
|
|
43
|
+
*
|
|
44
|
+
* return <div>Press ESC</div>;
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // 이벤트 옵션 사용
|
|
49
|
+
* function PassiveScrollListener() {
|
|
50
|
+
* useEventListener(
|
|
51
|
+
* 'scroll',
|
|
52
|
+
* () => console.log('Scrolling...'),
|
|
53
|
+
* window,
|
|
54
|
+
* { passive: true } // 성능 최적화
|
|
55
|
+
* );
|
|
56
|
+
*
|
|
57
|
+
* return <div>Scroll me</div>;
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // 여러 이벤트 처리
|
|
62
|
+
* function MultiEventHandler() {
|
|
63
|
+
* const handleInput = (e: Event) => {
|
|
64
|
+
* console.log('Input changed:', (e.target as HTMLInputElement).value);
|
|
65
|
+
* };
|
|
66
|
+
*
|
|
67
|
+
* const inputRef = useRef<HTMLInputElement>(null);
|
|
68
|
+
*
|
|
69
|
+
* useEventListener('input', handleInput, inputRef);
|
|
70
|
+
* useEventListener('focus', () => console.log('Focused'), inputRef);
|
|
71
|
+
* useEventListener('blur', () => console.log('Blurred'), inputRef);
|
|
72
|
+
*
|
|
73
|
+
* return <input ref={inputRef} />;
|
|
74
|
+
* }
|
|
75
|
+
*/
|
|
76
|
+
export declare function useEventListener<K extends keyof WindowEventMap>(eventName: K, handler: (event: WindowEventMap[K]) => void, element?: undefined, options?: boolean | AddEventListenerOptions): void;
|
|
77
|
+
export declare function useEventListener<K extends keyof HTMLElementEventMap, T extends HTMLElement = HTMLDivElement>(eventName: K, handler: (event: HTMLElementEventMap[K]) => void, element: RefObject<T>, options?: boolean | AddEventListenerOptions): void;
|
|
78
|
+
export declare function useEventListener<K extends keyof DocumentEventMap>(eventName: K, handler: (event: DocumentEventMap[K]) => void, element: Document, options?: boolean | AddEventListenerOptions): void;
|
|
79
|
+
//# sourceMappingURL=useEventListener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEventListener.d.ts","sourceRoot":"","sources":["../../src/hooks/useEventListener.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,SAAS,EAAE,MAAM,OAAO,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,cAAc,EAC7D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,KAAK,IAAI,EAC3C,OAAO,CAAC,EAAE,SAAS,EACnB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,mBAAmB,EAAE,CAAC,SAAS,WAAW,GAAG,cAAc,EAC1G,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,EAChD,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EACrB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC;AAER,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,gBAAgB,EAC/D,SAAS,EAAE,CAAC,EACZ,OAAO,EAAE,CAAC,KAAK,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC7C,OAAO,EAAE,QAAQ,EACjB,OAAO,CAAC,EAAE,OAAO,GAAG,uBAAuB,GAC1C,IAAI,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
export function useEventListener(eventName, handler, element, options) {
|
|
3
|
+
// handler를 ref에 저장하여 handler가 변경되어도 이벤트 리스너를 재등록하지 않도록 함
|
|
4
|
+
const savedHandler = useRef(handler);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
savedHandler.current = handler;
|
|
7
|
+
}, [handler]);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
// SSR 환경 체크
|
|
10
|
+
if (typeof window === 'undefined') {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
// 타겟 요소 결정
|
|
14
|
+
const targetElement = element instanceof Document
|
|
15
|
+
? element
|
|
16
|
+
: (element && 'current' in element && element.current)
|
|
17
|
+
? element.current
|
|
18
|
+
: window;
|
|
19
|
+
if (!targetElement?.addEventListener) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
// 이벤트 핸들러 래퍼 (savedHandler.current를 호출)
|
|
23
|
+
const eventListener = (event) => {
|
|
24
|
+
savedHandler.current(event);
|
|
25
|
+
};
|
|
26
|
+
// 이벤트 리스너 등록
|
|
27
|
+
targetElement.addEventListener(eventName, eventListener, options);
|
|
28
|
+
// 클린업: 컴포넌트 언마운트 시 이벤트 리스너 제거
|
|
29
|
+
return () => {
|
|
30
|
+
targetElement.removeEventListener(eventName, eventListener, options);
|
|
31
|
+
};
|
|
32
|
+
}, [eventName, element, options]);
|
|
33
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Intersection Observer 옵션
|
|
4
|
+
*/
|
|
5
|
+
export interface UseIntersectionObserverOptions extends IntersectionObserverInit {
|
|
6
|
+
/** 한 번만 감지하고 observer를 해제할지 여부 (기본값: false) */
|
|
7
|
+
freezeOnceVisible?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Intersection Observer를 사용하여 요소의 가시성을 감지하는 hook
|
|
11
|
+
*
|
|
12
|
+
* @param ref - 관찰할 요소의 ref
|
|
13
|
+
* @param options - Intersection Observer 옵션
|
|
14
|
+
* @returns IntersectionObserverEntry 또는 undefined
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // 기본 사용 - 요소가 화면에 보이는지 감지
|
|
18
|
+
* function LazyImage({ src }: { src: string }) {
|
|
19
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
20
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
21
|
+
* threshold: 0.1,
|
|
22
|
+
* freezeOnceVisible: true
|
|
23
|
+
* });
|
|
24
|
+
* const isVisible = entry?.isIntersecting;
|
|
25
|
+
*
|
|
26
|
+
* return (
|
|
27
|
+
* <img
|
|
28
|
+
* ref={imageRef}
|
|
29
|
+
* src={isVisible ? src : undefined}
|
|
30
|
+
* alt="lazy loaded"
|
|
31
|
+
* />
|
|
32
|
+
* );
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // 무한 스크롤
|
|
37
|
+
* function InfiniteScrollList() {
|
|
38
|
+
* const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
* const entry = useIntersectionObserver(loadMoreRef, {
|
|
40
|
+
* threshold: 1.0
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* useEffect(() => {
|
|
44
|
+
* if (entry?.isIntersecting) {
|
|
45
|
+
* loadMoreItems();
|
|
46
|
+
* }
|
|
47
|
+
* }, [entry?.isIntersecting]);
|
|
48
|
+
*
|
|
49
|
+
* return (
|
|
50
|
+
* <div>
|
|
51
|
+
* {items.map(item => <Item key={item.id} {...item} />)}
|
|
52
|
+
* <div ref={loadMoreRef}>Loading...</div>
|
|
53
|
+
* </div>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // 애니메이션 트리거
|
|
59
|
+
* function AnimatedSection() {
|
|
60
|
+
* const sectionRef = useRef<HTMLElement>(null);
|
|
61
|
+
* const entry = useIntersectionObserver(sectionRef, {
|
|
62
|
+
* threshold: 0.5,
|
|
63
|
+
* freezeOnceVisible: true
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* return (
|
|
67
|
+
* <section
|
|
68
|
+
* ref={sectionRef}
|
|
69
|
+
* className={entry?.isIntersecting ? 'fade-in' : 'fade-out'}
|
|
70
|
+
* >
|
|
71
|
+
* Content
|
|
72
|
+
* </section>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // rootMargin 사용 (요소가 화면에 들어오기 전에 미리 감지)
|
|
78
|
+
* function PreloadImage({ src }: { src: string }) {
|
|
79
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
80
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
81
|
+
* rootMargin: '200px', // 화면 기준 200px 전에 감지
|
|
82
|
+
* freezeOnceVisible: true
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* return <img ref={imageRef} src={entry?.isIntersecting ? src : undefined} />;
|
|
86
|
+
* }
|
|
87
|
+
*/
|
|
88
|
+
export declare function useIntersectionObserver(ref: RefObject<Element>, options?: UseIntersectionObserverOptions): IntersectionObserverEntry | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* 요소가 화면에 보이는지 여부만 반환하는 간단한 버전
|
|
91
|
+
*
|
|
92
|
+
* @param ref - 관찰할 요소의 ref
|
|
93
|
+
* @param options - Intersection Observer 옵션
|
|
94
|
+
* @returns 요소가 화면에 보이는지 여부
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* function Section() {
|
|
98
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
99
|
+
* const isVisible = useIsVisible(ref);
|
|
100
|
+
*
|
|
101
|
+
* return (
|
|
102
|
+
* <div ref={ref}>
|
|
103
|
+
* {isVisible ? 'I am visible!' : 'I am hidden'}
|
|
104
|
+
* </div>
|
|
105
|
+
* );
|
|
106
|
+
* }
|
|
107
|
+
*/
|
|
108
|
+
export declare function useIsVisible(ref: RefObject<Element>, options?: UseIntersectionObserverOptions): boolean;
|
|
109
|
+
//# sourceMappingURL=useIntersectionObserver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useIntersectionObserver.d.ts","sourceRoot":"","sources":["../../src/hooks/useIntersectionObserver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,8BAA+B,SAAQ,wBAAwB;IAC9E,+CAA+C;IAC/C,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,EACvB,OAAO,GAAE,8BAAmC,GAC3C,yBAAyB,GAAG,SAAS,CAoCvC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,EACvB,OAAO,CAAC,EAAE,8BAA8B,GACvC,OAAO,CAGT"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Intersection Observer를 사용하여 요소의 가시성을 감지하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param ref - 관찰할 요소의 ref
|
|
6
|
+
* @param options - Intersection Observer 옵션
|
|
7
|
+
* @returns IntersectionObserverEntry 또는 undefined
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 기본 사용 - 요소가 화면에 보이는지 감지
|
|
11
|
+
* function LazyImage({ src }: { src: string }) {
|
|
12
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
13
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
14
|
+
* threshold: 0.1,
|
|
15
|
+
* freezeOnceVisible: true
|
|
16
|
+
* });
|
|
17
|
+
* const isVisible = entry?.isIntersecting;
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <img
|
|
21
|
+
* ref={imageRef}
|
|
22
|
+
* src={isVisible ? src : undefined}
|
|
23
|
+
* alt="lazy loaded"
|
|
24
|
+
* />
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // 무한 스크롤
|
|
30
|
+
* function InfiniteScrollList() {
|
|
31
|
+
* const loadMoreRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
* const entry = useIntersectionObserver(loadMoreRef, {
|
|
33
|
+
* threshold: 1.0
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* useEffect(() => {
|
|
37
|
+
* if (entry?.isIntersecting) {
|
|
38
|
+
* loadMoreItems();
|
|
39
|
+
* }
|
|
40
|
+
* }, [entry?.isIntersecting]);
|
|
41
|
+
*
|
|
42
|
+
* return (
|
|
43
|
+
* <div>
|
|
44
|
+
* {items.map(item => <Item key={item.id} {...item} />)}
|
|
45
|
+
* <div ref={loadMoreRef}>Loading...</div>
|
|
46
|
+
* </div>
|
|
47
|
+
* );
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // 애니메이션 트리거
|
|
52
|
+
* function AnimatedSection() {
|
|
53
|
+
* const sectionRef = useRef<HTMLElement>(null);
|
|
54
|
+
* const entry = useIntersectionObserver(sectionRef, {
|
|
55
|
+
* threshold: 0.5,
|
|
56
|
+
* freezeOnceVisible: true
|
|
57
|
+
* });
|
|
58
|
+
*
|
|
59
|
+
* return (
|
|
60
|
+
* <section
|
|
61
|
+
* ref={sectionRef}
|
|
62
|
+
* className={entry?.isIntersecting ? 'fade-in' : 'fade-out'}
|
|
63
|
+
* >
|
|
64
|
+
* Content
|
|
65
|
+
* </section>
|
|
66
|
+
* );
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // rootMargin 사용 (요소가 화면에 들어오기 전에 미리 감지)
|
|
71
|
+
* function PreloadImage({ src }: { src: string }) {
|
|
72
|
+
* const imageRef = useRef<HTMLImageElement>(null);
|
|
73
|
+
* const entry = useIntersectionObserver(imageRef, {
|
|
74
|
+
* rootMargin: '200px', // 화면 기준 200px 전에 감지
|
|
75
|
+
* freezeOnceVisible: true
|
|
76
|
+
* });
|
|
77
|
+
*
|
|
78
|
+
* return <img ref={imageRef} src={entry?.isIntersecting ? src : undefined} />;
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
export function useIntersectionObserver(ref, options = {}) {
|
|
82
|
+
const { threshold = 0, root = null, rootMargin = '0%', freezeOnceVisible = false } = options;
|
|
83
|
+
const [entry, setEntry] = useState();
|
|
84
|
+
const frozen = entry?.isIntersecting && freezeOnceVisible;
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
const element = ref.current;
|
|
87
|
+
// 요소가 없거나, 이미 frozen 상태면 observer 생성하지 않음
|
|
88
|
+
if (!element || frozen) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Intersection Observer가 지원되지 않는 환경 체크
|
|
92
|
+
if (!window.IntersectionObserver) {
|
|
93
|
+
console.warn('IntersectionObserver is not supported in this browser');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const observer = new IntersectionObserver(([entry]) => {
|
|
97
|
+
setEntry(entry);
|
|
98
|
+
}, { threshold, root, rootMargin });
|
|
99
|
+
observer.observe(element);
|
|
100
|
+
return () => {
|
|
101
|
+
observer.disconnect();
|
|
102
|
+
};
|
|
103
|
+
}, [ref, threshold, root, rootMargin, frozen]);
|
|
104
|
+
return entry;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 요소가 화면에 보이는지 여부만 반환하는 간단한 버전
|
|
108
|
+
*
|
|
109
|
+
* @param ref - 관찰할 요소의 ref
|
|
110
|
+
* @param options - Intersection Observer 옵션
|
|
111
|
+
* @returns 요소가 화면에 보이는지 여부
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* function Section() {
|
|
115
|
+
* const ref = useRef<HTMLDivElement>(null);
|
|
116
|
+
* const isVisible = useIsVisible(ref);
|
|
117
|
+
*
|
|
118
|
+
* return (
|
|
119
|
+
* <div ref={ref}>
|
|
120
|
+
* {isVisible ? 'I am visible!' : 'I am hidden'}
|
|
121
|
+
* </div>
|
|
122
|
+
* );
|
|
123
|
+
* }
|
|
124
|
+
*/
|
|
125
|
+
export function useIsVisible(ref, options) {
|
|
126
|
+
const entry = useIntersectionObserver(ref, options);
|
|
127
|
+
return entry?.isIntersecting ?? false;
|
|
128
|
+
}
|
|
@@ -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"}
|