@musecat/functionkit 1.0.1 → 1.0.2

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 (120) hide show
  1. package/.agents/references/components/ScrolltoTop.md +36 -0
  2. package/.agents/references/components/SwitchCase.md +47 -0
  3. package/.agents/references/components/ViewportPortal.md +34 -0
  4. package/.agents/references/cookie/cookie.md +47 -0
  5. package/.agents/references/datetime/dateTime.client.md +42 -0
  6. package/.agents/references/datetime/dateTime.server.md +42 -0
  7. package/.agents/references/datetime/dateTime.shared.md +86 -0
  8. package/.agents/references/hooks/useAvoidKeyboard.md +29 -0
  9. package/.agents/references/hooks/useCheckInvisible.md +36 -0
  10. package/.agents/references/hooks/useCheckScroll.md +36 -0
  11. package/.agents/references/hooks/useClientDateTime.md +39 -0
  12. package/.agents/references/hooks/useDebounce.md +46 -0
  13. package/.agents/references/hooks/useDebouncedCallback.md +34 -0
  14. package/.agents/references/hooks/useDoubleClick.md +40 -0
  15. package/.agents/references/hooks/useGeolocation.md +37 -0
  16. package/.agents/references/hooks/useHasMounted.md +27 -0
  17. package/.agents/references/hooks/useIntersectionObserver.md +37 -0
  18. package/.agents/references/hooks/useInterval.md +40 -0
  19. package/.agents/references/hooks/useKeyboardHeight.md +33 -0
  20. package/.agents/references/hooks/useKeyboardListNavigation.md +58 -0
  21. package/.agents/references/hooks/useLongPress.md +48 -0
  22. package/.agents/references/hooks/usePreservedCallback.md +33 -0
  23. package/.agents/references/hooks/usePreservedReference.md +34 -0
  24. package/.agents/references/hooks/useRefEffect.md +35 -0
  25. package/.agents/references/hooks/useRelativeDateTime.md +35 -0
  26. package/.agents/references/hooks/useTimeout.md +42 -0
  27. package/.agents/references/hooks/useToggleState.md +39 -0
  28. package/.agents/references/hooks/useViewportHeight.md +26 -0
  29. package/.agents/references/hooks/useViewportMatch.md +32 -0
  30. package/.agents/references/utils/browserStorage.md +41 -0
  31. package/.agents/references/utils/buildContext.md +47 -0
  32. package/.agents/references/utils/clipboardShare.md +59 -0
  33. package/.agents/references/utils/floatingMotion.md +67 -0
  34. package/.agents/references/utils/getDeviceInfo.md +43 -0
  35. package/.agents/references/utils/isEditableKeyboardTarget.md +35 -0
  36. package/.agents/references/utils/mergeRefs.md +29 -0
  37. package/.agents/references/utils/seen.md +37 -0
  38. package/.agents/references/utils/subscribeKeyboardHeight.md +35 -0
  39. package/AGENTS.md +103 -396
  40. package/README.md +18 -5
  41. package/index.ts +63 -68
  42. package/package.json +27 -13
  43. package/packages/components/ScrolltoTop.tsx +3 -3
  44. package/packages/components/SwitchCase.tsx +7 -7
  45. package/packages/components/ViewportPortal.tsx +23 -23
  46. package/packages/cookie/cookie.shared.ts +97 -101
  47. package/packages/datetime/dateTime.client.ts +86 -86
  48. package/packages/datetime/dateTime.server.ts +86 -86
  49. package/packages/datetime/dateTime.shared.ts +281 -281
  50. package/packages/hooks/useAvoidKeyboard.ts +15 -15
  51. package/packages/hooks/useCheckInvisible.ts +16 -16
  52. package/packages/hooks/useCheckScroll.ts +10 -10
  53. package/packages/hooks/useClientDateTime.ts +33 -33
  54. package/packages/hooks/useDebounce.ts +95 -95
  55. package/packages/hooks/useDebouncedCallback.ts +45 -45
  56. package/packages/hooks/useDoubleClick.ts +43 -43
  57. package/packages/hooks/useGeolocation.ts +130 -130
  58. package/packages/hooks/useHasMounted.ts +5 -5
  59. package/packages/hooks/useIntersectionObserver.ts +20 -20
  60. package/packages/hooks/useInterval.ts +46 -45
  61. package/packages/hooks/useKeyboardHeight.ts +9 -9
  62. package/packages/hooks/useKeyboardListNavigation.ts +156 -156
  63. package/packages/hooks/useLongPress.ts +98 -98
  64. package/packages/hooks/usePreservedCallback.ts +12 -12
  65. package/packages/hooks/usePreservedReference.ts +10 -10
  66. package/packages/hooks/useRefEffect.ts +19 -19
  67. package/packages/hooks/useRelativeDateTime.ts +37 -37
  68. package/packages/hooks/useTimeout.ts +41 -41
  69. package/packages/hooks/useToggleState.ts +5 -5
  70. package/packages/hooks/useViewportHeight.ts +13 -13
  71. package/packages/hooks/useViewportMatch.ts +16 -22
  72. package/packages/utils/browserStorage.ts +26 -26
  73. package/packages/utils/buildContext.tsx +10 -10
  74. package/packages/utils/checkDevice.ts +64 -64
  75. package/packages/utils/clipboardShare.tsx +31 -31
  76. package/packages/utils/clipboardShare.types.ts +7 -7
  77. package/packages/utils/floatingMotion.ts +71 -71
  78. package/packages/utils/keyboardTarget.ts +9 -9
  79. package/packages/utils/mergeRefs.ts +9 -9
  80. package/packages/utils/seen.ts +15 -15
  81. package/packages/utils/subscribeKeyboardHeight.ts +46 -46
  82. package/tests/components/ScrolltoTop.test.tsx +15 -0
  83. package/tests/components/SwitchCase.test.tsx +34 -0
  84. package/tests/components/ViewportPortal.test.tsx +42 -0
  85. package/tests/cookie/cookie.test.ts +156 -0
  86. package/tests/datetime/datetime.test.ts +250 -0
  87. package/tests/hooks/useAvoidKeyboard.test.ts +12 -0
  88. package/tests/hooks/useCheckInvisible.test.ts +28 -0
  89. package/tests/hooks/useCheckScroll.test.ts +23 -0
  90. package/tests/hooks/useClientDateTime.test.ts +14 -0
  91. package/tests/hooks/useDebounce.test.ts +71 -0
  92. package/tests/hooks/useDebouncedCallback.test.ts +34 -0
  93. package/tests/hooks/useDoubleClick.test.ts +31 -0
  94. package/tests/hooks/useGeolocation.test.ts +50 -0
  95. package/tests/hooks/useHasMounted.test.ts +10 -0
  96. package/tests/hooks/useIntersectionObserver.test.ts +13 -0
  97. package/tests/hooks/useInterval.test.ts +31 -0
  98. package/tests/hooks/useKeyboardHeight.test.ts +10 -0
  99. package/tests/hooks/useKeyboardListNavigation.test.ts +82 -0
  100. package/tests/hooks/useLongPress.test.ts +52 -0
  101. package/tests/hooks/usePreservedCallback.test.ts +35 -0
  102. package/tests/hooks/usePreservedReference.test.ts +35 -0
  103. package/tests/hooks/useRefEffect.test.ts +30 -0
  104. package/tests/hooks/useRelativeDateTime.test.ts +19 -0
  105. package/tests/hooks/useTimeout.test.ts +34 -0
  106. package/tests/hooks/useToggleState.test.ts +35 -0
  107. package/tests/hooks/useViewportHeight.test.ts +10 -0
  108. package/tests/hooks/useViewportMatch.test.ts +35 -0
  109. package/tests/setup.ts +9 -0
  110. package/tests/utils/browserStorage.test.ts +54 -0
  111. package/tests/utils/buildContext.test.tsx +27 -0
  112. package/tests/utils/checkDevice.test.ts +84 -0
  113. package/tests/utils/clipboardShare.test.ts +78 -0
  114. package/tests/utils/floatingMotion.test.ts +84 -0
  115. package/tests/utils/keyboardTarget.test.ts +44 -0
  116. package/tests/utils/mergeRefs.test.ts +18 -0
  117. package/tests/utils/seen.test.ts +31 -0
  118. package/tests/utils/subscribeKeyboardHeight.test.ts +50 -0
  119. package/tsconfig.json +16 -16
  120. package/vitest.config.ts +16 -0
