@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
@@ -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
- export function LoginPage({ redirectPath = '/', onLoginSuccess, appName = 'MFA', logo, onGoogleLogin, showTestAccount = true, }) {
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
- if (email === 'admin@test.com' && password === '1234') {
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
- setError('이메일 또는 비밀번호가 올바르지 않습니다.');
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
- }, [email, password, store, onLoginSuccess, redirectPath]);
75
- return (_jsxs("div", { className: "login-page", children: [_jsxs("div", { className: "login-bg", children: [_jsx("div", { className: "login-bg-gradient" }), [...Array(12)].map((_, i) => (_jsx("div", { className: `login-particle login-particle--${i + 1}` }, i)))] }), _jsxs("div", { className: "login-card", children: [_jsxs("div", { className: "login-header", children: [_jsx("a", { href: "/", className: "login-logo-link", children: logo || (_jsxs(_Fragment, { children: [_jsx("svg", { viewBox: "0 0 48 48", fill: "none", width: "28", height: "28", children: _jsx("path", { d: "M 8 40 L 24 8 L 40 40", stroke: "#1E3A5F", strokeWidth: "14", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) }), _jsxs("svg", { viewBox: "0 0 48 48", fill: "none", width: "48", height: "48", children: [_jsx("rect", { x: "20", y: "2", width: "8", height: "16", rx: "4", fill: "#0EA5E9" }), _jsx("rect", { x: "6", y: "16", width: "36", height: "6", rx: "3", fill: "#0EA5E9" }), _jsx("ellipse", { cx: "24", cy: "36", rx: "18", ry: "12", fill: "#0EA5E9" }), _jsx("ellipse", { cx: "17", cy: "36", rx: "4", ry: "6", fill: "#FFFFFF" }), _jsx("ellipse", { cx: "31", cy: "36", rx: "4", ry: "6", fill: "#FFFFFF" })] }), _jsx("svg", { viewBox: "0 0 48 48", fill: "none", width: "28", height: "28", children: _jsx("path", { d: "M 8 40 L 24 8 L 40 40", stroke: "#1E3A5F", strokeWidth: "14", strokeLinecap: "round", strokeLinejoin: "round", fill: "none" }) })] })) }), _jsx("h1", { className: "login-title", children: "Welcome Back" }), _jsxs("p", { className: "login-subtitle", children: [appName, "\uC5D0 \uB85C\uADF8\uC778\uD558\uC138\uC694"] })] }), error && (_jsxs("div", { className: "login-error", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("line", { x1: "12", y1: "8", x2: "12", y2: "12" }), _jsx("line", { x1: "12", y1: "16", x2: "12.01", y2: "16" })] }), error] })), onGoogleLogin && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "login-google", onClick: handleGoogleLogin, disabled: isGoogleLoading, children: isGoogleLoading ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "login-spinner login-spinner--dark" }), "\uB85C\uADF8\uC778 \uC911..."] })) : (_jsxs(_Fragment, { children: [_jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", children: [_jsx("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" }), _jsx("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" }), _jsx("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" }), _jsx("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" })] }), "Google\uB85C \uACC4\uC18D\uD558\uAE30"] })) }), _jsx("div", { className: "login-divider", children: _jsx("span", { children: "\uB610\uB294" }) })] })), _jsxs("form", { className: "login-form", onSubmit: handleSubmit, children: [_jsxs("div", { className: `login-input-group ${focusedField === 'email' ? 'focused' : ''}`, children: [_jsx("label", { className: "login-label", children: "\uC774\uBA54\uC77C" }), _jsxs("div", { className: "login-input-wrapper", children: [_jsxs("svg", { className: "login-input-icon", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "2", y: "4", width: "20", height: "16", rx: "2" }), _jsx("path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" })] }), _jsx("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 })] })] }), _jsxs("div", { className: `login-input-group ${focusedField === 'password' ? 'focused' : ''}`, children: [_jsx("label", { className: "login-label", children: "\uBE44\uBC00\uBC88\uD638" }), _jsxs("div", { className: "login-input-wrapper", children: [_jsxs("svg", { className: "login-input-icon", width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [_jsx("rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }), _jsx("path", { d: "M7 11V7a5 5 0 0 1 10 0v4" })] }), _jsx("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 })] })] }), _jsx("button", { type: "submit", className: "login-button", disabled: isSubmitting, children: isSubmitting ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "login-spinner" }), "\uB85C\uADF8\uC778 \uC911..."] })) : (_jsxs(_Fragment, { children: ["\uB85C\uADF8\uC778", _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })) })] }), showTestAccount && (_jsxs("div", { className: "login-test-info", children: [_jsxs("div", { className: "login-test-badge", children: [_jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: _jsx("path", { d: "M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" }) }), "\uD14C\uC2A4\uD2B8 \uACC4\uC815"] }), _jsx("span", { className: "login-test-credentials", children: "admin@test.com / 1234" })] }))] })] }));
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,5 @@
1
+ /**
2
+ * Remote 관련 컴포넌트
3
+ */
4
+ export { RemoteErrorFallback } from './RemoteErrorFallback';
5
+ export { RemoteErrorBoundary } from './RemoteErrorBoundary';
@@ -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,4 @@
1
+ export * from './BrowserRouter';
2
+ export { PrivateRoute, PublicRoute, RoleRoute } from './RouteGuard';
3
+ export type { PrivateRouteProps, PublicRouteProps, RouteGuardProps } from './RouteGuard';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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"}
@@ -0,0 +1,2 @@
1
+ export * from './BrowserRouter';
2
+ export { PrivateRoute, PublicRoute, RoleRoute } from './RouteGuard';