@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.
- package/dist/components/button/ScrollTopButton.js +5 -3
- package/dist/components/error/ErrorBoundary.js +14 -4
- package/dist/components/error/NotFound.d.ts +20 -0
- package/dist/components/error/NotFound.d.ts.map +1 -0
- package/dist/components/error/NotFound.js +84 -0
- package/dist/components/error/index.d.ts +2 -0
- package/dist/components/error/index.d.ts.map +1 -1
- package/dist/components/error/index.js +1 -0
- package/dist/components/icons/Icons.d.ts +51 -0
- package/dist/components/icons/Icons.d.ts.map +1 -0
- package/dist/components/icons/Icons.js +100 -0
- package/dist/components/icons/index.d.ts +5 -0
- package/dist/components/icons/index.d.ts.map +1 -0
- package/dist/components/icons/index.js +4 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +6 -0
- package/dist/components/layout/Container.js +7 -2
- package/dist/components/loading/DeferredComponent.d.ts +19 -0
- package/dist/components/loading/DeferredComponent.d.ts.map +1 -0
- package/dist/components/loading/DeferredComponent.js +32 -0
- package/dist/components/loading/GlobalLoading.js +14 -3
- package/dist/components/loading/index.d.ts +1 -0
- package/dist/components/loading/index.d.ts.map +1 -1
- package/dist/components/loading/index.js +1 -0
- package/dist/components/logo/Logo.d.ts +2 -0
- package/dist/components/logo/Logo.d.ts.map +1 -1
- package/dist/components/logo/Logo.js +13 -4
- package/dist/components/modal/ModalContainer.js +17 -8
- package/dist/components/modal/ModalContext.js +2 -3
- package/dist/components/navigation/AppNavbar.js +21 -9
- package/dist/components/navigation/AppSidebar.css +58 -3
- package/dist/components/navigation/AppSidebar.d.ts +1 -1
- package/dist/components/navigation/AppSidebar.d.ts.map +1 -1
- package/dist/components/navigation/AppSidebar.js +58 -15
- package/dist/components/navigation/Footer.d.ts +15 -0
- package/dist/components/navigation/Footer.d.ts.map +1 -0
- package/dist/components/navigation/Footer.js +12 -0
- package/dist/components/navigation/Header.d.ts.map +1 -1
- package/dist/components/navigation/Header.js +17 -4
- package/dist/components/navigation/Lnb.d.ts +2 -7
- package/dist/components/navigation/Lnb.d.ts.map +1 -1
- package/dist/components/navigation/Lnb.js +34 -6
- package/dist/components/navigation/StickyNav.js +19 -11
- package/dist/components/navigation/index.d.ts +1 -0
- package/dist/components/navigation/index.d.ts.map +1 -1
- package/dist/components/navigation/index.js +1 -0
- package/dist/components/page/LoginPage.d.ts +4 -1
- package/dist/components/page/LoginPage.d.ts.map +1 -1
- package/dist/components/page/LoginPage.js +146 -21
- package/dist/components/remote/RemoteErrorBoundary.d.ts +28 -0
- package/dist/components/remote/RemoteErrorBoundary.d.ts.map +1 -0
- package/dist/components/remote/RemoteErrorBoundary.js +44 -0
- package/dist/components/remote/RemoteErrorFallback.d.ts +16 -0
- package/dist/components/remote/RemoteErrorFallback.d.ts.map +1 -0
- package/dist/components/remote/RemoteErrorFallback.js +76 -0
- package/dist/components/remote/index.d.ts +8 -0
- package/dist/components/remote/index.d.ts.map +1 -0
- package/dist/components/remote/index.js +5 -0
- package/dist/components/router/BrowserRouter.d.ts +13 -0
- package/dist/components/router/BrowserRouter.d.ts.map +1 -0
- package/dist/components/router/BrowserRouter.js +17 -0
- package/dist/components/router/RouteGuard.d.ts +79 -0
- package/dist/components/router/RouteGuard.d.ts.map +1 -0
- package/dist/components/router/RouteGuard.js +86 -0
- package/dist/components/router/index.d.ts +4 -0
- package/dist/components/router/index.d.ts.map +1 -0
- package/dist/components/router/index.js +2 -0
- package/dist/components/toast/ToastContainer.js +17 -6
- package/dist/components/toast/ToastContext.js +2 -3
- package/dist/hooks/index.d.ts +9 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -1
- package/dist/hooks/use-auth.d.ts +2 -1
- package/dist/hooks/use-auth.d.ts.map +1 -1
- package/dist/hooks/use-auth.js +19 -18
- package/dist/hooks/use-debounce.d.ts +56 -0
- package/dist/hooks/use-debounce.d.ts.map +1 -0
- package/dist/hooks/use-debounce.js +140 -0
- package/dist/hooks/use-effect-once.d.ts +77 -0
- package/dist/hooks/use-effect-once.d.ts.map +1 -0
- package/dist/hooks/use-effect-once.js +124 -0
- package/dist/hooks/use-error-notification.d.ts +1 -1
- package/dist/hooks/use-error-notification.js +1 -1
- package/dist/hooks/use-global-loading.d.ts +1 -1
- package/dist/hooks/use-global-loading.js +1 -1
- package/dist/hooks/use-initialize.d.ts +8 -1
- package/dist/hooks/use-initialize.d.ts.map +1 -1
- package/dist/hooks/use-initialize.js +126 -23
- package/dist/hooks/use-modal.d.ts +21 -5
- package/dist/hooks/use-modal.d.ts.map +1 -1
- package/dist/hooks/use-modal.js +57 -17
- package/dist/hooks/use-navigate.d.ts +1 -1
- package/dist/hooks/use-navigate.js +1 -1
- package/dist/hooks/use-network-status.d.ts +15 -0
- package/dist/hooks/use-network-status.d.ts.map +1 -0
- package/dist/hooks/use-network-status.js +49 -0
- package/dist/hooks/use-permission.d.ts +22 -0
- package/dist/hooks/use-permission.d.ts.map +1 -0
- package/dist/hooks/use-permission.js +73 -0
- package/dist/hooks/use-recent-menu.d.ts +46 -0
- package/dist/hooks/use-recent-menu.d.ts.map +1 -0
- package/dist/hooks/use-recent-menu.js +169 -0
- package/dist/hooks/use-scroll-restoration.d.ts +51 -0
- package/dist/hooks/use-scroll-restoration.d.ts.map +1 -0
- package/dist/hooks/use-scroll-restoration.js +143 -0
- package/dist/hooks/use-supabase-auth.d.ts +49 -0
- package/dist/hooks/use-supabase-auth.d.ts.map +1 -0
- package/dist/hooks/use-supabase-auth.js +229 -0
- package/dist/hooks/use-track-history.d.ts +2 -1
- package/dist/hooks/use-track-history.d.ts.map +1 -1
- package/dist/hooks/use-track-history.js +14 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/network/axios-factory.d.ts +30 -1
- package/dist/network/axios-factory.d.ts.map +1 -1
- package/dist/network/axios-factory.js +192 -24
- package/dist/network/index.d.ts +3 -1
- package/dist/network/index.d.ts.map +1 -1
- package/dist/network/index.js +5 -1
- package/dist/network/supabase-client.d.ts +28 -0
- package/dist/network/supabase-client.d.ts.map +1 -0
- package/dist/network/supabase-client.js +46 -0
- package/dist/store/app-store.d.ts +222 -12
- package/dist/store/app-store.d.ts.map +1 -1
- package/dist/store/app-store.js +46 -29
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +3 -0
- package/dist/store/menu-slice.d.ts +96 -0
- package/dist/store/menu-slice.d.ts.map +1 -0
- package/dist/store/menu-slice.js +98 -0
- package/dist/store/recent-menu-slice.d.ts +209 -0
- package/dist/store/recent-menu-slice.d.ts.map +1 -0
- package/dist/store/recent-menu-slice.js +110 -0
- package/dist/store/store-access.d.ts +1 -1
- package/dist/store/store-access.js +1 -1
- package/dist/types/index.d.ts +74 -17
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/service.d.ts +1 -1
- package/dist/types/service.js +1 -1
- package/dist/utils/classnames.d.ts +65 -0
- package/dist/utils/classnames.d.ts.map +1 -0
- package/dist/utils/classnames.js +98 -0
- package/dist/utils/formatter.d.ts +78 -0
- package/dist/utils/formatter.d.ts.map +1 -0
- package/dist/utils/formatter.js +216 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +5 -0
- package/dist/utils/permission.d.ts +33 -0
- package/dist/utils/permission.d.ts.map +1 -0
- package/dist/utils/permission.js +132 -0
- package/dist/utils/query-string.d.ts +67 -0
- package/dist/utils/query-string.d.ts.map +1 -0
- package/dist/utils/query-string.js +136 -0
- package/dist/utils/storage.d.ts +1 -1
- package/dist/utils/storage.js +1 -1
- package/dist/utils/validation.d.ts +98 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +260 -0
- package/package.json +5 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Initialize Hook
|
|
2
|
+
* Initialize Hook
|
|
3
3
|
* 앱 시작시 초기화 (토큰 갱신, 사용자 정보 로드)
|
|
4
4
|
*/
|
|
5
5
|
import { User } from '../types';
|
|
@@ -23,4 +23,11 @@ export declare function useInitialize(options?: InitializeOptions): {
|
|
|
23
23
|
export declare function useSimpleInitialize(): {
|
|
24
24
|
initialized: boolean;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* Supabase 초기화 Hook
|
|
28
|
+
* 앱 시작 시 Supabase 세션 복구 및 Auth 상태 변경 구독
|
|
29
|
+
*/
|
|
30
|
+
export declare function useSupabaseInitialize(): {
|
|
31
|
+
initialized: boolean;
|
|
32
|
+
};
|
|
26
33
|
//# sourceMappingURL=use-initialize.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-initialize.d.ts","sourceRoot":"","sources":["../../src/hooks/use-initialize.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"use-initialize.d.ts","sourceRoot":"","sources":["../../src/hooks/use-initialize.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAiBhC,MAAM,WAAW,iBAAiB;IAChC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC3C,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB;;;;EAmE5D;AAED;;GAEG;AACH,wBAAgB,mBAAmB;;EAyBlC;AAED;;;GAGG;AACH,wBAAgB,qBAAqB;;EAgHpC"}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Initialize Hook
|
|
2
|
+
* Initialize Hook
|
|
3
3
|
* 앱 시작시 초기화 (토큰 갱신, 사용자 정보 로드)
|
|
4
4
|
*/
|
|
5
5
|
import { useEffect, useState } from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { getStore, setAccessToken, setUser, logout } from '../store/app-store';
|
|
7
7
|
import { storage } from '../utils/storage';
|
|
8
|
+
import { getSupabase } from '../network/supabase-client';
|
|
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
|
+
}
|
|
8
21
|
/**
|
|
9
22
|
* 앱 초기화 Hook
|
|
10
23
|
*/
|
|
@@ -16,28 +29,25 @@ export function useInitialize(options = {}) {
|
|
|
16
29
|
const initialize = async () => {
|
|
17
30
|
try {
|
|
18
31
|
setLoading(true);
|
|
19
|
-
|
|
32
|
+
const store = getStore();
|
|
20
33
|
const savedToken = storage.getAccessToken();
|
|
21
34
|
const savedUser = storage.getUser();
|
|
22
35
|
if (savedToken) {
|
|
23
|
-
|
|
24
|
-
dispatchToHost({ type: 'app/setAccessToken', payload: savedToken });
|
|
36
|
+
store.dispatch(setAccessToken(savedToken));
|
|
25
37
|
if (savedUser) {
|
|
26
|
-
|
|
38
|
+
store.dispatch(setUser(savedUser));
|
|
27
39
|
}
|
|
28
|
-
// 2. 토큰 갱신 시도 (옵션)
|
|
29
40
|
if (options.refreshToken) {
|
|
30
41
|
try {
|
|
31
42
|
const newToken = await options.refreshToken();
|
|
32
43
|
if (newToken) {
|
|
33
|
-
|
|
44
|
+
store.dispatch(setAccessToken(newToken));
|
|
34
45
|
storage.setAccessToken(newToken);
|
|
35
46
|
console.log('[Initialize] 토큰 갱신 성공');
|
|
36
|
-
// 3. 사용자 정보 갱신 (옵션)
|
|
37
47
|
if (options.fetchUserInfo) {
|
|
38
48
|
const userInfo = await options.fetchUserInfo();
|
|
39
49
|
if (userInfo) {
|
|
40
|
-
|
|
50
|
+
store.dispatch(setUser(userInfo));
|
|
41
51
|
storage.setUser(userInfo);
|
|
42
52
|
console.log('[Initialize] 사용자 정보 갱신:', userInfo.email);
|
|
43
53
|
}
|
|
@@ -49,13 +59,10 @@ export function useInitialize(options = {}) {
|
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
//
|
|
62
|
+
// Recent Menu 복구
|
|
53
63
|
const savedRecentMenu = storage.getRecentMenu();
|
|
54
64
|
if (savedRecentMenu.length > 0) {
|
|
55
|
-
|
|
56
|
-
type: 'recentMenu/setRecentMenu',
|
|
57
|
-
payload: { list: savedRecentMenu },
|
|
58
|
-
});
|
|
65
|
+
store.dispatch({ type: 'recentMenu/setRecentMenu', payload: { list: savedRecentMenu } });
|
|
59
66
|
}
|
|
60
67
|
setInitialized(true);
|
|
61
68
|
options.onInitialized?.();
|
|
@@ -81,24 +88,120 @@ export function useInitialize(options = {}) {
|
|
|
81
88
|
export function useSimpleInitialize() {
|
|
82
89
|
const [initialized, setInitialized] = useState(false);
|
|
83
90
|
useEffect(() => {
|
|
84
|
-
|
|
91
|
+
const store = getStore();
|
|
85
92
|
const savedToken = storage.getAccessToken();
|
|
86
93
|
const savedUser = storage.getUser();
|
|
87
94
|
if (savedToken) {
|
|
88
|
-
|
|
95
|
+
store.dispatch(setAccessToken(savedToken));
|
|
89
96
|
}
|
|
90
97
|
if (savedUser) {
|
|
91
|
-
|
|
98
|
+
store.dispatch(setUser(savedUser));
|
|
92
99
|
}
|
|
93
|
-
// Recent Menu 복구
|
|
94
100
|
const savedRecentMenu = storage.getRecentMenu();
|
|
95
101
|
if (savedRecentMenu.length > 0) {
|
|
96
|
-
|
|
97
|
-
type: 'recentMenu/setRecentMenu',
|
|
98
|
-
payload: { list: savedRecentMenu },
|
|
99
|
-
});
|
|
102
|
+
store.dispatch({ type: 'recentMenu/setRecentMenu', payload: { list: savedRecentMenu } });
|
|
100
103
|
}
|
|
101
104
|
setInitialized(true);
|
|
102
105
|
}, []);
|
|
103
106
|
return { initialized };
|
|
104
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Supabase 초기화 Hook
|
|
110
|
+
* 앱 시작 시 Supabase 세션 복구 및 Auth 상태 변경 구독
|
|
111
|
+
*/
|
|
112
|
+
export function useSupabaseInitialize() {
|
|
113
|
+
const [initialized, setInitialized] = useState(false);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
let cleanup;
|
|
116
|
+
const initialize = async () => {
|
|
117
|
+
const store = getStore();
|
|
118
|
+
try {
|
|
119
|
+
// Supabase 클라이언트 가져오기 (없으면 fallback)
|
|
120
|
+
let supabase;
|
|
121
|
+
try {
|
|
122
|
+
supabase = getSupabase();
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Supabase가 초기화되지 않은 경우 localStorage에서 복구
|
|
126
|
+
console.warn('[Supabase Init] Supabase 미초기화, localStorage fallback');
|
|
127
|
+
const savedToken = storage.getAccessToken();
|
|
128
|
+
const savedUser = storage.getUser();
|
|
129
|
+
if (savedToken) {
|
|
130
|
+
store.dispatch(setAccessToken(savedToken));
|
|
131
|
+
}
|
|
132
|
+
if (savedUser) {
|
|
133
|
+
store.dispatch(setUser(savedUser));
|
|
134
|
+
}
|
|
135
|
+
// Recent Menu 복구
|
|
136
|
+
const savedRecentMenu = storage.getRecentMenu();
|
|
137
|
+
if (savedRecentMenu.length > 0) {
|
|
138
|
+
store.dispatch({ type: 'recentMenu/setRecentMenuList', payload: savedRecentMenu });
|
|
139
|
+
}
|
|
140
|
+
setInitialized(true);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// 1. 현재 세션 확인
|
|
144
|
+
const { data: { session }, error } = await supabase.auth.getSession();
|
|
145
|
+
if (error) {
|
|
146
|
+
console.warn('[Supabase Init] 세션 가져오기 실패:', error.message);
|
|
147
|
+
}
|
|
148
|
+
if (session) {
|
|
149
|
+
const user = mapSupabaseUser(session.user);
|
|
150
|
+
store.dispatch(setAccessToken(session.access_token));
|
|
151
|
+
store.dispatch(setUser(user));
|
|
152
|
+
storage.setAccessToken(session.access_token);
|
|
153
|
+
storage.setUser(user);
|
|
154
|
+
console.log('[Supabase Init] 세션 복구:', user.email);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
// 세션 없으면 localStorage에서 복구 시도
|
|
158
|
+
const savedToken = storage.getAccessToken();
|
|
159
|
+
const savedUser = storage.getUser();
|
|
160
|
+
if (savedToken) {
|
|
161
|
+
store.dispatch(setAccessToken(savedToken));
|
|
162
|
+
}
|
|
163
|
+
if (savedUser) {
|
|
164
|
+
store.dispatch(setUser(savedUser));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 2. Recent Menu 복구
|
|
168
|
+
const savedRecentMenu = storage.getRecentMenu();
|
|
169
|
+
if (savedRecentMenu.length > 0) {
|
|
170
|
+
store.dispatch({ type: 'recentMenu/setRecentMenuList', payload: savedRecentMenu });
|
|
171
|
+
}
|
|
172
|
+
// 3. Auth 상태 변경 구독
|
|
173
|
+
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
|
|
174
|
+
if (session) {
|
|
175
|
+
const user = mapSupabaseUser(session.user);
|
|
176
|
+
store.dispatch(setAccessToken(session.access_token));
|
|
177
|
+
store.dispatch(setUser(user));
|
|
178
|
+
storage.setAccessToken(session.access_token);
|
|
179
|
+
storage.setUser(user);
|
|
180
|
+
}
|
|
181
|
+
else if (event === 'SIGNED_OUT') {
|
|
182
|
+
store.dispatch(logout());
|
|
183
|
+
storage.clearAuth();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
cleanup = () => subscription.unsubscribe();
|
|
187
|
+
setInitialized(true);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
console.error('[Supabase Init] 초기화 실패:', err);
|
|
191
|
+
// 에러 발생해도 localStorage에서 복구 시도
|
|
192
|
+
const savedToken = storage.getAccessToken();
|
|
193
|
+
const savedUser = storage.getUser();
|
|
194
|
+
if (savedToken) {
|
|
195
|
+
store.dispatch(setAccessToken(savedToken));
|
|
196
|
+
}
|
|
197
|
+
if (savedUser) {
|
|
198
|
+
store.dispatch(setUser(savedUser));
|
|
199
|
+
}
|
|
200
|
+
setInitialized(true);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
initialize();
|
|
204
|
+
return () => cleanup?.();
|
|
205
|
+
}, []);
|
|
206
|
+
return { initialized };
|
|
207
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modal Hooks
|
|
2
|
+
* Modal Hooks
|
|
3
3
|
* Alert, Confirm 모달 관리
|
|
4
4
|
*/
|
|
5
5
|
export interface SimpleModalOptions {
|
|
@@ -29,11 +29,27 @@ export declare function useAlertModal(): AlertModalResult;
|
|
|
29
29
|
*/
|
|
30
30
|
export declare function useConfirmModal(): ConfirmModalResult;
|
|
31
31
|
/**
|
|
32
|
-
* 비동기 Alert 모달 -
|
|
32
|
+
* 비동기 Alert 모달 - ModalContext 연동
|
|
33
|
+
* KOMCA 패턴 업그레이드: Context 기반으로 커스텀 모달 사용
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const alert = useAsyncAlert();
|
|
37
|
+
* await alert('저장되었습니다.');
|
|
38
|
+
* await alert('오류가 발생했습니다.', '오류');
|
|
33
39
|
*/
|
|
34
|
-
export declare function
|
|
40
|
+
export declare function useAsyncAlert(): (message: string, title?: string) => Promise<void>;
|
|
35
41
|
/**
|
|
36
|
-
* 비동기 Confirm 모달 -
|
|
42
|
+
* 비동기 Confirm 모달 - ModalContext 연동
|
|
43
|
+
* KOMCA 패턴 업그레이드: Context 기반으로 커스텀 모달 사용
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const confirm = useAsyncConfirm();
|
|
47
|
+
* const result = await confirm('삭제하시겠습니까?');
|
|
48
|
+
* if (result) {
|
|
49
|
+
* // 삭제 처리
|
|
50
|
+
* }
|
|
37
51
|
*/
|
|
38
|
-
export declare function
|
|
52
|
+
export declare function useAsyncConfirm(): (message: string, title?: string) => Promise<boolean>;
|
|
53
|
+
export declare const useAsyncAlertModal: typeof useAsyncAlert;
|
|
54
|
+
export declare const useAsyncConfirmModal: typeof useAsyncConfirm;
|
|
39
55
|
//# sourceMappingURL=use-modal.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-modal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-modal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAGD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,gBAAgB,CA4BhD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,kBAAkB,CA4BpD;AAED
|
|
1
|
+
{"version":3,"file":"use-modal.d.ts","sourceRoot":"","sources":["../../src/hooks/use-modal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAGD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAGD,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,OAAO,EAAE,kBAAkB,GAAG,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,KAAK,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CACrC;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,gBAAgB,CA4BhD;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,kBAAkB,CA4BpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,cAYQ,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC,CAQ1E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,cAWM,MAAM,UAAU,MAAM,KAAG,OAAO,CAAC,OAAO,CAAC,CAQ7E;AAGD,eAAO,MAAM,kBAAkB,sBAAgB,CAAC;AAChD,eAAO,MAAM,oBAAoB,wBAAkB,CAAC"}
|
package/dist/hooks/use-modal.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modal Hooks
|
|
2
|
+
* Modal Hooks
|
|
3
3
|
* Alert, Confirm 모달 관리
|
|
4
4
|
*/
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
@@ -58,26 +58,66 @@ export function useConfirmModal() {
|
|
|
58
58
|
return { isOpen, options, show, close };
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* 비동기 Alert 모달 -
|
|
61
|
+
* 비동기 Alert 모달 - ModalContext 연동
|
|
62
|
+
* KOMCA 패턴 업그레이드: Context 기반으로 커스텀 모달 사용
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const alert = useAsyncAlert();
|
|
66
|
+
* await alert('저장되었습니다.');
|
|
67
|
+
* await alert('오류가 발생했습니다.', '오류');
|
|
62
68
|
*/
|
|
63
|
-
export function
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
export function useAsyncAlert() {
|
|
70
|
+
// ModalContext를 직접 import하지 않고 동적으로 가져옴 (순환 참조 방지)
|
|
71
|
+
const getModalContext = useCallback(() => {
|
|
72
|
+
try {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
74
|
+
const { useModalContext } = require('../components/modal/ModalContext');
|
|
75
|
+
return useModalContext();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
70
80
|
}, []);
|
|
81
|
+
return useCallback(async (message, title) => {
|
|
82
|
+
const context = getModalContext();
|
|
83
|
+
if (context?.alert) {
|
|
84
|
+
return context.alert(message, title);
|
|
85
|
+
}
|
|
86
|
+
// fallback: 브라우저 기본 alert
|
|
87
|
+
alert(title ? `${title}\n\n${message}` : message);
|
|
88
|
+
}, [getModalContext]);
|
|
71
89
|
}
|
|
72
90
|
/**
|
|
73
|
-
* 비동기 Confirm 모달 -
|
|
91
|
+
* 비동기 Confirm 모달 - ModalContext 연동
|
|
92
|
+
* KOMCA 패턴 업그레이드: Context 기반으로 커스텀 모달 사용
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* const confirm = useAsyncConfirm();
|
|
96
|
+
* const result = await confirm('삭제하시겠습니까?');
|
|
97
|
+
* if (result) {
|
|
98
|
+
* // 삭제 처리
|
|
99
|
+
* }
|
|
74
100
|
*/
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
}
|
|
101
|
+
export function useAsyncConfirm() {
|
|
102
|
+
const getModalContext = useCallback(() => {
|
|
103
|
+
try {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
105
|
+
const { useModalContext } = require('../components/modal/ModalContext');
|
|
106
|
+
return useModalContext();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
82
111
|
}, []);
|
|
112
|
+
return useCallback(async (message, title) => {
|
|
113
|
+
const context = getModalContext();
|
|
114
|
+
if (context?.confirm) {
|
|
115
|
+
return context.confirm(message, title);
|
|
116
|
+
}
|
|
117
|
+
// fallback: 브라우저 기본 confirm
|
|
118
|
+
return confirm(title ? `${title}\n\n${message}` : message);
|
|
119
|
+
}, [getModalContext]);
|
|
83
120
|
}
|
|
121
|
+
// 하위 호환성을 위한 alias
|
|
122
|
+
export const useAsyncAlertModal = useAsyncAlert;
|
|
123
|
+
export const useAsyncConfirmModal = useAsyncConfirm;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type ConnectionType = 'slow-2g' | '2g' | '3g' | '4g' | 'unknown';
|
|
2
|
+
interface NetworkStatus {
|
|
3
|
+
connectionType: ConnectionType;
|
|
4
|
+
isSlowNetwork: boolean;
|
|
5
|
+
isOnline: boolean;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 사용자의 네트워크 상태를 감지하는 훅
|
|
9
|
+
* - effectiveType: 연결 품질 (slow-2g, 2g, 3g, 4g)
|
|
10
|
+
* - isSlowNetwork: 느린 네트워크 여부 (slow-2g, 2g, 3g)
|
|
11
|
+
* - isOnline: 온라인 상태
|
|
12
|
+
*/
|
|
13
|
+
declare const useNetworkStatus: () => NetworkStatus;
|
|
14
|
+
export default useNetworkStatus;
|
|
15
|
+
//# sourceMappingURL=use-network-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-network-status.d.ts","sourceRoot":"","sources":["../../src/hooks/use-network-status.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC;AAExE,UAAU,aAAa;IACrB,cAAc,EAAE,cAAc,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;GAKG;AACH,QAAA,MAAM,gBAAgB,QAAO,aAiD5B,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 사용자의 네트워크 상태를 감지하는 훅
|
|
4
|
+
* - effectiveType: 연결 품질 (slow-2g, 2g, 3g, 4g)
|
|
5
|
+
* - isSlowNetwork: 느린 네트워크 여부 (slow-2g, 2g, 3g)
|
|
6
|
+
* - isOnline: 온라인 상태
|
|
7
|
+
*/
|
|
8
|
+
const useNetworkStatus = () => {
|
|
9
|
+
const getConnectionType = () => {
|
|
10
|
+
if ('connection' in navigator) {
|
|
11
|
+
const nav = navigator;
|
|
12
|
+
if (nav.connection?.effectiveType) {
|
|
13
|
+
return nav.connection.effectiveType;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return 'unknown';
|
|
17
|
+
};
|
|
18
|
+
const [connectionType, setConnectionType] = useState(getConnectionType());
|
|
19
|
+
const [isOnline, setIsOnline] = useState(navigator.onLine);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
// Network Information API
|
|
22
|
+
if ('connection' in navigator) {
|
|
23
|
+
const nav = navigator;
|
|
24
|
+
if (nav.connection) {
|
|
25
|
+
const updateConnectionStatus = () => {
|
|
26
|
+
setConnectionType(nav.connection?.effectiveType || 'unknown');
|
|
27
|
+
};
|
|
28
|
+
nav.connection.addEventListener('change', updateConnectionStatus);
|
|
29
|
+
return () => {
|
|
30
|
+
nav.connection?.removeEventListener('change', updateConnectionStatus);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, []);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
// Online/Offline 상태
|
|
37
|
+
const handleOnline = () => setIsOnline(true);
|
|
38
|
+
const handleOffline = () => setIsOnline(false);
|
|
39
|
+
window.addEventListener('online', handleOnline);
|
|
40
|
+
window.addEventListener('offline', handleOffline);
|
|
41
|
+
return () => {
|
|
42
|
+
window.removeEventListener('online', handleOnline);
|
|
43
|
+
window.removeEventListener('offline', handleOffline);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
const isSlowNetwork = ['slow-2g', '2g', '3g'].includes(connectionType);
|
|
47
|
+
return { connectionType, isSlowNetwork, isOnline };
|
|
48
|
+
};
|
|
49
|
+
export default useNetworkStatus;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePermission Hook
|
|
3
|
+
* 권한 기반 접근 제어를 위한 훅
|
|
4
|
+
*/
|
|
5
|
+
import { UserRole, PermissionAction, MenuItem, MenuPermission } from '../types';
|
|
6
|
+
/**
|
|
7
|
+
* usePermission Hook
|
|
8
|
+
* 현재 사용자의 권한을 확인하는 훅
|
|
9
|
+
*/
|
|
10
|
+
export declare function usePermission(): {
|
|
11
|
+
user: import("../types").User | null;
|
|
12
|
+
can: (code: string, action?: PermissionAction) => boolean;
|
|
13
|
+
canAny: (codes: string[], action?: PermissionAction) => boolean;
|
|
14
|
+
canAll: (codes: string[], action?: PermissionAction) => boolean;
|
|
15
|
+
isRole: (roles: UserRole | UserRole[]) => boolean;
|
|
16
|
+
isAdmin: boolean;
|
|
17
|
+
checkMenu: (permission?: MenuPermission) => boolean;
|
|
18
|
+
filterMenus: (menus: MenuItem[]) => MenuItem[];
|
|
19
|
+
canAccess: (menus: MenuItem[], path: string) => boolean;
|
|
20
|
+
};
|
|
21
|
+
export default usePermission;
|
|
22
|
+
//# sourceMappingURL=use-permission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-permission.d.ts","sourceRoot":"","sources":["../../src/hooks/use-permission.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAcH,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAEhF;;;GAGG;AACH,wBAAgB,aAAa;;gBAOlB,MAAM,WAAU,gBAAgB,KAAY,OAAO;oBAUlD,MAAM,EAAE,WAAU,gBAAgB,KAAY,OAAO;oBAUrD,MAAM,EAAE,WAAU,gBAAgB,KAAY,OAAO;oBAUrD,QAAQ,GAAG,QAAQ,EAAE,KAAG,OAAO;;6BAezB,cAAc,KAAG,OAAO;yBAU9B,QAAQ,EAAE,KAAG,QAAQ,EAAE;uBAUvB,QAAQ,EAAE,QAAQ,MAAM,KAAG,OAAO;EAiB7C;AAED,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePermission Hook
|
|
3
|
+
* 권한 기반 접근 제어를 위한 훅
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo, useCallback } from 'react';
|
|
6
|
+
import { useSelector } from 'react-redux';
|
|
7
|
+
import { selectUser } from '../store/app-store';
|
|
8
|
+
import { hasPermission, hasAnyPermission, hasAllPermissions, hasRole, checkMenuPermission, filterMenusByPermission, canAccessPath, } from '../utils/permission';
|
|
9
|
+
/**
|
|
10
|
+
* usePermission Hook
|
|
11
|
+
* 현재 사용자의 권한을 확인하는 훅
|
|
12
|
+
*/
|
|
13
|
+
export function usePermission() {
|
|
14
|
+
const user = useSelector(selectUser);
|
|
15
|
+
/**
|
|
16
|
+
* 특정 코드에 대한 권한 확인
|
|
17
|
+
*/
|
|
18
|
+
const can = useCallback((code, action = 'read') => {
|
|
19
|
+
return hasPermission(user, code, action);
|
|
20
|
+
}, [user]);
|
|
21
|
+
/**
|
|
22
|
+
* 여러 코드 중 하나라도 권한이 있는지 확인
|
|
23
|
+
*/
|
|
24
|
+
const canAny = useCallback((codes, action = 'read') => {
|
|
25
|
+
return hasAnyPermission(user, codes, action);
|
|
26
|
+
}, [user]);
|
|
27
|
+
/**
|
|
28
|
+
* 모든 코드에 대한 권한이 있는지 확인
|
|
29
|
+
*/
|
|
30
|
+
const canAll = useCallback((codes, action = 'read') => {
|
|
31
|
+
return hasAllPermissions(user, codes, action);
|
|
32
|
+
}, [user]);
|
|
33
|
+
/**
|
|
34
|
+
* 특정 역할인지 확인
|
|
35
|
+
*/
|
|
36
|
+
const isRole = useCallback((roles) => {
|
|
37
|
+
return hasRole(user, roles);
|
|
38
|
+
}, [user]);
|
|
39
|
+
/**
|
|
40
|
+
* 관리자 여부
|
|
41
|
+
*/
|
|
42
|
+
const isAdmin = useMemo(() => user?.role === 'admin', [user]);
|
|
43
|
+
/**
|
|
44
|
+
* 메뉴 권한 체크
|
|
45
|
+
*/
|
|
46
|
+
const checkMenu = useCallback((permission) => {
|
|
47
|
+
return checkMenuPermission(user, permission);
|
|
48
|
+
}, [user]);
|
|
49
|
+
/**
|
|
50
|
+
* 메뉴 필터링
|
|
51
|
+
*/
|
|
52
|
+
const filterMenus = useCallback((menus) => {
|
|
53
|
+
return filterMenusByPermission(menus, user);
|
|
54
|
+
}, [user]);
|
|
55
|
+
/**
|
|
56
|
+
* 특정 경로 접근 가능 여부
|
|
57
|
+
*/
|
|
58
|
+
const canAccess = useCallback((menus, path) => {
|
|
59
|
+
return canAccessPath(menus, path, user);
|
|
60
|
+
}, [user]);
|
|
61
|
+
return {
|
|
62
|
+
user,
|
|
63
|
+
can,
|
|
64
|
+
canAny,
|
|
65
|
+
canAll,
|
|
66
|
+
isRole,
|
|
67
|
+
isAdmin,
|
|
68
|
+
checkMenu,
|
|
69
|
+
filterMenus,
|
|
70
|
+
canAccess,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export default usePermission;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRecentMenu Hook
|
|
3
|
+
* 최근 방문 메뉴 상태 저장/복구
|
|
4
|
+
*/
|
|
5
|
+
import { RecentMenu } from '../types';
|
|
6
|
+
export interface UseRecentMenuOptions {
|
|
7
|
+
/** 자동으로 현재 경로를 최근 메뉴에 추가할지 여부 */
|
|
8
|
+
autoTrack?: boolean;
|
|
9
|
+
/** 제외할 경로 패턴 */
|
|
10
|
+
excludePaths?: (string | RegExp)[];
|
|
11
|
+
/** 메뉴 제목 생성 함수 */
|
|
12
|
+
getTitleFromPath?: (pathname: string) => string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* useRecentMenu Hook
|
|
16
|
+
*/
|
|
17
|
+
export declare function useRecentMenu(options?: UseRecentMenuOptions): {
|
|
18
|
+
/** 최근 메뉴 목록 */
|
|
19
|
+
list: RecentMenu[];
|
|
20
|
+
/** 현재 활성 메뉴 */
|
|
21
|
+
current: RecentMenu | null;
|
|
22
|
+
/** 현재 활성 메뉴 ID */
|
|
23
|
+
currentId: string;
|
|
24
|
+
/** 메뉴 추가 */
|
|
25
|
+
add: (menu: Omit<RecentMenu, "id"> & {
|
|
26
|
+
id?: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
/** 메뉴 제거 */
|
|
29
|
+
remove: (id: string) => void;
|
|
30
|
+
/** 현재 메뉴 설정 */
|
|
31
|
+
setCurrent: (id: string) => void;
|
|
32
|
+
/** 메뉴로 이동 */
|
|
33
|
+
goTo: (id: string) => void;
|
|
34
|
+
/** 메뉴 상태 업데이트 */
|
|
35
|
+
updateState: (id: string, state: any) => void;
|
|
36
|
+
/** 메뉴 데이터 업데이트 */
|
|
37
|
+
updateData: (id: string, data: any) => void;
|
|
38
|
+
/** 현재 메뉴 상태 업데이트 */
|
|
39
|
+
updateCurrentState: (state: any) => void;
|
|
40
|
+
/** 모든 메뉴 닫기 */
|
|
41
|
+
closeAll: () => void;
|
|
42
|
+
/** 다른 메뉴 모두 닫기 */
|
|
43
|
+
closeOthers: () => void;
|
|
44
|
+
};
|
|
45
|
+
export default useRecentMenu;
|
|
46
|
+
//# sourceMappingURL=use-recent-menu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-recent-menu.d.ts","sourceRoot":"","sources":["../../src/hooks/use-recent-menu.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEtC,MAAM,WAAW,oBAAoB;IACnC,iCAAiC;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB;IAChB,YAAY,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC;IACnC,kBAAkB;IAClB,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB;IAoL5D,eAAe;;IAEf,eAAe;;IAEf,kBAAkB;;IAElB,YAAY;gBArGL,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE;IAuG/C,YAAY;iBA5FP,MAAM;IA8FX,eAAe;qBApFV,MAAM;IAsFX,aAAa;eA5ER,MAAM;IA8EX,iBAAiB;sBAzDZ,MAAM,SAAS,GAAG;IA2DvB,kBAAkB;qBAjDb,MAAM,QAAQ,GAAG;IAmDtB,oBAAoB;gCAzCZ,GAAG;IA2CX,eAAe;;IAEf,kBAAkB;;EAGrB;AAED,eAAe,aAAa,CAAC"}
|