@sonhoseong/mfa-lib 1.2.6 → 1.3.0
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/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +2 -0
- package/dist/components/page/LoginPage.css +414 -0
- package/dist/components/page/LoginPage.d.ts +29 -0
- package/dist/components/page/LoginPage.d.ts.map +1 -0
- package/dist/components/page/LoginPage.js +77 -0
- package/dist/components/page/index.d.ts +2 -0
- package/dist/components/page/index.d.ts.map +1 -0
- package/dist/components/page/index.js +1 -0
- package/dist/store/app-store.d.ts +146 -0
- package/dist/store/app-store.d.ts.map +1 -0
- package/dist/store/app-store.js +140 -0
- package/dist/store/index.d.ts +1 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/utils/storage.d.ts +1 -0
- package/dist/utils/storage.d.ts.map +1 -1
- package/dist/utils/storage.js +3 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AACA,cAAc,UAAU,CAAC;AAGzB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,cAAc,CAAC;AAG7B,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AACA,cAAc,UAAU,CAAC;AAGzB,cAAc,WAAW,CAAC;AAG1B,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,SAAS,CAAC;AAGxB,cAAc,cAAc,CAAC;AAG7B,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/* ============================================
|
|
2
|
+
LoginPage - KOMCA 패턴
|
|
3
|
+
공통 로그인 페이지 스타일
|
|
4
|
+
============================================ */
|
|
5
|
+
|
|
6
|
+
.login-page {
|
|
7
|
+
position: relative;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
min-height: 100vh;
|
|
13
|
+
padding: 40px 24px;
|
|
14
|
+
background: var(--color-bg, #FFFFFF);
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* ===== Background ===== */
|
|
19
|
+
.login-bg {
|
|
20
|
+
position: absolute;
|
|
21
|
+
inset: 0;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
pointer-events: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.login-bg-gradient {
|
|
27
|
+
position: absolute;
|
|
28
|
+
inset: 0;
|
|
29
|
+
background:
|
|
30
|
+
radial-gradient(ellipse at 30% 20%, rgba(14, 165, 233, 0.06) 0%, transparent 50%),
|
|
31
|
+
radial-gradient(ellipse at 70% 80%, rgba(59, 130, 246, 0.05) 0%, transparent 50%),
|
|
32
|
+
radial-gradient(ellipse at 50% 50%, rgba(30, 58, 95, 0.03) 0%, transparent 60%);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Floating Particles */
|
|
36
|
+
.login-particle {
|
|
37
|
+
position: absolute;
|
|
38
|
+
background: #0EA5E9;
|
|
39
|
+
border-radius: 50%;
|
|
40
|
+
box-shadow: 0 0 6px rgba(14, 165, 233, 0.5);
|
|
41
|
+
animation: floatParticle linear infinite;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.login-particle--1 { width: 6px; height: 6px; left: 10%; top: 20%; animation-duration: 20s; }
|
|
45
|
+
.login-particle--2 { width: 10px; height: 10px; left: 20%; top: 60%; animation-duration: 25s; animation-delay: -5s; background: #3B82F6; }
|
|
46
|
+
.login-particle--3 { width: 5px; height: 5px; left: 80%; top: 30%; animation-duration: 18s; animation-delay: -10s; }
|
|
47
|
+
.login-particle--4 { width: 8px; height: 8px; left: 70%; top: 70%; animation-duration: 22s; animation-delay: -3s; background: #1E3A5F; }
|
|
48
|
+
.login-particle--5 { width: 7px; height: 7px; left: 40%; top: 10%; animation-duration: 28s; animation-delay: -8s; }
|
|
49
|
+
.login-particle--6 { width: 12px; height: 12px; left: 90%; top: 50%; animation-duration: 26s; animation-delay: -12s; }
|
|
50
|
+
.login-particle--7 { width: 5px; height: 5px; left: 5%; top: 80%; animation-duration: 21s; animation-delay: -15s; }
|
|
51
|
+
.login-particle--8 { width: 9px; height: 9px; left: 55%; top: 85%; animation-duration: 19s; animation-delay: -7s; background: #3B82F6; }
|
|
52
|
+
.login-particle--9 { width: 6px; height: 6px; left: 30%; top: 40%; animation-duration: 24s; animation-delay: -18s; }
|
|
53
|
+
.login-particle--10 { width: 10px; height: 10px; left: 85%; top: 15%; animation-duration: 20s; animation-delay: -2s; }
|
|
54
|
+
.login-particle--11 { width: 5px; height: 5px; left: 15%; top: 45%; animation-duration: 27s; animation-delay: -20s; }
|
|
55
|
+
.login-particle--12 { width: 8px; height: 8px; left: 60%; top: 25%; animation-duration: 23s; animation-delay: -11s; background: #1E3A5F; }
|
|
56
|
+
|
|
57
|
+
@keyframes floatParticle {
|
|
58
|
+
0% { transform: translate(0, 0) rotate(0deg); opacity: 0; }
|
|
59
|
+
5% { opacity: 1; }
|
|
60
|
+
50% { transform: translate(80px, -120px) rotate(180deg); opacity: 0.8; }
|
|
61
|
+
95% { opacity: 1; }
|
|
62
|
+
100% { transform: translate(160px, -240px) rotate(360deg); opacity: 0; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* ===== Logo ===== */
|
|
66
|
+
.login-logo-link {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: 8px;
|
|
71
|
+
margin-bottom: 24px;
|
|
72
|
+
padding: 12px 20px;
|
|
73
|
+
background: transparent;
|
|
74
|
+
border-radius: 9999px;
|
|
75
|
+
text-decoration: none;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: all 0.3s ease;
|
|
78
|
+
animation: logoPop 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s forwards;
|
|
79
|
+
opacity: 0;
|
|
80
|
+
transform: scale(0.8);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@keyframes logoPop {
|
|
84
|
+
to { opacity: 1; transform: scale(1); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.login-logo-link:hover {
|
|
88
|
+
transform: translateY(-4px) scale(1.05);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ===== Card ===== */
|
|
92
|
+
.login-card {
|
|
93
|
+
position: relative;
|
|
94
|
+
width: 100%;
|
|
95
|
+
max-width: 420px;
|
|
96
|
+
background: white;
|
|
97
|
+
border-radius: 24px;
|
|
98
|
+
padding: 48px 40px;
|
|
99
|
+
box-shadow: 0 4px 24px rgba(30, 58, 95, 0.06), 0 0 0 1px rgba(30, 58, 95, 0.04);
|
|
100
|
+
animation: cardAppear 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.1s forwards;
|
|
101
|
+
opacity: 0;
|
|
102
|
+
transform: translateY(30px) scale(0.98);
|
|
103
|
+
z-index: 5;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@keyframes cardAppear {
|
|
107
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ===== Header ===== */
|
|
111
|
+
.login-header {
|
|
112
|
+
text-align: center;
|
|
113
|
+
margin-bottom: 36px;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.login-title {
|
|
117
|
+
font-size: 28px;
|
|
118
|
+
font-weight: 700;
|
|
119
|
+
color: var(--color-primary, #1E3A5F);
|
|
120
|
+
margin: 0 0 8px;
|
|
121
|
+
letter-spacing: -0.02em;
|
|
122
|
+
animation: fadeUp 0.5s ease 0.4s forwards;
|
|
123
|
+
opacity: 0;
|
|
124
|
+
transform: translateY(10px);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.login-subtitle {
|
|
128
|
+
font-size: 15px;
|
|
129
|
+
color: var(--color-text-secondary, #64748B);
|
|
130
|
+
margin: 0;
|
|
131
|
+
animation: fadeUp 0.5s ease 0.45s forwards;
|
|
132
|
+
opacity: 0;
|
|
133
|
+
transform: translateY(10px);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@keyframes fadeUp {
|
|
137
|
+
to { opacity: 1; transform: translateY(0); }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ===== Google Login ===== */
|
|
141
|
+
.login-google {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: center;
|
|
145
|
+
gap: 10px;
|
|
146
|
+
width: 100%;
|
|
147
|
+
height: 52px;
|
|
148
|
+
font-family: inherit;
|
|
149
|
+
font-size: 15px;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: var(--color-text, #1E3A5F);
|
|
152
|
+
background: white;
|
|
153
|
+
border: 1.5px solid var(--color-border, #E2E8F0);
|
|
154
|
+
border-radius: 12px;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
transition: all 0.2s ease;
|
|
157
|
+
animation: fadeUp 0.5s ease 0.5s forwards;
|
|
158
|
+
opacity: 0;
|
|
159
|
+
transform: translateY(10px);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.login-google:hover {
|
|
163
|
+
background: var(--color-bg-secondary, #F8FAFC);
|
|
164
|
+
border-color: #cbd5e1;
|
|
165
|
+
transform: translateY(-2px);
|
|
166
|
+
box-shadow: 0 4px 12px rgba(30, 58, 95, 0.08);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.login-google:disabled {
|
|
170
|
+
opacity: 0.7;
|
|
171
|
+
cursor: not-allowed;
|
|
172
|
+
transform: none;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ===== Divider ===== */
|
|
176
|
+
.login-divider {
|
|
177
|
+
display: flex;
|
|
178
|
+
align-items: center;
|
|
179
|
+
gap: 16px;
|
|
180
|
+
margin: 24px 0;
|
|
181
|
+
animation: fadeUp 0.5s ease 0.55s forwards;
|
|
182
|
+
opacity: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.login-divider::before,
|
|
186
|
+
.login-divider::after {
|
|
187
|
+
content: '';
|
|
188
|
+
flex: 1;
|
|
189
|
+
height: 1px;
|
|
190
|
+
background: var(--color-border, #E2E8F0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.login-divider span {
|
|
194
|
+
font-size: 13px;
|
|
195
|
+
font-weight: 500;
|
|
196
|
+
color: var(--color-text-muted, #94A3B8);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ===== Form ===== */
|
|
200
|
+
.login-form {
|
|
201
|
+
display: flex;
|
|
202
|
+
flex-direction: column;
|
|
203
|
+
gap: 20px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.login-input-group {
|
|
207
|
+
animation: fadeUp 0.5s ease forwards;
|
|
208
|
+
opacity: 0;
|
|
209
|
+
transform: translateY(10px);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.login-input-group:nth-child(1) { animation-delay: 0.5s; }
|
|
213
|
+
.login-input-group:nth-child(2) { animation-delay: 0.55s; }
|
|
214
|
+
.login-input-group:nth-child(3) { animation-delay: 0.6s; }
|
|
215
|
+
|
|
216
|
+
.login-label {
|
|
217
|
+
display: block;
|
|
218
|
+
font-size: 13px;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
color: var(--color-text-secondary, #64748B);
|
|
221
|
+
margin-bottom: 8px;
|
|
222
|
+
transition: color 0.2s ease;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.login-input-group.focused .login-label {
|
|
226
|
+
color: var(--color-accent, #0EA5E9);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.login-input-wrapper {
|
|
230
|
+
position: relative;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.login-input-icon {
|
|
234
|
+
position: absolute;
|
|
235
|
+
left: 16px;
|
|
236
|
+
top: 50%;
|
|
237
|
+
transform: translateY(-50%);
|
|
238
|
+
color: var(--color-text-muted, #94A3B8);
|
|
239
|
+
transition: color 0.2s ease;
|
|
240
|
+
pointer-events: none;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.login-input-group.focused .login-input-icon {
|
|
244
|
+
color: var(--color-accent, #0EA5E9);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.login-input {
|
|
248
|
+
width: 100%;
|
|
249
|
+
height: 52px;
|
|
250
|
+
padding: 0 16px 0 48px;
|
|
251
|
+
font-family: inherit;
|
|
252
|
+
font-size: 15px;
|
|
253
|
+
color: var(--color-text, #1E3A5F);
|
|
254
|
+
background: var(--color-bg-secondary, #F8FAFC);
|
|
255
|
+
border: 1.5px solid var(--color-border, #E2E8F0);
|
|
256
|
+
border-radius: 12px;
|
|
257
|
+
outline: none;
|
|
258
|
+
transition: all 0.2s ease;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.login-input::placeholder {
|
|
262
|
+
color: var(--color-text-muted, #94A3B8);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.login-input:hover {
|
|
266
|
+
border-color: #cbd5e1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.login-input:focus {
|
|
270
|
+
background: white;
|
|
271
|
+
border-color: var(--color-accent, #0EA5E9);
|
|
272
|
+
box-shadow: 0 0 0 4px rgba(14, 165, 233, 0.1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ===== Error ===== */
|
|
276
|
+
.login-error {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 10px;
|
|
280
|
+
padding: 14px 16px;
|
|
281
|
+
background: #fef2f2;
|
|
282
|
+
border: 1px solid #fecaca;
|
|
283
|
+
border-radius: 12px;
|
|
284
|
+
color: #dc2626;
|
|
285
|
+
font-size: 14px;
|
|
286
|
+
font-weight: 500;
|
|
287
|
+
margin-bottom: 16px;
|
|
288
|
+
animation: shake 0.4s ease, fadeUp 0.3s ease;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
@keyframes shake {
|
|
292
|
+
0%, 100% { transform: translateX(0); }
|
|
293
|
+
20%, 60% { transform: translateX(-4px); }
|
|
294
|
+
40%, 80% { transform: translateX(4px); }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* ===== Button ===== */
|
|
298
|
+
.login-button {
|
|
299
|
+
display: flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
gap: 8px;
|
|
303
|
+
height: 52px;
|
|
304
|
+
margin-top: 8px;
|
|
305
|
+
font-family: inherit;
|
|
306
|
+
font-size: 16px;
|
|
307
|
+
font-weight: 600;
|
|
308
|
+
color: white;
|
|
309
|
+
background: var(--color-primary, #1E3A5F);
|
|
310
|
+
border: none;
|
|
311
|
+
border-radius: 12px;
|
|
312
|
+
cursor: pointer;
|
|
313
|
+
transition: all 0.3s ease;
|
|
314
|
+
animation: fadeUp 0.5s ease 0.65s forwards;
|
|
315
|
+
opacity: 0;
|
|
316
|
+
transform: translateY(10px);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.login-button:hover:not(:disabled) {
|
|
320
|
+
background: var(--color-accent, #0EA5E9);
|
|
321
|
+
transform: translateY(-2px);
|
|
322
|
+
box-shadow: 0 8px 24px rgba(14, 165, 233, 0.3);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.login-button:disabled {
|
|
326
|
+
opacity: 0.7;
|
|
327
|
+
cursor: not-allowed;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.login-button svg {
|
|
331
|
+
transition: transform 0.2s ease;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.login-button:hover:not(:disabled) svg {
|
|
335
|
+
transform: translateX(4px);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/* ===== Spinner ===== */
|
|
339
|
+
.login-spinner {
|
|
340
|
+
width: 18px;
|
|
341
|
+
height: 18px;
|
|
342
|
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
343
|
+
border-top-color: white;
|
|
344
|
+
border-radius: 50%;
|
|
345
|
+
animation: spin 0.7s linear infinite;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.login-spinner--dark {
|
|
349
|
+
border-color: rgba(30, 58, 95, 0.2);
|
|
350
|
+
border-top-color: var(--color-primary, #1E3A5F);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@keyframes spin {
|
|
354
|
+
to { transform: rotate(360deg); }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ===== Test Account ===== */
|
|
358
|
+
.login-test-info {
|
|
359
|
+
display: flex;
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
align-items: center;
|
|
362
|
+
gap: 8px;
|
|
363
|
+
margin-top: 28px;
|
|
364
|
+
padding-top: 24px;
|
|
365
|
+
border-top: 1px solid var(--color-border, #E2E8F0);
|
|
366
|
+
animation: fadeUp 0.5s ease 0.7s forwards;
|
|
367
|
+
opacity: 0;
|
|
368
|
+
transform: translateY(10px);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.login-test-badge {
|
|
372
|
+
display: inline-flex;
|
|
373
|
+
align-items: center;
|
|
374
|
+
gap: 6px;
|
|
375
|
+
padding: 6px 12px;
|
|
376
|
+
background: linear-gradient(135deg, var(--color-primary, #1E3A5F), var(--color-accent, #0EA5E9));
|
|
377
|
+
border-radius: 9999px;
|
|
378
|
+
font-size: 11px;
|
|
379
|
+
font-weight: 600;
|
|
380
|
+
color: white;
|
|
381
|
+
text-transform: uppercase;
|
|
382
|
+
letter-spacing: 0.05em;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.login-test-credentials {
|
|
386
|
+
font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
|
|
387
|
+
font-size: 14px;
|
|
388
|
+
color: var(--color-text-secondary, #64748B);
|
|
389
|
+
background: var(--color-bg-secondary, #F8FAFC);
|
|
390
|
+
padding: 8px 16px;
|
|
391
|
+
border-radius: 8px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ===== Responsive ===== */
|
|
395
|
+
@media (max-width: 480px) {
|
|
396
|
+
.login-page {
|
|
397
|
+
padding: 24px 16px;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.login-card {
|
|
401
|
+
padding: 36px 24px;
|
|
402
|
+
border-radius: 20px;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.login-title {
|
|
406
|
+
font-size: 24px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.login-input,
|
|
410
|
+
.login-button,
|
|
411
|
+
.login-google {
|
|
412
|
+
height: 48px;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoginPage - KOMCA 패턴
|
|
3
|
+
*
|
|
4
|
+
* 공통 로그인 페이지 컴포넌트
|
|
5
|
+
* Host/Remote 모두에서 사용 가능
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
import { User } from '../../types';
|
|
9
|
+
import './LoginPage.css';
|
|
10
|
+
export interface LoginPageProps {
|
|
11
|
+
/** 로그인 성공 후 이동할 경로 (기본: /) */
|
|
12
|
+
redirectPath?: string;
|
|
13
|
+
/** 로그인 성공 콜백 */
|
|
14
|
+
onLoginSuccess?: (user: User) => void;
|
|
15
|
+
/** 앱 이름 (로고 옆에 표시) */
|
|
16
|
+
appName?: string;
|
|
17
|
+
/** 커스텀 로고 컴포넌트 */
|
|
18
|
+
logo?: React.ReactNode;
|
|
19
|
+
/** Google 로그인 핸들러 (Firebase 등) */
|
|
20
|
+
onGoogleLogin?: () => Promise<{
|
|
21
|
+
token: string;
|
|
22
|
+
user: User;
|
|
23
|
+
}>;
|
|
24
|
+
/** 테스트 계정 표시 여부 */
|
|
25
|
+
showTestAccount?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function LoginPage({ redirectPath, onLoginSuccess, appName, logo, onGoogleLogin, showTestAccount, }: LoginPageProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export default LoginPage;
|
|
29
|
+
//# sourceMappingURL=LoginPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoginPage.d.ts","sourceRoot":"","sources":["../../../src/components/page/LoginPage.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;AAErD,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AACnC,OAAO,iBAAiB,CAAC;AAEzB,MAAM,WAAW,cAAc;IAC3B,8BAA8B;IAC9B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB;IAChB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACtC,sBAAsB;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kBAAkB;IAClB,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,kCAAkC;IAClC,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IAC7D,mBAAmB;IACnB,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAgB,SAAS,CAAC,EACtB,YAAkB,EAClB,cAAc,EACd,OAAe,EACf,IAAI,EACJ,aAAa,EACb,eAAsB,GACzB,EAAE,cAAc,2CAgOhB;AAED,eAAe,SAAS,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* LoginPage - KOMCA 패턴
|
|
4
|
+
*
|
|
5
|
+
* 공통 로그인 페이지 컴포넌트
|
|
6
|
+
* Host/Remote 모두에서 사용 가능
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useCallback } from 'react';
|
|
9
|
+
import { getStore, setAccessToken, setUser } from '../../store/app-store';
|
|
10
|
+
import './LoginPage.css';
|
|
11
|
+
export function LoginPage({ redirectPath = '/', onLoginSuccess, appName = 'MFA', logo, onGoogleLogin, showTestAccount = true, }) {
|
|
12
|
+
const [email, setEmail] = useState('');
|
|
13
|
+
const [password, setPassword] = useState('');
|
|
14
|
+
const [error, setError] = useState('');
|
|
15
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
16
|
+
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
|
17
|
+
const [focusedField, setFocusedField] = useState(null);
|
|
18
|
+
const store = getStore();
|
|
19
|
+
const handleGoogleLogin = useCallback(async () => {
|
|
20
|
+
if (!onGoogleLogin) {
|
|
21
|
+
setError('Google 로그인이 설정되지 않았습니다.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
setError('');
|
|
25
|
+
setIsGoogleLoading(true);
|
|
26
|
+
try {
|
|
27
|
+
const { token, user } = await onGoogleLogin();
|
|
28
|
+
store.dispatch(setAccessToken(token));
|
|
29
|
+
store.dispatch(setUser(user));
|
|
30
|
+
onLoginSuccess?.(user);
|
|
31
|
+
// 페이지 이동
|
|
32
|
+
window.location.href = redirectPath;
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err?.code === 'auth/popup-closed-by-user') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setError('Google 로그인에 실패했습니다.');
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
setIsGoogleLoading(false);
|
|
42
|
+
}
|
|
43
|
+
}, [onGoogleLogin, store, onLoginSuccess, redirectPath]);
|
|
44
|
+
const handleSubmit = useCallback(async (e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
setError('');
|
|
47
|
+
setIsSubmitting(true);
|
|
48
|
+
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;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
setError('이메일 또는 비밀번호가 올바르지 않습니다.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
setError('로그인 중 오류가 발생했습니다.');
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
setIsSubmitting(false);
|
|
73
|
+
}
|
|
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" })] }))] })] }));
|
|
76
|
+
}
|
|
77
|
+
export default LoginPage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/page/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './LoginPage';
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store - KOMCA 패턴
|
|
3
|
+
*
|
|
4
|
+
* Host/Remote 모두에서 사용할 수 있는 Store 설정
|
|
5
|
+
* - Host: 자체 store 생성 후 window.__REDUX_STORE__에 노출
|
|
6
|
+
* - Remote (standalone): 자체 store 생성
|
|
7
|
+
* - Remote (in Host): window.__REDUX_STORE__ 사용
|
|
8
|
+
*/
|
|
9
|
+
import { configureStore, PayloadAction, Reducer } from '@reduxjs/toolkit';
|
|
10
|
+
import { User, AppState } from '../types';
|
|
11
|
+
export declare const appSlice: import("@reduxjs/toolkit").Slice<AppState, {
|
|
12
|
+
setAccessToken: (state: {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
user: {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
email: string;
|
|
18
|
+
role?: "admin" | "user" | undefined;
|
|
19
|
+
} | null;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
globalLoadingTitle: string;
|
|
22
|
+
service: string;
|
|
23
|
+
selectedGnb: string;
|
|
24
|
+
}, action: PayloadAction<string>) => void;
|
|
25
|
+
setUser: (state: {
|
|
26
|
+
accessToken: string;
|
|
27
|
+
user: {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
email: string;
|
|
31
|
+
role?: "admin" | "user" | undefined;
|
|
32
|
+
} | null;
|
|
33
|
+
isLoading: boolean;
|
|
34
|
+
globalLoadingTitle: string;
|
|
35
|
+
service: string;
|
|
36
|
+
selectedGnb: string;
|
|
37
|
+
}, action: PayloadAction<User | null>) => void;
|
|
38
|
+
setLoading: (state: {
|
|
39
|
+
accessToken: string;
|
|
40
|
+
user: {
|
|
41
|
+
id: string;
|
|
42
|
+
name: string;
|
|
43
|
+
email: string;
|
|
44
|
+
role?: "admin" | "user" | undefined;
|
|
45
|
+
} | null;
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
globalLoadingTitle: string;
|
|
48
|
+
service: string;
|
|
49
|
+
selectedGnb: string;
|
|
50
|
+
}, action: PayloadAction<boolean>) => void;
|
|
51
|
+
setGlobalLoadingTitle: (state: {
|
|
52
|
+
accessToken: string;
|
|
53
|
+
user: {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
email: string;
|
|
57
|
+
role?: "admin" | "user" | undefined;
|
|
58
|
+
} | null;
|
|
59
|
+
isLoading: boolean;
|
|
60
|
+
globalLoadingTitle: string;
|
|
61
|
+
service: string;
|
|
62
|
+
selectedGnb: string;
|
|
63
|
+
}, action: PayloadAction<string>) => void;
|
|
64
|
+
setService: (state: {
|
|
65
|
+
accessToken: string;
|
|
66
|
+
user: {
|
|
67
|
+
id: string;
|
|
68
|
+
name: string;
|
|
69
|
+
email: string;
|
|
70
|
+
role?: "admin" | "user" | undefined;
|
|
71
|
+
} | null;
|
|
72
|
+
isLoading: boolean;
|
|
73
|
+
globalLoadingTitle: string;
|
|
74
|
+
service: string;
|
|
75
|
+
selectedGnb: string;
|
|
76
|
+
}, action: PayloadAction<string>) => void;
|
|
77
|
+
setSelectedGnb: (state: {
|
|
78
|
+
accessToken: string;
|
|
79
|
+
user: {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
email: string;
|
|
83
|
+
role?: "admin" | "user" | undefined;
|
|
84
|
+
} | null;
|
|
85
|
+
isLoading: boolean;
|
|
86
|
+
globalLoadingTitle: string;
|
|
87
|
+
service: string;
|
|
88
|
+
selectedGnb: string;
|
|
89
|
+
}, action: PayloadAction<string>) => void;
|
|
90
|
+
logout: (state: {
|
|
91
|
+
accessToken: string;
|
|
92
|
+
user: {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
email: string;
|
|
96
|
+
role?: "admin" | "user" | undefined;
|
|
97
|
+
} | null;
|
|
98
|
+
isLoading: boolean;
|
|
99
|
+
globalLoadingTitle: string;
|
|
100
|
+
service: string;
|
|
101
|
+
selectedGnb: string;
|
|
102
|
+
}) => void;
|
|
103
|
+
}, "app", "app", import("@reduxjs/toolkit").SliceSelectors<AppState>>;
|
|
104
|
+
export declare const setAccessToken: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setAccessToken">, setUser: import("@reduxjs/toolkit").ActionCreatorWithPayload<User | null, "app/setUser">, setLoading: import("@reduxjs/toolkit").ActionCreatorWithPayload<boolean, "app/setLoading">, setGlobalLoadingTitle: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setGlobalLoadingTitle">, setService: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setService">, setSelectedGnb: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "app/setSelectedGnb">, logout: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"app/logout">;
|
|
105
|
+
export declare const selectAccessToken: (state: {
|
|
106
|
+
app: AppState;
|
|
107
|
+
}) => string;
|
|
108
|
+
export declare const selectUser: (state: {
|
|
109
|
+
app: AppState;
|
|
110
|
+
}) => User | null;
|
|
111
|
+
export declare const selectIsLoading: (state: {
|
|
112
|
+
app: AppState;
|
|
113
|
+
}) => boolean;
|
|
114
|
+
export declare const selectIsAuthenticated: (state: {
|
|
115
|
+
app: AppState;
|
|
116
|
+
}) => boolean;
|
|
117
|
+
/**
|
|
118
|
+
* App Store 생성
|
|
119
|
+
* Host 또는 Remote 단독 실행 시 호출
|
|
120
|
+
*/
|
|
121
|
+
export declare const createAppStore: () => import("@reduxjs/toolkit").EnhancedStore<{
|
|
122
|
+
app: AppState;
|
|
123
|
+
}, import("redux").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("redux").StoreEnhancer<{
|
|
124
|
+
dispatch: import("redux-thunk").ThunkDispatch<{
|
|
125
|
+
app: AppState;
|
|
126
|
+
}, undefined, import("redux").UnknownAction>;
|
|
127
|
+
}>, import("redux").StoreEnhancer]>>;
|
|
128
|
+
/**
|
|
129
|
+
* Store 가져오기
|
|
130
|
+
* - Host App: 자신의 store 반환
|
|
131
|
+
* - Remote (standalone): 자신의 store 반환
|
|
132
|
+
* - Remote (in Host): window.__REDUX_STORE__ 반환
|
|
133
|
+
*/
|
|
134
|
+
export declare const getStore: () => import("../types").HostStore | import("redux").Store<unknown, import("redux").Action, unknown>;
|
|
135
|
+
/**
|
|
136
|
+
* 동적 Reducer 주입
|
|
137
|
+
*/
|
|
138
|
+
export declare const injectReducer: (key: string, reducer: Reducer) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Store를 전역에 노출 (Host App용)
|
|
141
|
+
*/
|
|
142
|
+
export declare const exposeStore: (store: ReturnType<typeof configureStore>) => void;
|
|
143
|
+
export type AppStore = ReturnType<typeof createAppStore>;
|
|
144
|
+
export type RootState = ReturnType<AppStore['getState']>;
|
|
145
|
+
export type AppDispatch = AppStore['dispatch'];
|
|
146
|
+
//# sourceMappingURL=app-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-store.d.ts","sourceRoot":"","sources":["../../src/store/app-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,cAAc,EAAgC,aAAa,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAExG,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAe1C,eAAO,MAAM,QAAQ;;;;;;;;;;;;;eAImB,aAAa,CAAC,MAAM,CAAC;;;;;;;;;;;;;eAK5B,aAAa,CAAC,IAAI,GAAG,IAAI,CAAC;;;;;;;;;;;;;eAKvB,aAAa,CAAC,OAAO,CAAC;;;;;;;;;;;;;eAGX,aAAa,CAAC,MAAM,CAAC;;;;;;;;;;;;;eAGhC,aAAa,CAAC,MAAM,CAAC;;;;;;;;;;;;;eAGjB,aAAa,CAAC,MAAM,CAAC;;;;;;;;;;;;;;qEAS3D,CAAC;AAEH,eAAO,MACH,cAAc,qFACd,OAAO,mFACP,UAAU,kFACV,qBAAqB,4FACrB,UAAU,iFACV,cAAc,qFACd,MAAM,sEACU,CAAC;AAGrB,eAAO,MAAM,iBAAiB,GAAI,OAAO;IAAE,GAAG,EAAE,QAAQ,CAAA;CAAE,WAA0B,CAAC;AACrF,eAAO,MAAM,UAAU,GAAI,OAAO;IAAE,GAAG,EAAE,QAAQ,CAAA;CAAE,gBAAmB,CAAC;AACvE,eAAO,MAAM,eAAe,GAAI,OAAO;IAAE,GAAG,EAAE,QAAQ,CAAA;CAAE,YAAwB,CAAC;AACjF,eAAO,MAAM,qBAAqB,GAAI,OAAO;IAAE,GAAG,EAAE,QAAQ,CAAA;CAAE,YAA4B,CAAC;AAuB3F;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;oCAmB1B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,sGAkBpB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,EAAE,SAAS,OAAO,SAU1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,UAAU,CAAC,OAAO,cAAc,CAAC,SAGnE,CAAC;AAGF,MAAM,MAAM,QAAQ,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACzD,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC;AACzD,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store - KOMCA 패턴
|
|
3
|
+
*
|
|
4
|
+
* Host/Remote 모두에서 사용할 수 있는 Store 설정
|
|
5
|
+
* - Host: 자체 store 생성 후 window.__REDUX_STORE__에 노출
|
|
6
|
+
* - Remote (standalone): 자체 store 생성
|
|
7
|
+
* - Remote (in Host): window.__REDUX_STORE__ 사용
|
|
8
|
+
*/
|
|
9
|
+
import { configureStore, combineReducers, createSlice } from '@reduxjs/toolkit';
|
|
10
|
+
import { storage } from '../utils/storage';
|
|
11
|
+
// ============================================
|
|
12
|
+
// App Slice (인증 상태 관리)
|
|
13
|
+
// ============================================
|
|
14
|
+
const initialAppState = {
|
|
15
|
+
accessToken: '',
|
|
16
|
+
user: null,
|
|
17
|
+
isLoading: false,
|
|
18
|
+
globalLoadingTitle: '',
|
|
19
|
+
service: '',
|
|
20
|
+
selectedGnb: '',
|
|
21
|
+
};
|
|
22
|
+
export const appSlice = createSlice({
|
|
23
|
+
name: 'app',
|
|
24
|
+
initialState: initialAppState,
|
|
25
|
+
reducers: {
|
|
26
|
+
setAccessToken: (state, action) => {
|
|
27
|
+
state.accessToken = action.payload;
|
|
28
|
+
// localStorage에도 저장
|
|
29
|
+
storage.setAccessToken(action.payload);
|
|
30
|
+
},
|
|
31
|
+
setUser: (state, action) => {
|
|
32
|
+
state.user = action.payload;
|
|
33
|
+
// localStorage에도 저장
|
|
34
|
+
storage.setUser(action.payload);
|
|
35
|
+
},
|
|
36
|
+
setLoading: (state, action) => {
|
|
37
|
+
state.isLoading = action.payload;
|
|
38
|
+
},
|
|
39
|
+
setGlobalLoadingTitle: (state, action) => {
|
|
40
|
+
state.globalLoadingTitle = action.payload;
|
|
41
|
+
},
|
|
42
|
+
setService: (state, action) => {
|
|
43
|
+
state.service = action.payload;
|
|
44
|
+
},
|
|
45
|
+
setSelectedGnb: (state, action) => {
|
|
46
|
+
state.selectedGnb = action.payload;
|
|
47
|
+
},
|
|
48
|
+
logout: (state) => {
|
|
49
|
+
state.accessToken = '';
|
|
50
|
+
state.user = null;
|
|
51
|
+
storage.clearAuth();
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export const { setAccessToken, setUser, setLoading, setGlobalLoadingTitle, setService, setSelectedGnb, logout, } = appSlice.actions;
|
|
56
|
+
// Selectors
|
|
57
|
+
export const selectAccessToken = (state) => state.app.accessToken;
|
|
58
|
+
export const selectUser = (state) => state.app.user;
|
|
59
|
+
export const selectIsLoading = (state) => state.app.isLoading;
|
|
60
|
+
export const selectIsAuthenticated = (state) => !!state.app.accessToken;
|
|
61
|
+
// ============================================
|
|
62
|
+
// Store 생성
|
|
63
|
+
// ============================================
|
|
64
|
+
// 동적 Reducer 저장소
|
|
65
|
+
let dynamicReducers = {};
|
|
66
|
+
// 기본 Reducer
|
|
67
|
+
const staticReducers = {
|
|
68
|
+
app: appSlice.reducer,
|
|
69
|
+
};
|
|
70
|
+
// Root Reducer 생성
|
|
71
|
+
const createRootReducer = () => combineReducers({
|
|
72
|
+
...staticReducers,
|
|
73
|
+
...dynamicReducers,
|
|
74
|
+
});
|
|
75
|
+
// Store 인스턴스 (단독 실행용)
|
|
76
|
+
let storeInstance = null;
|
|
77
|
+
/**
|
|
78
|
+
* App Store 생성
|
|
79
|
+
* Host 또는 Remote 단독 실행 시 호출
|
|
80
|
+
*/
|
|
81
|
+
export const createAppStore = () => {
|
|
82
|
+
const store = configureStore({
|
|
83
|
+
reducer: createRootReducer(),
|
|
84
|
+
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
|
|
85
|
+
serializableCheck: false,
|
|
86
|
+
}),
|
|
87
|
+
// localStorage에서 초기 상태 복원
|
|
88
|
+
preloadedState: {
|
|
89
|
+
app: {
|
|
90
|
+
...initialAppState,
|
|
91
|
+
accessToken: storage.getAccessToken(),
|
|
92
|
+
user: storage.getUser(),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
storeInstance = store;
|
|
97
|
+
return store;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Store 가져오기
|
|
101
|
+
* - Host App: 자신의 store 반환
|
|
102
|
+
* - Remote (standalone): 자신의 store 반환
|
|
103
|
+
* - Remote (in Host): window.__REDUX_STORE__ 반환
|
|
104
|
+
*/
|
|
105
|
+
export const getStore = () => {
|
|
106
|
+
// Host App인 경우 window.__REDUX_STORE__ 사용
|
|
107
|
+
if (storage.isHostApp() && window.__REDUX_STORE__) {
|
|
108
|
+
return window.__REDUX_STORE__;
|
|
109
|
+
}
|
|
110
|
+
// Remote가 Host 내에서 실행중인 경우
|
|
111
|
+
if (window.__REDUX_STORE__) {
|
|
112
|
+
return window.__REDUX_STORE__;
|
|
113
|
+
}
|
|
114
|
+
// 단독 실행 (store가 이미 생성되어 있으면 반환)
|
|
115
|
+
if (storeInstance) {
|
|
116
|
+
return storeInstance;
|
|
117
|
+
}
|
|
118
|
+
// 아직 store가 없으면 생성
|
|
119
|
+
return createAppStore();
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* 동적 Reducer 주입
|
|
123
|
+
*/
|
|
124
|
+
export const injectReducer = (key, reducer) => {
|
|
125
|
+
if (dynamicReducers[key]) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
dynamicReducers[key] = reducer;
|
|
129
|
+
const store = getStore();
|
|
130
|
+
if (store && 'replaceReducer' in store) {
|
|
131
|
+
store.replaceReducer(createRootReducer());
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
/**
|
|
135
|
+
* Store를 전역에 노출 (Host App용)
|
|
136
|
+
*/
|
|
137
|
+
export const exposeStore = (store) => {
|
|
138
|
+
storage.setHostApp();
|
|
139
|
+
window.__REDUX_STORE__ = store;
|
|
140
|
+
};
|
package/dist/store/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/store/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
package/dist/store/index.js
CHANGED
package/dist/utils/storage.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG5C,eAAO,MAAM,YAAY;;;;;CAKxB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/utils/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG5C,eAAO,MAAM,YAAY;;;;;CAKxB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO;;;;0BAeE,MAAM;4BAIF,MAAM;mBASjB,IAAI,GAAG,IAAI;oBASR,IAAI,GAAG,IAAI;yBASR,UAAU,EAAE;0BAST,UAAU,EAAE;;;CAiBnC,CAAC"}
|
package/dist/utils/storage.js
CHANGED
|
@@ -17,6 +17,9 @@ export const storage = {
|
|
|
17
17
|
setHostApp: () => {
|
|
18
18
|
sessionStorage.setItem(STORAGE_KEYS.IS_HOST_APP, 'true');
|
|
19
19
|
},
|
|
20
|
+
removeHostApp: () => {
|
|
21
|
+
sessionStorage.removeItem(STORAGE_KEYS.IS_HOST_APP);
|
|
22
|
+
},
|
|
20
23
|
isHostApp: () => {
|
|
21
24
|
return sessionStorage.getItem(STORAGE_KEYS.IS_HOST_APP) === 'true';
|
|
22
25
|
},
|