package/AGENTS.md CHANGED
@@ -1,398 +1,105 @@
1
1
  # FunctionKit (@musecat/functionkit)
2
2
 
3
- React 19 기반 프론트엔드 유틸 라이브러리. 단일 패키지, ESM. 모든 훅/유틸/컴포넌트는 `index.ts` 배럴을 통해 `@musecat/functionkit`에서 import한다. 서브패스(`@musecat/functionkit/hooks/useX`)도 가능하나 배럴优先.
4
-
5
- ## AI 사용 규칙 (필수)
6
-
7
- - **모든 구현에서 반드시 FunctionKit을 먼저 참조한다.** 훅·유틸·날짜포맷·쿠키·스토리지·컴포넌트가 필요하면 예외 없이 아래 목록을 먼저 확인하고 존재하면 그것을 사용한다.
8
- - **직접 구현 금지.** 여기 있는 기능을 새로 짜거나 유사 기능을 중복 구현하는 것은 금지한다. 여기 없는 것만 새로 만든다.
9
- - **import는 배럴 경로만 사용:** `import { useDebounce } from "@musecat/functionkit"`.
10
- - SSR 주의: `"use client"` 없는 순수 함수(shared, seen, cookie.shared, debounce, mergeRefs, isEditableKeyboardTarget, floatingMotion, SwitchCase)만 서버 컴포넌트에서 사용 가능. 나머지는 반드시 클라이언트 컴포넌트.
11
- - 타입 import도 배럴: `import type { AppLocale, DateInput } from "@musecat/functionkit"`.
12
-
13
- ---
14
-
15
- ## Hooks — 완전 분석
16
-
17
- ### useHasMounted
18
- `useHasMounted(): boolean` — 마운트 전 `false`, 후 `true`. hydration guard로 사용. useEffect 한 번으로 state 전환.
19
- ```tsx
20
- const mounted = useHasMounted()
21
- if (!mounted) return null // SSR/SSG에서 클라이언트 전용 UI 숨기기
22
- ```
23
-
24
- ### usePreservedCallback
25
- `usePreservedCallback<Args extends unknown[], Return>(callback: (...args: Args) => Return): (...args: Args) => Return`
26
- - ref에 callback을 저장해서 항상 최신 참조 유지. 반환된 함수는 절대 재생성되지 않음.
27
- - 자식 컴포넌트에 callback prop으로 넘길 때 불필요한 리렌더 방지.
28
- - 내부적으로 `useDebounce`, `useIntersectionObserver`, `useInterval`, `useTimeout` 등 많은 훅이 이 함수에 의존함.
29
-
30
- ### usePreservedReference
31
- `usePreservedReference<T>(value: T, areValuesEqual?: (a: T, b: T) => boolean): T`
32
- - 기본 비교는 `JSON.stringify(a) === JSON.stringify(b)`. 깊은 비교로 stable reference 반환.
33
- - 객체/배열이 내용은 같은데 참조만 바뀌는 경우 의존성 배열에 쓸 때 유용.
34
- - 예: `useEffect(fn, [usePreservedReference(options)])` — options 객체 내용이 같으면 effect 재실행 안 함.
35
- - 주의: JSON.stringify의 한계 undefined, Symbol, circular reference는 무시됨.
36
-
37
- ### useDebounce
38
- `useDebounce(callback, wait: number, options?: { leading?, trailing? }): { (...args): void, cancel(): void }`
39
- - `{ leading: false, trailing: true }`가 기본.
40
- - 내부적으로 `usePreservedCallback`으로 callback 고정 + `useMemo`로 debounced 함수 생성.
41
- - 반환된 함수의 `cancel()`로 pending 취소 가능. unmount 시 자동 cancel.
42
- - pure 함수 `debounce(fn, debounceMs, { edges })`도 export됨 — edges는 `["leading", "trailing"]` 배열.
43
- - **용례:** 검색어 입력(300ms), 저장 버튼 중복 방지, 스크롤 이벤트 제한.
44
- ```tsx
45
- const handleSearch = useDebounce((term: string) => fetchResults(term), 300)
46
- ```
47
-
48
- ### useDebouncedCallback
49
- `useDebouncedCallback({ onChange, timeThreshold, leading?, trailing? }): (nextValue: boolean) => void`
50
- - boolean 값만 받음. 동일한 값 들어오면 조기 return (no-op).
51
- - debounce 인스턴스를 호출마다 새로 생성 (이전 pending cancel 후 새로 등록).
52
- - **용례:** 토글 스위치 변경 이벤트 디바운스, 체크박스 onChange 지연 처리.
53
-
54
- ### useDoubleClick
55
- `useDoubleClick<E extends HTMLElement>({ delay?, click?, doubleClick }): (event: MouseEvent<E>) => void`
56
- - 반환된 함수를 `onClick`에 바인딩. `event.detail`로 싱글/더블클릭 구분.
57
- - delay(기본 250ms) 내 두 번째 클릭이 없으면 `click` 호출, 있으면 `doubleClick` 호출하고 pending 취소.
58
- - `click` 없으면 싱글클릭 아무 일도 일어남 (그래도 timer는 돔).
59
- - ref로 callback 관리 항상 최신 참조, 리렌더 영향 없음.
60
- - **용례:** 리스트 아이템 싱글클릭 미리보기 / 더블클릭 상세보기, 파일명 더블클릭 수정.
61
-
62
- ### useLongPress
63
- `useLongPress<E>(onLongPress, { delay?, moveThreshold?, onClick?, onLongPressEnd? }): { onMouseDown, onMouseUp, onMouseLeave, onTouchStart, onTouchEnd, onTouchMove?, onMouseMove? }`
64
- - delay 기본 500ms. `moveThreshold` 설정 시 지정 픽셀 이상 움직이면 long press 취소.
65
- - mouse/touch 이벤트 통합. `onMouseLeave`도 timeout 취소.
66
- - long press 성공 `onLongPress` 즉시 호출, `onLongPressEnd`는 mouseup/touchend 시 호출.
67
- - 실패 시 `onClick` 호출 (long press가 아니었다는 의미).
68
- - **용례:** 채팅 메시지 길게 눌러 컨텍스트 메뉴, 아이콘 길게 눌러 드래그 시작.
69
-
70
- ### useGeolocation
71
- `useGeolocation(options): { loading, error, data, getCurrentPosition, startTracking, stopTracking, isTracking }`
72
- - `mountBehavior: "get"`(기본, 한 번 조회) | `"watch"`(계속 추적).
73
- - SSR: `typeof window` 체크로 안전. 지원 하면 error 설정.
74
- - `enableHighAccuracy: false`, `maximumAge: 0`, `timeout: Infinity` 기본.
75
- - data 타입: `{ latitude, longitude, accuracy, altitude, altitudeAccuracy, heading, speed, timestamp }`.
76
- - `startTracking()`/`stopTracking()`으로 동적으로 watch on/off.
77
- - **용례:** 사용자 위치 기반 검색, 길찾기, 출퇴근 기록.
78
-
79
- ### useIntersectionObserver
80
- `useIntersectionObserver<Element extends HTMLElement>(callback, options): (element: Element | null) => void`
81
- - 반환값을 ref callback에 전달: `ref={observerRef}`. ref 콜백은 `useRefEffect` 기반.
82
- - SSR: `IntersectionObserver` 없으면 observer 생성 함 (no-op).
83
- - options 그대로 `IntersectionObserverInit` 전달 (threshold, rootMargin 등).
84
- - **용례:** 무한 스크롤 트리거, 이미지 lazy loading, 광고 노출 추적.
85
- ```tsx
86
- const ref = useIntersectionObserver((entry) => {
87
- if (entry.isIntersecting) loadMore()
88
- }, { threshold: 0.1 })
89
- ```
90
-
91
- ### useInterval
92
- `useInterval(callback, options: number | { delay, immediate?, enabled? }): void`
93
- - number = delay(ms). `{ immediate: true }`면 tick 전에 실행. `enabled: false`면 interval 정지.
94
- - StrictMode에서 immediate 중복 실행 방지 (ref로 guard).
95
- - `enabled` 토글 interval 정지/재시작.
96
- - **용례:** 폴링(5초마다 상태 갱신), 실시간 시계, 캐러셀 자동 재생.
97
-
98
- ### useTimeout
99
- `useTimeout(callback, delay?, options?: { enabled? }): { start, reset, clear, isPending }`
100
- - `enabled: true`(기본)면 mount 시 자동 시작, deps 변경 시 재시작.
101
- - 명령형 제어: `start(overrideDelay?)`/`reset(overrideDelay?)` 직접 실행, `clear()` 취소, `isPending()` 실행 중 여부.
102
- - `start`와 `reset`은 동일 함수 (alias). `enabled: false`면 pending timer도 clear.
103
- - **용례:** 토스트 메시지 자동 사라짐(3초), 애니메이션 순차 실행, idle 상태 감지.
104
-
105
- ### useToggleState
106
- `useToggleState(initialValue?: boolean): { value, setValue, setTrue, setFalse, toggle }`
107
- - `initialValue` 기본 `false`. 모든 setter 함수는 stable (useCallback deps=[]).
108
- - **용례:** 모달 열림/닫힘, 사이드바 토글, 접힘/펼침 UI.
109
-
110
- ### useKeyboardHeight / useAvoidKeyboard
111
- `useKeyboardHeight(): { keyboardHeight: number }`
112
- - `subscribeKeyboardHeight({ immediate: true })`로 visualViewport 변화 구독.
113
- - 모바일 키보드 높이를 px 단위로 반환 (닫히면 0).
114
-
115
- `useAvoidKeyboard({ safeAreaBottom?, transitionDuration?, transitionTimingFunction? }): { style: React.CSSProperties }`
116
- - `useKeyboardHeight()`로 높이 얻어 transform: translateY로 회피. keyboardHeight=0이고 safeAreaBottom=0이면 transform 없음.
117
- - transition: `200ms ease-out` 기본.
118
- - **용례:** 모바일에서 입력 필드가 키보드에 가려지지 않도록 뷰포트 밀어올리기.
119
- ```tsx
120
- const { style } = useAvoidKeyboard({ safeAreaBottom: 16 })
121
- return <input style={style} />
122
- ```
123
-
124
- ### useKeyboardListNavigation
125
- `useKeyboardListNavigation({ itemCount }): { activeIndex, setActiveIndex, setItemRef, clickActiveItem, focusBoundaryItem, handleListKeyDown }`
126
- - 방향키(ArrowDown/Up)로 리스트 탐색, Home/End로 처음/끝, Enter/Space로 활성 아이템 클릭, Escape로 선택 해제(activeIndex=-1).
127
- - `itemCount=0`이면 아무 동작 안 함. `event.defaultPrevented`면 스킵. editable input에서는 동작 안 함.
128
- - `setItemRef(index, element)`로 각 아이템 ref 등록. `focusBoundaryItem(key)`는 탭 네비게이션용 (첫 항목/마지막 항목 focus).
129
- - `clickActiveItem()`은 현재 active 아이템의 click() 호출. 없으면 false 반환.
130
- - **용례:** 드롭다운 메뉴 키보드 탐색, 검색 자동완성 리스트, 커맨드 팔레트.
131
-
132
- ### useViewportHeight
133
- `useViewportHeight(): { height: number }`
134
- - `window.visualViewport.height` 우선, 없으면 `window.innerHeight`. SSR에선 0.
135
- - `visualViewport.resize` + `window.resize` 모두 구독.
136
- - **용례:** 모바일 100vh 대체 (주소창 등으로 인한 실제 뷰포트 높이), 키보드 올라왔을 때 레이아웃 대응.
137
-
138
- ### useViewportMatch
139
- `useViewportMatch(mediaQuery: string): boolean`
140
- - `window.matchMedia(mediaQuery).matches` 반환. SSR에선 false.
141
- - media query change 이벤트 구독 → 실시간 반영.
142
- - **용례:** `const isMobile = useViewportMatch("(max-width: 768px)")` — 반응형 로직.
143
-
144
- ### useClientDateTime
145
- `useClientDateTime(value: DateInput, { locale?, timeZone?, datePreset?, timePreset? }): { ready, text, date }`
146
- - `ready`는 hydration guard (초기 false → effect에서 true).
147
- - `formatClientDateTime()`으로 텍스트 생성. value/options 바뀔 때마다 재포맷.
148
- - **용례:** 클라이언트에서만 날짜 표시 (SSR 불일치 방지).
149
-
150
- ### useRelativeDateTime
151
- `useRelativeDateTime(value: DateInput, { locale?, maxRelativeDays?, fallbackDatePreset?, updateIntervalMs? }): { ready, text, isRelative, date }`
152
- - `maxRelativeDays`(기본 7) 초과 시 절대 날짜(fallbackDatePreset 기본 "dot")로 표시.
153
- - `updateIntervalMs`(기본 1000ms)마다 갱신. `"3분 전"`, `"2시간 전"` 등 실시간 업데이트.
154
- - **용례:** 게시글 작성 시간, 댓글 시간, 채팅 메시지 timestamp.
155
-
156
- ### useRefEffect
157
- `useRefEffect<RefElement>(callback, deps): (element: RefElement | null) => void`
158
- - callback ref 생성. element 부착 시 callback 실행, cleanup 반환 가능. unmount 시 cleanup 실행.
159
- - deps 바뀌면 old cleanup → new callback.
160
- -**용례:** DOM에 직접 이벤트 리스너 부착, IntersectionObserver 연결, 외부 라이브러리 DOM 초기화.
161
- ```tsx
162
- const ref = useRefEffect((el) => {
163
- const handler = () => {}
164
- el.addEventListener("scroll", handler)
165
- return () => el.removeEventListener("scroll", handler)
166
- }, [])
167
- ```
168
-
169
- ---
170
-
171
- ## Components — 완전 분석
172
-
173
- ### `<ScrolltoTop />`
174
- `export function ScrolltoTop(): undefined`
175
- - `"use client"`. useEffect로 모든 렌더 시 `window.scrollTo(0, 0)` 실행. 의존성 배열 없음 → 매 렌더마다 실행.
176
- - **용례:** 페이지 이동 시 스크롤 최상단 리셋. `<ScrolltoTop />`를 layout에 한 번 배치.
177
- - 주의: 모든 상태 변경 시 스크롤이 리셋되므로 조건부 삽입하거나 최상위 layout에만 배치할 것.
178
-
179
- ### `<SwitchCase />`
180
- `SwitchCase<T extends string | number>({ value, cases, otherwise }): ReactNode`
181
- - `"use client"` 없음 → 서버에서도 사용 가능.
182
- - `cases[value] ?? otherwise`. value가 cases에 없으면 otherwise, otherwise도 없으면 null.
183
- - typesafe: case key가 T로 제한됨. Partial이라 모든 경우를 다룰 필요 없음.
184
- - **용례:** 조건부 렌더링을 삼항연산자 대신 선언적으로.
185
- ```tsx
186
- <SwitchCase
187
- value={status}
188
- cases={{
189
- loading: <Spinner />,
190
- error: <ErrorMsg />,
191
- success: <Content />,
192
- }}
193
- otherwise={<NotFound />}
194
- />
195
- ```
196
-
197
- ### `<ViewportPortal />` / `getViewportPortalRoot()`
198
- - `getViewportPortalRoot()`: SSR safe. 최초 호출 시 `position:fixed;inset:0;pointer-events:none;z-index:9999` div 생성.
199
- - `<ViewportPortal>`: `createPortal`로 자식들을 위 div에 렌더. useEffect로 root 연결 전까진 null 반환 (hydration safe).
200
- - pointer-events:none이라 portal 내부 요소는 별도로 pointer-events:auto 필요.
201
- - **용례:** 모달/툴팁/드롭다운을 DOM 트리 최상단에 렌더링 (z-index 충돌 방지, overflow:hidden 탈출).
202
- ```tsx
203
- <ViewportPortal>
204
- <div style={{ pointerEvents: "auto" }}>
205
- <Modal />
206
- </div>
207
- </ViewportPortal>
208
- ```
209
-
210
- ---
211
-
212
- ## Utils — 완전 분석
213
-
214
- ### browserStorage
215
- `getLocalStorage(key): unknown` / `updateLocalStorage(key, value): void` / `removeLocalStorage(key): void`
216
- - 동일한 API로 SessionStorage 버전도 있음: `getSessionStorage`, `updateSessionStorage`, `removeSessionStorage`.
217
- - 내부적으로 SSR 체크 후 JSON.stringify/parse. 파싱 실패, 용량 초과 등 모든 에러 조용히 처리 (try-catch).
218
- - 저장할 때 직접 `JSON.stringify`, 불러올 때 `JSON.parse`. 값이 없거나 파싱 실패 시 null.
219
- - **용례:** 사용자 설정 유지 (테마, 언어), 최근 검색어 저장.
220
- ```tsx
221
- updateLocalStorage("theme", "dark")
222
- const theme = getLocalStorage("theme") // "dark"
223
- ```
224
-
225
- ### buildContext
226
- `buildContext<T>(defaultValue): [Provider, useValue]`
227
- - `Provider`: value를 `useMemo`로 감싸 불필요한 리렌더 방지.
228
- - `useValue`: 단순 `useContext(context)`. Provider 없이 호출해도 에러 안 남 (defaultValue 반환).
229
- - createContext를 직접 쓰는 것보다 안전하고 간결.
230
- - **용례:** 전역 상태 관리가 필요할 때 Context Provider 패턴을 간단히.
231
- ```tsx
232
- const [ThemeProvider, useTheme] = buildContext("light")
233
- // <ThemeProvider value="dark">...</ThemeProvider>
234
- // const theme = useTheme() // "dark"
235
- ```
236
-
237
- ### getDeviceInfo
238
- `getDeviceInfo(): DeviceInfo`
239
- - 반환: `{ isMobile, isIOS, isAndroid, isSafari, isIOSSafari, isMacSafari, isSamsungBrowser, isTouchDevice, browser }`
240
- - SSR: window 없으면 모든 값 false. `isTouchDevice`는 `"ontouchstart" in window` 체크.
241
- - browser: samsung > safari > chrome > firefox > edge > unknown 순서로 판별.
242
- - UA로 판별하므로 User-Agent 스푸핑에 취약하지만 실제 환경에선 충분히 정확.
243
- - **용례:** 모바일 웹에서 iOS/Android 분기 처리, Safari 전용 스타일/로직.
244
-
245
- ### NavigatorClipboard / NavigatorShare
246
- `NavigatorClipboard({ text }): Promise<{ success: boolean }>` — `navigator.clipboard.writeText` 호출.
247
- `NavigatorShare({ title, text, link }): Promise<{ success: boolean, method: "share" | "clipboard" | "unsupported" }>`
248
- - `navigator.share()` 우선 시도. 사용자 취소(AbortError)면 `{ success: false, method: "share" }`.
249
- - share 실패 시 clipboard fallback. clipboard도 없으면 `{ method: "unsupported" }`.
250
- - **용례:** 공유 버튼 → 모바일 네이티브 Share Sheet, 데스크톱에선 링크 복사.
251
- ```tsx
252
- if (result.success && result.method === "share") trackShared()
253
- ```
254
-
255
- ### floatingMotion
256
- `getFloatingMotionPreset(mode): { enterMs, exitMs, ease }`
257
-
258
- | mode | enterMs | exitMs | ease |
259
- |---|---|---|---|
260
- | anchored | 230 | 170 | cubic-bezier(0.16, 1, 0.3, 1) |
261
- | center-selected | 190 | 140 | cubic-bezier(0.2, 0, 0, 1) |
262
- | modal-center | 340 | 260 | cubic-bezier(0.22, 1, 0.36, 1) |
263
- | mobile-sheet | 360 | 280 | cubic-bezier(0.2, 0.8, 0.2, 1) |
264
-
265
- `getFloatingTransformOrigin(placement?): string` — placement("bottom-left" 등)를 CSS transform-origin으로 변환. top→bottom, left→left, bottom→top, center→center. 기본값 "top center".
266
- `getFloatingHiddenTransform({ mode, placement }): string` — hidden 상태의 transform 값.
267
- - mobile-sheet: translateY(1.8rem) scale(1)
268
- - modal-center: translateY(.8rem) scale(.94)
269
- - center-selected: translateY(.45rem) scale(.99)
270
- - anchored: placement의 위/아래/좌우에 따라 translateY 또는 translateX
271
-
272
- **용례:** Floating UI(focus-trap, portal, auto-update)와 함께 사용하여 드롭다운/모달/시트의 enter/exit 애니메이션 구성. framer-motion의 initial/exit에 이 값들 전달.
273
-
274
- ### isEditableKeyboardTarget
275
- `isEditableKeyboardTarget(target: HTMLElement): boolean`
276
- - true 반환 조건: `contentEditable === "true"`, `TEXTAREA`, `SELECT`, `INPUT`(type이 hidden/file/submit/reset 등이 아닌 경우).
277
- - 키보드 이벤트 핸들러에서 `isEditableKeyboardTarget(event.target)`로 체크 → 입력 중에는 단축키 동작 막기.
278
- - **용례:** `onKeyDown`에서 방향키/Enter 이벤트를 입력 필드에선 무시.
279
-
280
- ### mergeRefs
281
- `mergeRefs<T>(...refs: (Ref<T> | undefined | null)[]): RefCallback<T>`
282
- - 함수 ref는 직접 호출, 객체 ref는 `.current` 할당. null/undefined는 안전하게 무시.
283
- - **용례:** 외부 ref와 내부 ref를 동시에 한 요소에 연결.
284
- ```tsx
285
- const internalRef = useRef(null)
286
- const externalRef = useRef(null)
287
- return <div ref={mergeRefs(internalRef, externalRef)} />
288
- ```
289
-
290
- ### seen
291
- `SEEN_STORAGE_KEY = "seen"`
292
- `parseSeen(raw: string | null): Record<string, boolean>` — JSON 파싱, 실패 시 `{}`.
293
- `hasSeenKey(raw: string | null, key: string): boolean` — 특정 키를 본 적 있는지.
294
- `buildSeenValue(raw: string | null, key: string): string` — 키를 seen에 추가한 JSON 문자열 반환.
295
- - 모두 순수 함수, SSR 안전. localStorage에 "seen" 키로 저장하는 패턴 전제.
296
- - **용례:** 공지사항/업데이트 알림 "다시 보지 않음" 체크, onboarding 진행 상태, 읽은 글 목록 관리.
297
- ```tsx
298
- const raw = getLocalStorage(SEEN_STORAGE_KEY)
299
- if (!hasSeenKey(raw, noticeId)) {
300
- updateLocalStorage(SEEN_STORAGE_KEY, buildSeenValue(raw, noticeId))
301
- showNotice()
302
- }
303
- ```
304
-
305
- ### subscribeKeyboardHeight
306
- `subscribeKeyboardHeight({ callback, immediate?, throttleMs? }): { unsubscribe }`
307
- - `visualViewport.resize`/`scroll` 구독. 높이 계산: `window.innerHeight - visualViewport.height` (0 이상 clamp).
308
- - throttleMs 기본 16ms(≈60fps). 0 이하로 설정하면 동기 실행.
309
- - `immediate: true`면 구독 즉시 callback 실행.
310
- - **용례:** `useKeyboardHeight()` 훅이 내부적으로 사용. 직접 구독이 필요한 경우 (React 외부에서 DOM 직접 조작).
311
-
312
- ---
313
-
314
- ## Cookie — 완전 분석
315
-
316
- ### cookie.shared.ts
317
- 모든 함수에 SSR guard 있음 → 서버에선 undefined/null 반환 또는 no-op.
318
-
319
- `getClientCookie(name): string | undefined` — `document.cookie` 파싱하여 value 반환. 없으면 undefined.
320
-
321
- `setClientCookie(name, value, days?): void` — `path=/`, URL 인코딩. days 생략 시 session cookie. `document.cookie = name=value; path=/; max-age=...`.
322
-
323
- `clearClientCookie(name, { hostname?, path?, documentRef?, cookieStore? }): void` — 과거 만료일로 설정하여 삭제. 모든 서브도메인, 경로 조합을 순회하며 삭제. cookieStore가 있으면 우선 사용.
324
-
325
- `clearAllClientCookies({ hostname?, path?, documentRef?, cookieStore?, includeRoot?, cookieString? }): string[]`
326
- - hostname의 서브도메인 조합을 모두 생성하여 각각의 모든 경로(현재 path + includeRoot면 "/")에 대해 쿠키 삭제 시도.
327
- - cookieStore가 있으면 `.delete()` 사용.
328
- - 삭제된 쿠키 이름 배열 반환.
329
-
330
- `parseClientCookieNames(cookieString: string): string[]` — "name=value; name2=value2" → ["name", "name2"].
331
- **용례:** 로그인 토큰 관리, 사용자 설정 쿠키, A/B 테스트 배리언트 저장.
332
-
333
- ---
334
-
335
- ## DateTime — 완전 분석
336
-
337
- ### Shared (dateTime.shared.ts) — 순수 함수, 서버 안전
338
-
339
- **타입:**
340
- - `AppLocale = "kr" | "en" | "jp"`
341
- - `DateInput = string | number | Date`
342
- - `DatePreset = "long" | "dot"`
343
- - `TimePreset = "ko" | "12h" | "24h-minute" | "24h-second"`
344
-
345
- **유틸:**
346
- - `toDate(value): Date | null` — Date 인스턴스면 복사 후 반환, 아니면 `new Date(value)`. 유효성 검사 후 null 가능.
347
- - `toUtcMidnight(date): Date` — UTC 기준 해당일 00:00:00.000Z.
348
- - `parseUtcDateInput(value?, fallback?): Date | null` — "YYYY-MM-DD" 문자열은 timezone 없이 UTC date로 파싱. 다른 형식은 toDate → toUtcMidnight.
349
- - `addUtcDays(date, days): Date` — 새 Date 반환. `date.setUTCDate(date.getUTCDate() + days)`.
350
- - `formatUtcDateKey(date): string` — "YYYY-MM-DD" UTC.
351
- - `getUtcWeekdayIndex(date): number` — 0(일)~6(토) UTC.
352
- - `normalizeAppLocale(locale?): AppLocale` — "ko"/"kr" → "kr", "ja"/"jp" → "jp", 나머지 → "en".
353
- - `toIntlLocale(locale?): string` — "ko-KR" / "ja-JP" / "en-US".
354
-
355
- **날짜 포맷:**
356
- - `formatLongDate(date, locale?, timeZone?): string` — kr: "2024년 1월 1일", jp: "2024年1月1日", en: "January 1, 2024". Intl.DateTimeFormat.formatToParts로 커스텀 포맷.
357
- - `formatDotDate(date, timeZone?): string` — "2024. 1. 1."
358
-
359
- **시간 포맷:**
360
- - `format24HourTime(date, { includeSeconds?, timeZone? }): string` — "14:30" / "14:30:00"
361
- - `formatKoreanTime(date, timeZone?): string` — "오후 2시 30분"
362
- - `formatTwelveHourTime(date, locale?, timeZone?): string` — kr: "오후 2:30", en/jp: Intl.DateTimeFormat hour12.
363
-
364
- **상대 시간:**
365
- - `formatRelativeText(diffMs, locale?): { text, isRelative }` — diffMs를 "N초 전", "N분 전", "N시간 전", "N일 전"(모든 locale)로 변환. diffMs는 0 이하로 clamp. 카운트는 최소 1.
366
- - `formatRemainingText(diffMs, locale?, { includeSuffix? }): string` — 최대 2개 단위 조합. "2일 3시간 남음". diffMs=0이면 "마감됨" / "締切終了" / "Closed".
367
-
368
- **용례 (공통 패턴):**
369
- ```tsx
370
- // UTC date 키 생성
371
- const dateKey = formatUtcDateKey(toUtcMidnight(new Date()))
372
- // 상대 시간
373
- const { text } = formatRelativeText(Date.now() - post.createdAt.getTime())
374
- ```
375
-
376
- ### Client (dateTime.client.ts) — "use client" 필요
377
-
378
- `formatClientDate(value, { locale?, timeZone?, preset }): string` — preset으로 "long"/"dot" 선택. 유효하지 않은 날짜는 "" 반환.
379
- `formatClientTime(value, { locale?, timeZone?, preset }): string` — "ko"/"12h"/"24h-minute"/"24h-second".
380
- `formatClientDateTime(value, { locale?, timeZone?, datePreset?, timePreset? }): string` — date + " " + time. 기본 datePreset "long", timePreset "24h-minute". 둘 중 하나가 빈 문자열이면 다른 쪽만 반환.
381
- `formatClientRelative(value, { locale?, now?, maxRelativeDays?, fallbackDatePreset? }): { text, isRelative }` — maxRelativeDays(기본 7) 초과 시 절대 날짜(fallbackDatePreset 기본 "dot"). 날짜 유효성 검사, 실패 시 `{ text: "", isRelative: false }`.
382
-
383
- ### Server (dateTime.server.ts) — 서버 렌더링용
384
- `formatServerDate`, `formatServerTime`, `formatServerDateTime`, `formatServerRelative` — client와 동일한 로직. 차이점: `formatServerRelative`의 `now` 기본값이 `new Date()` (client는 `Date.now()`).
385
-
386
- **datetime 선택 가이드:**
387
- - 서버 컴포넌트에서 날짜 표시 → shared 함수 사용. timezone은 서버 timezone 기준.
388
- - 클라이언트 hydration 후에도 계속 표시 → shared 함수. SSR/클라이언트 일관성 보장.
389
- - 클라이언트 로케일/timezone 반영 → client 함수. "use client" 필요.
390
- - 서버에서 렌더링된 HTML 그대로 → server 함수.
391
- - 상대 시간(실시간 갱신) → `useRelativeDateTime` 훅.
392
-
393
- **전체 datetime 함수 로직 요약:**
394
- 1. `toDate(value)`로 DateInput → Date | null 변환 (유효성 검사)
395
- 2. 선택적으로 `toUtcMidnight`로 UTC 자정 정규화
396
- 3. `normalizeAppLocale(locale)`로 AppLocale 결정
397
- 4. `Intl.DateTimeFormat.formatToParts` 또는 `Intl.DateTimeFormat.format`으로 포맷
398
- 5. 한국어/일본어는 커스텀 포맷팅 (toParts로 각 컴포넌트 추출 후 조립), 영어는 Intl 네이티브 포맷 사용
3
+ React 19 frontend utility library. Single package, ESM.
4
+
5
+ ## Import Convention Enforcement (Mandatory)
6
+
7
+ - **Barrel imports are the rule:** `import { useDebounce } from "@musecat/functionkit"`.
8
+ - **Subpath imports are only allowed when:** the export is not in the barrel (`useCheckInvisible`, `SwitchCase`, `ScrolltoTop`) or tree-shaking is absolutely required.
9
+ - **Type imports also go through the barrel:** `import type { AppLocale, DateInput } from "@musecat/functionkit"`.
10
+ - **No reimplementation:** If FunctionKit already provides a feature, reuse it. Never write a duplicate.
11
+
12
+ ## SSR Safety Classification
13
+
14
+ | Category | Condition | Examples |
15
+ |---|---|---|
16
+ | **SSR-safe** | No `"use client"`, pure function | `formatLongDate`, `mergeRefs`, `SwitchCase`, `getDeviceInfo`, `floatingMotion`, `seen`, `parseClientCookieNames`, `debounce` |
17
+ | **Client-only** | `"use client"` or `window` access | All hooks, `ViewportPortal`, `ScrolltoTop`, `browserStorage`, `cookie` (except `parseClientCookieNames`), `clipboardShare`, `isEditableKeyboardTarget` |
18
+
19
+ SSR-safe functions can be used freely in Server Components, RSC, and Client Components. Client-only items must only be used in `"use client"` components.
20
+
21
+ ## Barrel Export Mapping
22
+
23
+ The barrel (`index.ts`) exports 63 named exports. **Items NOT in the barrel (require subpath imports):**
24
+ - `SwitchCase` — component subpath
25
+ - `ScrolltoTop` component subpath
26
+ - `useCheckInvisible` `@musecat/functionkit/hooks/useCheckInvisible`
27
+ - `useCheckScroll` `@musecat/functionkit/hooks/useCheckScroll`
28
+
29
+ When adding new features, register them in the barrel.
30
+
31
+ ## Hook Selection Guidelines
32
+
33
+ | Scenario | Hook to Use |
34
+ |---|---|
35
+ | Debounce rapid state changes | `useDebounce` or `useDebouncedCallback` |
36
+ | Polling / repeated execution | `useInterval` |
37
+ | Delayed single execution | `useTimeout` |
38
+ | Keyboard list navigation | `useKeyboardListNavigation` |
39
+ | Long press gesture | `useLongPress` |
40
+ | Single/double click distinction | `useDoubleClick` |
41
+ | Geolocation tracking | `useGeolocation` |
42
+ | Client date formatting | `useClientDateTime` |
43
+ | Relative time ("3 minutes ago") | `useRelativeDateTime` |
44
+ | IntersectionObserver | `useIntersectionObserver` |
45
+ | Keyboard avoidance (mobile) | `useAvoidKeyboard` |
46
+ | Viewport height | `useViewportHeight` |
47
+ | Media query matching | `useViewportMatch` |
48
+ | Scroll position detection | `useCheckScroll` (subpath) |
49
+ | Element visibility detection | `useCheckInvisible` (subpath) |
50
+ | Toggle state | `useToggleState` |
51
+ | Hydration guard | `useHasMounted` |
52
+ | Stable callback reference | `usePreservedCallback` |
53
+ | Deep equality reference | `usePreservedReference` |
54
+ | Ref lifecycle | `useRefEffect` |
55
+
56
+ ## Component Selection Guidelines
57
+
58
+ - **SwitchCase** Prefer over inline &&/ternary for multi-branch rendering. SSR-safe.
59
+ - **ViewportPortal** Fixed-position overlays, modals, toasts. Client-only.
60
+ - **ScrolltoTop** Scroll reset on page navigation. Client-only.
61
+
62
+ Never write raw `createContext` + `Provider` + `useContext` boilerplate. Use `buildContext` instead.
63
+
64
+ ## DateTime Formatting Guidelines
65
+
66
+ | Context | Use | SSR-safe |
67
+ |---|---|---|
68
+ | Server static date | `formatServerDate` / `formatServerDateTime` | Yes |
69
+ | Client date | `formatClientDate` / `formatClientDateTime` | No |
70
+ | Relative time ("3분 전") | `formatServerRelative` (server) / `formatClientRelative` (client) | Mixed |
71
+ | Remaining time ("2일 3시간 남음") | `formatRemainingText` | Yes |
72
+ | UTC key ("YYYY-MM-DD") | `formatUtcDateKey` / `parseUtcDateInput` | Yes |
73
+ | Raw formatters | `formatLongDate`, `formatDotDate`, `formatKoreanTime` etc. | Yes |
74
+
75
+ **Locale handling:** `DateInput` accepts `string | number | Date`. Use `normalizeAppLocale("ko")` `"kr"`, `toIntlLocale("kr")` → `"ko-KR"`.
76
+
77
+ ## Cookie & Storage API Guidelines
78
+
79
+ | Operation | API | SSR-safe |
80
+ |---|---|---|
81
+ | Read cookie | `getClientCookie(name)` | No |
82
+ | Write cookie | `setClientCookie(name, value, days?)` | No |
83
+ | Delete cookie | `clearClientCookie(name)` / `clearAllClientCookies()` | No |
84
+ | Parse cookie names | `parseClientCookieNames(cookieString)` | **Yes** |
85
+ | localStorage | `getLocalStorage` / `updateLocalStorage` / `removeLocalStorage` | No |
86
+ | sessionStorage | `getSessionStorage` / `updateSessionStorage` / `removeSessionStorage` | No |
87
+
88
+ `clearAllClientCookies` tries all domain/path combinations — use with caution.
89
+
90
+ ## Context & Utility Patterns
91
+
92
+ - **`buildContext`:** Creates `createContext` + Provider + `useContext` in one call. `const [useMyCtx, MyProvider] = buildContext<MyType>()`.
93
+ - **`mergeRefs`:** Merges multiple refs into a single callback ref. Use when forwarding refs.
94
+ - **`getDeviceInfo`:** UA-based device detection. Callable at module scope. Returns `isMobile`, `isIOS`, `isAndroid`, etc.
95
+ - **`isEditableKeyboardTarget`:** Checks if a key event target is an editable element. Use before keyboard navigation.
96
+ - **`NavigatorClipboard` / `NavigatorShare`:** Clipboard copy/share with automatic fallback on failure.
97
+ - **`floatingMotion`:** `getFloatingMotionPreset` for tooltip/popover/modal animation configs. 4 modes (anchored, center-selected, modal-center, mobile-sheet).
98
+ - **`seen`:** `hasSeenKey` / `buildSeenValue` for "user has already seen" state management. SSR-safe.
99
+ - **`subscribeKeyboardHeight`:** Low-level keyboard height change detection. Used internally by `useKeyboardHeight`.
100
+
101
+ ## Documentation Maintenance
102
+
103
+ - When hooks, components, or utils are added, modified, or removed, update the corresponding `.agents/references/` documentation.
104
+ - When the barrel (`index.ts`) export list changes, update the Barrel Export Mapping section above.
105
+ - Before using any feature, read the corresponding `.agents/references/` doc first.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @musecat/functionkit
2
2
 
