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.
Files changed (61) hide show
  1. package/dist/components/dev/ApiLogger.d.ts +136 -0
  2. package/dist/components/dev/ApiLogger.d.ts.map +1 -0
  3. package/dist/components/dev/ApiLogger.js +408 -0
  4. package/dist/components/dev/DevPanel.d.ts +32 -0
  5. package/dist/components/dev/DevPanel.d.ts.map +1 -0
  6. package/dist/components/dev/DevPanel.js +196 -0
  7. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +75 -0
  8. package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -0
  9. package/dist/components/dev/FormDevTools/FormDevTools.js +218 -0
  10. package/dist/components/dev/FormDevTools/index.d.ts +3 -0
  11. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -0
  12. package/dist/components/dev/FormDevTools/index.js +1 -0
  13. package/dist/components/dev/FormDevTools/styles.d.ts +45 -0
  14. package/dist/components/dev/FormDevTools/styles.d.ts.map +1 -0
  15. package/dist/components/dev/FormDevTools/styles.js +187 -0
  16. package/dist/components/dev/FormDevTools.d.ts +76 -0
  17. package/dist/components/dev/FormDevTools.d.ts.map +1 -0
  18. package/dist/components/dev/FormDevTools.js +399 -0
  19. package/dist/components/dev/IdSelector.d.ts +50 -0
  20. package/dist/components/dev/IdSelector.d.ts.map +1 -0
  21. package/dist/components/dev/IdSelector.js +129 -0
  22. package/dist/components/dev/WindowSizeDisplay.d.ts +44 -0
  23. package/dist/components/dev/WindowSizeDisplay.d.ts.map +1 -0
  24. package/dist/components/dev/WindowSizeDisplay.js +74 -0
  25. package/dist/components/dev/ZIndexDebugger.d.ts +32 -0
  26. package/dist/components/dev/ZIndexDebugger.d.ts.map +1 -0
  27. package/dist/components/dev/ZIndexDebugger.js +184 -0
  28. package/dist/components/dev/index.d.ts +15 -0
  29. package/dist/components/dev/index.d.ts.map +1 -0
  30. package/dist/components/dev/index.js +12 -0
  31. package/dist/components/index.d.ts +8 -0
  32. package/dist/components/index.d.ts.map +1 -0
  33. package/dist/components/index.js +7 -0
  34. package/dist/hooks/index.d.ts +8 -0
  35. package/dist/hooks/index.d.ts.map +1 -1
  36. package/dist/hooks/index.js +11 -0
  37. package/dist/hooks/useCopyToClipboard.d.ts +67 -0
  38. package/dist/hooks/useCopyToClipboard.d.ts.map +1 -0
  39. package/dist/hooks/useCopyToClipboard.js +79 -0
  40. package/dist/hooks/useDebounce.d.ts +47 -0
  41. package/dist/hooks/useDebounce.d.ts.map +1 -0
  42. package/dist/hooks/useDebounce.js +60 -0
  43. package/dist/hooks/useEventListener.d.ts +79 -0
  44. package/dist/hooks/useEventListener.d.ts.map +1 -0
  45. package/dist/hooks/useEventListener.js +33 -0
  46. package/dist/hooks/useIntersectionObserver.d.ts +109 -0
  47. package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
  48. package/dist/hooks/useIntersectionObserver.js +128 -0
  49. package/dist/hooks/usePrevious.d.ts +58 -0
  50. package/dist/hooks/usePrevious.d.ts.map +1 -0
  51. package/dist/hooks/usePrevious.js +67 -0
  52. package/dist/hooks/useThrottle.d.ts +57 -0
  53. package/dist/hooks/useThrottle.d.ts.map +1 -0
  54. package/dist/hooks/useThrottle.js +80 -0
  55. package/dist/hooks/useToggle.d.ts +49 -0
  56. package/dist/hooks/useToggle.d.ts.map +1 -0
  57. package/dist/hooks/useToggle.js +56 -0
  58. package/dist/hooks/useWindowSize.d.ts +58 -0
  59. package/dist/hooks/useWindowSize.d.ts.map +1 -0
  60. package/dist/hooks/useWindowSize.js +79 -0
  61. 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"}