@sonhoseong/mfa-lib 1.3.7 → 1.3.10

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 (162) hide show
  1. package/dist/components/button/ScrollTopButton.js +5 -3
  2. package/dist/components/error/ErrorBoundary.js +14 -4
  3. package/dist/components/error/NotFound.d.ts +20 -0
  4. package/dist/components/error/NotFound.d.ts.map +1 -0
  5. package/dist/components/error/NotFound.js +84 -0
  6. package/dist/components/error/index.d.ts +2 -0
  7. package/dist/components/error/index.d.ts.map +1 -1
  8. package/dist/components/error/index.js +1 -0
  9. package/dist/components/icons/Icons.d.ts +51 -0
  10. package/dist/components/icons/Icons.d.ts.map +1 -0
  11. package/dist/components/icons/Icons.js +100 -0
  12. package/dist/components/icons/index.d.ts +5 -0
  13. package/dist/components/icons/index.d.ts.map +1 -0
  14. package/dist/components/icons/index.js +4 -0
  15. package/dist/components/index.d.ts +3 -0
  16. package/dist/components/index.d.ts.map +1 -1
  17. package/dist/components/index.js +6 -0
  18. package/dist/components/layout/Container.js +7 -2
  19. package/dist/components/loading/DeferredComponent.d.ts +19 -0
  20. package/dist/components/loading/DeferredComponent.d.ts.map +1 -0
  21. package/dist/components/loading/DeferredComponent.js +32 -0
  22. package/dist/components/loading/GlobalLoading.js +14 -3
  23. package/dist/components/loading/index.d.ts +1 -0
  24. package/dist/components/loading/index.d.ts.map +1 -1
  25. package/dist/components/loading/index.js +1 -0
  26. package/dist/components/logo/Logo.d.ts +2 -0
  27. package/dist/components/logo/Logo.d.ts.map +1 -1
  28. package/dist/components/logo/Logo.js +13 -4
  29. package/dist/components/modal/ModalContainer.js +17 -8
  30. package/dist/components/modal/ModalContext.js +2 -3
  31. package/dist/components/navigation/AppNavbar.js +21 -9
  32. package/dist/components/navigation/AppSidebar.css +58 -3
  33. package/dist/components/navigation/AppSidebar.d.ts +1 -1
  34. package/dist/components/navigation/AppSidebar.d.ts.map +1 -1
  35. package/dist/components/navigation/AppSidebar.js +58 -15
  36. package/dist/components/navigation/Footer.d.ts +15 -0
  37. package/dist/components/navigation/Footer.d.ts.map +1 -0
  38. package/dist/components/navigation/Footer.js +12 -0
  39. package/dist/components/navigation/Header.d.ts.map +1 -1
  40. package/dist/components/navigation/Header.js +17 -4
  41. package/dist/components/navigation/Lnb.d.ts +2 -7
  42. package/dist/components/navigation/Lnb.d.ts.map +1 -1
  43. package/dist/components/navigation/Lnb.js +34 -6
  44. package/dist/components/navigation/StickyNav.js +19 -11
  45. package/dist/components/navigation/index.d.ts +1 -0
  46. package/dist/components/navigation/index.d.ts.map +1 -1
  47. package/dist/components/navigation/index.js +1 -0
  48. package/dist/components/page/LoginPage.d.ts +4 -1
  49. package/dist/components/page/LoginPage.d.ts.map +1 -1
  50. package/dist/components/page/LoginPage.js +146 -21
  51. package/dist/components/remote/RemoteErrorBoundary.d.ts +28 -0
  52. package/dist/components/remote/RemoteErrorBoundary.d.ts.map +1 -0
  53. package/dist/components/remote/RemoteErrorBoundary.js +44 -0
  54. package/dist/components/remote/RemoteErrorFallback.d.ts +16 -0
  55. package/dist/components/remote/RemoteErrorFallback.d.ts.map +1 -0
  56. package/dist/components/remote/RemoteErrorFallback.js +76 -0
  57. package/dist/components/remote/index.d.ts +8 -0
  58. package/dist/components/remote/index.d.ts.map +1 -0
  59. package/dist/components/remote/index.js +5 -0
  60. package/dist/components/router/BrowserRouter.d.ts +13 -0
  61. package/dist/components/router/BrowserRouter.d.ts.map +1 -0
  62. package/dist/components/router/BrowserRouter.js +17 -0
  63. package/dist/components/router/RouteGuard.d.ts +79 -0
  64. package/dist/components/router/RouteGuard.d.ts.map +1 -0
  65. package/dist/components/router/RouteGuard.js +86 -0
  66. package/dist/components/router/index.d.ts +4 -0
  67. package/dist/components/router/index.d.ts.map +1 -0
  68. package/dist/components/router/index.js +2 -0
  69. package/dist/components/toast/ToastContainer.js +17 -6
  70. package/dist/components/toast/ToastContext.js +2 -3
  71. package/dist/hooks/index.d.ts +9 -1
  72. package/dist/hooks/index.d.ts.map +1 -1
  73. package/dist/hooks/index.js +15 -1
  74. package/dist/hooks/use-auth.d.ts +2 -1
  75. package/dist/hooks/use-auth.d.ts.map +1 -1
  76. package/dist/hooks/use-auth.js +19 -18
  77. package/dist/hooks/use-debounce.d.ts +56 -0
  78. package/dist/hooks/use-debounce.d.ts.map +1 -0
  79. package/dist/hooks/use-debounce.js +140 -0
  80. package/dist/hooks/use-effect-once.d.ts +77 -0
  81. package/dist/hooks/use-effect-once.d.ts.map +1 -0
  82. package/dist/hooks/use-effect-once.js +124 -0
  83. package/dist/hooks/use-error-notification.d.ts +1 -1
  84. package/dist/hooks/use-error-notification.js +1 -1
  85. package/dist/hooks/use-global-loading.d.ts +1 -1
  86. package/dist/hooks/use-global-loading.js +1 -1
  87. package/dist/hooks/use-initialize.d.ts +8 -1
  88. package/dist/hooks/use-initialize.d.ts.map +1 -1
  89. package/dist/hooks/use-initialize.js +126 -23
  90. package/dist/hooks/use-modal.d.ts +21 -5
  91. package/dist/hooks/use-modal.d.ts.map +1 -1
  92. package/dist/hooks/use-modal.js +57 -17
  93. package/dist/hooks/use-navigate.d.ts +1 -1
  94. package/dist/hooks/use-navigate.js +1 -1
  95. package/dist/hooks/use-network-status.d.ts +15 -0
  96. package/dist/hooks/use-network-status.d.ts.map +1 -0
  97. package/dist/hooks/use-network-status.js +49 -0
  98. package/dist/hooks/use-permission.d.ts +22 -0
  99. package/dist/hooks/use-permission.d.ts.map +1 -0
  100. package/dist/hooks/use-permission.js +73 -0
  101. package/dist/hooks/use-recent-menu.d.ts +46 -0
  102. package/dist/hooks/use-recent-menu.d.ts.map +1 -0
  103. package/dist/hooks/use-recent-menu.js +169 -0
  104. package/dist/hooks/use-scroll-restoration.d.ts +51 -0
  105. package/dist/hooks/use-scroll-restoration.d.ts.map +1 -0
  106. package/dist/hooks/use-scroll-restoration.js +143 -0
  107. package/dist/hooks/use-supabase-auth.d.ts +49 -0
  108. package/dist/hooks/use-supabase-auth.d.ts.map +1 -0
  109. package/dist/hooks/use-supabase-auth.js +229 -0
  110. package/dist/hooks/use-track-history.d.ts +2 -1
  111. package/dist/hooks/use-track-history.d.ts.map +1 -1
  112. package/dist/hooks/use-track-history.js +14 -2
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.js +1 -1
  115. package/dist/network/axios-factory.d.ts +30 -1
  116. package/dist/network/axios-factory.d.ts.map +1 -1
  117. package/dist/network/axios-factory.js +192 -24
  118. package/dist/network/index.d.ts +3 -1
  119. package/dist/network/index.d.ts.map +1 -1
  120. package/dist/network/index.js +5 -1
  121. package/dist/network/supabase-client.d.ts +28 -0
  122. package/dist/network/supabase-client.d.ts.map +1 -0
  123. package/dist/network/supabase-client.js +46 -0
  124. package/dist/store/app-store.d.ts +222 -12
  125. package/dist/store/app-store.d.ts.map +1 -1
  126. package/dist/store/app-store.js +46 -29
  127. package/dist/store/index.d.ts +2 -0
  128. package/dist/store/index.d.ts.map +1 -1
  129. package/dist/store/index.js +3 -0
  130. package/dist/store/menu-slice.d.ts +96 -0
  131. package/dist/store/menu-slice.d.ts.map +1 -0
  132. package/dist/store/menu-slice.js +98 -0
  133. package/dist/store/recent-menu-slice.d.ts +209 -0
  134. package/dist/store/recent-menu-slice.d.ts.map +1 -0
  135. package/dist/store/recent-menu-slice.js +110 -0
  136. package/dist/store/store-access.d.ts +1 -1
  137. package/dist/store/store-access.js +1 -1
  138. package/dist/types/index.d.ts +74 -17
  139. package/dist/types/index.d.ts.map +1 -1
  140. package/dist/types/service.d.ts +1 -1
  141. package/dist/types/service.js +1 -1
  142. package/dist/utils/classnames.d.ts +65 -0
  143. package/dist/utils/classnames.d.ts.map +1 -0
  144. package/dist/utils/classnames.js +98 -0
  145. package/dist/utils/formatter.d.ts +78 -0
  146. package/dist/utils/formatter.d.ts.map +1 -0
  147. package/dist/utils/formatter.js +216 -0
  148. package/dist/utils/index.d.ts +5 -0
  149. package/dist/utils/index.d.ts.map +1 -1
  150. package/dist/utils/index.js +5 -0
  151. package/dist/utils/permission.d.ts +33 -0
  152. package/dist/utils/permission.d.ts.map +1 -0
  153. package/dist/utils/permission.js +132 -0
  154. package/dist/utils/query-string.d.ts +67 -0
  155. package/dist/utils/query-string.d.ts.map +1 -0
  156. package/dist/utils/query-string.js +136 -0
  157. package/dist/utils/storage.d.ts +1 -1
  158. package/dist/utils/storage.js +1 -1
  159. package/dist/utils/validation.d.ts +98 -0
  160. package/dist/utils/validation.d.ts.map +1 -0
  161. package/dist/utils/validation.js +260 -0
  162. package/package.json +5 -3