3
- React 19 기반 프론트엔드 유틸리티 라이브러리입니다.
3
+ A React 19 + TypeScript UI F.E. utility library.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,16 +10,29 @@ npm install @musecat/functionkit
10
10
 
11
11
  ## Usage
12
12
 
13
- 모든 훅, 컴포넌트, 유틸리티는 배럴 파일을 통해 직접 임포트하여 사용할 수 있습니다.
14
-
15
13
  ```tsx
16
14
  import { useDebounce, formatClientDateTime } from "@musecat/functionkit";
15
+
16
+ export default function Example() {
17
+ const handleSearch = useDebounce((term: string) => fetchResults(term), 300);
18
+ const now = formatClientDateTime(new Date(), {
19
+ datePreset: "long",
20
+ timePreset: "24h-minute",
21
+ });
22
+
23
+ return (
24
+ <View column gap={24}>
25
+ <Input onChange={(e) => handleSearch(e.target.value)} />
26
+ <Text type="Caption1">{now}</Text>
27
+ </View>
28
+ );
29
+ }
17
30
  ```
18
31
 
19
32
  ## Acknowledgements
20
33
 
21
- 프로젝트의 많은 유틸리티 및 훅 코드는 [toss/react-simplikit](https://github.com/toss/react-simplikit/) 프로젝트를 참조 및 영감을 받아 작성되었습니다.
34
+ - [toss/react-simplikit](https://github.com/toss/react-simplikit/)
22
35
 
23
36
  ## License
24
37
 
25
- MIT License © Musecat Team. 여기에서 [라이센스](./LICENSE) 정보 확인하기
38
+ [MIT License](./LICENSE) © Musecat Team.