goodchuck-utils 1.1.0 → 1.3.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/date/index.d.ts +64 -0
- package/dist/date/index.d.ts.map +1 -0
- package/dist/date/index.js +92 -0
- package/dist/date/index.test.d.ts +2 -0
- package/dist/date/index.test.d.ts.map +1 -0
- package/dist/date/index.test.js +166 -0
- package/dist/form/__tests__/formatter.test.d.ts +2 -0
- package/dist/form/__tests__/formatter.test.d.ts.map +1 -0
- package/dist/form/__tests__/formatter.test.js +74 -0
- package/dist/form/__tests__/helpers.test.d.ts +2 -0
- package/dist/form/__tests__/helpers.test.d.ts.map +1 -0
- package/dist/form/__tests__/helpers.test.js +42 -0
- package/dist/form/__tests__/validation.test.d.ts +2 -0
- package/dist/form/__tests__/validation.test.d.ts.map +1 -0
- package/dist/form/__tests__/validation.test.js +67 -0
- package/dist/form/formatter.d.ts +34 -0
- package/dist/form/formatter.d.ts.map +1 -0
- package/dist/form/formatter.js +76 -0
- package/dist/form/helpers.d.ts +16 -0
- package/dist/form/helpers.d.ts.map +1 -0
- package/dist/form/helpers.js +34 -0
- package/dist/form/index.d.ts +9 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +11 -0
- package/dist/form/validation.d.ts +33 -0
- package/dist/form/validation.d.ts.map +1 -0
- package/dist/form/validation.js +56 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +12 -0
- package/dist/hooks/useClickOutside.d.ts +49 -0
- package/dist/hooks/useClickOutside.d.ts.map +1 -0
- package/dist/hooks/useClickOutside.js +94 -0
- package/dist/hooks/useLocalStorage.d.ts +19 -0
- package/dist/hooks/useLocalStorage.d.ts.map +1 -0
- package/dist/hooks/useLocalStorage.js +91 -0
- package/dist/hooks/useMediaQuery.d.ts +56 -0
- package/dist/hooks/useMediaQuery.d.ts.map +1 -0
- package/dist/hooks/useMediaQuery.js +104 -0
- package/dist/hooks/useSessionStorage.d.ts +19 -0
- package/dist/hooks/useSessionStorage.d.ts.map +1 -0
- package/dist/hooks/useSessionStorage.js +85 -0
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -18
- package/dist/string/__tests__/case.test.d.ts +2 -0
- package/dist/string/__tests__/case.test.d.ts.map +1 -0
- package/dist/string/__tests__/case.test.js +61 -0
- package/dist/string/__tests__/manipulation.test.d.ts +2 -0
- package/dist/string/__tests__/manipulation.test.d.ts.map +1 -0
- package/dist/string/__tests__/manipulation.test.js +109 -0
- package/dist/string/__tests__/validation.test.d.ts +2 -0
- package/dist/string/__tests__/validation.test.d.ts.map +1 -0
- package/dist/string/__tests__/validation.test.js +101 -0
- package/dist/string/case.d.ts +42 -0
- package/dist/string/case.d.ts.map +1 -0
- package/dist/string/case.js +71 -0
- package/dist/string/index.d.ts +9 -0
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +11 -0
- package/dist/string/manipulation.d.ts +61 -0
- package/dist/string/manipulation.d.ts.map +1 -0
- package/dist/string/manipulation.js +106 -0
- package/dist/string/validation.d.ts +79 -0
- package/dist/string/validation.d.ts.map +1 -0
- package/dist/string/validation.js +115 -0
- package/package.json +27 -3
- package/src/date/index.test.ts +206 -0
- package/src/date/index.ts +123 -0
- package/src/form/__tests__/formatter.test.ts +97 -0
- package/src/form/__tests__/helpers.test.ts +53 -0
- package/src/form/__tests__/validation.test.ts +84 -0
- package/src/form/formatter.ts +85 -0
- package/src/form/helpers.ts +44 -0
- package/src/form/index.ts +14 -0
- package/src/form/validation.ts +72 -0
- package/src/hooks/index.ts +14 -0
- package/src/hooks/useClickOutside.ts +114 -0
- package/src/hooks/useLocalStorage.ts +112 -0
- package/src/hooks/useMediaQuery.ts +116 -0
- package/src/hooks/useSessionStorage.ts +106 -0
- package/src/index.ts +14 -13
- package/src/string/__tests__/case.test.ts +78 -0
- package/src/string/__tests__/manipulation.test.ts +142 -0
- package/src/string/__tests__/validation.test.ts +128 -0
- package/src/string/case.ts +76 -0
- package/src/string/index.ts +14 -0
- package/src/string/manipulation.ts +124 -0
- package/src/string/validation.ts +126 -0
- package/tsconfig.json +15 -11
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form State Helpers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 폼 에러가 있는지 확인
|
|
6
|
+
*/
|
|
7
|
+
export declare function hasFormErrors(errors: Record<string, any>): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 변경된 필드만 추출
|
|
10
|
+
*/
|
|
11
|
+
export declare function getChangedFields<T extends Record<string, any>>(original: T, current: T): Partial<T>;
|
|
12
|
+
/**
|
|
13
|
+
* 빈 값 제거 (null, undefined, 빈 문자열)
|
|
14
|
+
*/
|
|
15
|
+
export declare function removeEmptyValues<T extends Record<string, any>>(obj: T): Partial<T>;
|
|
16
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/form/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAElE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5D,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,CAAC,GACT,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAWnF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form State Helpers
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 폼 에러가 있는지 확인
|
|
6
|
+
*/
|
|
7
|
+
export function hasFormErrors(errors) {
|
|
8
|
+
return Object.keys(errors).length > 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 변경된 필드만 추출
|
|
12
|
+
*/
|
|
13
|
+
export function getChangedFields(original, current) {
|
|
14
|
+
const changed = {};
|
|
15
|
+
for (const key in current) {
|
|
16
|
+
if (current[key] !== original[key]) {
|
|
17
|
+
changed[key] = current[key];
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return changed;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 빈 값 제거 (null, undefined, 빈 문자열)
|
|
24
|
+
*/
|
|
25
|
+
export function removeEmptyValues(obj) {
|
|
26
|
+
const result = {};
|
|
27
|
+
for (const key in obj) {
|
|
28
|
+
const value = obj[key];
|
|
29
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/form/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,cAAc,cAAc,CAAC;AAG7B,cAAc,aAAa,CAAC;AAG5B,cAAc,WAAW,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for form validation, formatting, and state management.
|
|
5
|
+
*/
|
|
6
|
+
// Validation utilities
|
|
7
|
+
export * from './validation';
|
|
8
|
+
// Formatter utilities
|
|
9
|
+
export * from './formatter';
|
|
10
|
+
// Helper utilities
|
|
11
|
+
export * from './helpers';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 이메일 형식 검증
|
|
6
|
+
*/
|
|
7
|
+
export declare function isEmail(value: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 한국 전화번호 형식 검증
|
|
10
|
+
* 010-1234-5678, 01012345678, 02-1234-5678 등 허용
|
|
11
|
+
*/
|
|
12
|
+
export declare function isPhoneNumber(value: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* URL 형식 검증
|
|
15
|
+
*/
|
|
16
|
+
export declare function isUrl(value: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* 비밀번호 강도 검증
|
|
19
|
+
* @param value - 검증할 비밀번호
|
|
20
|
+
* @param options - 검증 옵션
|
|
21
|
+
*/
|
|
22
|
+
export declare function isStrongPassword(value: string, options?: {
|
|
23
|
+
minLength?: number;
|
|
24
|
+
requireUppercase?: boolean;
|
|
25
|
+
requireLowercase?: boolean;
|
|
26
|
+
requireNumbers?: boolean;
|
|
27
|
+
requireSpecialChars?: boolean;
|
|
28
|
+
}): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* 한국 사업자등록번호 형식 검증 (10자리)
|
|
31
|
+
*/
|
|
32
|
+
export declare function isBusinessNumber(value: string): boolean;
|
|
33
|
+
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/form/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAG9C;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAO5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,OAAO,GAAE;IACP,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC1B,GACL,OAAO,CAgBT;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAGvD"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* 이메일 형식 검증
|
|
6
|
+
*/
|
|
7
|
+
export function isEmail(value) {
|
|
8
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
9
|
+
return emailRegex.test(value);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 한국 전화번호 형식 검증
|
|
13
|
+
* 010-1234-5678, 01012345678, 02-1234-5678 등 허용
|
|
14
|
+
*/
|
|
15
|
+
export function isPhoneNumber(value) {
|
|
16
|
+
const phoneRegex = /^(01[016789]|02|0[3-9]{1}[0-9]{1})-?[0-9]{3,4}-?[0-9]{4}$/;
|
|
17
|
+
return phoneRegex.test(value.replace(/\s/g, ''));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* URL 형식 검증
|
|
21
|
+
*/
|
|
22
|
+
export function isUrl(value) {
|
|
23
|
+
try {
|
|
24
|
+
new URL(value);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 비밀번호 강도 검증
|
|
33
|
+
* @param value - 검증할 비밀번호
|
|
34
|
+
* @param options - 검증 옵션
|
|
35
|
+
*/
|
|
36
|
+
export function isStrongPassword(value, options = {}) {
|
|
37
|
+
const { minLength = 8, requireUppercase = true, requireLowercase = true, requireNumbers = true, requireSpecialChars = true, } = options;
|
|
38
|
+
if (value.length < minLength)
|
|
39
|
+
return false;
|
|
40
|
+
if (requireUppercase && !/[A-Z]/.test(value))
|
|
41
|
+
return false;
|
|
42
|
+
if (requireLowercase && !/[a-z]/.test(value))
|
|
43
|
+
return false;
|
|
44
|
+
if (requireNumbers && !/[0-9]/.test(value))
|
|
45
|
+
return false;
|
|
46
|
+
if (requireSpecialChars && !/[!@#$%^&*(),.?":{}|<>]/.test(value))
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 한국 사업자등록번호 형식 검증 (10자리)
|
|
52
|
+
*/
|
|
53
|
+
export function isBusinessNumber(value) {
|
|
54
|
+
const cleaned = value.replace(/[^0-9]/g, '');
|
|
55
|
+
return cleaned.length === 10;
|
|
56
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Hooks
|
|
3
|
+
*
|
|
4
|
+
* This module provides React hooks for common use cases.
|
|
5
|
+
* Note: Requires React as a peer dependency.
|
|
6
|
+
*/
|
|
7
|
+
export * from './useLocalStorage';
|
|
8
|
+
export * from './useSessionStorage';
|
|
9
|
+
export * from './useMediaQuery';
|
|
10
|
+
export * from './useClickOutside';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Hooks
|
|
3
|
+
*
|
|
4
|
+
* This module provides React hooks for common use cases.
|
|
5
|
+
* Note: Requires React as a peer dependency.
|
|
6
|
+
*/
|
|
7
|
+
// Storage hooks
|
|
8
|
+
export * from './useLocalStorage';
|
|
9
|
+
export * from './useSessionStorage';
|
|
10
|
+
// UI/UX hooks
|
|
11
|
+
export * from './useMediaQuery';
|
|
12
|
+
export * from './useClickOutside';
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 요소 외부 클릭을 감지하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param ref - 외부 클릭을 감지할 요소의 ref
|
|
6
|
+
* @param handler - 외부 클릭 시 실행할 콜백 함수
|
|
7
|
+
* @param enabled - hook 활성화 여부 (기본값: true)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* function Dropdown() {
|
|
11
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
* const dropdownRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
*
|
|
14
|
+
* useClickOutside(dropdownRef, () => setIsOpen(false));
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <div ref={dropdownRef}>
|
|
18
|
+
* <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
|
|
19
|
+
* {isOpen && <div>Dropdown Content</div>}
|
|
20
|
+
* </div>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 조건부 활성화
|
|
26
|
+
* useClickOutside(modalRef, handleClose, isModalOpen);
|
|
27
|
+
*/
|
|
28
|
+
export declare function useClickOutside<T extends HTMLElement = HTMLElement>(ref: RefObject<T>, handler: (event: MouseEvent | TouchEvent) => void, enabled?: boolean): void;
|
|
29
|
+
/**
|
|
30
|
+
* 여러 요소의 외부 클릭을 감지하는 hook
|
|
31
|
+
*
|
|
32
|
+
* @param refs - 외부 클릭을 감지할 요소들의 ref 배열
|
|
33
|
+
* @param handler - 외부 클릭 시 실행할 콜백 함수
|
|
34
|
+
* @param enabled - hook 활성화 여부 (기본값: true)
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* function Modal() {
|
|
38
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
39
|
+
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
40
|
+
*
|
|
41
|
+
* // 모달과 트리거 버튼 외부 클릭 시 닫기
|
|
42
|
+
* useClickOutsideMultiple(
|
|
43
|
+
* [modalRef, triggerRef],
|
|
44
|
+
* () => setIsOpen(false)
|
|
45
|
+
* );
|
|
46
|
+
* }
|
|
47
|
+
*/
|
|
48
|
+
export declare function useClickOutsideMultiple<T extends HTMLElement = HTMLElement>(refs: RefObject<T>[], handler: (event: MouseEvent | TouchEvent) => void, enabled?: boolean): void;
|
|
49
|
+
//# sourceMappingURL=useClickOutside.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useClickOutside.d.ts","sourceRoot":"","sources":["../../src/hooks/useClickOutside.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACjE,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA2BN;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EACzE,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EACpB,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,KAAK,IAAI,EACjD,OAAO,GAAE,OAAc,GACtB,IAAI,CA6BN"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 요소 외부 클릭을 감지하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param ref - 외부 클릭을 감지할 요소의 ref
|
|
6
|
+
* @param handler - 외부 클릭 시 실행할 콜백 함수
|
|
7
|
+
* @param enabled - hook 활성화 여부 (기본값: true)
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* function Dropdown() {
|
|
11
|
+
* const [isOpen, setIsOpen] = useState(false);
|
|
12
|
+
* const dropdownRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
*
|
|
14
|
+
* useClickOutside(dropdownRef, () => setIsOpen(false));
|
|
15
|
+
*
|
|
16
|
+
* return (
|
|
17
|
+
* <div ref={dropdownRef}>
|
|
18
|
+
* <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
|
|
19
|
+
* {isOpen && <div>Dropdown Content</div>}
|
|
20
|
+
* </div>
|
|
21
|
+
* );
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 조건부 활성화
|
|
26
|
+
* useClickOutside(modalRef, handleClose, isModalOpen);
|
|
27
|
+
*/
|
|
28
|
+
export function useClickOutside(ref, handler, enabled = true) {
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!enabled) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const listener = (event) => {
|
|
34
|
+
const element = ref.current;
|
|
35
|
+
// ref가 없거나, 클릭한 요소가 ref 내부인 경우 무시
|
|
36
|
+
if (!element || element.contains(event.target)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// 외부 클릭 시 handler 실행
|
|
40
|
+
handler(event);
|
|
41
|
+
};
|
|
42
|
+
// mousedown과 touchstart 이벤트 모두 처리 (모바일 지원)
|
|
43
|
+
document.addEventListener('mousedown', listener);
|
|
44
|
+
document.addEventListener('touchstart', listener);
|
|
45
|
+
return () => {
|
|
46
|
+
document.removeEventListener('mousedown', listener);
|
|
47
|
+
document.removeEventListener('touchstart', listener);
|
|
48
|
+
};
|
|
49
|
+
}, [ref, handler, enabled]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 여러 요소의 외부 클릭을 감지하는 hook
|
|
53
|
+
*
|
|
54
|
+
* @param refs - 외부 클릭을 감지할 요소들의 ref 배열
|
|
55
|
+
* @param handler - 외부 클릭 시 실행할 콜백 함수
|
|
56
|
+
* @param enabled - hook 활성화 여부 (기본값: true)
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* function Modal() {
|
|
60
|
+
* const modalRef = useRef<HTMLDivElement>(null);
|
|
61
|
+
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
62
|
+
*
|
|
63
|
+
* // 모달과 트리거 버튼 외부 클릭 시 닫기
|
|
64
|
+
* useClickOutsideMultiple(
|
|
65
|
+
* [modalRef, triggerRef],
|
|
66
|
+
* () => setIsOpen(false)
|
|
67
|
+
* );
|
|
68
|
+
* }
|
|
69
|
+
*/
|
|
70
|
+
export function useClickOutsideMultiple(refs, handler, enabled = true) {
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!enabled) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const listener = (event) => {
|
|
76
|
+
// 모든 ref를 확인하여 하나라도 내부 클릭이면 무시
|
|
77
|
+
const isInside = refs.some((ref) => {
|
|
78
|
+
const element = ref.current;
|
|
79
|
+
return element && element.contains(event.target);
|
|
80
|
+
});
|
|
81
|
+
if (isInside) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// 모든 요소 외부 클릭 시 handler 실행
|
|
85
|
+
handler(event);
|
|
86
|
+
};
|
|
87
|
+
document.addEventListener('mousedown', listener);
|
|
88
|
+
document.addEventListener('touchstart', listener);
|
|
89
|
+
return () => {
|
|
90
|
+
document.removeEventListener('mousedown', listener);
|
|
91
|
+
document.removeEventListener('touchstart', listener);
|
|
92
|
+
};
|
|
93
|
+
}, [refs, handler, enabled]);
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* localStorage와 동기화되는 state hook
|
|
4
|
+
*
|
|
5
|
+
* @param key - localStorage 키
|
|
6
|
+
* @param initialValue - 초기값
|
|
7
|
+
* @returns [storedValue, setValue, removeValue]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [name, setName, removeName] = useLocalStorage('username', 'Guest');
|
|
11
|
+
*
|
|
12
|
+
* // 값 설정
|
|
13
|
+
* setName('John');
|
|
14
|
+
*
|
|
15
|
+
* // 값 제거
|
|
16
|
+
* removeName();
|
|
17
|
+
*/
|
|
18
|
+
export declare function useLocalStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>, () => void];
|
|
19
|
+
//# sourceMappingURL=useLocalStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useLocalStorage.d.ts","sourceRoot":"","sources":["../../src/hooks/useLocalStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,GACd,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CA0F9C"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* localStorage와 동기화되는 state hook
|
|
4
|
+
*
|
|
5
|
+
* @param key - localStorage 키
|
|
6
|
+
* @param initialValue - 초기값
|
|
7
|
+
* @returns [storedValue, setValue, removeValue]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [name, setName, removeName] = useLocalStorage('username', 'Guest');
|
|
11
|
+
*
|
|
12
|
+
* // 값 설정
|
|
13
|
+
* setName('John');
|
|
14
|
+
*
|
|
15
|
+
* // 값 제거
|
|
16
|
+
* removeName();
|
|
17
|
+
*/
|
|
18
|
+
export function useLocalStorage(key, initialValue) {
|
|
19
|
+
// SSR 안전성 체크
|
|
20
|
+
const isBrowser = typeof window !== 'undefined';
|
|
21
|
+
// 초기값을 localStorage에서 가져오기
|
|
22
|
+
const readValue = useCallback(() => {
|
|
23
|
+
if (!isBrowser) {
|
|
24
|
+
return initialValue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const item = window.localStorage.getItem(key);
|
|
28
|
+
return item ? JSON.parse(item) : initialValue;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn(`Error reading localStorage key "${key}":`, error);
|
|
32
|
+
return initialValue;
|
|
33
|
+
}
|
|
34
|
+
}, [initialValue, key, isBrowser]);
|
|
35
|
+
const [storedValue, setStoredValue] = useState(readValue);
|
|
36
|
+
// 값 설정 함수
|
|
37
|
+
const setValue = useCallback((value) => {
|
|
38
|
+
if (!isBrowser) {
|
|
39
|
+
console.warn(`Tried setting localStorage key "${key}" even though environment is not a client`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
// useState와 동일하게 함수형 업데이트 지원
|
|
44
|
+
const newValue = value instanceof Function ? value(storedValue) : value;
|
|
45
|
+
// localStorage에 저장
|
|
46
|
+
window.localStorage.setItem(key, JSON.stringify(newValue));
|
|
47
|
+
// state 업데이트
|
|
48
|
+
setStoredValue(newValue);
|
|
49
|
+
// storage event 발생 (다른 탭/윈도우에 알림)
|
|
50
|
+
window.dispatchEvent(new Event('local-storage'));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`Error setting localStorage key "${key}":`, error);
|
|
54
|
+
}
|
|
55
|
+
}, [key, storedValue, isBrowser]);
|
|
56
|
+
// 값 제거 함수
|
|
57
|
+
const removeValue = useCallback(() => {
|
|
58
|
+
if (!isBrowser) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
window.localStorage.removeItem(key);
|
|
63
|
+
setStoredValue(initialValue);
|
|
64
|
+
window.dispatchEvent(new Event('local-storage'));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Error removing localStorage key "${key}":`, error);
|
|
68
|
+
}
|
|
69
|
+
}, [key, initialValue, isBrowser]);
|
|
70
|
+
// 다른 탭/윈도우의 변경사항 감지
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isBrowser) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const handleStorageChange = (e) => {
|
|
76
|
+
if ('key' in e && e.key && e.key !== key) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setStoredValue(readValue());
|
|
80
|
+
};
|
|
81
|
+
// storage event 리스너 (다른 탭의 변경사항)
|
|
82
|
+
window.addEventListener('storage', handleStorageChange);
|
|
83
|
+
// 같은 페이지 내의 변경사항
|
|
84
|
+
window.addEventListener('local-storage', handleStorageChange);
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener('storage', handleStorageChange);
|
|
87
|
+
window.removeEventListener('local-storage', handleStorageChange);
|
|
88
|
+
};
|
|
89
|
+
}, [key, readValue, isBrowser]);
|
|
90
|
+
return [storedValue, setValue, removeValue];
|
|
91
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 미디어 쿼리 매칭 여부를 반환하는 hook
|
|
3
|
+
*
|
|
4
|
+
* @param query - CSS 미디어 쿼리 문자열
|
|
5
|
+
* @returns 미디어 쿼리 매칭 여부
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 모바일 체크
|
|
9
|
+
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // 태블릿 체크
|
|
13
|
+
* const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // 데스크톱 체크
|
|
17
|
+
* const isDesktop = useMediaQuery('(min-width: 1025px)');
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* // 다크모드 체크
|
|
21
|
+
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // 가로 모드 체크
|
|
25
|
+
* const isLandscape = useMediaQuery('(orientation: landscape)');
|
|
26
|
+
*/
|
|
27
|
+
export declare function useMediaQuery(query: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 일반적인 브레이크포인트를 위한 헬퍼 hook들
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* 모바일 디바이스 체크 (768px 이하)
|
|
33
|
+
* @example
|
|
34
|
+
* const isMobile = useIsMobile();
|
|
35
|
+
* if (isMobile) return <MobileView />;
|
|
36
|
+
*/
|
|
37
|
+
export declare function useIsMobile(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* 태블릿 디바이스 체크 (769px ~ 1024px)
|
|
40
|
+
* @example
|
|
41
|
+
* const isTablet = useIsTablet();
|
|
42
|
+
*/
|
|
43
|
+
export declare function useIsTablet(): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* 데스크톱 디바이스 체크 (1025px 이상)
|
|
46
|
+
* @example
|
|
47
|
+
* const isDesktop = useIsDesktop();
|
|
48
|
+
*/
|
|
49
|
+
export declare function useIsDesktop(): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* 다크모드 체크
|
|
52
|
+
* @example
|
|
53
|
+
* const isDarkMode = useIsDarkMode();
|
|
54
|
+
*/
|
|
55
|
+
export declare function useIsDarkMode(): boolean;
|
|
56
|
+
//# sourceMappingURL=useMediaQuery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMediaQuery.d.ts","sourceRoot":"","sources":["../../src/hooks/useMediaQuery.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CA8CpD;AAED;;GAEG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAErC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAEtC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 미디어 쿼리 매칭 여부를 반환하는 hook
|
|
4
|
+
*
|
|
5
|
+
* @param query - CSS 미디어 쿼리 문자열
|
|
6
|
+
* @returns 미디어 쿼리 매칭 여부
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // 모바일 체크
|
|
10
|
+
* const isMobile = useMediaQuery('(max-width: 768px)');
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // 태블릿 체크
|
|
14
|
+
* const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // 데스크톱 체크
|
|
18
|
+
* const isDesktop = useMediaQuery('(min-width: 1025px)');
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // 다크모드 체크
|
|
22
|
+
* const isDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* // 가로 모드 체크
|
|
26
|
+
* const isLandscape = useMediaQuery('(orientation: landscape)');
|
|
27
|
+
*/
|
|
28
|
+
export function useMediaQuery(query) {
|
|
29
|
+
// SSR 안전성을 위한 초기값 설정
|
|
30
|
+
const getMatches = (query) => {
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
return window.matchMedia(query).matches;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
const [matches, setMatches] = useState(getMatches(query));
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (typeof window === 'undefined') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const mediaQuery = window.matchMedia(query);
|
|
42
|
+
// 초기 상태 설정
|
|
43
|
+
setMatches(mediaQuery.matches);
|
|
44
|
+
// 미디어 쿼리 변경 감지 (최신 API)
|
|
45
|
+
const handleChange = (event) => {
|
|
46
|
+
setMatches(event.matches);
|
|
47
|
+
};
|
|
48
|
+
// 이벤트 리스너 등록
|
|
49
|
+
// addEventListener를 지원하는 브라우저
|
|
50
|
+
if (mediaQuery.addEventListener) {
|
|
51
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// 구형 브라우저 지원 (deprecated)
|
|
55
|
+
mediaQuery.addListener(handleChange);
|
|
56
|
+
}
|
|
57
|
+
// 클린업
|
|
58
|
+
return () => {
|
|
59
|
+
if (mediaQuery.removeEventListener) {
|
|
60
|
+
mediaQuery.removeEventListener('change', handleChange);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
mediaQuery.removeListener(handleChange);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [query]);
|
|
67
|
+
return matches;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 일반적인 브레이크포인트를 위한 헬퍼 hook들
|
|
71
|
+
*/
|
|
72
|
+
/**
|
|
73
|
+
* 모바일 디바이스 체크 (768px 이하)
|
|
74
|
+
* @example
|
|
75
|
+
* const isMobile = useIsMobile();
|
|
76
|
+
* if (isMobile) return <MobileView />;
|
|
77
|
+
*/
|
|
78
|
+
export function useIsMobile() {
|
|
79
|
+
return useMediaQuery('(max-width: 768px)');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* 태블릿 디바이스 체크 (769px ~ 1024px)
|
|
83
|
+
* @example
|
|
84
|
+
* const isTablet = useIsTablet();
|
|
85
|
+
*/
|
|
86
|
+
export function useIsTablet() {
|
|
87
|
+
return useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 데스크톱 디바이스 체크 (1025px 이상)
|
|
91
|
+
* @example
|
|
92
|
+
* const isDesktop = useIsDesktop();
|
|
93
|
+
*/
|
|
94
|
+
export function useIsDesktop() {
|
|
95
|
+
return useMediaQuery('(min-width: 1025px)');
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 다크모드 체크
|
|
99
|
+
* @example
|
|
100
|
+
* const isDarkMode = useIsDarkMode();
|
|
101
|
+
*/
|
|
102
|
+
export function useIsDarkMode() {
|
|
103
|
+
return useMediaQuery('(prefers-color-scheme: dark)');
|
|
104
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* sessionStorage와 동기화되는 state hook
|
|
4
|
+
*
|
|
5
|
+
* @param key - sessionStorage 키
|
|
6
|
+
* @param initialValue - 초기값
|
|
7
|
+
* @returns [storedValue, setValue, removeValue]
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const [token, setToken, removeToken] = useSessionStorage('auth-token', '');
|
|
11
|
+
*
|
|
12
|
+
* // 값 설정
|
|
13
|
+
* setToken('abc123');
|
|
14
|
+
*
|
|
15
|
+
* // 값 제거
|
|
16
|
+
* removeToken();
|
|
17
|
+
*/
|
|
18
|
+
export declare function useSessionStorage<T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>, () => void];
|
|
19
|
+
//# sourceMappingURL=useSessionStorage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSessionStorage.d.ts","sourceRoot":"","sources":["../../src/hooks/useSessionStorage.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,CAAC,GACd,CAAC,CAAC,EAAE,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,CAAC,CAoF9C"}
|