@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,14 +1,28 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
1
|
/**
|
|
3
2
|
* LoginPage - KOMCA 패턴
|
|
4
3
|
*
|
|
5
4
|
* 공통 로그인 페이지 컴포넌트
|
|
6
5
|
* Host/Remote 모두에서 사용 가능
|
|
6
|
+
* Supabase Auth 지원
|
|
7
7
|
*/
|
|
8
|
-
import { useState, useCallback } from 'react';
|
|
8
|
+
import React, { useState, useCallback } from 'react';
|
|
9
9
|
import { getStore, setAccessToken, setUser } from '../../store/app-store';
|
|
10
|
+
import { storage } from '../../utils/storage';
|
|
11
|
+
import { getSupabase } from '../../network/supabase-client';
|
|
10
12
|
import './LoginPage.css';
|
|
11
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Supabase User를 앱 User 타입으로 변환
|
|
15
|
+
*/
|
|
16
|
+
function mapSupabaseUser(supabaseUser) {
|
|
17
|
+
return {
|
|
18
|
+
id: supabaseUser.id,
|
|
19
|
+
email: supabaseUser.email || '',
|
|
20
|
+
name: supabaseUser.user_metadata?.name || supabaseUser.email?.split('@')[0] || '',
|
|
21
|
+
role: supabaseUser.user_metadata?.role || 'user',
|
|
22
|
+
avatar: supabaseUser.user_metadata?.avatar_url,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function LoginPage({ redirectPath = '/', onLoginSuccess, appName = 'MFA', logo, onGoogleLogin, showTestAccount = false, useSupabase = true, }) {
|
|
12
26
|
const [email, setEmail] = useState('');
|
|
13
27
|
const [password, setPassword] = useState('');
|
|
14
28
|
const [error, setError] = useState('');
|
|
@@ -25,8 +39,12 @@ export function LoginPage({ redirectPath = '/', onLoginSuccess, appName = 'MFA',
|
|
|
25
39
|
setIsGoogleLoading(true);
|
|
26
40
|
try {
|
|
27
41
|
const { token, user } = await onGoogleLogin();
|
|
42
|
+
// Redux store에 저장
|
|
28
43
|
store.dispatch(setAccessToken(token));
|
|
29
44
|
store.dispatch(setUser(user));
|
|
45
|
+
// localStorage에도 저장 (페이지 새로고침 대비)
|
|
46
|
+
storage.setAccessToken(token);
|
|
47
|
+
storage.setUser(user);
|
|
30
48
|
onLoginSuccess?.(user);
|
|
31
49
|
// 페이지 이동
|
|
32
50
|
window.location.href = redirectPath;
|
|
@@ -41,37 +59,144 @@ export function LoginPage({ redirectPath = '/', onLoginSuccess, appName = 'MFA',
|
|
|
41
59
|
setIsGoogleLoading(false);
|
|
42
60
|
}
|
|
43
61
|
}, [onGoogleLogin, store, onLoginSuccess, redirectPath]);
|
|
62
|
+
// Supabase 로그인 핸들러
|
|
63
|
+
const handleSupabaseLogin = useCallback(async () => {
|
|
64
|
+
try {
|
|
65
|
+
const supabase = getSupabase();
|
|
66
|
+
const { data, error: authError } = await supabase.auth.signInWithPassword({
|
|
67
|
+
email,
|
|
68
|
+
password,
|
|
69
|
+
});
|
|
70
|
+
if (authError) {
|
|
71
|
+
// 에러 메시지 한글화
|
|
72
|
+
if (authError.message.includes('Invalid login credentials')) {
|
|
73
|
+
throw new Error('이메일 또는 비밀번호가 올바르지 않습니다.');
|
|
74
|
+
}
|
|
75
|
+
throw new Error(authError.message);
|
|
76
|
+
}
|
|
77
|
+
if (!data.session || !data.user) {
|
|
78
|
+
throw new Error('로그인 응답이 올바르지 않습니다.');
|
|
79
|
+
}
|
|
80
|
+
const user = mapSupabaseUser(data.user);
|
|
81
|
+
// Redux store에 저장
|
|
82
|
+
store.dispatch(setAccessToken(data.session.access_token));
|
|
83
|
+
store.dispatch(setUser(user));
|
|
84
|
+
// localStorage에도 저장
|
|
85
|
+
storage.setAccessToken(data.session.access_token);
|
|
86
|
+
storage.setUser(user);
|
|
87
|
+
onLoginSuccess?.(user);
|
|
88
|
+
// 페이지 이동
|
|
89
|
+
window.location.href = redirectPath;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}, [email, password, store, onLoginSuccess, redirectPath]);
|
|
95
|
+
// Mock 로그인 핸들러 (테스트용)
|
|
96
|
+
const handleMockLogin = useCallback(async () => {
|
|
97
|
+
if (email === 'admin@test.com' && password === '1234') {
|
|
98
|
+
const mockToken = `mock-token-${Date.now()}`;
|
|
99
|
+
const user = {
|
|
100
|
+
id: '1',
|
|
101
|
+
name: '관리자',
|
|
102
|
+
email: email,
|
|
103
|
+
role: 'admin',
|
|
104
|
+
};
|
|
105
|
+
store.dispatch(setAccessToken(mockToken));
|
|
106
|
+
store.dispatch(setUser(user));
|
|
107
|
+
storage.setAccessToken(mockToken);
|
|
108
|
+
storage.setUser(user);
|
|
109
|
+
onLoginSuccess?.(user);
|
|
110
|
+
window.location.href = redirectPath;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
throw new Error('이메일 또는 비밀번호가 올바르지 않습니다.');
|
|
114
|
+
}
|
|
115
|
+
}, [email, password, store, onLoginSuccess, redirectPath]);
|
|
44
116
|
const handleSubmit = useCallback(async (e) => {
|
|
45
117
|
e.preventDefault();
|
|
46
118
|
setError('');
|
|
47
119
|
setIsSubmitting(true);
|
|
48
120
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const mockToken = `mock-token-${Date.now()}`;
|
|
52
|
-
const user = {
|
|
53
|
-
id: '1',
|
|
54
|
-
name: '관리자',
|
|
55
|
-
email: email,
|
|
56
|
-
role: 'admin',
|
|
57
|
-
};
|
|
58
|
-
store.dispatch(setAccessToken(mockToken));
|
|
59
|
-
store.dispatch(setUser(user));
|
|
60
|
-
onLoginSuccess?.(user);
|
|
61
|
-
// 페이지 이동
|
|
62
|
-
window.location.href = redirectPath;
|
|
121
|
+
if (useSupabase) {
|
|
122
|
+
await handleSupabaseLogin();
|
|
63
123
|
}
|
|
64
124
|
else {
|
|
65
|
-
|
|
125
|
+
await handleMockLogin();
|
|
66
126
|
}
|
|
67
127
|
}
|
|
68
128
|
catch (err) {
|
|
69
|
-
setError('로그인 중 오류가 발생했습니다.');
|
|
129
|
+
setError(err.message || '로그인 중 오류가 발생했습니다.');
|
|
70
130
|
}
|
|
71
131
|
finally {
|
|
72
132
|
setIsSubmitting(false);
|
|
73
133
|
}
|
|
74
|
-
}, [
|
|
75
|
-
return (
|
|
134
|
+
}, [useSupabase, handleSupabaseLogin, handleMockLogin]);
|
|
135
|
+
return (React.createElement("div", { className: "login-page" },
|
|
136
|
+
React.createElement("div", { className: "login-bg" },
|
|
137
|
+
React.createElement("div", { className: "login-bg-gradient" }),
|
|
138
|
+
[...Array(12)].map((_, i) => (React.createElement("div", { key: i, className: `login-particle login-particle--${i + 1}` })))),
|
|
139
|
+
React.createElement("div", { className: "login-card" },
|
|
140
|
+
React.createElement("div", { className: "login-header" },
|
|
141
|
+
React.createElement("a", { href: "/", className: "login-logo-link" }, logo || (React.createElement(React.Fragment, null,
|
|
142
|
+
React.createElement("svg", { viewBox: "0 0 48 48", fill: "none", width: "28", height: "28" },
|
|
143
|
+
React.createElement("path", { d: "M 8 40 L 24 8 L 40 40", stroke: "#1E3A5F", strokeWidth: "14", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" })),
|
|
144
|
+
React.createElement("svg", { viewBox: "0 0 48 48", fill: "none", width: "48", height: "48" },
|
|
145
|
+
React.createElement("rect", { x: "20", y: "2", width: "8", height: "16", rx: "4", fill: "#0EA5E9" }),
|
|
146
|
+
React.createElement("rect", { x: "6", y: "16", width: "36", height: "6", rx: "3", fill: "#0EA5E9" }),
|
|
147
|
+
React.createElement("ellipse", { cx: "24", cy: "36", rx: "18", ry: "12", fill: "#0EA5E9" }),
|
|
148
|
+
React.createElement("ellipse", { cx: "17", cy: "36", rx: "4", ry: "6", fill: "#FFFFFF" }),
|
|
149
|
+
React.createElement("ellipse", { cx: "31", cy: "36", rx: "4", ry: "6", fill: "#FFFFFF" })),
|
|
150
|
+
React.createElement("svg", { viewBox: "0 0 48 48", fill: "none", width: "28", height: "28" },
|
|
151
|
+
React.createElement("path", { d: "M 8 40 L 24 8 L 40 40", stroke: "#1E3A5F", strokeWidth: "14", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }))))),
|
|
152
|
+
React.createElement("h1", { className: "login-title" }, "Welcome Back"),
|
|
153
|
+
React.createElement("p", { className: "login-subtitle" },
|
|
154
|
+
appName,
|
|
155
|
+
"\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694")),
|
|
156
|
+
error && (React.createElement("div", { className: "login-error" },
|
|
157
|
+
React.createElement("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
|
|
158
|
+
React.createElement("circle", { cx: "12", cy: "12", r: "10" }),
|
|
159
|
+
React.createElement("line", { x1: "12", y1: "8", x2: "12", y2: "12" }),
|
|
160
|
+
React.createElement("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })),
|
|
161
|
+
error)),
|
|
162
|
+
onGoogleLogin && (React.createElement(React.Fragment, null,
|
|
163
|
+
React.createElement("button", { type: "button", className: "login-google", onClick: handleGoogleLogin, disabled: isGoogleLoading }, isGoogleLoading ? (React.createElement(React.Fragment, null,
|
|
164
|
+
React.createElement("span", { className: "login-spinner login-spinner--dark" }),
|
|
165
|
+
"\uB85C\uADF8\uC778 \uC911...")) : (React.createElement(React.Fragment, null,
|
|
166
|
+
React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24" },
|
|
167
|
+
React.createElement("path", { fill: "#4285F4", d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" }),
|
|
168
|
+
React.createElement("path", { fill: "#34A853", d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" }),
|
|
169
|
+
React.createElement("path", { fill: "#FBBC05", d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" }),
|
|
170
|
+
React.createElement("path", { fill: "#EA4335", d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" })),
|
|
171
|
+
"Google\uB85C \uACC4\uC18D\uD558\uAE30"))),
|
|
172
|
+
React.createElement("div", { className: "login-divider" },
|
|
173
|
+
React.createElement("span", null, "\uB610\uB294")))),
|
|
174
|
+
React.createElement("form", { className: "login-form", onSubmit: handleSubmit },
|
|
175
|
+
React.createElement("div", { className: `login-input-group ${focusedField === 'email' ? 'focused' : ''}` },
|
|
176
|
+
React.createElement("label", { className: "login-label" }, "\uC774\uBA54\uC77C"),
|
|
177
|
+
React.createElement("div", { className: "login-input-wrapper" },
|
|
178
|
+
React.createElement("svg", { className: "login-input-icon", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
|
|
179
|
+
React.createElement("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }),
|
|
180
|
+
React.createElement("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })),
|
|
181
|
+
React.createElement("input", { type: "email", className: "login-input", value: email, onChange: (e) => setEmail(e.target.value), onFocus: () => setFocusedField('email'), onBlur: () => setFocusedField(null), placeholder: "name@example.com", required: true }))),
|
|
182
|
+
React.createElement("div", { className: `login-input-group ${focusedField === 'password' ? 'focused' : ''}` },
|
|
183
|
+
React.createElement("label", { className: "login-label" }, "\uBE44\uBC00\uBC88\uD638"),
|
|
184
|
+
React.createElement("div", { className: "login-input-wrapper" },
|
|
185
|
+
React.createElement("svg", { className: "login-input-icon", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
|
|
186
|
+
React.createElement("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }),
|
|
187
|
+
React.createElement("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })),
|
|
188
|
+
React.createElement("input", { type: "password", className: "login-input", value: password, onChange: (e) => setPassword(e.target.value), onFocus: () => setFocusedField('password'), onBlur: () => setFocusedField(null), placeholder: "\uBE44\uBC00\uBC88\uD638\uB97C \uC785\uB825\uD558\uC138\uC694", required: true }))),
|
|
189
|
+
React.createElement("button", { type: "submit", className: "login-button", disabled: isSubmitting }, isSubmitting ? (React.createElement(React.Fragment, null,
|
|
190
|
+
React.createElement("span", { className: "login-spinner" }),
|
|
191
|
+
"\uB85C\uADF8\uC778 \uC911...")) : (React.createElement(React.Fragment, null,
|
|
192
|
+
"\uB85C\uADF8\uC778",
|
|
193
|
+
React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
|
|
194
|
+
React.createElement("path", { d: "M5 12h14M12 5l7 7-7 7" })))))),
|
|
195
|
+
showTestAccount && (React.createElement("div", { className: "login-test-info" },
|
|
196
|
+
React.createElement("div", { className: "login-test-badge" },
|
|
197
|
+
React.createElement("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2" },
|
|
198
|
+
React.createElement("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" })),
|
|
199
|
+
"\uD14C\uC2A4\uD2B8 \uACC4\uC815"),
|
|
200
|
+
React.createElement("span", { className: "login-test-credentials" }, "admin@test.com / 1234"))))));
|
|
76
201
|
}
|
|
77
202
|
export default LoginPage;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteErrorBoundary
|
|
3
|
+
* Remote 앱 로드 실패 시 에러를 캡처하고 Fallback UI 표시
|
|
4
|
+
*/
|
|
5
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
6
|
+
export interface RemoteErrorBoundaryProps {
|
|
7
|
+
/** 자식 요소 (Remote 앱) */
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
/** Remote 앱 이름 (Fallback UI에 표시) */
|
|
10
|
+
remoteName: string;
|
|
11
|
+
/** 커스텀 Fallback 컴포넌트 */
|
|
12
|
+
fallback?: ReactNode;
|
|
13
|
+
/** 에러 발생 시 콜백 */
|
|
14
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
15
|
+
}
|
|
16
|
+
interface RemoteErrorBoundaryState {
|
|
17
|
+
hasError: boolean;
|
|
18
|
+
error: Error | null;
|
|
19
|
+
}
|
|
20
|
+
export declare class RemoteErrorBoundary extends Component<RemoteErrorBoundaryProps, RemoteErrorBoundaryState> {
|
|
21
|
+
constructor(props: RemoteErrorBoundaryProps);
|
|
22
|
+
static getDerivedStateFromError(error: Error): RemoteErrorBoundaryState;
|
|
23
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
|
|
24
|
+
handleRetry: () => void;
|
|
25
|
+
render(): string | number | bigint | boolean | Iterable<React.ReactNode> | Promise<string | number | bigint | boolean | React.ReactPortal | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | null | undefined> | React.JSX.Element | null | undefined;
|
|
26
|
+
}
|
|
27
|
+
export default RemoteErrorBoundary;
|
|
28
|
+
//# sourceMappingURL=RemoteErrorBoundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteErrorBoundary.d.ts","sourceRoot":"","sources":["../../../src/components/remote/RemoteErrorBoundary.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG/D,MAAM,WAAW,wBAAwB;IACvC,uBAAuB;IACvB,QAAQ,EAAE,SAAS,CAAC;IACpB,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,wBAAwB;IACxB,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iBAAiB;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;CACxD;AAED,UAAU,wBAAwB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED,qBAAa,mBAAoB,SAAQ,SAAS,CAChD,wBAAwB,EACxB,wBAAwB,CACzB;gBACa,KAAK,EAAE,wBAAwB;IAQ3C,MAAM,CAAC,wBAAwB,CAAC,KAAK,EAAE,KAAK,GAAG,wBAAwB;IAOvE,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS;IAQpD,WAAW,aAET;IAEF,MAAM;CAsBP;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteErrorBoundary
|
|
3
|
+
* Remote 앱 로드 실패 시 에러를 캡처하고 Fallback UI 표시
|
|
4
|
+
*/
|
|
5
|
+
import React, { Component } from 'react';
|
|
6
|
+
import { RemoteErrorFallback } from './RemoteErrorFallback';
|
|
7
|
+
export class RemoteErrorBoundary extends Component {
|
|
8
|
+
constructor(props) {
|
|
9
|
+
super(props);
|
|
10
|
+
this.handleRetry = () => {
|
|
11
|
+
this.setState({ hasError: false, error: null });
|
|
12
|
+
};
|
|
13
|
+
this.state = {
|
|
14
|
+
hasError: false,
|
|
15
|
+
error: null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
static getDerivedStateFromError(error) {
|
|
19
|
+
return {
|
|
20
|
+
hasError: true,
|
|
21
|
+
error,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
componentDidCatch(error, errorInfo) {
|
|
25
|
+
console.error(`[RemoteErrorBoundary] ${this.props.remoteName} 로드 실패:`, error);
|
|
26
|
+
console.error('Error Info:', errorInfo);
|
|
27
|
+
// 에러 콜백 실행
|
|
28
|
+
this.props.onError?.(error, errorInfo);
|
|
29
|
+
}
|
|
30
|
+
render() {
|
|
31
|
+
const { hasError, error } = this.state;
|
|
32
|
+
const { children, remoteName, fallback } = this.props;
|
|
33
|
+
if (hasError) {
|
|
34
|
+
// 커스텀 Fallback이 있으면 사용
|
|
35
|
+
if (fallback) {
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|
|
38
|
+
// 기본 Fallback UI
|
|
39
|
+
return (React.createElement(RemoteErrorFallback, { remoteName: remoteName, error: error, onRetry: this.handleRetry }));
|
|
40
|
+
}
|
|
41
|
+
return children;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export default RemoteErrorBoundary;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteErrorFallback
|
|
3
|
+
* Remote 앱 로드 실패 시 표시되는 Fallback UI
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
export interface RemoteErrorFallbackProps {
|
|
7
|
+
/** Remote 앱 이름 (예: "이력서", "블로그") */
|
|
8
|
+
remoteName: string;
|
|
9
|
+
/** 재시도 콜백 */
|
|
10
|
+
onRetry?: () => void;
|
|
11
|
+
/** 에러 메시지 (개발 환경에서만 표시) */
|
|
12
|
+
error?: Error | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function RemoteErrorFallback({ remoteName, onRetry, error }: RemoteErrorFallbackProps): React.JSX.Element;
|
|
15
|
+
export default RemoteErrorFallback;
|
|
16
|
+
//# sourceMappingURL=RemoteErrorFallback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RemoteErrorFallback.d.ts","sourceRoot":"","sources":["../../../src/components/remote/RemoteErrorFallback.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,WAAW,wBAAwB;IACvC,oCAAoC;IACpC,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa;IACb,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,UAAU,EACV,OAAO,EACP,KAAK,EACN,EAAE,wBAAwB,qBAgH1B;AAED,eAAe,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RemoteErrorFallback
|
|
3
|
+
* Remote 앱 로드 실패 시 표시되는 Fallback UI
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
export function RemoteErrorFallback({ remoteName, onRetry, error }) {
|
|
7
|
+
const isDev = process.env.NODE_ENV === 'development';
|
|
8
|
+
return (React.createElement("div", { style: {
|
|
9
|
+
display: 'flex',
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
padding: '48px 24px',
|
|
14
|
+
textAlign: 'center',
|
|
15
|
+
minHeight: '300px',
|
|
16
|
+
backgroundColor: '#f8f9fa',
|
|
17
|
+
borderRadius: '8px',
|
|
18
|
+
margin: '24px',
|
|
19
|
+
} },
|
|
20
|
+
React.createElement("div", { style: {
|
|
21
|
+
fontSize: '48px',
|
|
22
|
+
marginBottom: '16px',
|
|
23
|
+
opacity: 0.5,
|
|
24
|
+
} }, "\u26A0\uFE0F"),
|
|
25
|
+
React.createElement("h2", { style: {
|
|
26
|
+
margin: '0 0 8px 0',
|
|
27
|
+
fontSize: '20px',
|
|
28
|
+
fontWeight: 600,
|
|
29
|
+
color: '#343a40',
|
|
30
|
+
} },
|
|
31
|
+
remoteName,
|
|
32
|
+
" \uC571\uC744 \uBD88\uB7EC\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4"),
|
|
33
|
+
React.createElement("p", { style: {
|
|
34
|
+
margin: '0 0 24px 0',
|
|
35
|
+
fontSize: '14px',
|
|
36
|
+
color: '#6c757d',
|
|
37
|
+
maxWidth: '400px',
|
|
38
|
+
} },
|
|
39
|
+
"\uC11C\uBE44\uC2A4\uC5D0 \uC77C\uC2DC\uC801\uC778 \uBB38\uC81C\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
|
|
40
|
+
React.createElement("br", null),
|
|
41
|
+
"\uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574 \uC8FC\uC138\uC694."),
|
|
42
|
+
onRetry && (React.createElement("button", { onClick: onRetry, style: {
|
|
43
|
+
padding: '10px 24px',
|
|
44
|
+
fontSize: '14px',
|
|
45
|
+
fontWeight: 500,
|
|
46
|
+
color: '#fff',
|
|
47
|
+
backgroundColor: '#007bff',
|
|
48
|
+
border: 'none',
|
|
49
|
+
borderRadius: '6px',
|
|
50
|
+
cursor: 'pointer',
|
|
51
|
+
transition: 'background-color 0.2s',
|
|
52
|
+
}, onMouseOver: (e) => {
|
|
53
|
+
e.currentTarget.style.backgroundColor = '#0056b3';
|
|
54
|
+
}, onMouseOut: (e) => {
|
|
55
|
+
e.currentTarget.style.backgroundColor = '#007bff';
|
|
56
|
+
} }, "\uB2E4\uC2DC \uC2DC\uB3C4")),
|
|
57
|
+
isDev && error && (React.createElement("details", { style: {
|
|
58
|
+
marginTop: '24px',
|
|
59
|
+
padding: '12px',
|
|
60
|
+
backgroundColor: '#fff3cd',
|
|
61
|
+
borderRadius: '4px',
|
|
62
|
+
fontSize: '12px',
|
|
63
|
+
color: '#856404',
|
|
64
|
+
maxWidth: '500px',
|
|
65
|
+
textAlign: 'left',
|
|
66
|
+
} },
|
|
67
|
+
React.createElement("summary", { style: { cursor: 'pointer', fontWeight: 500 } }, "\uAC1C\uBC1C\uC790 \uC815\uBCF4 (\uAC1C\uBC1C \uD658\uACBD\uC5D0\uC11C\uB9CC \uD45C\uC2DC)"),
|
|
68
|
+
React.createElement("pre", { style: {
|
|
69
|
+
marginTop: '8px',
|
|
70
|
+
whiteSpace: 'pre-wrap',
|
|
71
|
+
wordBreak: 'break-all',
|
|
72
|
+
} },
|
|
73
|
+
error.message,
|
|
74
|
+
error.stack && `\n\n${error.stack}`)))));
|
|
75
|
+
}
|
|
76
|
+
export default RemoteErrorFallback;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote 관련 컴포넌트
|
|
3
|
+
*/
|
|
4
|
+
export { RemoteErrorFallback } from './RemoteErrorFallback';
|
|
5
|
+
export type { RemoteErrorFallbackProps } from './RemoteErrorFallback';
|
|
6
|
+
export { RemoteErrorBoundary } from './RemoteErrorBoundary';
|
|
7
|
+
export type { RemoteErrorBoundaryProps } from './RemoteErrorBoundary';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/remote/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,wBAAwB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom BrowserRouter - KOMCA 패턴
|
|
3
|
+
* history 객체를 받아서 사용
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import type { BrowserHistory } from 'history';
|
|
7
|
+
interface BrowserRouterProps {
|
|
8
|
+
history: BrowserHistory;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
export declare const BrowserRouter: React.FC<BrowserRouterProps>;
|
|
12
|
+
export default BrowserRouter;
|
|
13
|
+
//# sourceMappingURL=BrowserRouter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BrowserRouter.d.ts","sourceRoot":"","sources":["../../../src/components/router/BrowserRouter.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAoC,MAAM,OAAO,CAAA;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7C,UAAU,kBAAkB;IACxB,OAAO,EAAE,cAAc,CAAA;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmBtD,CAAA;AAED,eAAe,aAAa,CAAA"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom BrowserRouter - KOMCA 패턴
|
|
3
|
+
* history 객체를 받아서 사용
|
|
4
|
+
*/
|
|
5
|
+
import React, { useLayoutEffect, useState } from 'react';
|
|
6
|
+
import { Router } from 'react-router-dom';
|
|
7
|
+
export const BrowserRouter = ({ history, children }) => {
|
|
8
|
+
const [state, setState] = useState({
|
|
9
|
+
action: history.action,
|
|
10
|
+
location: history.location,
|
|
11
|
+
});
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
return history.listen(setState);
|
|
14
|
+
}, [history]);
|
|
15
|
+
return (React.createElement(Router, { location: state.location, navigationType: state.action, navigator: history }, children));
|
|
16
|
+
};
|
|
17
|
+
export default BrowserRouter;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 라우트 가드 컴포넌트
|
|
3
|
+
* 인증/권한 기반 라우팅 보호
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
export interface RouteGuardProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
export interface PrivateRouteProps extends RouteGuardProps {
|
|
10
|
+
/** 미인증 시 리다이렉트 경로 */
|
|
11
|
+
redirectTo?: string;
|
|
12
|
+
/** 필요한 역할 (roles 중 하나라도 있으면 통과) */
|
|
13
|
+
roles?: string[];
|
|
14
|
+
/** 권한 없음 시 리다이렉트 경로 */
|
|
15
|
+
forbiddenRedirectTo?: string;
|
|
16
|
+
/** 로딩 컴포넌트 */
|
|
17
|
+
fallback?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export interface PublicRouteProps extends RouteGuardProps {
|
|
20
|
+
/** 인증된 경우 리다이렉트 경로 */
|
|
21
|
+
redirectTo?: string;
|
|
22
|
+
/** 인증 여부와 관계없이 접근 허용 */
|
|
23
|
+
restricted?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* PrivateRoute - 인증된 사용자만 접근 가능
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <PrivateRoute redirectTo="/login">
|
|
30
|
+
* <Dashboard />
|
|
31
|
+
* </PrivateRoute>
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* <PrivateRoute roles={['admin']} forbiddenRedirectTo="/forbidden">
|
|
35
|
+
* <AdminPanel />
|
|
36
|
+
* </PrivateRoute>
|
|
37
|
+
*/
|
|
38
|
+
export declare const PrivateRoute: React.FC<PrivateRouteProps>;
|
|
39
|
+
/**
|
|
40
|
+
* PublicRoute - 비인증 사용자만 접근 가능 (로그인, 회원가입 등)
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <PublicRoute redirectTo="/dashboard">
|
|
44
|
+
* <LoginPage />
|
|
45
|
+
* </PublicRoute>
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // restricted=false: 인증 여부와 관계없이 접근 가능
|
|
49
|
+
* <PublicRoute restricted={false}>
|
|
50
|
+
* <AboutPage />
|
|
51
|
+
* </PublicRoute>
|
|
52
|
+
*/
|
|
53
|
+
export declare const PublicRoute: React.FC<PublicRouteProps>;
|
|
54
|
+
/**
|
|
55
|
+
* RoleRoute - 특정 역할만 접근 가능
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* <RoleRoute roles={['admin', 'manager']}>
|
|
59
|
+
* <AdminDashboard />
|
|
60
|
+
* </RoleRoute>
|
|
61
|
+
*/
|
|
62
|
+
export declare const RoleRoute: React.FC<{
|
|
63
|
+
children: React.ReactNode;
|
|
64
|
+
roles: string[];
|
|
65
|
+
fallback?: React.ReactNode;
|
|
66
|
+
redirectTo?: string;
|
|
67
|
+
}>;
|
|
68
|
+
declare const _default: {
|
|
69
|
+
PrivateRoute: React.FC<PrivateRouteProps>;
|
|
70
|
+
PublicRoute: React.FC<PublicRouteProps>;
|
|
71
|
+
RoleRoute: React.FC<{
|
|
72
|
+
children: React.ReactNode;
|
|
73
|
+
roles: string[];
|
|
74
|
+
fallback?: React.ReactNode;
|
|
75
|
+
redirectTo?: string;
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
export default _default;
|
|
79
|
+
//# sourceMappingURL=RouteGuard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RouteGuard.d.ts","sourceRoot":"","sources":["../../../src/components/router/RouteGuard.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,MAAM,OAAO,CAAC;AAK1B,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,qBAAqB;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,uBAAuB;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc;IACd,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAiB,SAAQ,eAAe;IACvD,sBAAsB;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB;IACxB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAkCpD,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CAgBlD,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC;IAC/B,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAmBA,CAAC;;;;;kBAvBU,KAAK,CAAC,SAAS;eAClB,MAAM,EAAE;mBACJ,KAAK,CAAC,SAAS;qBACb,MAAM;;;AAsBrB,wBAAwD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 라우트 가드 컴포넌트
|
|
3
|
+
* 인증/권한 기반 라우팅 보호
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { Navigate, useLocation } from 'react-router-dom';
|
|
7
|
+
import { useSelector } from 'react-redux';
|
|
8
|
+
import { selectIsAuthenticated, selectUser } from '../../store/app-store';
|
|
9
|
+
/**
|
|
10
|
+
* PrivateRoute - 인증된 사용자만 접근 가능
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <PrivateRoute redirectTo="/login">
|
|
14
|
+
* <Dashboard />
|
|
15
|
+
* </PrivateRoute>
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <PrivateRoute roles={['admin']} forbiddenRedirectTo="/forbidden">
|
|
19
|
+
* <AdminPanel />
|
|
20
|
+
* </PrivateRoute>
|
|
21
|
+
*/
|
|
22
|
+
export const PrivateRoute = ({ children, redirectTo = '/login', roles, forbiddenRedirectTo = '/forbidden', fallback = null, }) => {
|
|
23
|
+
const location = useLocation();
|
|
24
|
+
const isAuthenticated = useSelector(selectIsAuthenticated);
|
|
25
|
+
const user = useSelector(selectUser);
|
|
26
|
+
// 인증되지 않은 경우
|
|
27
|
+
if (!isAuthenticated) {
|
|
28
|
+
// 현재 경로를 state로 전달하여 로그인 후 복귀 가능하게
|
|
29
|
+
return (React.createElement(Navigate, { to: redirectTo, state: { from: location.pathname + location.search }, replace: true }));
|
|
30
|
+
}
|
|
31
|
+
// 역할 기반 권한 체크
|
|
32
|
+
if (roles && roles.length > 0) {
|
|
33
|
+
const userRole = user?.role;
|
|
34
|
+
const hasRequiredRole = userRole && roles.includes(userRole);
|
|
35
|
+
if (!hasRequiredRole) {
|
|
36
|
+
return React.createElement(Navigate, { to: forbiddenRedirectTo, replace: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return React.createElement(React.Fragment, null, children);
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* PublicRoute - 비인증 사용자만 접근 가능 (로그인, 회원가입 등)
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* <PublicRoute redirectTo="/dashboard">
|
|
46
|
+
* <LoginPage />
|
|
47
|
+
* </PublicRoute>
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // restricted=false: 인증 여부와 관계없이 접근 가능
|
|
51
|
+
* <PublicRoute restricted={false}>
|
|
52
|
+
* <AboutPage />
|
|
53
|
+
* </PublicRoute>
|
|
54
|
+
*/
|
|
55
|
+
export const PublicRoute = ({ children, redirectTo = '/', restricted = true, }) => {
|
|
56
|
+
const location = useLocation();
|
|
57
|
+
const isAuthenticated = useSelector(selectIsAuthenticated);
|
|
58
|
+
// restricted가 true이고 인증된 경우 리다이렉트
|
|
59
|
+
if (restricted && isAuthenticated) {
|
|
60
|
+
// 이전 페이지로 복귀하거나 기본 경로로 이동
|
|
61
|
+
const from = location.state?.from || redirectTo;
|
|
62
|
+
return React.createElement(Navigate, { to: from, replace: true });
|
|
63
|
+
}
|
|
64
|
+
return React.createElement(React.Fragment, null, children);
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* RoleRoute - 특정 역할만 접근 가능
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* <RoleRoute roles={['admin', 'manager']}>
|
|
71
|
+
* <AdminDashboard />
|
|
72
|
+
* </RoleRoute>
|
|
73
|
+
*/
|
|
74
|
+
export const RoleRoute = ({ children, roles, fallback = null, redirectTo, }) => {
|
|
75
|
+
const user = useSelector(selectUser);
|
|
76
|
+
const userRole = user?.role;
|
|
77
|
+
const hasRole = userRole && roles.includes(userRole);
|
|
78
|
+
if (!hasRole) {
|
|
79
|
+
if (redirectTo) {
|
|
80
|
+
return React.createElement(Navigate, { to: redirectTo, replace: true });
|
|
81
|
+
}
|
|
82
|
+
return React.createElement(React.Fragment, null, fallback);
|
|
83
|
+
}
|
|
84
|
+
return React.createElement(React.Fragment, null, children);
|
|
85
|
+
};
|
|
86
|
+
export default { PrivateRoute, PublicRoute, RoleRoute };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/router/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACpE,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
|