goodchuck-utils 1.2.0 → 1.4.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/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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- 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 +22 -7
- package/src/date/index.test.ts +0 -206
- package/src/date/index.ts +0 -123
- package/src/index.ts +0 -11
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -13
|
@@ -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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } 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 function useSessionStorage(key, initialValue) {
|
|
19
|
+
// SSR 안전성 체크
|
|
20
|
+
const isBrowser = typeof window !== 'undefined';
|
|
21
|
+
// 초기값을 sessionStorage에서 가져오기
|
|
22
|
+
const readValue = useCallback(() => {
|
|
23
|
+
if (!isBrowser) {
|
|
24
|
+
return initialValue;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const item = window.sessionStorage.getItem(key);
|
|
28
|
+
return item ? JSON.parse(item) : initialValue;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn(`Error reading sessionStorage 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 sessionStorage 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
|
+
// sessionStorage에 저장
|
|
46
|
+
window.sessionStorage.setItem(key, JSON.stringify(newValue));
|
|
47
|
+
// state 업데이트
|
|
48
|
+
setStoredValue(newValue);
|
|
49
|
+
// storage event 발생
|
|
50
|
+
window.dispatchEvent(new Event('session-storage'));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.warn(`Error setting sessionStorage key "${key}":`, error);
|
|
54
|
+
}
|
|
55
|
+
}, [key, storedValue, isBrowser]);
|
|
56
|
+
// 값 제거 함수
|
|
57
|
+
const removeValue = useCallback(() => {
|
|
58
|
+
if (!isBrowser) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
window.sessionStorage.removeItem(key);
|
|
63
|
+
setStoredValue(initialValue);
|
|
64
|
+
window.dispatchEvent(new Event('session-storage'));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.warn(`Error removing sessionStorage key "${key}":`, error);
|
|
68
|
+
}
|
|
69
|
+
}, [key, initialValue, isBrowser]);
|
|
70
|
+
// 같은 페이지 내의 변경사항 감지
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isBrowser) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const handleStorageChange = () => {
|
|
76
|
+
setStoredValue(readValue());
|
|
77
|
+
};
|
|
78
|
+
// 같은 페이지 내의 변경사항
|
|
79
|
+
window.addEventListener('session-storage', handleStorageChange);
|
|
80
|
+
return () => {
|
|
81
|
+
window.removeEventListener('session-storage', handleStorageChange);
|
|
82
|
+
};
|
|
83
|
+
}, [key, readValue, isBrowser]);
|
|
84
|
+
return [storedValue, setValue, removeValue];
|
|
85
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,UAAU,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// Date utilities
|
|
2
2
|
export * from './date';
|
|
3
|
-
//
|
|
4
|
-
|
|
3
|
+
// Form utilities
|
|
4
|
+
export * from './form';
|
|
5
|
+
// String utilities
|
|
6
|
+
export * from './string';
|
|
7
|
+
// React Hooks (import separately: 'goodchuck-utils/hooks')
|
|
8
|
+
// Note: Hooks are not exported from main entry to avoid React dependency for non-React users
|
|
9
|
+
// export * from './hooks';
|
|
5
10
|
// Array utilities (placeholder for future)
|
|
6
11
|
// export * from './array';
|
|
7
12
|
// Object utilities (placeholder for future)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"case.test.d.ts","sourceRoot":"","sources":["../../../src/string/__tests__/case.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { capitalize, capitalizeWords, camelCase, pascalCase, snakeCase, kebabCase, constantCase, } from '../case';
|
|
3
|
+
describe('String Case Conversion', () => {
|
|
4
|
+
describe('capitalize', () => {
|
|
5
|
+
it('should capitalize first letter', () => {
|
|
6
|
+
expect(capitalize('hello')).toBe('Hello');
|
|
7
|
+
expect(capitalize('hello world')).toBe('Hello world');
|
|
8
|
+
expect(capitalize('HELLO')).toBe('HELLO');
|
|
9
|
+
});
|
|
10
|
+
it('should handle empty string', () => {
|
|
11
|
+
expect(capitalize('')).toBe('');
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
describe('capitalizeWords', () => {
|
|
15
|
+
it('should capitalize each word', () => {
|
|
16
|
+
expect(capitalizeWords('hello world')).toBe('Hello World');
|
|
17
|
+
expect(capitalizeWords('the quick brown fox')).toBe('The Quick Brown Fox');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('camelCase', () => {
|
|
21
|
+
it('should convert to camelCase', () => {
|
|
22
|
+
expect(camelCase('hello world')).toBe('helloWorld');
|
|
23
|
+
expect(camelCase('hello-world')).toBe('helloWorld');
|
|
24
|
+
expect(camelCase('hello_world')).toBe('helloWorld');
|
|
25
|
+
expect(camelCase('Hello World')).toBe('helloWorld');
|
|
26
|
+
});
|
|
27
|
+
it('should handle already camelCase strings', () => {
|
|
28
|
+
expect(camelCase('helloWorld')).toBe('helloworld');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('pascalCase', () => {
|
|
32
|
+
it('should convert to PascalCase', () => {
|
|
33
|
+
expect(pascalCase('hello world')).toBe('HelloWorld');
|
|
34
|
+
expect(pascalCase('hello-world')).toBe('HelloWorld');
|
|
35
|
+
expect(pascalCase('hello_world')).toBe('HelloWorld');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe('snakeCase', () => {
|
|
39
|
+
it('should convert to snake_case', () => {
|
|
40
|
+
expect(snakeCase('helloWorld')).toBe('hello_world');
|
|
41
|
+
expect(snakeCase('Hello World')).toBe('hello_world');
|
|
42
|
+
expect(snakeCase('hello-world')).toBe('hello_world');
|
|
43
|
+
expect(snakeCase('HelloWorld')).toBe('hello_world');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('kebabCase', () => {
|
|
47
|
+
it('should convert to kebab-case', () => {
|
|
48
|
+
expect(kebabCase('helloWorld')).toBe('hello-world');
|
|
49
|
+
expect(kebabCase('Hello World')).toBe('hello-world');
|
|
50
|
+
expect(kebabCase('hello_world')).toBe('hello-world');
|
|
51
|
+
expect(kebabCase('HelloWorld')).toBe('hello-world');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('constantCase', () => {
|
|
55
|
+
it('should convert to CONSTANT_CASE', () => {
|
|
56
|
+
expect(constantCase('helloWorld')).toBe('HELLO_WORLD');
|
|
57
|
+
expect(constantCase('Hello World')).toBe('HELLO_WORLD');
|
|
58
|
+
expect(constantCase('hello-world')).toBe('HELLO_WORLD');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manipulation.test.d.ts","sourceRoot":"","sources":["../../../src/string/__tests__/manipulation.test.ts"],"names":[],"mappings":""}
|