@@ -0,0 +1,169 @@
1
+ /**
2
+ * useRecentMenu Hook
3
+ * 최근 방문 메뉴 상태 저장/복구
4
+ */
5
+ import { useCallback, useEffect, useRef } from 'react';
6
+ import { useDispatch, useSelector } from 'react-redux';
7
+ import { useLocation, useNavigate } from 'react-router-dom';
8
+ import { addRecentMenu, removeRecentMenu, setCurrentRecentMenu, updateRecentMenuState, clearRecentMenu, closeOtherMenus, setRecentMenuList, selectRecentMenuList, selectCurrentRecentMenu, selectCurrentRecentMenuId, } from '../store/recent-menu-slice';
9
+ import { storage } from '../utils/storage';
10
+ /**
11
+ * useRecentMenu Hook
12
+ */
13
+ export function useRecentMenu(options = {}) {
14
+ const { autoTrack = false, excludePaths = ['/login', '/error'], getTitleFromPath = (pathname) => pathname.split('/').pop() || 'Home', } = options;
15
+ const dispatch = useDispatch();
16
+ const location = useLocation();
17
+ const navigate = useNavigate();
18
+ const recentMenuList = useSelector(selectRecentMenuList);
19
+ const currentMenu = useSelector(selectCurrentRecentMenu);
20
+ const currentMenuId = useSelector(selectCurrentRecentMenuId);
21
+ // 이전 위치 저장 (상태 저장용)
22
+ const prevLocationRef = useRef(location);
23
+ /**
24
+ * 초기화: localStorage에서 복구
25
+ */
26
+ useEffect(() => {
27
+ const saved = storage.getRecentMenu();
28
+ if (saved && saved.length > 0) {
29
+ dispatch(setRecentMenuList(saved));
30
+ }
31
+ }, [dispatch]);
32
+ /**
33
+ * 자동 추적 모드: 경로 변경 시 자동으로 추가
34
+ */
35
+ useEffect(() => {
36
+ if (!autoTrack)
37
+ return;
38
+ // 제외 경로 체크
39
+ const isExcluded = excludePaths.some((pattern) => {
40
+ if (typeof pattern === 'string') {
41
+ return location.pathname === pattern || location.pathname.startsWith(pattern);
42
+ }
43
+ return pattern.test(location.pathname);
44
+ });
45
+ if (isExcluded)
46
+ return;
47
+ // 메뉴 ID 생성 (pathname + search 기반)
48
+ const menuId = `${location.pathname}${location.search}`;
49
+ // 현재 경로를 최근 메뉴에 추가
50
+ dispatch(addRecentMenu({
51
+ id: menuId,
52
+ pathname: location.pathname,
53
+ search: location.search,
54
+ title: getTitleFromPath(location.pathname),
55
+ }));
56
+ }, [autoTrack, location.pathname, location.search, dispatch, excludePaths, getTitleFromPath]);
57
+ /**
58
+ * 이전 페이지 상태 저장 (페이지 이탈 시)
59
+ */
60
+ useEffect(() => {
61
+ const prevLocation = prevLocationRef.current;
62
+ if (prevLocation.pathname !== location.pathname) {
63
+ // 이전 페이지의 스크롤 위치 저장
64
+ const prevMenuId = `${prevLocation.pathname}${prevLocation.search}`;
65
+ dispatch(updateRecentMenuState({
66
+ id: prevMenuId,
67
+ state: {
68
+ scrollY: window.scrollY,
69
+ scrollX: window.scrollX,
70
+ },
71
+ }));
72
+ }
73
+ prevLocationRef.current = location;
74
+ }, [location, dispatch]);
75
+ /**
76
+ * 메뉴 추가
77
+ */
78
+ const add = useCallback((menu) => {
79
+ const id = menu.id || `${menu.pathname}${menu.search || ''}`;
80
+ dispatch(addRecentMenu({ ...menu, id }));
81
+ }, [dispatch]);
82
+ /**
83
+ * 메뉴 제거
84
+ */
85
+ const remove = useCallback((id) => {
86
+ dispatch(removeRecentMenu(id));
87
+ }, [dispatch]);
88
+ /**
89
+ * 현재 메뉴 변경
90
+ */
91
+ const setCurrent = useCallback((id) => {
92
+ dispatch(setCurrentRecentMenu(id));
93
+ }, [dispatch]);
94
+ /**
95
+ * 메뉴로 이동
96
+ */
97
+ const goTo = useCallback((id) => {
98
+ const menu = recentMenuList.find((m) => m.id === id);
99
+ if (menu) {
100
+ dispatch(setCurrentRecentMenu(id));
101
+ navigate(`${menu.pathname}${menu.search || ''}`);
102
+ // 저장된 스크롤 위치 복구
103
+ if (menu.state?.scrollY !== undefined) {
104
+ setTimeout(() => {
105
+ window.scrollTo(menu.state.scrollX || 0, menu.state.scrollY);
106
+ }, 100);
107
+ }
108
+ }
109
+ }, [recentMenuList, dispatch, navigate]);
110
+ /**
111
+ * 메뉴 상태 업데이트 (검색 조건, 선택된 항목 등)
112
+ */
113
+ const updateState = useCallback((id, state) => {
114
+ dispatch(updateRecentMenuState({ id, state }));
115
+ }, [dispatch]);
116
+ /**
117
+ * 메뉴 데이터 업데이트
118
+ */
119
+ const updateData = useCallback((id, data) => {
120
+ dispatch(updateRecentMenuState({ id, data }));
121
+ }, [dispatch]);
122
+ /**
123
+ * 현재 메뉴 상태 업데이트
124
+ */
125
+ const updateCurrentState = useCallback((state) => {
126
+ if (currentMenuId) {
127
+ dispatch(updateRecentMenuState({ id: currentMenuId, state }));
128
+ }
129
+ }, [currentMenuId, dispatch]);
130
+ /**
131
+ * 모든 메뉴 닫기
132
+ */
133
+ const closeAll = useCallback(() => {
134
+ dispatch(clearRecentMenu());
135
+ }, [dispatch]);
136
+ /**
137
+ * 다른 메뉴 모두 닫기
138
+ */
139
+ const closeOthers = useCallback(() => {
140
+ dispatch(closeOtherMenus());
141
+ }, [dispatch]);
142
+ return {
143
+ /** 최근 메뉴 목록 */
144
+ list: recentMenuList,
145
+ /** 현재 활성 메뉴 */
146
+ current: currentMenu,
147
+ /** 현재 활성 메뉴 ID */
148
+ currentId: currentMenuId,
149
+ /** 메뉴 추가 */
150
+ add,
151
+ /** 메뉴 제거 */
152
+ remove,
153
+ /** 현재 메뉴 설정 */
154
+ setCurrent,
155
+ /** 메뉴로 이동 */
156
+ goTo,
157
+ /** 메뉴 상태 업데이트 */
158
+ updateState,
159
+ /** 메뉴 데이터 업데이트 */
160
+ updateData,
161
+ /** 현재 메뉴 상태 업데이트 */
162
+ updateCurrentState,
163
+ /** 모든 메뉴 닫기 */
164
+ closeAll,
165
+ /** 다른 메뉴 모두 닫기 */
166
+ closeOthers,
167
+ };
168
+ }
169
+ export default useRecentMenu;
@@ -0,0 +1,51 @@
1
+ export interface ScrollRestorationOptions {
2
+ /** 스크롤 복원 활성화 여부 */
3
+ enabled?: boolean;
4
+ /** 스크롤 복원 동작 */
5
+ behavior?: ScrollBehavior;
6
+ /** 새 페이지 진입 시 상단으로 스크롤 */
7
+ scrollToTopOnNewPage?: boolean;
8
+ /** 스크롤 복원 제외 경로 */
9
+ excludePaths?: string[];
10
+ /** 스크롤 대상 요소 (기본: window) */
11
+ scrollElement?: HTMLElement | null;
12
+ }
13
+ /**
14
+ * useScrollRestoration
15
+ * 페이지 이동 시 스크롤 위치를 자동으로 저장하고 복원
16
+ *
17
+ * @example
18
+ * function App() {
19
+ * useScrollRestoration();
20
+ * return <Routes>...</Routes>;
21
+ * }
22
+ *
23
+ * @example
24
+ * // 커스텀 설정
25
+ * useScrollRestoration({
26
+ * behavior: 'smooth',
27
+ * excludePaths: ['/modal'],
28
+ * scrollToTopOnNewPage: true,
29
+ * });
30
+ */
31
+ export declare function useScrollRestoration(options?: ScrollRestorationOptions): {
32
+ saveScrollPosition: () => void;
33
+ restoreScrollPosition: () => boolean;
34
+ scrollToTop: () => void;
35
+ clearScrollPositions: () => void;
36
+ };
37
+ /**
38
+ * useScrollToTop
39
+ * 페이지 이동 시 항상 상단으로 스크롤
40
+ *
41
+ * @example
42
+ * function App() {
43
+ * useScrollToTop();
44
+ * return <Routes>...</Routes>;
45
+ * }
46
+ */
47
+ export declare function useScrollToTop(options?: {
48
+ behavior?: ScrollBehavior;
49
+ }): void;
50
+ export default useScrollRestoration;
51
+ //# sourceMappingURL=use-scroll-restoration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-scroll-restoration.d.ts","sourceRoot":"","sources":["../../src/hooks/use-scroll-restoration.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,wBAAwB;IACvC,oBAAoB;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,gBAAgB;IAChB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,0BAA0B;IAC1B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,mBAAmB;IACnB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,6BAA6B;IAC7B,aAAa,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,wBAA6B;;;;;EAuH1E;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAO,QAOzE;AAED,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * 스크롤 복원 Hook
3
+ * 페이지 이동 시 스크롤 위치를 저장하고 복원
4
+ */
5
+ import { useEffect, useRef, useCallback } from 'react';
6
+ import { useLocation } from 'react-router-dom';
7
+ // 스크롤 위치 저장소
8
+ const scrollPositions = new Map();
9
+ /**
10
+ * useScrollRestoration
11
+ * 페이지 이동 시 스크롤 위치를 자동으로 저장하고 복원
12
+ *
13
+ * @example
14
+ * function App() {
15
+ * useScrollRestoration();
16
+ * return <Routes>...</Routes>;
17
+ * }
18
+ *
19
+ * @example
20
+ * // 커스텀 설정
21
+ * useScrollRestoration({
22
+ * behavior: 'smooth',
23
+ * excludePaths: ['/modal'],
24
+ * scrollToTopOnNewPage: true,
25
+ * });
26
+ */
27
+ export function useScrollRestoration(options = {}) {
28
+ const { enabled = true, behavior = 'auto', scrollToTopOnNewPage = true, excludePaths = [], scrollElement = null, } = options;
29
+ const location = useLocation();
30
+ const prevPathRef = useRef(null);
31
+ const isRestoringRef = useRef(false);
32
+ // 스크롤 위치 가져오기
33
+ const getScrollPosition = useCallback(() => {
34
+ if (scrollElement) {
35
+ return scrollElement.scrollTop;
36
+ }
37
+ return window.scrollY || document.documentElement.scrollTop;
38
+ }, [scrollElement]);
39
+ // 스크롤 위치 설정
40
+ const setScrollPosition = useCallback((position) => {
41
+ if (scrollElement) {
42
+ scrollElement.scrollTo({ top: position, behavior });
43
+ }
44
+ else {
45
+ window.scrollTo({ top: position, behavior });
46
+ }
47
+ }, [scrollElement, behavior]);
48
+ // 현재 경로의 스크롤 위치 저장
49
+ const saveScrollPosition = useCallback(() => {
50
+ const path = location.pathname + location.search;
51
+ const position = getScrollPosition();
52
+ scrollPositions.set(path, position);
53
+ }, [location.pathname, location.search, getScrollPosition]);
54
+ // 스크롤 위치 복원
55
+ const restoreScrollPosition = useCallback(() => {
56
+ const path = location.pathname + location.search;
57
+ const savedPosition = scrollPositions.get(path);
58
+ if (savedPosition !== undefined) {
59
+ // 약간의 딜레이를 두고 복원 (DOM 렌더링 대기)
60
+ requestAnimationFrame(() => {
61
+ isRestoringRef.current = true;
62
+ setScrollPosition(savedPosition);
63
+ setTimeout(() => {
64
+ isRestoringRef.current = false;
65
+ }, 100);
66
+ });
67
+ return true;
68
+ }
69
+ return false;
70
+ }, [location.pathname, location.search, setScrollPosition]);
71
+ // 상단으로 스크롤
72
+ const scrollToTop = useCallback(() => {
73
+ setScrollPosition(0);
74
+ }, [setScrollPosition]);
75
+ // 페이지 이동 감지 및 스크롤 처리
76
+ useEffect(() => {
77
+ if (!enabled)
78
+ return;
79
+ const currentPath = location.pathname + location.search;
80
+ const prevPath = prevPathRef.current;
81
+ // 제외 경로인 경우 무시
82
+ if (excludePaths.some(path => location.pathname.startsWith(path))) {
83
+ prevPathRef.current = currentPath;
84
+ return;
85
+ }
86
+ // 이전 페이지 스크롤 위치 저장
87
+ if (prevPath && prevPath !== currentPath) {
88
+ const prevPosition = getScrollPosition();
89
+ scrollPositions.set(prevPath, prevPosition);
90
+ }
91
+ // 스크롤 복원 또는 상단 이동
92
+ const restored = restoreScrollPosition();
93
+ if (!restored && scrollToTopOnNewPage && prevPath !== currentPath) {
94
+ scrollToTop();
95
+ }
96
+ prevPathRef.current = currentPath;
97
+ }, [
98
+ enabled,
99
+ location.pathname,
100
+ location.search,
101
+ excludePaths,
102
+ getScrollPosition,
103
+ restoreScrollPosition,
104
+ scrollToTop,
105
+ scrollToTopOnNewPage,
106
+ ]);
107
+ // 페이지 이탈 시 스크롤 위치 저장
108
+ useEffect(() => {
109
+ if (!enabled)
110
+ return;
111
+ const handleBeforeUnload = () => {
112
+ saveScrollPosition();
113
+ };
114
+ window.addEventListener('beforeunload', handleBeforeUnload);
115
+ return () => {
116
+ window.removeEventListener('beforeunload', handleBeforeUnload);
117
+ };
118
+ }, [enabled, saveScrollPosition]);
119
+ return {
120
+ saveScrollPosition,
121
+ restoreScrollPosition,
122
+ scrollToTop,
123
+ clearScrollPositions: () => scrollPositions.clear(),
124
+ };
125
+ }
126
+ /**
127
+ * useScrollToTop
128
+ * 페이지 이동 시 항상 상단으로 스크롤
129
+ *
130
+ * @example
131
+ * function App() {
132
+ * useScrollToTop();
133
+ * return <Routes>...</Routes>;
134
+ * }
135
+ */
136
+ export function useScrollToTop(options = {}) {
137
+ const { behavior = 'auto' } = options;
138
+ const location = useLocation();
139
+ useEffect(() => {
140
+ window.scrollTo({ top: 0, behavior });
141
+ }, [location.pathname, behavior]);
142
+ }
143
+ export default useScrollRestoration;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Supabase Auth Hooks
3
+ * Supabase 인증을 위한 전용 hooks
4
+ */
5
+ import { Session } from '@supabase/supabase-js';
6
+ import { User } from '../types';
7
+ /**
8
+ * Supabase 로그인 Hook
9
+ */
10
+ export declare function useSupabaseLogin(): {
11
+ login: (email: string, password: string) => Promise<{
12
+ session: Session;
13
+ user: User;
14
+ }>;
15
+ isLoading: boolean;
16
+ error: string | null;
17
+ };
18
+ /**
19
+ * Supabase 로그아웃 Hook
20
+ */
21
+ export declare function useSupabaseLogout(): {
22
+ logout: () => Promise<void>;
23
+ isLoading: boolean;
24
+ };
25
+ /**
26
+ * Supabase 세션 Hook
27
+ * 현재 세션 상태를 반환하고 변경을 구독
28
+ */
29
+ export declare function useSupabaseSession(): {
30
+ session: Session | null;
31
+ user: User | null;
32
+ isLoading: boolean;
33
+ isAuthenticated: boolean;
34
+ };
35
+ /**
36
+ * Supabase Auth 상태 변경 구독 Hook
37
+ * Redux store와 동기화
38
+ */
39
+ export declare function useSupabaseAuthSync(): void;
40
+ /**
41
+ * 토큰 만료 전 선제 갱신 Hook
42
+ * KOMCA 스타일: 만료 5분 전 자동으로 토큰 갱신
43
+ * @param refreshBeforeMinutes 만료 전 갱신 시점 (분), 기본값 5분
44
+ */
45
+ export declare function useTokenAutoRefresh(refreshBeforeMinutes?: number): {
46
+ isRefreshing: boolean;
47
+ lastRefreshed: Date | null;
48
+ };
49
+ //# sourceMappingURL=use-supabase-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-supabase-auth.d.ts","sourceRoot":"","sources":["../../src/hooks/use-supabase-auth.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,OAAO,EAAmB,MAAM,uBAAuB,CAAC;AAIjE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAehC;;GAEG;AACH,wBAAgB,gBAAgB;mBAIU,MAAM,YAAY,MAAM;;;;;;EA4CjE;AAED;;GAEG;AACH,wBAAgB,iBAAiB;;;EAiChC;AAED;;;GAGG;AACH,wBAAgB,kBAAkB;;;;;EA6BjC;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,SAwBlC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,oBAAoB,GAAE,MAAU;;;EA6FnE"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Supabase Auth Hooks
3
+ * Supabase 인증을 위한 전용 hooks
4
+ */
5
+ import { useCallback, useEffect, useState } from 'react';
6
+ import { getSupabase } from '../network/supabase-client';
7
+ import { getStore, setAccessToken, setUser, logout } from '../store/app-store';
8
+ import { storage } from '../utils/storage';
9
+ /**
10
+ * Supabase User를 앱 User 타입으로 변환
11
+ */
12
+ function mapSupabaseUser(supabaseUser) {
13
+ return {
14
+ id: supabaseUser.id,
15
+ email: supabaseUser.email || '',
16
+ name: supabaseUser.user_metadata?.name || supabaseUser.email?.split('@')[0] || '',
17
+ role: supabaseUser.user_metadata?.role || 'user',
18
+ avatar: supabaseUser.user_metadata?.avatar_url,
19
+ };
20
+ }
21
+ /**
22
+ * Supabase 로그인 Hook
23
+ */
24
+ export function useSupabaseLogin() {
25
+ const [isLoading, setIsLoading] = useState(false);
26
+ const [error, setError] = useState(null);
27
+ const login = useCallback(async (email, password) => {
28
+ setIsLoading(true);
29
+ setError(null);
30
+ try {
31
+ const supabase = getSupabase();
32
+ const store = getStore();
33
+ const { data, error: authError } = await supabase.auth.signInWithPassword({
34
+ email,
35
+ password,
36
+ });
37
+ if (authError) {
38
+ throw new Error(authError.message);
39
+ }
40
+ if (!data.session || !data.user) {
41
+ throw new Error('로그인 응답이 올바르지 않습니다.');
42
+ }
43
+ const user = mapSupabaseUser(data.user);
44
+ // Redux store 업데이트
45
+ store.dispatch(setAccessToken(data.session.access_token));
46
+ store.dispatch(setUser(user));
47
+ // Storage에도 저장 (backup)
48
+ storage.setAccessToken(data.session.access_token);
49
+ storage.setUser(user);
50
+ console.log('[Supabase Login] 로그인 성공:', user.email);
51
+ return { session: data.session, user };
52
+ }
53
+ catch (err) {
54
+ const message = err.message || '로그인에 실패했습니다.';
55
+ setError(message);
56
+ console.error('[Supabase Login] 로그인 실패:', message);
57
+ throw err;
58
+ }
59
+ finally {
60
+ setIsLoading(false);
61
+ }
62
+ }, []);
63
+ return { login, isLoading, error };
64
+ }
65
+ /**
66
+ * Supabase 로그아웃 Hook
67
+ */
68
+ export function useSupabaseLogout() {
69
+ const [isLoading, setIsLoading] = useState(false);
70
+ const doLogout = useCallback(async () => {
71
+ setIsLoading(true);
72
+ try {
73
+ const supabase = getSupabase();
74
+ const store = getStore();
75
+ const { error } = await supabase.auth.signOut();
76
+ if (error) {
77
+ console.warn('[Supabase Logout] 서버 로그아웃 실패:', error.message);
78
+ }
79
+ // Redux store 초기화
80
+ store.dispatch(logout());
81
+ store.dispatch({ type: 'recentMenu/resetRecentMenu' });
82
+ // Storage 초기화
83
+ storage.clearAuth();
84
+ console.log('[Supabase Logout] 로그아웃 완료');
85
+ }
86
+ catch (err) {
87
+ console.error('[Supabase Logout] 로그아웃 실패:', err);
88
+ storage.clearAuth();
89
+ }
90
+ finally {
91
+ setIsLoading(false);
92
+ }
93
+ }, []);
94
+ return { logout: doLogout, isLoading };
95
+ }
96
+ /**
97
+ * Supabase 세션 Hook
98
+ * 현재 세션 상태를 반환하고 변경을 구독
99
+ */
100
+ export function useSupabaseSession() {
101
+ const [session, setSession] = useState(null);
102
+ const [isLoading, setIsLoading] = useState(true);
103
+ useEffect(() => {
104
+ const supabase = getSupabase();
105
+ // 현재 세션 가져오기
106
+ supabase.auth.getSession().then(({ data: { session } }) => {
107
+ setSession(session);
108
+ setIsLoading(false);
109
+ });
110
+ // 세션 변경 구독
111
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
112
+ setSession(session);
113
+ });
114
+ return () => subscription.unsubscribe();
115
+ }, []);
116
+ return {
117
+ session,
118
+ user: session?.user ? mapSupabaseUser(session.user) : null,
119
+ isLoading,
120
+ isAuthenticated: !!session,
121
+ };
122
+ }
123
+ /**
124
+ * Supabase Auth 상태 변경 구독 Hook
125
+ * Redux store와 동기화
126
+ */
127
+ export function useSupabaseAuthSync() {
128
+ useEffect(() => {
129
+ const supabase = getSupabase();
130
+ const store = getStore();
131
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
132
+ console.log('[Supabase Auth] 상태 변경:', event);
133
+ if (session) {
134
+ const user = mapSupabaseUser(session.user);
135
+ store.dispatch(setAccessToken(session.access_token));
136
+ store.dispatch(setUser(user));
137
+ storage.setAccessToken(session.access_token);
138
+ storage.setUser(user);
139
+ }
140
+ else {
141
+ store.dispatch(logout());
142
+ storage.clearAuth();
143
+ }
144
+ });
145
+ return () => subscription.unsubscribe();
146
+ }, []);
147
+ }
148
+ /**
149
+ * 토큰 만료 전 선제 갱신 Hook
150
+ * KOMCA 스타일: 만료 5분 전 자동으로 토큰 갱신
151
+ * @param refreshBeforeMinutes 만료 전 갱신 시점 (분), 기본값 5분
152
+ */
153
+ export function useTokenAutoRefresh(refreshBeforeMinutes = 5) {
154
+ const [isRefreshing, setIsRefreshing] = useState(false);
155
+ const [lastRefreshed, setLastRefreshed] = useState(null);
156
+ useEffect(() => {
157
+ const supabase = getSupabase();
158
+ const store = getStore();
159
+ let timeoutId = null;
160
+ const scheduleRefresh = async () => {
161
+ try {
162
+ const { data: { session } } = await supabase.auth.getSession();
163
+ if (!session?.expires_at) {
164
+ return;
165
+ }
166
+ const expiresAt = session.expires_at * 1000; // seconds to ms
167
+ const refreshTime = expiresAt - (refreshBeforeMinutes * 60 * 1000);
168
+ const now = Date.now();
169
+ const timeout = refreshTime - now;
170
+ // 이미 만료 시점이 지났으면 즉시 갱신
171
+ if (timeout <= 0) {
172
+ await performRefresh();
173
+ return;
174
+ }
175
+ // 만료 전 갱신 스케줄링
176
+ console.log(`[Token Auto Refresh] ${Math.round(timeout / 1000 / 60)}분 후 토큰 갱신 예정`);
177
+ timeoutId = setTimeout(async () => {
178
+ await performRefresh();
179
+ }, timeout);
180
+ }
181
+ catch (err) {
182
+ console.error('[Token Auto Refresh] 스케줄링 실패:', err);
183
+ }
184
+ };
185
+ const performRefresh = async () => {
186
+ if (isRefreshing)
187
+ return;
188
+ setIsRefreshing(true);
189
+ try {
190
+ const { data, error } = await supabase.auth.refreshSession();
191
+ if (error) {
192
+ throw error;
193
+ }
194
+ if (data.session) {
195
+ const user = mapSupabaseUser(data.session.user);
196
+ store.dispatch(setAccessToken(data.session.access_token));
197
+ store.dispatch(setUser(user));
198
+ storage.setAccessToken(data.session.access_token);
199
+ storage.setUser(user);
200
+ setLastRefreshed(new Date());
201
+ console.log('[Token Auto Refresh] 토큰 선제 갱신 완료');
202
+ // 다음 갱신 스케줄링
203
+ scheduleRefresh();
204
+ }
205
+ }
206
+ catch (err) {
207
+ console.error('[Token Auto Refresh] 토큰 갱신 실패:', err);
208
+ }
209
+ finally {
210
+ setIsRefreshing(false);
211
+ }
212
+ };
213
+ // 초기 스케줄링
214
+ scheduleRefresh();
215
+ // 세션 변경 시 재스케줄링
216
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((event) => {
217
+ if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
218
+ scheduleRefresh();
219
+ }
220
+ });
221
+ return () => {
222
+ if (timeoutId) {
223
+ clearTimeout(timeoutId);
224
+ }
225
+ subscription.unsubscribe();
226
+ };
227
+ }, [refreshBeforeMinutes, isRefreshing]);
228
+ return { isRefreshing, lastRefreshed };
229
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Track History Hook - KOMCA 패턴
2
+ * Track History Hook
3
3
  * 라우팅 변경 감지 및 자동 탭(Recent Menu) 관리
4
4
  */
5
5
  import { RecentMenu } from '../types';
@@ -22,6 +22,7 @@ export declare function useTrackHistory(options: TrackHistoryOptions): {
22
22
  };
23
23
  /**
24
24
  * Recent Menu 상태 Hook
25
+ * useSelector를 사용하여 상태 변경 시 리렌더링 보장
25
26
  */
26
27
  export declare function useRecentMenuState<D = any>(): {
27
28
  list: RecentMenu[];
@@ -1 +1 @@
1
- {"version":3,"file":"use-track-history.d.ts","sourceRoot":"","sources":["../../src/hooks/use-track-history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAItC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAGD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAkBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB;;EA0H3D;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,GAAG;;;;UAYX,CAAC,GAAG,SAAS;;EAG3C"}
1
+ {"version":3,"file":"use-track-history.d.ts","sourceRoot":"","sources":["../../src/hooks/use-track-history.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAItC,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAGD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAkBD;;GAEG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,mBAAmB;;EA0H3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,GAAG,GAAG;;;;UA0BX,CAAC,GAAG,SAAS;;EAG3C"}