goodchuck-utils 1.4.2 → 1.5.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 (37) hide show
  1. package/dist/components/dev/IdSelector.d.ts +41 -0
  2. package/dist/components/dev/IdSelector.d.ts.map +1 -0
  3. package/dist/components/dev/IdSelector.js +52 -0
  4. package/dist/components/dev/index.d.ts +8 -0
  5. package/dist/components/dev/index.d.ts.map +1 -0
  6. package/dist/components/dev/index.js +7 -0
  7. package/dist/components/index.d.ts +8 -0
  8. package/dist/components/index.d.ts.map +1 -0
  9. package/dist/components/index.js +7 -0
  10. package/dist/hooks/index.d.ts +8 -0
  11. package/dist/hooks/index.d.ts.map +1 -1
  12. package/dist/hooks/index.js +11 -0
  13. package/dist/hooks/useCopyToClipboard.d.ts +67 -0
  14. package/dist/hooks/useCopyToClipboard.d.ts.map +1 -0
  15. package/dist/hooks/useCopyToClipboard.js +79 -0
  16. package/dist/hooks/useDebounce.d.ts +47 -0
  17. package/dist/hooks/useDebounce.d.ts.map +1 -0
  18. package/dist/hooks/useDebounce.js +60 -0
  19. package/dist/hooks/useEventListener.d.ts +79 -0
  20. package/dist/hooks/useEventListener.d.ts.map +1 -0
  21. package/dist/hooks/useEventListener.js +33 -0
  22. package/dist/hooks/useIntersectionObserver.d.ts +109 -0
  23. package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
  24. package/dist/hooks/useIntersectionObserver.js +128 -0
  25. package/dist/hooks/usePrevious.d.ts +58 -0
  26. package/dist/hooks/usePrevious.d.ts.map +1 -0
  27. package/dist/hooks/usePrevious.js +67 -0
  28. package/dist/hooks/useThrottle.d.ts +57 -0
  29. package/dist/hooks/useThrottle.d.ts.map +1 -0
  30. package/dist/hooks/useThrottle.js +80 -0
  31. package/dist/hooks/useToggle.d.ts +49 -0
  32. package/dist/hooks/useToggle.d.ts.map +1 -0
  33. package/dist/hooks/useToggle.js +56 -0
  34. package/dist/hooks/useWindowSize.d.ts +58 -0
  35. package/dist/hooks/useWindowSize.d.ts.map +1 -0
  36. package/dist/hooks/useWindowSize.js +79 -0
  37. package/package.json +22 -2
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ type LoginInfo = {
3
+ id: string;
4
+ pw: string;
5
+ memo: string;
6
+ };
7
+ type Props = {
8
+ onLogin: (email: string, password: string) => Promise<void>;
9
+ infos: LoginInfo[];
10
+ };
11
+ /**
12
+ * 개발용 로그인 shortcut 컴포넌트
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * import { IdSelector } from 'goodchuck-utils/components/dev';
17
+ *
18
+ * function LoginPage() {
19
+ * const handleLogin = async (email: string, password: string) => {
20
+ * await loginAPI(email, password);
21
+ * };
22
+ *
23
+ * const devAccounts = [
24
+ * { id: 'admin@test.com', pw: 'admin123', memo: '관리자' },
25
+ * { id: 'user@test.com', pw: 'user123', memo: '일반 사용자' },
26
+ * ];
27
+ *
28
+ * return (
29
+ * <div>
30
+ * <LoginForm />
31
+ * {process.env.NODE_ENV === 'development' && (
32
+ * <IdSelector onLogin={handleLogin} infos={devAccounts} />
33
+ * )}
34
+ * </div>
35
+ * );
36
+ * }
37
+ * ```
38
+ */
39
+ export default function IdSelector({ onLogin, infos }: Props): React.JSX.Element;
40
+ export {};
41
+ //# sourceMappingURL=IdSelector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IdSelector.d.ts","sourceRoot":"","sources":["../../../src/components/dev/IdSelector.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,qBAyC3D"}
@@ -0,0 +1,52 @@
1
+ import React, { useState } from 'react';
2
+ /**
3
+ * 개발용 로그인 shortcut 컴포넌트
4
+ *
5
+ * @example
6
+ * ```tsx
7
+ * import { IdSelector } from 'goodchuck-utils/components/dev';
8
+ *
9
+ * function LoginPage() {
10
+ * const handleLogin = async (email: string, password: string) => {
11
+ * await loginAPI(email, password);
12
+ * };
13
+ *
14
+ * const devAccounts = [
15
+ * { id: 'admin@test.com', pw: 'admin123', memo: '관리자' },
16
+ * { id: 'user@test.com', pw: 'user123', memo: '일반 사용자' },
17
+ * ];
18
+ *
19
+ * return (
20
+ * <div>
21
+ * <LoginForm />
22
+ * {process.env.NODE_ENV === 'development' && (
23
+ * <IdSelector onLogin={handleLogin} infos={devAccounts} />
24
+ * )}
25
+ * </div>
26
+ * );
27
+ * }
28
+ * ```
29
+ */
30
+ export default function IdSelector({ onLogin, infos }) {
31
+ const [loading, setLoading] = useState(null);
32
+ const handleQuickLogin = async (info, index) => {
33
+ setLoading(index);
34
+ try {
35
+ await onLogin(info.id, info.pw);
36
+ }
37
+ finally {
38
+ setLoading(null);
39
+ }
40
+ };
41
+ return (React.createElement("div", { className: "fixed top-1/2 right-4 -translate-y-1/2 flex flex-col gap-3 p-4 bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700 min-w-[280px] z-50" },
42
+ React.createElement("div", { className: "text-gray-900 dark:text-gray-100 text-sm font-bold pb-2 border-b border-gray-200 dark:border-gray-700" }, "\uD83D\uDE80 \uAC1C\uBC1C\uC6A9 \uBE60\uB978 \uB85C\uADF8\uC778"),
43
+ infos.map((info, index) => (React.createElement("div", { key: index, className: "flex flex-col gap-2 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-500 transition-all" },
44
+ React.createElement("button", { onClick: () => handleQuickLogin(info, index), disabled: loading === index, className: "w-full px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white font-semibold rounded-lg transition-colors disabled:cursor-not-allowed" }, loading === index ? '로그인 중...' : info.memo),
45
+ React.createElement("div", { className: "flex flex-col gap-1 text-xs text-gray-600 dark:text-gray-400 px-1" },
46
+ React.createElement("div", { className: "flex items-center gap-2" },
47
+ React.createElement("span", { className: "font-semibold min-w-[24px]" }, "ID"),
48
+ React.createElement("span", { className: "font-mono text-gray-700 dark:text-gray-300" }, info.id)),
49
+ React.createElement("div", { className: "flex items-center gap-2" },
50
+ React.createElement("span", { className: "font-semibold min-w-[24px]" }, "PW"),
51
+ React.createElement("span", { className: "font-mono text-gray-700 dark:text-gray-300" }, info.pw))))))));
52
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Development Components
3
+ *
4
+ * 개발 환경에서 사용하는 유틸리티 컴포넌트들입니다.
5
+ * production 환경에서는 제외하는 것을 권장합니다.
6
+ */
7
+ export { default as IdSelector } from './IdSelector';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Development Components
3
+ *
4
+ * 개발 환경에서 사용하는 유틸리티 컴포넌트들입니다.
5
+ * production 환경에서는 제외하는 것을 권장합니다.
6
+ */
7
+ export { default as IdSelector } from './IdSelector';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React Components
3
+ *
4
+ * Note: Requires React and React DOM as peer dependencies.
5
+ * Note: Uses Tailwind CSS classes - ensure Tailwind is configured in your project.
6
+ */
7
+ export * from './dev';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * React Components
3
+ *
4
+ * Note: Requires React and React DOM as peer dependencies.
5
+ * Note: Uses Tailwind CSS classes - ensure Tailwind is configured in your project.
6
+ */
7
+ export * from './dev';
@@ -8,4 +8,12 @@ export * from './useLocalStorage';
8
8
  export * from './useSessionStorage';
9
9
  export * from './useMediaQuery';
10
10
  export * from './useClickOutside';
11
+ export * from './useDebounce';
12
+ export * from './useToggle';
13
+ export * from './useCopyToClipboard';
14
+ export * from './useWindowSize';
15
+ export * from './usePrevious';
16
+ export * from './useThrottle';
17
+ export * from './useIntersectionObserver';
18
+ export * from './useEventListener';
11
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAG9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAG1C,cAAc,oBAAoB,CAAC"}
@@ -10,3 +10,14 @@ export * from './useSessionStorage';
10
10
  // UI/UX hooks
11
11
  export * from './useMediaQuery';
12
12
  export * from './useClickOutside';
13
+ // Utility hooks
14
+ export * from './useDebounce';
15
+ export * from './useToggle';
16
+ export * from './useCopyToClipboard';
17
+ export * from './useWindowSize';
18
+ export * from './usePrevious';
19
+ // Performance hooks
20
+ export * from './useThrottle';
21
+ export * from './useIntersectionObserver';
22
+ // Event hooks
23
+ export * from './useEventListener';
@@ -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"}