goodchuck-utils 1.4.1 → 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.
- package/dist/components/dev/IdSelector.d.ts +41 -0
- package/dist/components/dev/IdSelector.d.ts.map +1 -0
- package/dist/components/dev/IdSelector.js +52 -0
- package/dist/components/dev/index.d.ts +8 -0
- package/dist/components/dev/index.d.ts.map +1 -0
- package/dist/components/dev/index.js +7 -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 +31 -4
|
@@ -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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC"}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -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"}
|