viewlogic 1.2.6 → 1.2.7
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/README.md +35 -7
- package/dist/viewlogic-router.esm.js +4 -0
- package/dist/{viewlogic-router.js.map → viewlogic-router.esm.js.map} +3 -3
- package/dist/viewlogic-router.min.js +9 -52
- package/dist/viewlogic-router.min.js.map +4 -4
- package/package.json +7 -9
- package/dist/viewlogic-router.js +0 -2984
- package/dist/viewlogic-router.umd.js +0 -139
package/dist/viewlogic-router.js
DELETED
|
@@ -1,2984 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ViewLogic v1.0.0
|
|
3
|
-
* (c) 2024 hopegiver
|
|
4
|
-
* @license MIT
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
// src/plugins/I18nManager.js
|
|
8
|
-
var I18nManager = class {
|
|
9
|
-
constructor(router, options = {}) {
|
|
10
|
-
this.config = {
|
|
11
|
-
enabled: options.useI18n !== void 0 ? options.useI18n : true,
|
|
12
|
-
defaultLanguage: options.defaultLanguage || "en",
|
|
13
|
-
fallbackLanguage: options.defaultLanguage || "en"
|
|
14
|
-
};
|
|
15
|
-
this.router = router;
|
|
16
|
-
this.messages = /* @__PURE__ */ new Map();
|
|
17
|
-
this.currentLanguage = this.config.defaultLanguage;
|
|
18
|
-
this.isLoading = false;
|
|
19
|
-
this.loadPromises = /* @__PURE__ */ new Map();
|
|
20
|
-
this.listeners = {
|
|
21
|
-
languageChanged: []
|
|
22
|
-
};
|
|
23
|
-
this.initPromise = this.init();
|
|
24
|
-
}
|
|
25
|
-
async init() {
|
|
26
|
-
if (!this.config.enabled) {
|
|
27
|
-
this.log("info", "I18n system disabled");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
this.loadLanguageFromCache();
|
|
31
|
-
if (!this.messages.has(this.currentLanguage)) {
|
|
32
|
-
try {
|
|
33
|
-
await this.loadMessages(this.currentLanguage);
|
|
34
|
-
} catch (error) {
|
|
35
|
-
this.log("error", "Failed to load initial language file:", error);
|
|
36
|
-
this.messages.set(this.currentLanguage, {});
|
|
37
|
-
this.log("info", "Using empty message object as fallback");
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
this.log("debug", "Language messages already loaded:", this.currentLanguage);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* 캐시에서 언어 설정 로드
|
|
45
|
-
*/
|
|
46
|
-
loadLanguageFromCache() {
|
|
47
|
-
try {
|
|
48
|
-
const cachedLang = this.router.cacheManager?.get("viewlogic_lang");
|
|
49
|
-
if (cachedLang && this.isValidLanguage(cachedLang)) {
|
|
50
|
-
this.currentLanguage = cachedLang;
|
|
51
|
-
this.log("debug", "Language loaded from cache:", cachedLang);
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
this.log("warn", "Failed to load language from cache:", error);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* 언어 유효성 검사
|
|
59
|
-
*/
|
|
60
|
-
isValidLanguage(lang) {
|
|
61
|
-
return typeof lang === "string" && /^[a-z]{2}$/.test(lang);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* 현재 언어 반환
|
|
65
|
-
*/
|
|
66
|
-
getCurrentLanguage() {
|
|
67
|
-
return this.currentLanguage;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* 언어 변경
|
|
71
|
-
*/
|
|
72
|
-
async setLanguage(language) {
|
|
73
|
-
if (!this.isValidLanguage(language)) {
|
|
74
|
-
this.log("warn", "Invalid language code:", language);
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
if (this.currentLanguage === language) {
|
|
78
|
-
this.log("debug", "Language already set to:", language);
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
const oldLanguage = this.currentLanguage;
|
|
82
|
-
this.currentLanguage = language;
|
|
83
|
-
try {
|
|
84
|
-
await this.loadMessages(language);
|
|
85
|
-
this.saveLanguageToCache(language);
|
|
86
|
-
this.emit("languageChanged", {
|
|
87
|
-
from: oldLanguage,
|
|
88
|
-
to: language,
|
|
89
|
-
messages: this.messages.get(language)
|
|
90
|
-
});
|
|
91
|
-
this.log("info", "Language changed successfully", { from: oldLanguage, to: language });
|
|
92
|
-
return true;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
this.log("error", "Failed to load messages for language change, using empty messages:", error);
|
|
95
|
-
this.messages.set(language, {});
|
|
96
|
-
this.saveLanguageToCache(language);
|
|
97
|
-
this.emit("languageChanged", {
|
|
98
|
-
from: oldLanguage,
|
|
99
|
-
to: language,
|
|
100
|
-
messages: {},
|
|
101
|
-
error: true
|
|
102
|
-
});
|
|
103
|
-
this.log("warn", "Language changed with empty messages", { from: oldLanguage, to: language });
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* 언어를 캐시에 저장
|
|
109
|
-
*/
|
|
110
|
-
saveLanguageToCache(language) {
|
|
111
|
-
try {
|
|
112
|
-
this.router.cacheManager?.set("viewlogic_lang", language);
|
|
113
|
-
this.log("debug", "Language saved to cache:", language);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
this.log("warn", "Failed to save language to cache:", error);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* 언어 메시지 파일 로드
|
|
120
|
-
*/
|
|
121
|
-
async loadMessages(language) {
|
|
122
|
-
if (this.messages.has(language)) {
|
|
123
|
-
this.log("debug", "Messages already loaded for:", language);
|
|
124
|
-
return this.messages.get(language);
|
|
125
|
-
}
|
|
126
|
-
if (this.loadPromises.has(language)) {
|
|
127
|
-
this.log("debug", "Messages loading in progress for:", language);
|
|
128
|
-
return await this.loadPromises.get(language);
|
|
129
|
-
}
|
|
130
|
-
const loadPromise = this._loadMessagesFromFile(language);
|
|
131
|
-
this.loadPromises.set(language, loadPromise);
|
|
132
|
-
try {
|
|
133
|
-
const messages = await loadPromise;
|
|
134
|
-
this.messages.set(language, messages);
|
|
135
|
-
this.loadPromises.delete(language);
|
|
136
|
-
this.log("debug", "Messages loaded successfully for:", language);
|
|
137
|
-
return messages;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
this.loadPromises.delete(language);
|
|
140
|
-
this.log("error", "Failed to load messages, using empty fallback for:", language, error);
|
|
141
|
-
const emptyMessages = {};
|
|
142
|
-
this.messages.set(language, emptyMessages);
|
|
143
|
-
return emptyMessages;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* 파일에서 메시지 로드 (캐싱 지원)
|
|
148
|
-
*/
|
|
149
|
-
async _loadMessagesFromFile(language) {
|
|
150
|
-
const cacheKey = `i18n_${language}`;
|
|
151
|
-
const cachedData = this.router.cacheManager?.get(cacheKey);
|
|
152
|
-
if (cachedData) {
|
|
153
|
-
this.log("debug", "Messages loaded from cache:", language);
|
|
154
|
-
return cachedData;
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const i18nPath = `${this.router.config.i18nPath}/${language}.json`;
|
|
158
|
-
const response = await fetch(i18nPath);
|
|
159
|
-
if (!response.ok) {
|
|
160
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
161
|
-
}
|
|
162
|
-
const messages = await response.json();
|
|
163
|
-
this.router.cacheManager?.set(cacheKey, messages);
|
|
164
|
-
return messages;
|
|
165
|
-
} catch (error) {
|
|
166
|
-
this.log("error", "Failed to load messages file for:", language, error);
|
|
167
|
-
if (language !== this.config.fallbackLanguage) {
|
|
168
|
-
this.log("info", "Trying fallback language:", this.config.fallbackLanguage);
|
|
169
|
-
try {
|
|
170
|
-
return await this._loadMessagesFromFile(this.config.fallbackLanguage);
|
|
171
|
-
} catch (fallbackError) {
|
|
172
|
-
this.log("error", "Fallback language also failed:", fallbackError);
|
|
173
|
-
return {};
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
this.log("warn", `No messages available for language: ${language}, using empty fallback`);
|
|
177
|
-
return {};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
/**
|
|
181
|
-
* 메시지 번역
|
|
182
|
-
*/
|
|
183
|
-
t(key, params = {}) {
|
|
184
|
-
if (!this.config.enabled) {
|
|
185
|
-
return key;
|
|
186
|
-
}
|
|
187
|
-
const messages = this.messages.get(this.currentLanguage);
|
|
188
|
-
if (!messages) {
|
|
189
|
-
this.log("warn", "No messages loaded for current language:", this.currentLanguage);
|
|
190
|
-
return key;
|
|
191
|
-
}
|
|
192
|
-
const message = this.getNestedValue(messages, key);
|
|
193
|
-
if (message === void 0) {
|
|
194
|
-
this.log("warn", "Translation not found for key:", key);
|
|
195
|
-
const fallbackMessages = this.messages.get(this.config.fallbackLanguage);
|
|
196
|
-
if (fallbackMessages && this.currentLanguage !== this.config.fallbackLanguage) {
|
|
197
|
-
const fallbackMessage = this.getNestedValue(fallbackMessages, key);
|
|
198
|
-
if (fallbackMessage !== void 0) {
|
|
199
|
-
return this.interpolate(fallbackMessage, params);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return key;
|
|
203
|
-
}
|
|
204
|
-
return this.interpolate(message, params);
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* 중첩된 객체에서 값 가져오기
|
|
208
|
-
*/
|
|
209
|
-
getNestedValue(obj, path) {
|
|
210
|
-
return path.split(".").reduce((current, key) => {
|
|
211
|
-
return current && current[key] !== void 0 ? current[key] : void 0;
|
|
212
|
-
}, obj);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* 문자열 보간 처리
|
|
216
|
-
*/
|
|
217
|
-
interpolate(message, params) {
|
|
218
|
-
if (typeof message !== "string") {
|
|
219
|
-
return message;
|
|
220
|
-
}
|
|
221
|
-
return message.replace(/\{(\w+)\}/g, (match, key) => {
|
|
222
|
-
return params.hasOwnProperty(key) ? params[key] : match;
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* 복수형 처리
|
|
227
|
-
*/
|
|
228
|
-
plural(key, count, params = {}) {
|
|
229
|
-
const pluralKey = count === 1 ? `${key}.singular` : `${key}.plural`;
|
|
230
|
-
return this.t(pluralKey, { ...params, count });
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* 언어 변경 이벤트 리스너 등록
|
|
234
|
-
*/
|
|
235
|
-
on(event, callback) {
|
|
236
|
-
if (this.listeners[event]) {
|
|
237
|
-
this.listeners[event].push(callback);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* 언어 변경 이벤트 리스너 제거
|
|
242
|
-
*/
|
|
243
|
-
off(event, callback) {
|
|
244
|
-
if (this.listeners[event]) {
|
|
245
|
-
const index = this.listeners[event].indexOf(callback);
|
|
246
|
-
if (index > -1) {
|
|
247
|
-
this.listeners[event].splice(index, 1);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* 이벤트 발생
|
|
253
|
-
*/
|
|
254
|
-
emit(event, data) {
|
|
255
|
-
if (this.listeners[event]) {
|
|
256
|
-
this.listeners[event].forEach((callback) => {
|
|
257
|
-
try {
|
|
258
|
-
callback(data);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
this.log("error", "Error in event listener:", error);
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* 현재 언어의 모든 메시지 반환
|
|
267
|
-
*/
|
|
268
|
-
getMessages() {
|
|
269
|
-
return this.messages.get(this.currentLanguage) || {};
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* 언어별 날짜 포맷팅
|
|
273
|
-
*/
|
|
274
|
-
formatDate(date, options = {}) {
|
|
275
|
-
const locale = this.currentLanguage === "ko" ? "ko-KR" : "en-US";
|
|
276
|
-
return new Intl.DateTimeFormat(locale, options).format(new Date(date));
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* 언어별 숫자 포맷팅
|
|
280
|
-
*/
|
|
281
|
-
formatNumber(number, options = {}) {
|
|
282
|
-
const locale = this.currentLanguage === "ko" ? "ko-KR" : "en-US";
|
|
283
|
-
return new Intl.NumberFormat(locale, options).format(number);
|
|
284
|
-
}
|
|
285
|
-
/**
|
|
286
|
-
* 로깅 래퍼 메서드
|
|
287
|
-
*/
|
|
288
|
-
log(level, ...args) {
|
|
289
|
-
if (this.router?.errorHandler) {
|
|
290
|
-
this.router.errorHandler.log(level, "I18nManager", ...args);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* i18n 활성화 여부 확인
|
|
295
|
-
*/
|
|
296
|
-
isEnabled() {
|
|
297
|
-
return this.config.enabled;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* 초기 로딩이 완료되었는지 확인
|
|
301
|
-
*/
|
|
302
|
-
async isReady() {
|
|
303
|
-
if (!this.config.enabled) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
await this.initPromise;
|
|
308
|
-
return true;
|
|
309
|
-
} catch (error) {
|
|
310
|
-
this.log("error", "I18n initialization failed:", error);
|
|
311
|
-
this.log("info", "I18n system ready with fallback behavior");
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* 캐시 초기화
|
|
317
|
-
*/
|
|
318
|
-
clearCache() {
|
|
319
|
-
try {
|
|
320
|
-
const clearedCount = this.router.cacheManager?.deleteByPattern("i18n_");
|
|
321
|
-
this.log("debug", "Cache cleared, removed", clearedCount, "items");
|
|
322
|
-
} catch (error) {
|
|
323
|
-
this.log("warn", "Failed to clear cache:", error);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* 시스템 초기화 (현재 언어의 메시지 로드)
|
|
328
|
-
*/
|
|
329
|
-
async initialize() {
|
|
330
|
-
if (!this.config.enabled) {
|
|
331
|
-
this.log("info", "I18n system is disabled, skipping initialization");
|
|
332
|
-
return true;
|
|
333
|
-
}
|
|
334
|
-
try {
|
|
335
|
-
await this.initPromise;
|
|
336
|
-
this.log("info", "I18n system fully initialized");
|
|
337
|
-
return true;
|
|
338
|
-
} catch (error) {
|
|
339
|
-
this.log("error", "Failed to initialize I18n system:", error);
|
|
340
|
-
this.log("info", "I18n system will continue with fallback behavior");
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
// src/plugins/AuthManager.js
|
|
347
|
-
var AuthManager = class {
|
|
348
|
-
constructor(router, options = {}) {
|
|
349
|
-
this.config = {
|
|
350
|
-
enabled: options.authEnabled || false,
|
|
351
|
-
loginRoute: options.loginRoute || "login",
|
|
352
|
-
protectedRoutes: options.protectedRoutes || [],
|
|
353
|
-
protectedPrefixes: options.protectedPrefixes || [],
|
|
354
|
-
publicRoutes: options.publicRoutes || ["login", "register", "home"],
|
|
355
|
-
checkAuthFunction: options.checkAuthFunction || null,
|
|
356
|
-
redirectAfterLogin: options.redirectAfterLogin || "home",
|
|
357
|
-
authCookieName: options.authCookieName || "authToken",
|
|
358
|
-
authStorage: options.authStorage || "localStorage"
|
|
359
|
-
};
|
|
360
|
-
this.router = router;
|
|
361
|
-
this.eventListeners = /* @__PURE__ */ new Map();
|
|
362
|
-
this.log("info", "AuthManager initialized", { enabled: this.config.enabled });
|
|
363
|
-
}
|
|
364
|
-
/**
|
|
365
|
-
* 로깅 래퍼 메서드
|
|
366
|
-
*/
|
|
367
|
-
log(level, ...args) {
|
|
368
|
-
if (this.router?.errorHandler) {
|
|
369
|
-
this.router.errorHandler.log(level, "AuthManager", ...args);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* 라우트 인증 확인
|
|
374
|
-
*/
|
|
375
|
-
async checkAuthentication(routeName) {
|
|
376
|
-
if (!this.config.enabled) {
|
|
377
|
-
return { allowed: true, reason: "auth_disabled" };
|
|
378
|
-
}
|
|
379
|
-
this.log("debug", `\u{1F510} Checking authentication for route: ${routeName}`);
|
|
380
|
-
if (this.isPublicRoute(routeName)) {
|
|
381
|
-
return { allowed: true, reason: "public_route" };
|
|
382
|
-
}
|
|
383
|
-
const isProtected = this.isProtectedRoute(routeName);
|
|
384
|
-
if (!isProtected) {
|
|
385
|
-
return { allowed: true, reason: "not_protected" };
|
|
386
|
-
}
|
|
387
|
-
if (typeof this.config.checkAuthFunction === "function") {
|
|
388
|
-
try {
|
|
389
|
-
const route = {
|
|
390
|
-
name: routeName,
|
|
391
|
-
$api: this.router.routeLoader.apiHandler.bindToComponent({}),
|
|
392
|
-
$state: this.router.stateHandler
|
|
393
|
-
};
|
|
394
|
-
const isAuthenticated2 = await this.config.checkAuthFunction(route);
|
|
395
|
-
return {
|
|
396
|
-
allowed: isAuthenticated2,
|
|
397
|
-
reason: isAuthenticated2 ? "custom_auth_success" : "custom_auth_failed",
|
|
398
|
-
routeName
|
|
399
|
-
};
|
|
400
|
-
} catch (error) {
|
|
401
|
-
this.log("error", "Custom auth function failed:", error);
|
|
402
|
-
return { allowed: false, reason: "custom_auth_error", error };
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
const isAuthenticated = this.isAuthenticated();
|
|
406
|
-
return {
|
|
407
|
-
allowed: isAuthenticated,
|
|
408
|
-
reason: isAuthenticated ? "authenticated" : "not_authenticated",
|
|
409
|
-
routeName
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* JWT 토큰 검증
|
|
414
|
-
*/
|
|
415
|
-
isTokenValid(token) {
|
|
416
|
-
if (!token) return false;
|
|
417
|
-
try {
|
|
418
|
-
if (token.includes(".")) {
|
|
419
|
-
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
420
|
-
if (payload.exp && Date.now() >= payload.exp * 1e3) {
|
|
421
|
-
return false;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
return true;
|
|
425
|
-
} catch (error) {
|
|
426
|
-
this.log("warn", "Token validation failed:", error);
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
/**
|
|
431
|
-
* 사용자 인증 상태 확인
|
|
432
|
-
*/
|
|
433
|
-
isAuthenticated() {
|
|
434
|
-
this.log("debug", "\u{1F50D} Checking user authentication status");
|
|
435
|
-
const token = this.getAccessToken();
|
|
436
|
-
if (!token) {
|
|
437
|
-
this.log("debug", "\u274C No token found");
|
|
438
|
-
return false;
|
|
439
|
-
}
|
|
440
|
-
if (!this.isTokenValid(token)) {
|
|
441
|
-
this.log("debug", "Token expired, removing...");
|
|
442
|
-
this.removeAccessToken();
|
|
443
|
-
return false;
|
|
444
|
-
}
|
|
445
|
-
this.log("debug", "\u2705 Valid token found");
|
|
446
|
-
return true;
|
|
447
|
-
}
|
|
448
|
-
/**
|
|
449
|
-
* 공개 라우트인지 확인
|
|
450
|
-
*/
|
|
451
|
-
isPublicRoute(routeName) {
|
|
452
|
-
return this.config.publicRoutes.includes(routeName);
|
|
453
|
-
}
|
|
454
|
-
/**
|
|
455
|
-
* 보호된 라우트인지 확인
|
|
456
|
-
*/
|
|
457
|
-
isProtectedRoute(routeName) {
|
|
458
|
-
if (this.config.protectedRoutes.includes(routeName)) {
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
for (const prefix of this.config.protectedPrefixes) {
|
|
462
|
-
if (routeName.startsWith(prefix)) {
|
|
463
|
-
return true;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* 인증 쿠키 가져오기
|
|
470
|
-
*/
|
|
471
|
-
getAuthCookie() {
|
|
472
|
-
return this.getCookieValue(this.config.authCookieName);
|
|
473
|
-
}
|
|
474
|
-
/**
|
|
475
|
-
* 쿠키 값 가져오기
|
|
476
|
-
*/
|
|
477
|
-
getCookieValue(name) {
|
|
478
|
-
const value = `; ${document.cookie}`;
|
|
479
|
-
const parts = value.split(`; ${name}=`);
|
|
480
|
-
if (parts.length === 2) {
|
|
481
|
-
return decodeURIComponent(parts.pop().split(";").shift());
|
|
482
|
-
}
|
|
483
|
-
return null;
|
|
484
|
-
}
|
|
485
|
-
/**
|
|
486
|
-
* 인증 쿠키 제거
|
|
487
|
-
*/
|
|
488
|
-
removeAuthCookie() {
|
|
489
|
-
document.cookie = `${this.config.authCookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
490
|
-
this.log("debug", "Auth cookie removed");
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* 액세스 토큰 가져오기
|
|
494
|
-
*/
|
|
495
|
-
getAccessToken() {
|
|
496
|
-
let token = localStorage.getItem("authToken");
|
|
497
|
-
if (token) return token;
|
|
498
|
-
token = sessionStorage.getItem("authToken");
|
|
499
|
-
if (token) return token;
|
|
500
|
-
return this.getAuthCookie();
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* 액세스 토큰 설정
|
|
504
|
-
*/
|
|
505
|
-
setAccessToken(token, options = {}) {
|
|
506
|
-
if (!token) {
|
|
507
|
-
this.log("warn", "Empty token provided");
|
|
508
|
-
return false;
|
|
509
|
-
}
|
|
510
|
-
const {
|
|
511
|
-
storage = this.config.authStorage,
|
|
512
|
-
cookieOptions = this.config.authCookieOptions
|
|
513
|
-
} = options;
|
|
514
|
-
try {
|
|
515
|
-
if (!this.isTokenValid(token)) {
|
|
516
|
-
this.log("warn", "\u274C Token is expired or invalid");
|
|
517
|
-
return false;
|
|
518
|
-
}
|
|
519
|
-
switch (storage) {
|
|
520
|
-
case "localStorage":
|
|
521
|
-
localStorage.setItem("authToken", token);
|
|
522
|
-
this.log("debug", "Token saved to localStorage");
|
|
523
|
-
break;
|
|
524
|
-
case "sessionStorage":
|
|
525
|
-
sessionStorage.setItem("authToken", token);
|
|
526
|
-
this.log("debug", "Token saved to sessionStorage");
|
|
527
|
-
break;
|
|
528
|
-
case "cookie":
|
|
529
|
-
this.setAuthCookie(token);
|
|
530
|
-
break;
|
|
531
|
-
default:
|
|
532
|
-
localStorage.setItem("authToken", token);
|
|
533
|
-
this.log("debug", "Token saved to localStorage (default)");
|
|
534
|
-
}
|
|
535
|
-
this.emitAuthEvent("token_set", {
|
|
536
|
-
storage,
|
|
537
|
-
tokenLength: token.length,
|
|
538
|
-
hasExpiration: token.includes(".")
|
|
539
|
-
});
|
|
540
|
-
return true;
|
|
541
|
-
} catch (error) {
|
|
542
|
-
this.log("error", "Failed to set token:", error);
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* 인증 쿠키 설정
|
|
548
|
-
*/
|
|
549
|
-
setAuthCookie(token) {
|
|
550
|
-
const secure = window.location.protocol === "https:";
|
|
551
|
-
let cookieString = `${this.config.authCookieName}=${encodeURIComponent(token)}; path=/; SameSite=Strict`;
|
|
552
|
-
if (secure) {
|
|
553
|
-
cookieString += "; Secure";
|
|
554
|
-
}
|
|
555
|
-
if (token.includes(".")) {
|
|
556
|
-
try {
|
|
557
|
-
const payload = JSON.parse(atob(token.split(".")[1]));
|
|
558
|
-
if (payload.exp) {
|
|
559
|
-
const expireDate = new Date(payload.exp * 1e3);
|
|
560
|
-
cookieString += `; Expires=${expireDate.toUTCString()}`;
|
|
561
|
-
}
|
|
562
|
-
} catch (error) {
|
|
563
|
-
this.log("warn", "Could not extract expiration from JWT token");
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
document.cookie = cookieString;
|
|
567
|
-
this.log("debug", "Auth cookie set");
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* 토큰 제거
|
|
571
|
-
*/
|
|
572
|
-
removeAccessToken(storage = "all") {
|
|
573
|
-
switch (storage) {
|
|
574
|
-
case "localStorage":
|
|
575
|
-
localStorage.removeItem("authToken");
|
|
576
|
-
localStorage.removeItem("accessToken");
|
|
577
|
-
break;
|
|
578
|
-
case "sessionStorage":
|
|
579
|
-
sessionStorage.removeItem("authToken");
|
|
580
|
-
sessionStorage.removeItem("accessToken");
|
|
581
|
-
break;
|
|
582
|
-
case "cookie":
|
|
583
|
-
this.removeAuthCookie();
|
|
584
|
-
break;
|
|
585
|
-
case "all":
|
|
586
|
-
default:
|
|
587
|
-
localStorage.removeItem("authToken");
|
|
588
|
-
sessionStorage.removeItem("authToken");
|
|
589
|
-
this.removeAuthCookie();
|
|
590
|
-
break;
|
|
591
|
-
}
|
|
592
|
-
this.emitAuthEvent("token_removed", { storage });
|
|
593
|
-
this.log("debug", `Token removed from: ${storage}`);
|
|
594
|
-
}
|
|
595
|
-
/**
|
|
596
|
-
* 로그인 성공 처리
|
|
597
|
-
*/
|
|
598
|
-
loginSuccess(targetRoute = null) {
|
|
599
|
-
const redirectRoute = targetRoute || this.config.redirectAfterLogin;
|
|
600
|
-
this.log("info", `\u{1F389} Login success, redirecting to: ${redirectRoute}`);
|
|
601
|
-
this.emitAuthEvent("login_success", { targetRoute: redirectRoute });
|
|
602
|
-
if (this.router && typeof this.router.navigateTo === "function") {
|
|
603
|
-
this.router.navigateTo(redirectRoute);
|
|
604
|
-
}
|
|
605
|
-
return redirectRoute;
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* 로그아웃 처리
|
|
609
|
-
*/
|
|
610
|
-
logout() {
|
|
611
|
-
this.log("info", "\u{1F44B} Logging out user");
|
|
612
|
-
this.removeAccessToken();
|
|
613
|
-
this.emitAuthEvent("logout", {});
|
|
614
|
-
if (this.router && typeof this.router.navigateTo === "function") {
|
|
615
|
-
this.router.navigateTo(this.config.loginRoute);
|
|
616
|
-
}
|
|
617
|
-
return this.config.loginRoute;
|
|
618
|
-
}
|
|
619
|
-
/**
|
|
620
|
-
* 인증 이벤트 발생
|
|
621
|
-
*/
|
|
622
|
-
emitAuthEvent(eventType, data) {
|
|
623
|
-
const event = new CustomEvent("router:auth", {
|
|
624
|
-
detail: {
|
|
625
|
-
type: eventType,
|
|
626
|
-
timestamp: Date.now(),
|
|
627
|
-
...data
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
document.dispatchEvent(event);
|
|
631
|
-
if (this.eventListeners.has(eventType)) {
|
|
632
|
-
this.eventListeners.get(eventType).forEach((listener) => {
|
|
633
|
-
try {
|
|
634
|
-
listener(data);
|
|
635
|
-
} catch (error) {
|
|
636
|
-
this.log("error", "Event listener error:", error);
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
this.log("debug", `\u{1F514} Auth event emitted: ${eventType}`, data);
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* 이벤트 리스너 등록
|
|
644
|
-
*/
|
|
645
|
-
on(eventType, listener) {
|
|
646
|
-
if (!this.eventListeners.has(eventType)) {
|
|
647
|
-
this.eventListeners.set(eventType, []);
|
|
648
|
-
}
|
|
649
|
-
this.eventListeners.get(eventType).push(listener);
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* 이벤트 리스너 제거
|
|
653
|
-
*/
|
|
654
|
-
off(eventType, listener) {
|
|
655
|
-
if (this.eventListeners.has(eventType)) {
|
|
656
|
-
const listeners = this.eventListeners.get(eventType);
|
|
657
|
-
const index = listeners.indexOf(listener);
|
|
658
|
-
if (index > -1) {
|
|
659
|
-
listeners.splice(index, 1);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* 인증 상태 통계
|
|
665
|
-
*/
|
|
666
|
-
getAuthStats() {
|
|
667
|
-
return {
|
|
668
|
-
enabled: this.config.enabled,
|
|
669
|
-
isAuthenticated: this.isAuthenticated(),
|
|
670
|
-
hasToken: !!this.getAccessToken(),
|
|
671
|
-
protectedRoutesCount: this.config.protectedRoutes.length,
|
|
672
|
-
protectedPrefixesCount: this.config.protectedPrefixes.length,
|
|
673
|
-
publicRoutesCount: this.config.publicRoutes.length,
|
|
674
|
-
storage: this.config.authStorage,
|
|
675
|
-
loginRoute: this.config.loginRoute
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
/**
|
|
679
|
-
* 정리 (메모리 누수 방지)
|
|
680
|
-
*/
|
|
681
|
-
destroy() {
|
|
682
|
-
this.eventListeners.clear();
|
|
683
|
-
this.log("debug", "AuthManager destroyed");
|
|
684
|
-
}
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
// src/plugins/CacheManager.js
|
|
688
|
-
var CacheManager = class {
|
|
689
|
-
constructor(router, options = {}) {
|
|
690
|
-
this.config = {
|
|
691
|
-
cacheMode: options.cacheMode || "memory",
|
|
692
|
-
// 'memory' 또는 'lru'
|
|
693
|
-
cacheTTL: options.cacheTTL || 3e5,
|
|
694
|
-
// 5분 (밀리초)
|
|
695
|
-
maxCacheSize: options.maxCacheSize || 50
|
|
696
|
-
// LRU 캐시 최대 크기
|
|
697
|
-
};
|
|
698
|
-
this.router = router;
|
|
699
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
700
|
-
this.cacheTimestamps = /* @__PURE__ */ new Map();
|
|
701
|
-
this.lruOrder = [];
|
|
702
|
-
this.log("info", "CacheManager initialized with config:", this.config);
|
|
703
|
-
}
|
|
704
|
-
/**
|
|
705
|
-
* 로깅 래퍼 메서드
|
|
706
|
-
*/
|
|
707
|
-
log(level, ...args) {
|
|
708
|
-
if (this.router?.errorHandler) {
|
|
709
|
-
this.router.errorHandler.log(level, "CacheManager", ...args);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* 캐시에 값 저장
|
|
714
|
-
*/
|
|
715
|
-
set(key, value) {
|
|
716
|
-
const now = Date.now();
|
|
717
|
-
if (this.config.cacheMode === "lru") {
|
|
718
|
-
if (this.cache.size >= this.config.maxCacheSize && !this.cache.has(key)) {
|
|
719
|
-
const oldestKey = this.lruOrder.shift();
|
|
720
|
-
if (oldestKey) {
|
|
721
|
-
this.cache.delete(oldestKey);
|
|
722
|
-
this.cacheTimestamps.delete(oldestKey);
|
|
723
|
-
this.log("debug", `\u{1F5D1}\uFE0F LRU evicted cache key: ${oldestKey}`);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
const existingIndex = this.lruOrder.indexOf(key);
|
|
727
|
-
if (existingIndex > -1) {
|
|
728
|
-
this.lruOrder.splice(existingIndex, 1);
|
|
729
|
-
}
|
|
730
|
-
this.lruOrder.push(key);
|
|
731
|
-
}
|
|
732
|
-
this.cache.set(key, value);
|
|
733
|
-
this.cacheTimestamps.set(key, now);
|
|
734
|
-
this.log("debug", `\u{1F4BE} Cached: ${key} (size: ${this.cache.size})`);
|
|
735
|
-
}
|
|
736
|
-
/**
|
|
737
|
-
* 캐시에서 값 가져오기
|
|
738
|
-
*/
|
|
739
|
-
get(key) {
|
|
740
|
-
const now = Date.now();
|
|
741
|
-
const timestamp = this.cacheTimestamps.get(key);
|
|
742
|
-
if (timestamp && now - timestamp > this.config.cacheTTL) {
|
|
743
|
-
this.cache.delete(key);
|
|
744
|
-
this.cacheTimestamps.delete(key);
|
|
745
|
-
if (this.config.cacheMode === "lru") {
|
|
746
|
-
const index = this.lruOrder.indexOf(key);
|
|
747
|
-
if (index > -1) {
|
|
748
|
-
this.lruOrder.splice(index, 1);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
this.log("debug", `\u23F0 Cache expired and removed: ${key}`);
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
const value = this.cache.get(key);
|
|
755
|
-
if (value && this.config.cacheMode === "lru") {
|
|
756
|
-
const index = this.lruOrder.indexOf(key);
|
|
757
|
-
if (index > -1) {
|
|
758
|
-
this.lruOrder.splice(index, 1);
|
|
759
|
-
this.lruOrder.push(key);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
if (value) {
|
|
763
|
-
this.log("debug", `\u{1F3AF} Cache hit: ${key}`);
|
|
764
|
-
} else {
|
|
765
|
-
this.log("debug", `\u274C Cache miss: ${key}`);
|
|
766
|
-
}
|
|
767
|
-
return value;
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* 캐시에 키가 있는지 확인
|
|
771
|
-
*/
|
|
772
|
-
has(key) {
|
|
773
|
-
return this.cache.has(key) && this.get(key) !== null;
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* 특정 키 패턴의 캐시 삭제
|
|
777
|
-
*/
|
|
778
|
-
deleteByPattern(pattern) {
|
|
779
|
-
const keysToDelete = [];
|
|
780
|
-
for (const key of this.cache.keys()) {
|
|
781
|
-
if (key.includes(pattern) || key.startsWith(pattern)) {
|
|
782
|
-
keysToDelete.push(key);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
keysToDelete.forEach((key) => {
|
|
786
|
-
this.cache.delete(key);
|
|
787
|
-
this.cacheTimestamps.delete(key);
|
|
788
|
-
if (this.config.cacheMode === "lru") {
|
|
789
|
-
const index = this.lruOrder.indexOf(key);
|
|
790
|
-
if (index > -1) {
|
|
791
|
-
this.lruOrder.splice(index, 1);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
});
|
|
795
|
-
this.log("debug", `\u{1F9F9} Deleted ${keysToDelete.length} cache entries matching: ${pattern}`);
|
|
796
|
-
return keysToDelete.length;
|
|
797
|
-
}
|
|
798
|
-
/**
|
|
799
|
-
* 특정 컴포넌트 캐시 삭제
|
|
800
|
-
*/
|
|
801
|
-
deleteComponent(routeName) {
|
|
802
|
-
const patterns = [
|
|
803
|
-
`component_${routeName}`,
|
|
804
|
-
`script_${routeName}`,
|
|
805
|
-
`template_${routeName}`,
|
|
806
|
-
`style_${routeName}`,
|
|
807
|
-
`layout_${routeName}`
|
|
808
|
-
];
|
|
809
|
-
let totalInvalidated = 0;
|
|
810
|
-
patterns.forEach((pattern) => {
|
|
811
|
-
totalInvalidated += this.deleteByPattern(pattern);
|
|
812
|
-
});
|
|
813
|
-
this.log("debug", `\u{1F504} Deleted component cache for route: ${routeName} (${totalInvalidated} entries)`);
|
|
814
|
-
return totalInvalidated;
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* 모든 컴포넌트 캐시 삭제
|
|
818
|
-
*/
|
|
819
|
-
deleteAllComponents() {
|
|
820
|
-
const componentPatterns = ["component_", "script_", "template_", "style_", "layout_"];
|
|
821
|
-
let totalCleared = 0;
|
|
822
|
-
componentPatterns.forEach((pattern) => {
|
|
823
|
-
totalCleared += this.deleteByPattern(pattern);
|
|
824
|
-
});
|
|
825
|
-
this.log("debug", `\u{1F9FD} Deleted all component caches (${totalCleared} entries)`);
|
|
826
|
-
return totalCleared;
|
|
827
|
-
}
|
|
828
|
-
/**
|
|
829
|
-
* 전체 캐시 삭제
|
|
830
|
-
*/
|
|
831
|
-
clearAll() {
|
|
832
|
-
const size = this.cache.size;
|
|
833
|
-
this.cache.clear();
|
|
834
|
-
this.cacheTimestamps.clear();
|
|
835
|
-
this.lruOrder = [];
|
|
836
|
-
this.log("debug", `\u{1F525} Cleared all cache (${size} entries)`);
|
|
837
|
-
return size;
|
|
838
|
-
}
|
|
839
|
-
/**
|
|
840
|
-
* 만료된 캐시 항목들 정리
|
|
841
|
-
*/
|
|
842
|
-
cleanExpired() {
|
|
843
|
-
const now = Date.now();
|
|
844
|
-
const expiredKeys = [];
|
|
845
|
-
for (const [key, timestamp] of this.cacheTimestamps.entries()) {
|
|
846
|
-
if (now - timestamp > this.config.cacheTTL) {
|
|
847
|
-
expiredKeys.push(key);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
expiredKeys.forEach((key) => {
|
|
851
|
-
this.cache.delete(key);
|
|
852
|
-
this.cacheTimestamps.delete(key);
|
|
853
|
-
if (this.config.cacheMode === "lru") {
|
|
854
|
-
const index = this.lruOrder.indexOf(key);
|
|
855
|
-
if (index > -1) {
|
|
856
|
-
this.lruOrder.splice(index, 1);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
if (expiredKeys.length > 0) {
|
|
861
|
-
this.log("debug", `\u23F1\uFE0F Cleaned ${expiredKeys.length} expired cache entries`);
|
|
862
|
-
}
|
|
863
|
-
return expiredKeys.length;
|
|
864
|
-
}
|
|
865
|
-
/**
|
|
866
|
-
* 캐시 통계 정보
|
|
867
|
-
*/
|
|
868
|
-
getStats() {
|
|
869
|
-
return {
|
|
870
|
-
size: this.cache.size,
|
|
871
|
-
maxSize: this.config.maxCacheSize,
|
|
872
|
-
mode: this.config.cacheMode,
|
|
873
|
-
ttl: this.config.cacheTTL,
|
|
874
|
-
memoryUsage: this.getMemoryUsage(),
|
|
875
|
-
hitRatio: this.getHitRate(),
|
|
876
|
-
categories: this.getStatsByCategory()
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
|
-
/**
|
|
880
|
-
* 메모리 사용량 추정
|
|
881
|
-
*/
|
|
882
|
-
getMemoryUsage() {
|
|
883
|
-
let estimatedBytes = 0;
|
|
884
|
-
for (const [key, value] of this.cache.entries()) {
|
|
885
|
-
estimatedBytes += key.length * 2;
|
|
886
|
-
if (typeof value === "string") {
|
|
887
|
-
estimatedBytes += value.length * 2;
|
|
888
|
-
} else if (typeof value === "object" && value !== null) {
|
|
889
|
-
estimatedBytes += JSON.stringify(value).length * 2;
|
|
890
|
-
} else {
|
|
891
|
-
estimatedBytes += 8;
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
return {
|
|
895
|
-
bytes: estimatedBytes,
|
|
896
|
-
kb: Math.round(estimatedBytes / 1024 * 100) / 100,
|
|
897
|
-
mb: Math.round(estimatedBytes / (1024 * 1024) * 100) / 100
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
/**
|
|
901
|
-
* 히트 비율 계산 (간단한 추정)
|
|
902
|
-
*/
|
|
903
|
-
getHitRate() {
|
|
904
|
-
const ratio = this.cache.size > 0 ? Math.min(this.cache.size / this.config.maxCacheSize, 1) : 0;
|
|
905
|
-
return Math.round(ratio * 100);
|
|
906
|
-
}
|
|
907
|
-
/**
|
|
908
|
-
* 카테고리별 캐시 통계
|
|
909
|
-
*/
|
|
910
|
-
getStatsByCategory() {
|
|
911
|
-
const categories = {
|
|
912
|
-
components: 0,
|
|
913
|
-
scripts: 0,
|
|
914
|
-
templates: 0,
|
|
915
|
-
styles: 0,
|
|
916
|
-
layouts: 0,
|
|
917
|
-
others: 0
|
|
918
|
-
};
|
|
919
|
-
for (const key of this.cache.keys()) {
|
|
920
|
-
if (key.startsWith("component_")) categories.components++;
|
|
921
|
-
else if (key.startsWith("script_")) categories.scripts++;
|
|
922
|
-
else if (key.startsWith("template_")) categories.templates++;
|
|
923
|
-
else if (key.startsWith("style_")) categories.styles++;
|
|
924
|
-
else if (key.startsWith("layout_")) categories.layouts++;
|
|
925
|
-
else categories.others++;
|
|
926
|
-
}
|
|
927
|
-
return categories;
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* 캐시 키 목록 반환
|
|
931
|
-
*/
|
|
932
|
-
getKeys() {
|
|
933
|
-
return Array.from(this.cache.keys());
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* 특정 패턴의 캐시 키들 반환
|
|
937
|
-
*/
|
|
938
|
-
getKeysByPattern(pattern) {
|
|
939
|
-
return this.getKeys().filter(
|
|
940
|
-
(key) => key.includes(pattern) || key.startsWith(pattern)
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
/**
|
|
944
|
-
* 자동 정리 시작 (백그라운드에서 만료된 캐시 정리)
|
|
945
|
-
*/
|
|
946
|
-
startAutoCleanup(interval = 6e4) {
|
|
947
|
-
if (this.cleanupInterval) {
|
|
948
|
-
clearInterval(this.cleanupInterval);
|
|
949
|
-
}
|
|
950
|
-
this.cleanupInterval = setInterval(() => {
|
|
951
|
-
this.cleanExpired();
|
|
952
|
-
}, interval);
|
|
953
|
-
this.log("debug", `\u{1F916} Auto cleanup started (interval: ${interval}ms)`);
|
|
954
|
-
}
|
|
955
|
-
/**
|
|
956
|
-
* 자동 정리 중지
|
|
957
|
-
*/
|
|
958
|
-
stopAutoCleanup() {
|
|
959
|
-
if (this.cleanupInterval) {
|
|
960
|
-
clearInterval(this.cleanupInterval);
|
|
961
|
-
this.cleanupInterval = null;
|
|
962
|
-
this.log("debug", "\u{1F6D1} Auto cleanup stopped");
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
/**
|
|
966
|
-
* 정리 (메모리 누수 방지)
|
|
967
|
-
*/
|
|
968
|
-
destroy() {
|
|
969
|
-
this.stopAutoCleanup();
|
|
970
|
-
this.clearAll();
|
|
971
|
-
this.log("debug", "CacheManager destroyed");
|
|
972
|
-
}
|
|
973
|
-
};
|
|
974
|
-
|
|
975
|
-
// src/plugins/QueryManager.js
|
|
976
|
-
var QueryManager = class {
|
|
977
|
-
constructor(router) {
|
|
978
|
-
this.router = router;
|
|
979
|
-
this.currentQueryParams = {};
|
|
980
|
-
this.currentRouteParams = {};
|
|
981
|
-
this.log("debug", "QueryManager initialized");
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* 로깅 래퍼 메서드
|
|
985
|
-
*/
|
|
986
|
-
log(level, ...args) {
|
|
987
|
-
if (this.router?.errorHandler) {
|
|
988
|
-
this.router.errorHandler.log(level, "QueryManager", ...args);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
/**
|
|
992
|
-
* 쿼리스트링 파싱
|
|
993
|
-
*/
|
|
994
|
-
parseQueryString(queryString) {
|
|
995
|
-
const params = {};
|
|
996
|
-
if (!queryString) return params;
|
|
997
|
-
const pairs = queryString.split("&");
|
|
998
|
-
for (const pair of pairs) {
|
|
999
|
-
const [rawKey, rawValue] = pair.split("=");
|
|
1000
|
-
if (!rawKey) continue;
|
|
1001
|
-
try {
|
|
1002
|
-
const key = decodeURIComponent(rawKey);
|
|
1003
|
-
const value = rawValue ? decodeURIComponent(rawValue) : "";
|
|
1004
|
-
if (key.endsWith("[]")) {
|
|
1005
|
-
const arrayKey = key.slice(0, -2);
|
|
1006
|
-
if (!params[arrayKey]) params[arrayKey] = [];
|
|
1007
|
-
params[arrayKey].push(value);
|
|
1008
|
-
} else {
|
|
1009
|
-
params[key] = value;
|
|
1010
|
-
}
|
|
1011
|
-
} catch (error) {
|
|
1012
|
-
this.log("warn", "Failed to decode query parameter:", pair);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
return params;
|
|
1016
|
-
}
|
|
1017
|
-
/**
|
|
1018
|
-
* 쿼리스트링 생성
|
|
1019
|
-
*/
|
|
1020
|
-
buildQueryString(params) {
|
|
1021
|
-
if (!params || Object.keys(params).length === 0) return "";
|
|
1022
|
-
const pairs = [];
|
|
1023
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1024
|
-
if (Array.isArray(value)) {
|
|
1025
|
-
for (const item of value) {
|
|
1026
|
-
pairs.push(`${encodeURIComponent(key)}[]=${encodeURIComponent(item)}`);
|
|
1027
|
-
}
|
|
1028
|
-
} else if (value !== void 0 && value !== null) {
|
|
1029
|
-
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
return pairs.join("&");
|
|
1033
|
-
}
|
|
1034
|
-
/**
|
|
1035
|
-
* 쿼리 파라미터 변경 감지
|
|
1036
|
-
*/
|
|
1037
|
-
hasQueryParamsChanged(newParams) {
|
|
1038
|
-
if (!this.currentQueryParams && !newParams) return false;
|
|
1039
|
-
if (!this.currentQueryParams || !newParams) return true;
|
|
1040
|
-
const oldKeys = Object.keys(this.currentQueryParams);
|
|
1041
|
-
const newKeys = Object.keys(newParams);
|
|
1042
|
-
if (oldKeys.length !== newKeys.length) return true;
|
|
1043
|
-
for (const key of oldKeys) {
|
|
1044
|
-
if (JSON.stringify(this.currentQueryParams[key]) !== JSON.stringify(newParams[key])) {
|
|
1045
|
-
return true;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
return false;
|
|
1049
|
-
}
|
|
1050
|
-
/**
|
|
1051
|
-
* 현재 쿼리 파라미터 전체 가져오기
|
|
1052
|
-
*/
|
|
1053
|
-
getQueryParams() {
|
|
1054
|
-
return { ...this.currentQueryParams };
|
|
1055
|
-
}
|
|
1056
|
-
/**
|
|
1057
|
-
* 특정 쿼리 파라미터 가져오기
|
|
1058
|
-
*/
|
|
1059
|
-
getQueryParam(key, defaultValue = void 0) {
|
|
1060
|
-
const value = this.currentQueryParams ? this.currentQueryParams[key] : void 0;
|
|
1061
|
-
return value !== void 0 ? value : defaultValue;
|
|
1062
|
-
}
|
|
1063
|
-
/**
|
|
1064
|
-
* 쿼리 파라미터 설정
|
|
1065
|
-
*/
|
|
1066
|
-
setQueryParams(params, replace = false) {
|
|
1067
|
-
if (!params || typeof params !== "object") {
|
|
1068
|
-
this.log("warn", "Invalid parameters object provided to setQueryParams");
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
const currentParams = replace ? {} : { ...this.currentQueryParams };
|
|
1072
|
-
for (const [key, value] of Object.entries(params)) {
|
|
1073
|
-
if (value !== void 0 && value !== null && value !== "") {
|
|
1074
|
-
currentParams[key] = value;
|
|
1075
|
-
} else {
|
|
1076
|
-
delete currentParams[key];
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
this.currentQueryParams = currentParams;
|
|
1080
|
-
this.updateURL();
|
|
1081
|
-
}
|
|
1082
|
-
/**
|
|
1083
|
-
* 쿼리 파라미터 제거
|
|
1084
|
-
*/
|
|
1085
|
-
removeQueryParams(keys) {
|
|
1086
|
-
if (!keys) return;
|
|
1087
|
-
const keysToRemove = Array.isArray(keys) ? keys : [keys];
|
|
1088
|
-
for (const key of keysToRemove) {
|
|
1089
|
-
delete this.currentQueryParams[key];
|
|
1090
|
-
}
|
|
1091
|
-
this.updateURL();
|
|
1092
|
-
}
|
|
1093
|
-
/**
|
|
1094
|
-
* 쿼리 파라미터 초기화
|
|
1095
|
-
*/
|
|
1096
|
-
clearQueryParams() {
|
|
1097
|
-
this.currentQueryParams = {};
|
|
1098
|
-
this.updateURL();
|
|
1099
|
-
}
|
|
1100
|
-
/**
|
|
1101
|
-
* 현재 쿼리 파라미터 설정 (라우터에서 호출)
|
|
1102
|
-
*/
|
|
1103
|
-
setCurrentQueryParams(params) {
|
|
1104
|
-
this.currentQueryParams = params || {};
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* 현재 라우팅 파라미터 설정 (navigateTo에서 호출)
|
|
1108
|
-
*/
|
|
1109
|
-
setCurrentRouteParams(params) {
|
|
1110
|
-
this.currentRouteParams = params || {};
|
|
1111
|
-
this.log("debug", "Route params set:", this.currentRouteParams);
|
|
1112
|
-
}
|
|
1113
|
-
/**
|
|
1114
|
-
* 통합된 파라미터 반환 (라우팅 파라미터 + 쿼리 파라미터)
|
|
1115
|
-
*/
|
|
1116
|
-
getAllParams() {
|
|
1117
|
-
return {
|
|
1118
|
-
...this.currentRouteParams,
|
|
1119
|
-
...this.currentQueryParams
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
/**
|
|
1123
|
-
* 통합된 파라미터에서 특정 키 값 반환
|
|
1124
|
-
*/
|
|
1125
|
-
getParam(key, defaultValue = void 0) {
|
|
1126
|
-
const value = this.currentQueryParams[key] !== void 0 ? this.currentQueryParams[key] : this.currentRouteParams[key];
|
|
1127
|
-
return value !== void 0 ? value : defaultValue;
|
|
1128
|
-
}
|
|
1129
|
-
/**
|
|
1130
|
-
* 라우팅 파라미터만 반환
|
|
1131
|
-
*/
|
|
1132
|
-
getRouteParams() {
|
|
1133
|
-
return { ...this.currentRouteParams };
|
|
1134
|
-
}
|
|
1135
|
-
/**
|
|
1136
|
-
* 라우팅 파라미터에서 특정 키 값 반환
|
|
1137
|
-
*/
|
|
1138
|
-
getRouteParam(key, defaultValue = void 0) {
|
|
1139
|
-
const value = this.currentRouteParams[key];
|
|
1140
|
-
return value !== void 0 ? value : defaultValue;
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* URL 업데이트 (라우터의 updateURL 메소드 호출)
|
|
1144
|
-
*/
|
|
1145
|
-
updateURL() {
|
|
1146
|
-
if (this.router && typeof this.router.updateURL === "function") {
|
|
1147
|
-
const route = this.router.currentHash || "home";
|
|
1148
|
-
this.router.updateURL(route, this.currentQueryParams);
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* 쿼리 파라미터 통계
|
|
1153
|
-
*/
|
|
1154
|
-
getStats() {
|
|
1155
|
-
return {
|
|
1156
|
-
currentParams: Object.keys(this.currentQueryParams).length,
|
|
1157
|
-
currentQueryString: this.buildQueryString(this.currentQueryParams)
|
|
1158
|
-
};
|
|
1159
|
-
}
|
|
1160
|
-
/**
|
|
1161
|
-
* 정리 (메모리 누수 방지)
|
|
1162
|
-
*/
|
|
1163
|
-
destroy() {
|
|
1164
|
-
this.currentQueryParams = {};
|
|
1165
|
-
this.currentRouteParams = {};
|
|
1166
|
-
this.router = null;
|
|
1167
|
-
this.log("debug", "QueryManager destroyed");
|
|
1168
|
-
}
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
// src/core/FormHandler.js
|
|
1172
|
-
var FormHandler = class {
|
|
1173
|
-
constructor(router, options = {}) {
|
|
1174
|
-
this.router = router;
|
|
1175
|
-
this.requestTimeout = options.requestTimeout || 3e4;
|
|
1176
|
-
this.uploadTimeout = options.uploadTimeout || 3e5;
|
|
1177
|
-
this.log("debug", "FormHandler initialized");
|
|
1178
|
-
}
|
|
1179
|
-
/**
|
|
1180
|
-
* 로깅 래퍼 메서드
|
|
1181
|
-
*/
|
|
1182
|
-
log(level, ...args) {
|
|
1183
|
-
if (this.router?.errorHandler) {
|
|
1184
|
-
this.router.errorHandler.log(level, "FormHandler", ...args);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* 중복 요청 체크
|
|
1189
|
-
*/
|
|
1190
|
-
isDuplicateRequest(form) {
|
|
1191
|
-
if (form._isSubmitting) {
|
|
1192
|
-
this.log("debug", "Duplicate request blocked");
|
|
1193
|
-
return true;
|
|
1194
|
-
}
|
|
1195
|
-
return false;
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* 폼 제출 시작
|
|
1199
|
-
*/
|
|
1200
|
-
startFormSubmission(form) {
|
|
1201
|
-
form._isSubmitting = true;
|
|
1202
|
-
form._abortController = new AbortController();
|
|
1203
|
-
const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
|
|
1204
|
-
const timeout = hasFile ? this.uploadTimeout : this.requestTimeout;
|
|
1205
|
-
form._timeoutId = setTimeout(() => {
|
|
1206
|
-
if (form._isSubmitting) {
|
|
1207
|
-
this.abortFormSubmission(form);
|
|
1208
|
-
}
|
|
1209
|
-
}, timeout);
|
|
1210
|
-
}
|
|
1211
|
-
/**
|
|
1212
|
-
* 폼 제출 완료
|
|
1213
|
-
*/
|
|
1214
|
-
finishFormSubmission(form) {
|
|
1215
|
-
form._isSubmitting = false;
|
|
1216
|
-
if (form._timeoutId) {
|
|
1217
|
-
clearTimeout(form._timeoutId);
|
|
1218
|
-
delete form._timeoutId;
|
|
1219
|
-
}
|
|
1220
|
-
delete form._abortController;
|
|
1221
|
-
}
|
|
1222
|
-
/**
|
|
1223
|
-
* 폼 제출 중단
|
|
1224
|
-
*/
|
|
1225
|
-
abortFormSubmission(form) {
|
|
1226
|
-
if (form._abortController) {
|
|
1227
|
-
form._abortController.abort();
|
|
1228
|
-
}
|
|
1229
|
-
this.finishFormSubmission(form);
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* 자동 폼 바인딩
|
|
1233
|
-
*/
|
|
1234
|
-
bindAutoForms(component) {
|
|
1235
|
-
const forms = document.querySelectorAll("form.auto-form, form[action]");
|
|
1236
|
-
forms.forEach((form) => {
|
|
1237
|
-
form.removeEventListener("submit", form._boundSubmitHandler);
|
|
1238
|
-
const boundHandler = (e) => this.handleFormSubmit(e, component);
|
|
1239
|
-
form._boundSubmitHandler = boundHandler;
|
|
1240
|
-
form.addEventListener("submit", boundHandler);
|
|
1241
|
-
this.log("debug", `Form auto-bound: ${form.getAttribute("action")}`);
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
/**
|
|
1245
|
-
* 폼 서브밋 핸들러
|
|
1246
|
-
*/
|
|
1247
|
-
async handleFormSubmit(event, component) {
|
|
1248
|
-
event.preventDefault();
|
|
1249
|
-
const form = event.target;
|
|
1250
|
-
let action = form.getAttribute("action");
|
|
1251
|
-
const method = form.getAttribute("method") || "POST";
|
|
1252
|
-
const successHandler = form.getAttribute("data-success");
|
|
1253
|
-
const errorHandler = form.getAttribute("data-error");
|
|
1254
|
-
const loadingHandler = form.getAttribute("data-loading");
|
|
1255
|
-
const redirectTo = form.getAttribute("data-redirect");
|
|
1256
|
-
action = this.processActionParams(action, component);
|
|
1257
|
-
if (!this.validateForm(form, component)) {
|
|
1258
|
-
return;
|
|
1259
|
-
}
|
|
1260
|
-
if (this.isDuplicateRequest(form)) {
|
|
1261
|
-
return;
|
|
1262
|
-
}
|
|
1263
|
-
this.startFormSubmission(form);
|
|
1264
|
-
const formData = new FormData(form);
|
|
1265
|
-
const data = Object.fromEntries(formData.entries());
|
|
1266
|
-
try {
|
|
1267
|
-
if (loadingHandler && component[loadingHandler]) {
|
|
1268
|
-
component[loadingHandler](true, form);
|
|
1269
|
-
}
|
|
1270
|
-
this.log("debug", `Form submitting to: ${action}`, data);
|
|
1271
|
-
const response = await this.submitFormData(action, method, data, form, component, form._abortController.signal);
|
|
1272
|
-
if (successHandler && component[successHandler]) {
|
|
1273
|
-
component[successHandler](response, form);
|
|
1274
|
-
}
|
|
1275
|
-
this.finishFormSubmission(form);
|
|
1276
|
-
if (redirectTo) {
|
|
1277
|
-
requestAnimationFrame(() => {
|
|
1278
|
-
setTimeout(() => {
|
|
1279
|
-
component.navigateTo(redirectTo);
|
|
1280
|
-
}, 1e3);
|
|
1281
|
-
});
|
|
1282
|
-
}
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
if (error.name === "AbortError") {
|
|
1285
|
-
this.log("debug", "Form submission aborted");
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
this.log("warn", "Form submission error:", error);
|
|
1289
|
-
this.finishFormSubmission(form);
|
|
1290
|
-
if (errorHandler && component[errorHandler]) {
|
|
1291
|
-
component[errorHandler](error, form);
|
|
1292
|
-
} else {
|
|
1293
|
-
this.log("error", "Form submission error (no error handler defined):", error);
|
|
1294
|
-
}
|
|
1295
|
-
} finally {
|
|
1296
|
-
if (loadingHandler && component[loadingHandler]) {
|
|
1297
|
-
component[loadingHandler](false, form);
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
/**
|
|
1302
|
-
* 액션 파라미터 처리 (ApiHandler 재사용)
|
|
1303
|
-
*/
|
|
1304
|
-
processActionParams(actionTemplate, component) {
|
|
1305
|
-
return this.router.routeLoader.apiHandler.processURLParameters(actionTemplate, component);
|
|
1306
|
-
}
|
|
1307
|
-
/**
|
|
1308
|
-
* 폼 데이터 서브밋 (ApiHandler 활용)
|
|
1309
|
-
*/
|
|
1310
|
-
async submitFormData(action, method, data, form, component, signal = null) {
|
|
1311
|
-
const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
|
|
1312
|
-
const options = {
|
|
1313
|
-
method: method.toUpperCase(),
|
|
1314
|
-
headers: {},
|
|
1315
|
-
signal
|
|
1316
|
-
// AbortController 신호 추가
|
|
1317
|
-
};
|
|
1318
|
-
if (hasFile) {
|
|
1319
|
-
options.data = new FormData(form);
|
|
1320
|
-
} else {
|
|
1321
|
-
options.data = data;
|
|
1322
|
-
options.headers["Content-Type"] = "application/json";
|
|
1323
|
-
}
|
|
1324
|
-
return await this.router.routeLoader.apiHandler.fetchData(action, component, options);
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* 클라이언트 사이드 폼 검증
|
|
1328
|
-
*/
|
|
1329
|
-
validateForm(form, component) {
|
|
1330
|
-
let isValid = true;
|
|
1331
|
-
const inputs = form.querySelectorAll("input, textarea, select");
|
|
1332
|
-
inputs.forEach((input) => {
|
|
1333
|
-
if (!input.checkValidity()) {
|
|
1334
|
-
isValid = false;
|
|
1335
|
-
input.classList.add("error");
|
|
1336
|
-
return;
|
|
1337
|
-
}
|
|
1338
|
-
const validationFunction = input.getAttribute("data-validation");
|
|
1339
|
-
if (validationFunction) {
|
|
1340
|
-
const isInputValid = this.validateInput(input, validationFunction, component);
|
|
1341
|
-
if (!isInputValid) {
|
|
1342
|
-
isValid = false;
|
|
1343
|
-
input.classList.add("error");
|
|
1344
|
-
} else {
|
|
1345
|
-
input.classList.remove("error");
|
|
1346
|
-
}
|
|
1347
|
-
} else {
|
|
1348
|
-
input.classList.remove("error");
|
|
1349
|
-
}
|
|
1350
|
-
});
|
|
1351
|
-
return isValid;
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* 개별 입력 검증
|
|
1355
|
-
*/
|
|
1356
|
-
validateInput(input, validationFunction, component) {
|
|
1357
|
-
const value = input.value;
|
|
1358
|
-
if (typeof component[validationFunction] === "function") {
|
|
1359
|
-
try {
|
|
1360
|
-
return component[validationFunction](value, input);
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
this.log("warn", `Validation function '${validationFunction}' error:`, error);
|
|
1363
|
-
return false;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
this.log("warn", `Validation function '${validationFunction}' not found`);
|
|
1367
|
-
return true;
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* 모든 폼 요청 취소
|
|
1371
|
-
*/
|
|
1372
|
-
cancelAllRequests() {
|
|
1373
|
-
const forms = document.querySelectorAll("form");
|
|
1374
|
-
forms.forEach((form) => {
|
|
1375
|
-
if (form._isSubmitting) {
|
|
1376
|
-
this.abortFormSubmission(form);
|
|
1377
|
-
}
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
/**
|
|
1381
|
-
* 정리 (메모리 누수 방지)
|
|
1382
|
-
*/
|
|
1383
|
-
destroy() {
|
|
1384
|
-
this.cancelAllRequests();
|
|
1385
|
-
const forms = document.querySelectorAll("form.auto-form, form[action]");
|
|
1386
|
-
forms.forEach((form) => {
|
|
1387
|
-
if (form._boundSubmitHandler) {
|
|
1388
|
-
form.removeEventListener("submit", form._boundSubmitHandler);
|
|
1389
|
-
delete form._boundSubmitHandler;
|
|
1390
|
-
}
|
|
1391
|
-
this.cleanupFormState(form);
|
|
1392
|
-
});
|
|
1393
|
-
this.log("debug", "FormHandler destroyed");
|
|
1394
|
-
this.router = null;
|
|
1395
|
-
}
|
|
1396
|
-
/**
|
|
1397
|
-
* 폼 상태 정리
|
|
1398
|
-
*/
|
|
1399
|
-
cleanupFormState(form) {
|
|
1400
|
-
delete form._isSubmitting;
|
|
1401
|
-
delete form._abortController;
|
|
1402
|
-
if (form._timeoutId) {
|
|
1403
|
-
clearTimeout(form._timeoutId);
|
|
1404
|
-
delete form._timeoutId;
|
|
1405
|
-
}
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
// src/core/ApiHandler.js
|
|
1410
|
-
var ApiHandler = class {
|
|
1411
|
-
constructor(router, options = {}) {
|
|
1412
|
-
this.router = router;
|
|
1413
|
-
this.apiBaseURL = options.apiBaseURL || "";
|
|
1414
|
-
this.log("debug", "ApiHandler initialized");
|
|
1415
|
-
}
|
|
1416
|
-
/**
|
|
1417
|
-
* 로깅 래퍼 메서드
|
|
1418
|
-
*/
|
|
1419
|
-
log(level, ...args) {
|
|
1420
|
-
if (this.router?.errorHandler) {
|
|
1421
|
-
this.router.errorHandler.log(level, "ApiHandler", ...args);
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
/**
|
|
1425
|
-
* 컴포넌트 데이터 가져오기 (파라미터 치환 지원)
|
|
1426
|
-
*/
|
|
1427
|
-
async fetchData(dataURL, component = null, options = {}) {
|
|
1428
|
-
try {
|
|
1429
|
-
let processedURL = this.processURLParameters(dataURL, component);
|
|
1430
|
-
if (this.apiBaseURL && !this.isAbsoluteURL(processedURL)) {
|
|
1431
|
-
processedURL = this.combineURLs(this.apiBaseURL, processedURL);
|
|
1432
|
-
}
|
|
1433
|
-
const queryString = this.router.queryManager?.buildQueryString(this.router.queryManager?.getQueryParams()) || "";
|
|
1434
|
-
const fullURL = queryString ? `${processedURL}?${queryString}` : processedURL;
|
|
1435
|
-
this.log("debug", `Fetching data from: ${fullURL}`);
|
|
1436
|
-
const requestOptions = {
|
|
1437
|
-
method: options.method || "GET",
|
|
1438
|
-
headers: {
|
|
1439
|
-
"Content-Type": "application/json",
|
|
1440
|
-
"Accept": "application/json",
|
|
1441
|
-
...options.headers
|
|
1442
|
-
},
|
|
1443
|
-
...options
|
|
1444
|
-
};
|
|
1445
|
-
if (component?.$getToken && component.$getToken()) {
|
|
1446
|
-
requestOptions.headers["Authorization"] = `Bearer ${component.$getToken()}`;
|
|
1447
|
-
}
|
|
1448
|
-
if (options.data && ["POST", "PUT", "PATCH"].includes(requestOptions.method.toUpperCase())) {
|
|
1449
|
-
if (options.data instanceof FormData) {
|
|
1450
|
-
requestOptions.body = options.data;
|
|
1451
|
-
delete requestOptions.headers["Content-Type"];
|
|
1452
|
-
} else {
|
|
1453
|
-
requestOptions.body = JSON.stringify(options.data);
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
const response = await fetch(fullURL, requestOptions);
|
|
1457
|
-
if (!response.ok) {
|
|
1458
|
-
let error;
|
|
1459
|
-
try {
|
|
1460
|
-
error = await response.json();
|
|
1461
|
-
} catch (e) {
|
|
1462
|
-
error = { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
1463
|
-
}
|
|
1464
|
-
throw new Error(error.message || `HTTP ${response.status}`);
|
|
1465
|
-
}
|
|
1466
|
-
try {
|
|
1467
|
-
const data = await response.json();
|
|
1468
|
-
if (typeof data !== "object" || data === null) {
|
|
1469
|
-
throw new Error("Invalid data format: expected object");
|
|
1470
|
-
}
|
|
1471
|
-
return data;
|
|
1472
|
-
} catch (e) {
|
|
1473
|
-
return { success: true };
|
|
1474
|
-
}
|
|
1475
|
-
} catch (error) {
|
|
1476
|
-
this.log("error", "Failed to fetch data:", error);
|
|
1477
|
-
throw error;
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
/**
|
|
1481
|
-
* 여러 API 엔드포인트에서 데이터 가져오기
|
|
1482
|
-
*/
|
|
1483
|
-
async fetchMultipleData(dataConfig, component = null) {
|
|
1484
|
-
if (!dataConfig || typeof dataConfig !== "object") {
|
|
1485
|
-
return {};
|
|
1486
|
-
}
|
|
1487
|
-
const results = {};
|
|
1488
|
-
const errors = {};
|
|
1489
|
-
const promises = Object.entries(dataConfig).map(async ([key, config]) => {
|
|
1490
|
-
try {
|
|
1491
|
-
let url, options = {};
|
|
1492
|
-
if (typeof config === "string") {
|
|
1493
|
-
url = config;
|
|
1494
|
-
} else if (typeof config === "object") {
|
|
1495
|
-
url = config.url;
|
|
1496
|
-
options = { ...config };
|
|
1497
|
-
delete options.url;
|
|
1498
|
-
}
|
|
1499
|
-
if (url) {
|
|
1500
|
-
const data = await this.fetchData(url, component, options);
|
|
1501
|
-
results[key] = data;
|
|
1502
|
-
}
|
|
1503
|
-
} catch (error) {
|
|
1504
|
-
errors[key] = error;
|
|
1505
|
-
this.log("warn", `Failed to fetch data for '${key}':`, error);
|
|
1506
|
-
}
|
|
1507
|
-
});
|
|
1508
|
-
await Promise.all(promises);
|
|
1509
|
-
return { results, errors };
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* URL에서 파라미터 치환 처리 ({param} 형식)
|
|
1513
|
-
*/
|
|
1514
|
-
processURLParameters(url, component = null) {
|
|
1515
|
-
if (!url || typeof url !== "string") return url;
|
|
1516
|
-
let processedURL = url;
|
|
1517
|
-
const paramMatches = url.match(/\{([^}]+)\}/g);
|
|
1518
|
-
if (paramMatches && component) {
|
|
1519
|
-
paramMatches.forEach((match) => {
|
|
1520
|
-
const paramName = match.slice(1, -1);
|
|
1521
|
-
try {
|
|
1522
|
-
let paramValue = null;
|
|
1523
|
-
if (component.$options?.computed?.[paramName]) {
|
|
1524
|
-
paramValue = component[paramName];
|
|
1525
|
-
}
|
|
1526
|
-
if (paramValue === null || paramValue === void 0) {
|
|
1527
|
-
paramValue = component[paramName];
|
|
1528
|
-
}
|
|
1529
|
-
if (paramValue === null || paramValue === void 0) {
|
|
1530
|
-
if (component.getParam) {
|
|
1531
|
-
paramValue = component.getParam(paramName);
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
if (paramValue === null || paramValue === void 0) {
|
|
1535
|
-
paramValue = this.router.queryManager?.getParam(paramName);
|
|
1536
|
-
}
|
|
1537
|
-
if (paramValue !== null && paramValue !== void 0) {
|
|
1538
|
-
processedURL = processedURL.replace(
|
|
1539
|
-
match,
|
|
1540
|
-
encodeURIComponent(paramValue)
|
|
1541
|
-
);
|
|
1542
|
-
this.log("debug", `URL parameter resolved: ${paramName} = ${paramValue}`);
|
|
1543
|
-
} else {
|
|
1544
|
-
this.log("warn", `URL parameter '${paramName}' not found, keeping original: ${match}`);
|
|
1545
|
-
}
|
|
1546
|
-
} catch (error) {
|
|
1547
|
-
this.log("warn", `Error processing URL parameter '${paramName}':`, error);
|
|
1548
|
-
}
|
|
1549
|
-
});
|
|
1550
|
-
}
|
|
1551
|
-
return processedURL;
|
|
1552
|
-
}
|
|
1553
|
-
/**
|
|
1554
|
-
* HTTP 메서드별 헬퍼 함수들
|
|
1555
|
-
*/
|
|
1556
|
-
async get(url, component = null, options = {}) {
|
|
1557
|
-
return this.fetchData(url, component, { ...options, method: "GET" });
|
|
1558
|
-
}
|
|
1559
|
-
async post(url, data, component = null, options = {}) {
|
|
1560
|
-
return this.fetchData(url, component, { ...options, method: "POST", data });
|
|
1561
|
-
}
|
|
1562
|
-
async put(url, data, component = null, options = {}) {
|
|
1563
|
-
return this.fetchData(url, component, { ...options, method: "PUT", data });
|
|
1564
|
-
}
|
|
1565
|
-
async patch(url, data, component = null, options = {}) {
|
|
1566
|
-
return this.fetchData(url, component, { ...options, method: "PATCH", data });
|
|
1567
|
-
}
|
|
1568
|
-
async delete(url, component = null, options = {}) {
|
|
1569
|
-
return this.fetchData(url, component, { ...options, method: "DELETE" });
|
|
1570
|
-
}
|
|
1571
|
-
/**
|
|
1572
|
-
* 컴포넌트에 바인딩된 API 객체 생성
|
|
1573
|
-
*/
|
|
1574
|
-
bindToComponent(component) {
|
|
1575
|
-
return {
|
|
1576
|
-
get: (url, options = {}) => this.get(url, component, options),
|
|
1577
|
-
post: (url, data, options = {}) => this.post(url, data, component, options),
|
|
1578
|
-
put: (url, data, options = {}) => this.put(url, data, component, options),
|
|
1579
|
-
patch: (url, data, options = {}) => this.patch(url, data, component, options),
|
|
1580
|
-
delete: (url, options = {}) => this.delete(url, component, options),
|
|
1581
|
-
fetchData: (url, options = {}) => this.fetchData(url, component, options),
|
|
1582
|
-
fetchMultipleData: (dataConfig) => this.fetchMultipleData(dataConfig, component)
|
|
1583
|
-
};
|
|
1584
|
-
}
|
|
1585
|
-
/**
|
|
1586
|
-
* 절대 URL인지 확인
|
|
1587
|
-
*/
|
|
1588
|
-
isAbsoluteURL(url) {
|
|
1589
|
-
return /^https?:\/\//.test(url) || url.startsWith("//");
|
|
1590
|
-
}
|
|
1591
|
-
/**
|
|
1592
|
-
* 두 URL을 조합
|
|
1593
|
-
*/
|
|
1594
|
-
combineURLs(baseURL, relativeURL) {
|
|
1595
|
-
const cleanBase = baseURL.replace(/\/$/, "");
|
|
1596
|
-
const cleanRelative = relativeURL.startsWith("/") ? relativeURL : `/${relativeURL}`;
|
|
1597
|
-
return `${cleanBase}${cleanRelative}`;
|
|
1598
|
-
}
|
|
1599
|
-
/**
|
|
1600
|
-
* 정리 (메모리 누수 방지)
|
|
1601
|
-
*/
|
|
1602
|
-
destroy() {
|
|
1603
|
-
this.log("debug", "ApiHandler destroyed");
|
|
1604
|
-
this.router = null;
|
|
1605
|
-
}
|
|
1606
|
-
};
|
|
1607
|
-
|
|
1608
|
-
// src/core/ComponentLoader.js
|
|
1609
|
-
var ComponentLoader = class {
|
|
1610
|
-
constructor(router = null, options = {}) {
|
|
1611
|
-
this.config = {
|
|
1612
|
-
componentsPath: options.componentsPath || "/components",
|
|
1613
|
-
// srcPath 기준 상대 경로
|
|
1614
|
-
environment: options.environment || "development",
|
|
1615
|
-
...options
|
|
1616
|
-
};
|
|
1617
|
-
this.router = router;
|
|
1618
|
-
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
1619
|
-
this.unifiedComponents = null;
|
|
1620
|
-
}
|
|
1621
|
-
/**
|
|
1622
|
-
* 로깅 래퍼 메서드
|
|
1623
|
-
*/
|
|
1624
|
-
log(level, ...args) {
|
|
1625
|
-
if (this.router?.errorHandler) {
|
|
1626
|
-
this.router.errorHandler.log(level, "ComponentLoader", ...args);
|
|
1627
|
-
}
|
|
1628
|
-
}
|
|
1629
|
-
/**
|
|
1630
|
-
* 컴포넌트를 비동기로 로드 (캐시 지원)
|
|
1631
|
-
*/
|
|
1632
|
-
async loadComponent(componentName) {
|
|
1633
|
-
if (!componentName || typeof componentName !== "string") {
|
|
1634
|
-
throw new Error("Component name must be a non-empty string");
|
|
1635
|
-
}
|
|
1636
|
-
const cacheKey = `component_${componentName}`;
|
|
1637
|
-
const cachedComponent = this.router?.cacheManager?.get(cacheKey);
|
|
1638
|
-
if (cachedComponent) {
|
|
1639
|
-
this.log("debug", `Component '${componentName}' loaded from cache`);
|
|
1640
|
-
return cachedComponent;
|
|
1641
|
-
}
|
|
1642
|
-
if (this.loadingPromises.has(componentName)) {
|
|
1643
|
-
return this.loadingPromises.get(componentName);
|
|
1644
|
-
}
|
|
1645
|
-
const loadPromise = this._loadComponentFromFile(componentName);
|
|
1646
|
-
this.loadingPromises.set(componentName, loadPromise);
|
|
1647
|
-
try {
|
|
1648
|
-
const component = await loadPromise;
|
|
1649
|
-
if (component && this.router?.cacheManager) {
|
|
1650
|
-
this.router.cacheManager.set(cacheKey, component);
|
|
1651
|
-
this.log("debug", `Component '${componentName}' cached successfully`);
|
|
1652
|
-
}
|
|
1653
|
-
return component;
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
throw error;
|
|
1656
|
-
} finally {
|
|
1657
|
-
this.loadingPromises.delete(componentName);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* 파일에서 컴포넌트 로드
|
|
1662
|
-
*/
|
|
1663
|
-
async _loadComponentFromFile(componentName) {
|
|
1664
|
-
const componentRelativePath = `${this.config.componentsPath}/${componentName}.js`;
|
|
1665
|
-
let componentPath;
|
|
1666
|
-
if (this.router && this.router.config.srcPath) {
|
|
1667
|
-
const srcPath = this.router.config.srcPath;
|
|
1668
|
-
if (srcPath.startsWith("http")) {
|
|
1669
|
-
const cleanSrcPath = srcPath.endsWith("/") ? srcPath.slice(0, -1) : srcPath;
|
|
1670
|
-
const cleanComponentPath = componentRelativePath.startsWith("/") ? componentRelativePath : `/${componentRelativePath}`;
|
|
1671
|
-
componentPath = `${cleanSrcPath}${cleanComponentPath}`;
|
|
1672
|
-
} else {
|
|
1673
|
-
componentPath = this.router.resolvePath(`${srcPath}${componentRelativePath}`);
|
|
1674
|
-
}
|
|
1675
|
-
} else {
|
|
1676
|
-
componentPath = this.router ? this.router.resolvePath(`/src${componentRelativePath}`) : `/src${componentRelativePath}`;
|
|
1677
|
-
}
|
|
1678
|
-
try {
|
|
1679
|
-
const module = await import(componentPath);
|
|
1680
|
-
const component = module.default;
|
|
1681
|
-
if (!component) {
|
|
1682
|
-
throw new Error(`Component '${componentName}' has no default export`);
|
|
1683
|
-
}
|
|
1684
|
-
if (!component.name) {
|
|
1685
|
-
component.name = componentName;
|
|
1686
|
-
}
|
|
1687
|
-
this.log("debug", `Component '${componentName}' loaded successfully`);
|
|
1688
|
-
return component;
|
|
1689
|
-
} catch (error) {
|
|
1690
|
-
this.log("error", `Failed to load component '${componentName}':`, error);
|
|
1691
|
-
throw new Error(`Component '${componentName}' not found: ${error.message}`);
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
/**
|
|
1695
|
-
* 컴포넌트 모듈 클리어
|
|
1696
|
-
*/
|
|
1697
|
-
clearComponents() {
|
|
1698
|
-
this.loadingPromises.clear();
|
|
1699
|
-
this.unifiedComponents = null;
|
|
1700
|
-
this.log("debug", "All components cleared");
|
|
1701
|
-
}
|
|
1702
|
-
/**
|
|
1703
|
-
* 환경에 따른 모든 컴포넌트 로딩 (캐싱 지원)
|
|
1704
|
-
*/
|
|
1705
|
-
async loadAllComponents(componentNames = null) {
|
|
1706
|
-
let components;
|
|
1707
|
-
if (this.config.environment === "production") {
|
|
1708
|
-
if (this.unifiedComponents) {
|
|
1709
|
-
this.log("debug", "Using existing unified components");
|
|
1710
|
-
return this.unifiedComponents;
|
|
1711
|
-
}
|
|
1712
|
-
components = await this._loadProductionComponents();
|
|
1713
|
-
} else {
|
|
1714
|
-
components = await this._loadDevelopmentComponents(componentNames);
|
|
1715
|
-
}
|
|
1716
|
-
return components;
|
|
1717
|
-
}
|
|
1718
|
-
/**
|
|
1719
|
-
* 운영 모드: 통합 컴포넌트 로딩
|
|
1720
|
-
*/
|
|
1721
|
-
async _loadProductionComponents() {
|
|
1722
|
-
try {
|
|
1723
|
-
const componentsPath = `${this.router?.config?.routesPath || "/routes"}/_components.js`;
|
|
1724
|
-
this.log("debug", "[PRODUCTION] Loading unified components from:", componentsPath);
|
|
1725
|
-
const componentsModule = await import(componentsPath);
|
|
1726
|
-
if (typeof componentsModule.registerComponents === "function") {
|
|
1727
|
-
this.unifiedComponents = componentsModule.components || {};
|
|
1728
|
-
this.log("debug", `[PRODUCTION] Unified components loaded: ${Object.keys(this.unifiedComponents).length} components`);
|
|
1729
|
-
return this.unifiedComponents;
|
|
1730
|
-
} else {
|
|
1731
|
-
throw new Error("registerComponents function not found in components module");
|
|
1732
|
-
}
|
|
1733
|
-
} catch (error) {
|
|
1734
|
-
this.log("warn", "[PRODUCTION] Failed to load unified components:", error.message);
|
|
1735
|
-
this.unifiedComponents = {};
|
|
1736
|
-
return {};
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
/**
|
|
1740
|
-
* 개발 모드: 개별 컴포넌트 로딩
|
|
1741
|
-
*/
|
|
1742
|
-
async _loadDevelopmentComponents(componentNames = null) {
|
|
1743
|
-
const namesToLoad = componentNames || [];
|
|
1744
|
-
const components = {};
|
|
1745
|
-
if (namesToLoad.length === 0) {
|
|
1746
|
-
this.log("debug", "[DEVELOPMENT] No components to load");
|
|
1747
|
-
return components;
|
|
1748
|
-
}
|
|
1749
|
-
this.log("debug", `[DEVELOPMENT] Loading individual components: ${namesToLoad.join(", ")}`);
|
|
1750
|
-
for (const name of namesToLoad) {
|
|
1751
|
-
try {
|
|
1752
|
-
const component = await this.loadComponent(name);
|
|
1753
|
-
if (component) {
|
|
1754
|
-
components[name] = component;
|
|
1755
|
-
}
|
|
1756
|
-
} catch (loadError) {
|
|
1757
|
-
this.log("warn", `[DEVELOPMENT] Failed to load component ${name}:`, loadError.message);
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
this.log("debug", `[DEVELOPMENT] Individual components loaded: ${Object.keys(components).length} components`);
|
|
1761
|
-
return components;
|
|
1762
|
-
}
|
|
1763
|
-
/**
|
|
1764
|
-
* 템플릿과 레이아웃에서 사용된 컴포넌트 추출
|
|
1765
|
-
*/
|
|
1766
|
-
getComponentNames(template, layout = null, layoutName = null) {
|
|
1767
|
-
const componentSet = layout ? this._getLayoutComponents(layout, layoutName) : /* @__PURE__ */ new Set();
|
|
1768
|
-
if (template) {
|
|
1769
|
-
this._extractComponentsFromContent(template, componentSet);
|
|
1770
|
-
}
|
|
1771
|
-
const components = Array.from(componentSet);
|
|
1772
|
-
this.log("debug", `Discovered ${components.length} components:`, components);
|
|
1773
|
-
return components;
|
|
1774
|
-
}
|
|
1775
|
-
/**
|
|
1776
|
-
* 레이아웃에서 컴포넌트 추출 (캐시 활용)
|
|
1777
|
-
*/
|
|
1778
|
-
_getLayoutComponents(layout, layoutName) {
|
|
1779
|
-
if (!layout || typeof layout !== "string") return /* @__PURE__ */ new Set();
|
|
1780
|
-
if (!layoutName || typeof layoutName !== "string") return /* @__PURE__ */ new Set();
|
|
1781
|
-
const cacheKey = `layout_components_${layoutName}`;
|
|
1782
|
-
const cachedComponents = this.router?.cacheManager?.get(cacheKey);
|
|
1783
|
-
if (cachedComponents) {
|
|
1784
|
-
this.log("debug", `Using cached layout components for '${layoutName}'`);
|
|
1785
|
-
return cachedComponents;
|
|
1786
|
-
}
|
|
1787
|
-
const componentSet = /* @__PURE__ */ new Set();
|
|
1788
|
-
this._extractComponentsFromContent(layout, componentSet);
|
|
1789
|
-
if (this.router?.cacheManager) {
|
|
1790
|
-
this.router.cacheManager.set(cacheKey, componentSet);
|
|
1791
|
-
this.log("debug", `Cached layout components for '${layoutName}': ${Array.from(componentSet).join(", ")}`);
|
|
1792
|
-
}
|
|
1793
|
-
return componentSet;
|
|
1794
|
-
}
|
|
1795
|
-
/**
|
|
1796
|
-
* HTML 컨텐츠에서 Vue 컴포넌트 추출
|
|
1797
|
-
*/
|
|
1798
|
-
_extractComponentsFromContent(content, componentSet) {
|
|
1799
|
-
if (!content || typeof content !== "string") return;
|
|
1800
|
-
const componentPattern = /<([A-Z][a-zA-Z0-9]*)(?:\s[^>]*)?\/?>|<\/([A-Z][a-zA-Z0-9]*)\s*>/gs;
|
|
1801
|
-
let match;
|
|
1802
|
-
while ((match = componentPattern.exec(content)) !== null) {
|
|
1803
|
-
const componentName = match[1] || match[2];
|
|
1804
|
-
if (componentName && !this._isHtmlTag(componentName)) {
|
|
1805
|
-
componentSet.add(componentName);
|
|
1806
|
-
this.log("debug", `Found component: ${componentName}`);
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
/**
|
|
1811
|
-
* HTML 기본 태그인지 확인
|
|
1812
|
-
*/
|
|
1813
|
-
_isHtmlTag(tagName) {
|
|
1814
|
-
const htmlTags = [
|
|
1815
|
-
"div",
|
|
1816
|
-
"span",
|
|
1817
|
-
"p",
|
|
1818
|
-
"a",
|
|
1819
|
-
"img",
|
|
1820
|
-
"ul",
|
|
1821
|
-
"ol",
|
|
1822
|
-
"li",
|
|
1823
|
-
"h1",
|
|
1824
|
-
"h2",
|
|
1825
|
-
"h3",
|
|
1826
|
-
"h4",
|
|
1827
|
-
"h5",
|
|
1828
|
-
"h6",
|
|
1829
|
-
"table",
|
|
1830
|
-
"tr",
|
|
1831
|
-
"td",
|
|
1832
|
-
"th",
|
|
1833
|
-
"form",
|
|
1834
|
-
"select",
|
|
1835
|
-
"option",
|
|
1836
|
-
"textarea",
|
|
1837
|
-
"nav",
|
|
1838
|
-
"header",
|
|
1839
|
-
"footer",
|
|
1840
|
-
"main",
|
|
1841
|
-
"section",
|
|
1842
|
-
"article",
|
|
1843
|
-
"aside",
|
|
1844
|
-
"figure",
|
|
1845
|
-
"figcaption",
|
|
1846
|
-
"video",
|
|
1847
|
-
"audio",
|
|
1848
|
-
"canvas",
|
|
1849
|
-
"svg",
|
|
1850
|
-
"iframe",
|
|
1851
|
-
"script",
|
|
1852
|
-
"style",
|
|
1853
|
-
"link",
|
|
1854
|
-
"meta",
|
|
1855
|
-
"title",
|
|
1856
|
-
"body",
|
|
1857
|
-
"html",
|
|
1858
|
-
"head",
|
|
1859
|
-
"template",
|
|
1860
|
-
"slot"
|
|
1861
|
-
];
|
|
1862
|
-
return htmlTags.includes(tagName);
|
|
1863
|
-
}
|
|
1864
|
-
/**
|
|
1865
|
-
* 메모리 정리
|
|
1866
|
-
*/
|
|
1867
|
-
dispose() {
|
|
1868
|
-
this.clearComponents();
|
|
1869
|
-
this.log("debug", "ComponentLoader disposed");
|
|
1870
|
-
this.router = null;
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
|
|
1874
|
-
// src/core/RouteLoader.js
|
|
1875
|
-
var RouteLoader = class {
|
|
1876
|
-
constructor(router, options = {}) {
|
|
1877
|
-
this.config = {
|
|
1878
|
-
srcPath: options.srcPath || router.config.srcPath || "/src",
|
|
1879
|
-
// 소스 파일 경로
|
|
1880
|
-
routesPath: options.routesPath || router.config.routesPath || "/routes",
|
|
1881
|
-
// 프로덕션 라우트 경로
|
|
1882
|
-
environment: options.environment || "development",
|
|
1883
|
-
useLayout: options.useLayout !== false,
|
|
1884
|
-
defaultLayout: options.defaultLayout || "default"
|
|
1885
|
-
};
|
|
1886
|
-
this.router = router;
|
|
1887
|
-
this.formHandler = new FormHandler(router, this.config);
|
|
1888
|
-
this.apiHandler = new ApiHandler(router, this.config);
|
|
1889
|
-
this.componentLoader = new ComponentLoader(router, this.config);
|
|
1890
|
-
this.log("debug", "RouteLoader initialized with config:", this.config);
|
|
1891
|
-
}
|
|
1892
|
-
/**
|
|
1893
|
-
* 스크립트 파일 로드
|
|
1894
|
-
*/
|
|
1895
|
-
async loadScript(routeName) {
|
|
1896
|
-
let script;
|
|
1897
|
-
try {
|
|
1898
|
-
if (this.config.environment === "production") {
|
|
1899
|
-
const importPath = `${this.config.routesPath}/${routeName}.js`;
|
|
1900
|
-
this.log("debug", `Loading production route: ${importPath}`);
|
|
1901
|
-
const module = await import(importPath);
|
|
1902
|
-
script = module.default;
|
|
1903
|
-
} else {
|
|
1904
|
-
const importPath = `${this.config.srcPath}/logic/${routeName}.js`;
|
|
1905
|
-
this.log("debug", `Loading development route: ${importPath}`);
|
|
1906
|
-
const module = await import(importPath);
|
|
1907
|
-
script = module.default;
|
|
1908
|
-
}
|
|
1909
|
-
if (!script) {
|
|
1910
|
-
throw new Error(`Route '${routeName}' not found - no default export`);
|
|
1911
|
-
}
|
|
1912
|
-
} catch (error) {
|
|
1913
|
-
if (error.message.includes("Failed to resolve") || error.message.includes("Failed to fetch") || error.message.includes("not found") || error.name === "TypeError") {
|
|
1914
|
-
throw new Error(`Route '${routeName}' not found - 404`);
|
|
1915
|
-
}
|
|
1916
|
-
throw error;
|
|
1917
|
-
}
|
|
1918
|
-
return script;
|
|
1919
|
-
}
|
|
1920
|
-
/**
|
|
1921
|
-
* 템플릿 파일 로드 (실패시 기본값 반환)
|
|
1922
|
-
*/
|
|
1923
|
-
async loadTemplate(routeName) {
|
|
1924
|
-
try {
|
|
1925
|
-
const templatePath = `${this.config.srcPath}/views/${routeName}.html`;
|
|
1926
|
-
const response = await fetch(templatePath);
|
|
1927
|
-
if (!response.ok) throw new Error(`Template not found: ${response.status}`);
|
|
1928
|
-
const template = await response.text();
|
|
1929
|
-
this.log("debug", `Template '${routeName}' loaded successfully`);
|
|
1930
|
-
return template;
|
|
1931
|
-
} catch (error) {
|
|
1932
|
-
this.log("warn", `Template '${routeName}' not found, using default:`, error.message);
|
|
1933
|
-
return this.generateDefaultTemplate(routeName);
|
|
1934
|
-
}
|
|
1935
|
-
}
|
|
1936
|
-
/**
|
|
1937
|
-
* 스타일 파일 로드 (실패시 빈 문자열 반환)
|
|
1938
|
-
*/
|
|
1939
|
-
async loadStyle(routeName) {
|
|
1940
|
-
try {
|
|
1941
|
-
const stylePath = `${this.config.srcPath}/styles/${routeName}.css`;
|
|
1942
|
-
const response = await fetch(stylePath);
|
|
1943
|
-
if (!response.ok) throw new Error(`Style not found: ${response.status}`);
|
|
1944
|
-
const style = await response.text();
|
|
1945
|
-
this.log("debug", `Style '${routeName}' loaded successfully`);
|
|
1946
|
-
return style;
|
|
1947
|
-
} catch (error) {
|
|
1948
|
-
this.log("debug", `Style '${routeName}' not found, no styles applied:`, error.message);
|
|
1949
|
-
return "";
|
|
1950
|
-
}
|
|
1951
|
-
}
|
|
1952
|
-
/**
|
|
1953
|
-
* 레이아웃 파일 로드 (실패시 null 반환)
|
|
1954
|
-
*/
|
|
1955
|
-
async loadLayout(layoutName) {
|
|
1956
|
-
try {
|
|
1957
|
-
const layoutPath = `${this.config.srcPath}/layouts/${layoutName}.html`;
|
|
1958
|
-
const response = await fetch(layoutPath);
|
|
1959
|
-
if (!response.ok) throw new Error(`Layout not found: ${response.status}`);
|
|
1960
|
-
const layout = await response.text();
|
|
1961
|
-
this.log("debug", `Layout '${layoutName}' loaded successfully`);
|
|
1962
|
-
return layout;
|
|
1963
|
-
} catch (error) {
|
|
1964
|
-
this.log("debug", `Layout '${layoutName}' not found, no layout applied:`, error.message);
|
|
1965
|
-
return null;
|
|
1966
|
-
}
|
|
1967
|
-
}
|
|
1968
|
-
/**
|
|
1969
|
-
* 레이아웃과 템플릿 병합
|
|
1970
|
-
*/
|
|
1971
|
-
mergeLayoutWithTemplate(routeName, layout, template) {
|
|
1972
|
-
let result;
|
|
1973
|
-
if (layout.includes("{{ content }}")) {
|
|
1974
|
-
result = layout.replace(
|
|
1975
|
-
/{{ content }}/s,
|
|
1976
|
-
template
|
|
1977
|
-
);
|
|
1978
|
-
} else if (layout.includes('class="main-content"')) {
|
|
1979
|
-
this.log("debug", "Using main-content replacement");
|
|
1980
|
-
result = layout.replace(
|
|
1981
|
-
/(<div class="container">).*?(<\/div>\s*<\/main>)/s,
|
|
1982
|
-
`$1${template}$2`
|
|
1983
|
-
);
|
|
1984
|
-
} else {
|
|
1985
|
-
this.log("debug", "Wrapping template with layout");
|
|
1986
|
-
result = `${layout}
|
|
1987
|
-
${template}`;
|
|
1988
|
-
}
|
|
1989
|
-
return result;
|
|
1990
|
-
}
|
|
1991
|
-
/**
|
|
1992
|
-
* Vue 컴포넌트 생성
|
|
1993
|
-
*/
|
|
1994
|
-
async createVueComponent(routeName) {
|
|
1995
|
-
const cacheKey = `component_${routeName}`;
|
|
1996
|
-
const cached = this.router.cacheManager?.get(cacheKey);
|
|
1997
|
-
if (cached) {
|
|
1998
|
-
return cached;
|
|
1999
|
-
}
|
|
2000
|
-
const script = await this.loadScript(routeName);
|
|
2001
|
-
const router = this.router;
|
|
2002
|
-
const isProduction = this.config.environment === "production";
|
|
2003
|
-
let template, style = "", layout = null;
|
|
2004
|
-
if (isProduction) {
|
|
2005
|
-
template = script.template || this.generateDefaultTemplate(routeName);
|
|
2006
|
-
} else {
|
|
2007
|
-
const loadPromises = [
|
|
2008
|
-
this.loadTemplate(routeName),
|
|
2009
|
-
this.loadStyle(routeName)
|
|
2010
|
-
];
|
|
2011
|
-
if (this.config.useLayout && script.layout !== null) {
|
|
2012
|
-
loadPromises.push(this.loadLayout(script.layout || this.config.defaultLayout));
|
|
2013
|
-
} else {
|
|
2014
|
-
loadPromises.push(Promise.resolve(null));
|
|
2015
|
-
}
|
|
2016
|
-
const [loadedTemplate, loadedStyle, loadedLayout] = await Promise.all(loadPromises);
|
|
2017
|
-
template = loadedTemplate;
|
|
2018
|
-
style = loadedStyle;
|
|
2019
|
-
layout = loadedLayout;
|
|
2020
|
-
if (layout) {
|
|
2021
|
-
template = this.mergeLayoutWithTemplate(routeName, layout, template);
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
let loadedComponents = {};
|
|
2025
|
-
if (this.componentLoader) {
|
|
2026
|
-
try {
|
|
2027
|
-
let componentNames = null;
|
|
2028
|
-
if (!isProduction) {
|
|
2029
|
-
const layoutName = script.layout || this.config.defaultLayout;
|
|
2030
|
-
componentNames = this.componentLoader.getComponentNames(template, layout, layoutName);
|
|
2031
|
-
this.log("debug", `[DEVELOPMENT] Discovered components for route '${routeName}':`, componentNames);
|
|
2032
|
-
}
|
|
2033
|
-
loadedComponents = await this.componentLoader.loadAllComponents(componentNames);
|
|
2034
|
-
this.log("debug", `Components loaded successfully for route: ${routeName}`);
|
|
2035
|
-
} catch (error) {
|
|
2036
|
-
this.log("warn", `Component loading failed for route '${routeName}', continuing without components:`, error.message);
|
|
2037
|
-
loadedComponents = {};
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
const component = {
|
|
2041
|
-
...script,
|
|
2042
|
-
name: script.name || this.toPascalCase(routeName),
|
|
2043
|
-
template,
|
|
2044
|
-
components: loadedComponents,
|
|
2045
|
-
data() {
|
|
2046
|
-
const originalData = script.data ? script.data() : {};
|
|
2047
|
-
const commonData = {
|
|
2048
|
-
...originalData,
|
|
2049
|
-
currentRoute: routeName,
|
|
2050
|
-
$query: router.queryManager?.getQueryParams() || {},
|
|
2051
|
-
$params: router.queryManager?.getRouteParams() || {},
|
|
2052
|
-
$lang: (() => {
|
|
2053
|
-
try {
|
|
2054
|
-
return router.i18nManager?.getCurrentLanguage() || router.config.i18nDefaultLanguage || router.config.defaultLanguage || "ko";
|
|
2055
|
-
} catch (error) {
|
|
2056
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", "Failed to get current language:", error);
|
|
2057
|
-
return router.config.defaultLanguage || "ko";
|
|
2058
|
-
}
|
|
2059
|
-
})(),
|
|
2060
|
-
$dataLoading: false
|
|
2061
|
-
};
|
|
2062
|
-
return commonData;
|
|
2063
|
-
},
|
|
2064
|
-
computed: {
|
|
2065
|
-
...script.computed || {},
|
|
2066
|
-
// 하위 호환성을 위해 params는 유지하되 getAllParams 사용
|
|
2067
|
-
params() {
|
|
2068
|
-
return router.queryManager?.getAllParams() || {};
|
|
2069
|
-
}
|
|
2070
|
-
},
|
|
2071
|
-
async mounted() {
|
|
2072
|
-
this.$api = router.routeLoader.apiHandler.bindToComponent(this);
|
|
2073
|
-
this.$state = router.stateHandler;
|
|
2074
|
-
if (script.mounted) {
|
|
2075
|
-
await script.mounted.call(this);
|
|
2076
|
-
}
|
|
2077
|
-
if (script.dataURL) {
|
|
2078
|
-
await this.fetchData();
|
|
2079
|
-
}
|
|
2080
|
-
await this.$nextTick();
|
|
2081
|
-
router.routeLoader.formHandler.bindAutoForms(this);
|
|
2082
|
-
},
|
|
2083
|
-
methods: {
|
|
2084
|
-
...script.methods,
|
|
2085
|
-
// 라우팅 관련
|
|
2086
|
-
navigateTo: (route, params) => router.navigateTo(route, params),
|
|
2087
|
-
getCurrentRoute: () => router.getCurrentRoute(),
|
|
2088
|
-
// 통합된 파라미터 관리 (라우팅 + 쿼리 파라미터)
|
|
2089
|
-
getParams: () => router.queryManager?.getAllParams() || {},
|
|
2090
|
-
getParam: (key, defaultValue) => router.queryManager?.getParam(key, defaultValue),
|
|
2091
|
-
// i18n 관련 (resilient - i18n 실패해도 key 반환)
|
|
2092
|
-
$t: (key, params) => {
|
|
2093
|
-
try {
|
|
2094
|
-
return router.i18nManager?.t(key, params) || key;
|
|
2095
|
-
} catch (error) {
|
|
2096
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", "i18n translation failed, returning key:", error);
|
|
2097
|
-
return key;
|
|
2098
|
-
}
|
|
2099
|
-
},
|
|
2100
|
-
// 인증 관련 (핵심 4개 메소드만)
|
|
2101
|
-
isAuth: () => router.authManager?.isAuthenticated() || false,
|
|
2102
|
-
logout: () => router.authManager ? router.navigateTo(router.authManager.logout()) : null,
|
|
2103
|
-
getToken: () => router.authManager?.getAccessToken() || null,
|
|
2104
|
-
setToken: (token, options) => router.authManager?.setAccessToken(token, options) || false,
|
|
2105
|
-
// i18n 언어 관리
|
|
2106
|
-
getLanguage: () => router.i18nManager?.getCurrentLanguage() || router.config.defaultLanguage || "ko",
|
|
2107
|
-
setLanguage: (lang) => router.i18nManager?.setLanguage(lang),
|
|
2108
|
-
// 로깅 및 에러 처리
|
|
2109
|
-
log: (level, ...args) => {
|
|
2110
|
-
if (router.errorHandler) {
|
|
2111
|
-
router.errorHandler.log(level, `[${routeName}]`, ...args);
|
|
2112
|
-
}
|
|
2113
|
-
},
|
|
2114
|
-
// 데이터 fetch (ApiHandler 래퍼)
|
|
2115
|
-
async fetchData(dataConfig = null) {
|
|
2116
|
-
const configToUse = dataConfig || script.dataURL;
|
|
2117
|
-
if (!configToUse) return null;
|
|
2118
|
-
this.$dataLoading = true;
|
|
2119
|
-
try {
|
|
2120
|
-
if (typeof configToUse === "string") {
|
|
2121
|
-
const data = await router.routeLoader.apiHandler.fetchData(configToUse, this);
|
|
2122
|
-
Object.assign(this, data);
|
|
2123
|
-
this.$emit("data-loaded", data);
|
|
2124
|
-
return data;
|
|
2125
|
-
} else if (typeof configToUse === "object") {
|
|
2126
|
-
const { results, errors } = await router.routeLoader.apiHandler.fetchMultipleData(configToUse, this);
|
|
2127
|
-
Object.assign(this, results);
|
|
2128
|
-
if (Object.keys(results).length > 0) {
|
|
2129
|
-
this.$emit("data-loaded", results);
|
|
2130
|
-
}
|
|
2131
|
-
if (Object.keys(errors).length > 0) {
|
|
2132
|
-
this.$emit("data-error", errors);
|
|
2133
|
-
}
|
|
2134
|
-
return results;
|
|
2135
|
-
}
|
|
2136
|
-
return null;
|
|
2137
|
-
} catch (error) {
|
|
2138
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch data for ${routeName}:`, error);
|
|
2139
|
-
this.$emit("data-error", error);
|
|
2140
|
-
throw error;
|
|
2141
|
-
} finally {
|
|
2142
|
-
this.$dataLoading = false;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
},
|
|
2146
|
-
_routeName: routeName
|
|
2147
|
-
};
|
|
2148
|
-
if (!isProduction && style) {
|
|
2149
|
-
component._style = style;
|
|
2150
|
-
}
|
|
2151
|
-
this.router.cacheManager?.set(cacheKey, component);
|
|
2152
|
-
return component;
|
|
2153
|
-
}
|
|
2154
|
-
/**
|
|
2155
|
-
* 문자열을 PascalCase로 변환
|
|
2156
|
-
*/
|
|
2157
|
-
toPascalCase(str) {
|
|
2158
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
2159
|
-
}
|
|
2160
|
-
/**
|
|
2161
|
-
* 기본 템플릿 생성
|
|
2162
|
-
*/
|
|
2163
|
-
generateDefaultTemplate(routeName) {
|
|
2164
|
-
return `<div class="route-${routeName}"><h1>Route: ${routeName}</h1></div>`;
|
|
2165
|
-
}
|
|
2166
|
-
/**
|
|
2167
|
-
* 로깅 래퍼 메서드
|
|
2168
|
-
*/
|
|
2169
|
-
log(level, ...args) {
|
|
2170
|
-
if (this.router?.errorHandler) {
|
|
2171
|
-
this.router.errorHandler.log(level, "RouteLoader", ...args);
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* 정리 (메모리 누수 방지)
|
|
2176
|
-
*/
|
|
2177
|
-
destroy() {
|
|
2178
|
-
if (this.formHandler) {
|
|
2179
|
-
this.formHandler.destroy();
|
|
2180
|
-
this.formHandler = null;
|
|
2181
|
-
}
|
|
2182
|
-
if (this.apiHandler) {
|
|
2183
|
-
this.apiHandler.destroy();
|
|
2184
|
-
this.apiHandler = null;
|
|
2185
|
-
}
|
|
2186
|
-
if (this.componentLoader) {
|
|
2187
|
-
this.componentLoader.dispose();
|
|
2188
|
-
this.componentLoader = null;
|
|
2189
|
-
}
|
|
2190
|
-
this.log("debug", "RouteLoader destroyed");
|
|
2191
|
-
this.router = null;
|
|
2192
|
-
}
|
|
2193
|
-
};
|
|
2194
|
-
|
|
2195
|
-
// src/core/ErrorHandler.js
|
|
2196
|
-
var ErrorHandler = class {
|
|
2197
|
-
constructor(router, options = {}) {
|
|
2198
|
-
this.config = {
|
|
2199
|
-
logLevel: options.logLevel || "info",
|
|
2200
|
-
environment: options.environment || "development"
|
|
2201
|
-
};
|
|
2202
|
-
this.router = router;
|
|
2203
|
-
this.logLevels = {
|
|
2204
|
-
error: 0,
|
|
2205
|
-
warn: 1,
|
|
2206
|
-
info: 2,
|
|
2207
|
-
debug: 3
|
|
2208
|
-
};
|
|
2209
|
-
this.log("debug", "ErrorHandler", "ErrorHandler initialized with config:", this.config);
|
|
2210
|
-
}
|
|
2211
|
-
/**
|
|
2212
|
-
* 라우트 에러 처리
|
|
2213
|
-
*/
|
|
2214
|
-
async handleRouteError(routeName, error) {
|
|
2215
|
-
let errorCode = 500;
|
|
2216
|
-
let errorMessage = "\uD398\uC774\uC9C0\uB97C \uB85C\uB4DC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
2217
|
-
this.debug("ErrorHandler", "\uC5D0\uB7EC \uC0C1\uC138:", error.message, error.name);
|
|
2218
|
-
if (error.message.includes("not found") || error.message.includes("404") || error.message.includes("Failed to resolve") || error.message.includes("Failed to fetch") || error.name === "TypeError" && error.message.includes("resolve")) {
|
|
2219
|
-
errorCode = 404;
|
|
2220
|
-
errorMessage = `'${routeName}' \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`;
|
|
2221
|
-
} else if (error.message.includes("network") && !error.message.includes("not found")) {
|
|
2222
|
-
errorCode = 503;
|
|
2223
|
-
errorMessage = "\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD574 \uC8FC\uC138\uC694.";
|
|
2224
|
-
} else if (error.message.includes("permission") || error.message.includes("403")) {
|
|
2225
|
-
errorCode = 403;
|
|
2226
|
-
errorMessage = "\uD398\uC774\uC9C0\uC5D0 \uC811\uADFC\uD560 \uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.";
|
|
2227
|
-
}
|
|
2228
|
-
this.debug("ErrorHandler", `\uC5D0\uB7EC \uCF54\uB4DC \uACB0\uC815: ${errorCode} (\uB77C\uC6B0\uD2B8: ${routeName})`);
|
|
2229
|
-
this.reportError(routeName, error, errorCode);
|
|
2230
|
-
try {
|
|
2231
|
-
if (errorCode === 404) {
|
|
2232
|
-
await this.load404Page();
|
|
2233
|
-
} else {
|
|
2234
|
-
await this.loadErrorPage(errorCode, errorMessage);
|
|
2235
|
-
}
|
|
2236
|
-
} catch (fallbackError) {
|
|
2237
|
-
this.error("ErrorHandler", "\uC5D0\uB7EC \uD398\uC774\uC9C0 \uB85C\uB529 \uC2E4\uD328:", fallbackError);
|
|
2238
|
-
this.showFallbackErrorPage(errorCode, errorMessage);
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
/**
|
|
2242
|
-
* 404 페이지 로딩
|
|
2243
|
-
*/
|
|
2244
|
-
async load404Page() {
|
|
2245
|
-
try {
|
|
2246
|
-
this.info("ErrorHandler", "Loading 404 page...");
|
|
2247
|
-
const component = await this.createVueComponent("404");
|
|
2248
|
-
await this.renderComponentWithTransition(component, "404");
|
|
2249
|
-
this.info("ErrorHandler", "404 page loaded successfully");
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
this.error("ErrorHandler", "404 page loading failed:", error);
|
|
2252
|
-
this.showFallbackErrorPage("404", "\uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
/**
|
|
2256
|
-
* 에러 페이지 로딩
|
|
2257
|
-
*/
|
|
2258
|
-
async loadErrorPage(errorCode, errorMessage) {
|
|
2259
|
-
try {
|
|
2260
|
-
this.info("ErrorHandler", `Loading error page for ${errorCode}...`);
|
|
2261
|
-
const errorComponent = await this.createErrorComponent(errorCode, errorMessage);
|
|
2262
|
-
await this.renderComponentWithTransition(errorComponent, "error");
|
|
2263
|
-
this.info("ErrorHandler", `Error page ${errorCode} loaded successfully`);
|
|
2264
|
-
} catch (error) {
|
|
2265
|
-
this.error("ErrorHandler", `Error page ${errorCode} loading failed:`, error);
|
|
2266
|
-
this.showFallbackErrorPage(errorCode, errorMessage);
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
/**
|
|
2270
|
-
* 에러 컴포넌트 생성
|
|
2271
|
-
*/
|
|
2272
|
-
async createErrorComponent(errorCode, errorMessage) {
|
|
2273
|
-
try {
|
|
2274
|
-
const component = await this.createVueComponent("error");
|
|
2275
|
-
const errorComponent = {
|
|
2276
|
-
...component,
|
|
2277
|
-
data() {
|
|
2278
|
-
const originalData = component.data ? component.data() : {};
|
|
2279
|
-
return {
|
|
2280
|
-
...originalData,
|
|
2281
|
-
errorCode,
|
|
2282
|
-
errorMessage,
|
|
2283
|
-
showRetry: true,
|
|
2284
|
-
showGoHome: true
|
|
2285
|
-
};
|
|
2286
|
-
}
|
|
2287
|
-
};
|
|
2288
|
-
return errorComponent;
|
|
2289
|
-
} catch (error) {
|
|
2290
|
-
this.error("ErrorHandler", "Error component load failed:", error);
|
|
2291
|
-
throw new Error(`Cannot load error page: ${error.message}`);
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
/**
|
|
2295
|
-
* 폴백 에러 페이지 표시 (모든 에러 페이지가 실패했을 때)
|
|
2296
|
-
*/
|
|
2297
|
-
showFallbackErrorPage(errorCode, errorMessage) {
|
|
2298
|
-
const appElement = document.getElementById("app");
|
|
2299
|
-
if (!appElement) return;
|
|
2300
|
-
const fallbackHTML = `
|
|
2301
|
-
<div class="fallback-error-page" style="
|
|
2302
|
-
display: flex;
|
|
2303
|
-
flex-direction: column;
|
|
2304
|
-
align-items: center;
|
|
2305
|
-
justify-content: center;
|
|
2306
|
-
min-height: 100vh;
|
|
2307
|
-
padding: 2rem;
|
|
2308
|
-
text-align: center;
|
|
2309
|
-
background: #f8f9fa;
|
|
2310
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
2311
|
-
">
|
|
2312
|
-
<div style="
|
|
2313
|
-
background: white;
|
|
2314
|
-
padding: 3rem;
|
|
2315
|
-
border-radius: 12px;
|
|
2316
|
-
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
|
2317
|
-
max-width: 500px;
|
|
2318
|
-
">
|
|
2319
|
-
<h1 style="
|
|
2320
|
-
font-size: 4rem;
|
|
2321
|
-
margin: 0;
|
|
2322
|
-
color: #dc3545;
|
|
2323
|
-
font-weight: 300;
|
|
2324
|
-
">${errorCode}</h1>
|
|
2325
|
-
<h2 style="
|
|
2326
|
-
margin: 1rem 0;
|
|
2327
|
-
color: #495057;
|
|
2328
|
-
font-weight: 400;
|
|
2329
|
-
">${errorMessage}</h2>
|
|
2330
|
-
<p style="
|
|
2331
|
-
color: #6c757d;
|
|
2332
|
-
margin-bottom: 2rem;
|
|
2333
|
-
line-height: 1.5;
|
|
2334
|
-
">\uC694\uCCAD\uD558\uC2E0 \uD398\uC774\uC9C0\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.</p>
|
|
2335
|
-
<button onclick="window.location.hash = '#/'" style="
|
|
2336
|
-
background: #007bff;
|
|
2337
|
-
color: white;
|
|
2338
|
-
border: none;
|
|
2339
|
-
padding: 12px 24px;
|
|
2340
|
-
border-radius: 6px;
|
|
2341
|
-
cursor: pointer;
|
|
2342
|
-
font-size: 1rem;
|
|
2343
|
-
transition: background 0.2s;
|
|
2344
|
-
" onmouseover="this.style.background='#0056b3'" onmouseout="this.style.background='#007bff'">
|
|
2345
|
-
\uD648\uC73C\uB85C \uB3CC\uC544\uAC00\uAE30
|
|
2346
|
-
</button>
|
|
2347
|
-
</div>
|
|
2348
|
-
</div>
|
|
2349
|
-
`;
|
|
2350
|
-
appElement.innerHTML = fallbackHTML;
|
|
2351
|
-
this.info("ErrorHandler", `Fallback error page displayed for ${errorCode}`);
|
|
2352
|
-
}
|
|
2353
|
-
/**
|
|
2354
|
-
* 에러 리포팅
|
|
2355
|
-
*/
|
|
2356
|
-
reportError(routeName, error, errorCode) {
|
|
2357
|
-
const errorReport = {
|
|
2358
|
-
route: routeName,
|
|
2359
|
-
errorCode,
|
|
2360
|
-
errorMessage: error.message,
|
|
2361
|
-
stack: error.stack,
|
|
2362
|
-
url: window.location.href,
|
|
2363
|
-
userAgent: navigator.userAgent,
|
|
2364
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2365
|
-
routerConfig: {
|
|
2366
|
-
environment: this.router?.config?.environment || "unknown",
|
|
2367
|
-
mode: this.router?.config?.mode || "unknown"
|
|
2368
|
-
}
|
|
2369
|
-
};
|
|
2370
|
-
this.error("ErrorHandler", "\uB77C\uC6B0\uD130 \uC5D0\uB7EC \uB9AC\uD3EC\uD2B8:", errorReport);
|
|
2371
|
-
}
|
|
2372
|
-
/**
|
|
2373
|
-
* Vue 컴포넌트 생성 (RouteLoader 위임)
|
|
2374
|
-
*/
|
|
2375
|
-
async createVueComponent(routeName) {
|
|
2376
|
-
if (this.router.routeLoader) {
|
|
2377
|
-
return await this.router.routeLoader.createVueComponent(routeName);
|
|
2378
|
-
}
|
|
2379
|
-
throw new Error("RouteLoader not available");
|
|
2380
|
-
}
|
|
2381
|
-
/**
|
|
2382
|
-
* 컴포넌트 렌더링 (ViewManager 위임)
|
|
2383
|
-
*/
|
|
2384
|
-
async renderComponentWithTransition(component, routeName) {
|
|
2385
|
-
if (this.router.renderComponentWithTransition) {
|
|
2386
|
-
return await this.router.renderComponentWithTransition(component, routeName);
|
|
2387
|
-
}
|
|
2388
|
-
throw new Error("Render function not available");
|
|
2389
|
-
}
|
|
2390
|
-
/**
|
|
2391
|
-
* 통합 로깅 시스템
|
|
2392
|
-
* @param {string} level - 로그 레벨 (error, warn, info, debug)
|
|
2393
|
-
* @param {string} component - 컴포넌트 이름 (선택적)
|
|
2394
|
-
* @param {...any} args - 로그 메시지
|
|
2395
|
-
*/
|
|
2396
|
-
log(level, component, ...args) {
|
|
2397
|
-
if (typeof level !== "string" || !this.logLevels.hasOwnProperty(level)) {
|
|
2398
|
-
args = [component, ...args];
|
|
2399
|
-
component = level;
|
|
2400
|
-
level = "info";
|
|
2401
|
-
}
|
|
2402
|
-
const currentLevelValue = this.logLevels[this.config.logLevel] || this.logLevels.info;
|
|
2403
|
-
const messageLevelValue = this.logLevels[level] || this.logLevels.info;
|
|
2404
|
-
if (messageLevelValue > currentLevelValue) {
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
|
-
if (this.config.environment === "production" && messageLevelValue > this.logLevels.warn) {
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
const prefix = component ? `[${component}]` : "[ViewLogic]";
|
|
2411
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().substring(11, 23);
|
|
2412
|
-
switch (level) {
|
|
2413
|
-
case "error":
|
|
2414
|
-
console.error(`${timestamp} ${prefix}`, ...args);
|
|
2415
|
-
break;
|
|
2416
|
-
case "warn":
|
|
2417
|
-
console.warn(`${timestamp} ${prefix}`, ...args);
|
|
2418
|
-
break;
|
|
2419
|
-
case "info":
|
|
2420
|
-
console.info(`${timestamp} ${prefix}`, ...args);
|
|
2421
|
-
break;
|
|
2422
|
-
case "debug":
|
|
2423
|
-
console.log(`${timestamp} ${prefix}`, ...args);
|
|
2424
|
-
break;
|
|
2425
|
-
default:
|
|
2426
|
-
console.log(`${timestamp} ${prefix}`, ...args);
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
/**
|
|
2430
|
-
* 에러 로그 (항상 출력)
|
|
2431
|
-
*/
|
|
2432
|
-
error(component, ...args) {
|
|
2433
|
-
this.log("error", component, ...args);
|
|
2434
|
-
}
|
|
2435
|
-
/**
|
|
2436
|
-
* 경고 로그
|
|
2437
|
-
*/
|
|
2438
|
-
warn(component, ...args) {
|
|
2439
|
-
this.log("warn", component, ...args);
|
|
2440
|
-
}
|
|
2441
|
-
/**
|
|
2442
|
-
* 정보 로그
|
|
2443
|
-
*/
|
|
2444
|
-
info(component, ...args) {
|
|
2445
|
-
this.log("info", component, ...args);
|
|
2446
|
-
}
|
|
2447
|
-
/**
|
|
2448
|
-
* 디버그 로그
|
|
2449
|
-
*/
|
|
2450
|
-
debug(component, ...args) {
|
|
2451
|
-
this.log("debug", component, ...args);
|
|
2452
|
-
}
|
|
2453
|
-
/**
|
|
2454
|
-
* 정리 (메모리 누수 방지)
|
|
2455
|
-
*/
|
|
2456
|
-
destroy() {
|
|
2457
|
-
this.router = null;
|
|
2458
|
-
this.info("ErrorHandler", "ErrorHandler destroyed");
|
|
2459
|
-
}
|
|
2460
|
-
};
|
|
2461
|
-
|
|
2462
|
-
// src/core/StateHandler.js
|
|
2463
|
-
var StateHandler = class {
|
|
2464
|
-
constructor(router) {
|
|
2465
|
-
this.router = router;
|
|
2466
|
-
this.state = {};
|
|
2467
|
-
this.listeners = /* @__PURE__ */ new Map();
|
|
2468
|
-
this.log("debug", "StateHandler initialized");
|
|
2469
|
-
}
|
|
2470
|
-
/**
|
|
2471
|
-
* 로깅 래퍼 메서드
|
|
2472
|
-
*/
|
|
2473
|
-
log(level, ...args) {
|
|
2474
|
-
if (this.router?.errorHandler) {
|
|
2475
|
-
this.router.errorHandler.log(level, "StateHandler", ...args);
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
/**
|
|
2479
|
-
* 상태 값 설정
|
|
2480
|
-
*/
|
|
2481
|
-
set(key, value) {
|
|
2482
|
-
const oldValue = this.state[key];
|
|
2483
|
-
this.state[key] = value;
|
|
2484
|
-
this.emitChange(key, value, oldValue);
|
|
2485
|
-
this.log("debug", `State set: ${key}`, value);
|
|
2486
|
-
return value;
|
|
2487
|
-
}
|
|
2488
|
-
/**
|
|
2489
|
-
* 상태 값 가져오기
|
|
2490
|
-
*/
|
|
2491
|
-
get(key, defaultValue = void 0) {
|
|
2492
|
-
const value = this.state.hasOwnProperty(key) ? this.state[key] : defaultValue;
|
|
2493
|
-
this.log("debug", `State get: ${key}`, value);
|
|
2494
|
-
return value;
|
|
2495
|
-
}
|
|
2496
|
-
/**
|
|
2497
|
-
* 상태 존재 확인
|
|
2498
|
-
*/
|
|
2499
|
-
has(key) {
|
|
2500
|
-
return this.state.hasOwnProperty(key);
|
|
2501
|
-
}
|
|
2502
|
-
/**
|
|
2503
|
-
* 상태 삭제
|
|
2504
|
-
*/
|
|
2505
|
-
delete(key) {
|
|
2506
|
-
if (this.has(key)) {
|
|
2507
|
-
const oldValue = this.state[key];
|
|
2508
|
-
delete this.state[key];
|
|
2509
|
-
this.emitChange(key, void 0, oldValue);
|
|
2510
|
-
this.log("debug", `State deleted: ${key}`);
|
|
2511
|
-
return true;
|
|
2512
|
-
}
|
|
2513
|
-
return false;
|
|
2514
|
-
}
|
|
2515
|
-
/**
|
|
2516
|
-
* 모든 상태 초기화
|
|
2517
|
-
*/
|
|
2518
|
-
clear() {
|
|
2519
|
-
const keys = Object.keys(this.state);
|
|
2520
|
-
this.state = {};
|
|
2521
|
-
keys.forEach((key) => {
|
|
2522
|
-
this.emitChange(key, void 0, this.state[key]);
|
|
2523
|
-
});
|
|
2524
|
-
this.log("debug", "All state cleared");
|
|
2525
|
-
return keys.length;
|
|
2526
|
-
}
|
|
2527
|
-
/**
|
|
2528
|
-
* 여러 상태 한 번에 설정
|
|
2529
|
-
*/
|
|
2530
|
-
update(updates) {
|
|
2531
|
-
if (!updates || typeof updates !== "object") {
|
|
2532
|
-
this.log("warn", "Invalid updates object provided");
|
|
2533
|
-
return;
|
|
2534
|
-
}
|
|
2535
|
-
Object.entries(updates).forEach(([key, value]) => {
|
|
2536
|
-
this.set(key, value);
|
|
2537
|
-
});
|
|
2538
|
-
}
|
|
2539
|
-
/**
|
|
2540
|
-
* 모든 상태 반환
|
|
2541
|
-
*/
|
|
2542
|
-
getAll() {
|
|
2543
|
-
return { ...this.state };
|
|
2544
|
-
}
|
|
2545
|
-
/**
|
|
2546
|
-
* 상태 변경 리스너 등록
|
|
2547
|
-
*/
|
|
2548
|
-
watch(key, callback) {
|
|
2549
|
-
if (!this.listeners.has(key)) {
|
|
2550
|
-
this.listeners.set(key, []);
|
|
2551
|
-
}
|
|
2552
|
-
this.listeners.get(key).push(callback);
|
|
2553
|
-
this.log("debug", `Watcher added for: ${key}`);
|
|
2554
|
-
}
|
|
2555
|
-
/**
|
|
2556
|
-
* 상태 변경 리스너 제거
|
|
2557
|
-
*/
|
|
2558
|
-
unwatch(key, callback) {
|
|
2559
|
-
if (this.listeners.has(key)) {
|
|
2560
|
-
const callbacks = this.listeners.get(key);
|
|
2561
|
-
const index = callbacks.indexOf(callback);
|
|
2562
|
-
if (index > -1) {
|
|
2563
|
-
callbacks.splice(index, 1);
|
|
2564
|
-
this.log("debug", `Watcher removed for: ${key}`);
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
}
|
|
2568
|
-
/**
|
|
2569
|
-
* 상태 변경 이벤트 발생
|
|
2570
|
-
*/
|
|
2571
|
-
emitChange(key, newValue, oldValue) {
|
|
2572
|
-
if (this.listeners.has(key)) {
|
|
2573
|
-
this.listeners.get(key).forEach((callback) => {
|
|
2574
|
-
try {
|
|
2575
|
-
callback(newValue, oldValue, key);
|
|
2576
|
-
} catch (error) {
|
|
2577
|
-
this.log("error", "State watcher error:", error);
|
|
2578
|
-
}
|
|
2579
|
-
});
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
/**
|
|
2583
|
-
* 상태 통계
|
|
2584
|
-
*/
|
|
2585
|
-
getStats() {
|
|
2586
|
-
return {
|
|
2587
|
-
stateCount: Object.keys(this.state).length,
|
|
2588
|
-
watcherCount: Array.from(this.listeners.values()).reduce((sum, arr) => sum + arr.length, 0),
|
|
2589
|
-
keys: Object.keys(this.state)
|
|
2590
|
-
};
|
|
2591
|
-
}
|
|
2592
|
-
/**
|
|
2593
|
-
* 정리 (메모리 누수 방지)
|
|
2594
|
-
*/
|
|
2595
|
-
destroy() {
|
|
2596
|
-
this.state = {};
|
|
2597
|
-
this.listeners.clear();
|
|
2598
|
-
this.log("debug", "StateHandler destroyed");
|
|
2599
|
-
}
|
|
2600
|
-
};
|
|
2601
|
-
|
|
2602
|
-
// src/viewlogic-router.js
|
|
2603
|
-
var ViewLogicRouter = class {
|
|
2604
|
-
constructor(options = {}) {
|
|
2605
|
-
this.version = options.version || "1.0.0";
|
|
2606
|
-
this.config = this._buildConfig(options);
|
|
2607
|
-
this.currentHash = "";
|
|
2608
|
-
this.currentVueApp = null;
|
|
2609
|
-
this.previousVueApp = null;
|
|
2610
|
-
this.transitionInProgress = false;
|
|
2611
|
-
this.isReady = false;
|
|
2612
|
-
this.readyPromise = null;
|
|
2613
|
-
this._boundHandleRouteChange = this.handleRouteChange.bind(this);
|
|
2614
|
-
this.readyPromise = this.initialize();
|
|
2615
|
-
}
|
|
2616
|
-
/**
|
|
2617
|
-
* 설정 빌드 (분리하여 가독성 향상)
|
|
2618
|
-
*/
|
|
2619
|
-
_buildConfig(options) {
|
|
2620
|
-
const currentOrigin = window.location.origin;
|
|
2621
|
-
const defaults = {
|
|
2622
|
-
basePath: "/",
|
|
2623
|
-
// 애플리케이션 기본 경로 (서브폴더 배포용)
|
|
2624
|
-
srcPath: "/src",
|
|
2625
|
-
// 소스 파일 경로
|
|
2626
|
-
mode: "hash",
|
|
2627
|
-
cacheMode: "memory",
|
|
2628
|
-
cacheTTL: 3e5,
|
|
2629
|
-
maxCacheSize: 50,
|
|
2630
|
-
useLayout: true,
|
|
2631
|
-
defaultLayout: "default",
|
|
2632
|
-
environment: "development",
|
|
2633
|
-
routesPath: "/routes",
|
|
2634
|
-
// 프로덕션 라우트 경로
|
|
2635
|
-
enableErrorReporting: true,
|
|
2636
|
-
useI18n: false,
|
|
2637
|
-
defaultLanguage: "ko",
|
|
2638
|
-
i18nPath: "/i18n",
|
|
2639
|
-
// 다국어 파일 경로
|
|
2640
|
-
logLevel: "info",
|
|
2641
|
-
apiBaseURL: "",
|
|
2642
|
-
requestTimeout: 3e4,
|
|
2643
|
-
uploadTimeout: 3e5,
|
|
2644
|
-
authEnabled: false,
|
|
2645
|
-
loginRoute: "login",
|
|
2646
|
-
protectedRoutes: [],
|
|
2647
|
-
protectedPrefixes: [],
|
|
2648
|
-
publicRoutes: ["login", "register", "home"],
|
|
2649
|
-
checkAuthFunction: null,
|
|
2650
|
-
redirectAfterLogin: "home",
|
|
2651
|
-
authCookieName: "authToken",
|
|
2652
|
-
authStorage: "localStorage"
|
|
2653
|
-
};
|
|
2654
|
-
const config = { ...defaults, ...options };
|
|
2655
|
-
config.srcPath = this.resolvePath(config.srcPath, config.basePath);
|
|
2656
|
-
config.routesPath = this.resolvePath(config.routesPath, config.basePath);
|
|
2657
|
-
config.i18nPath = this.resolvePath(config.i18nPath, config.basePath);
|
|
2658
|
-
return config;
|
|
2659
|
-
}
|
|
2660
|
-
/**
|
|
2661
|
-
* 두 경로를 안전하게 조합 및 정규화 (기본 유틸리티)
|
|
2662
|
-
* @param {string} basePath - 기본 경로
|
|
2663
|
-
* @param {string} relativePath - 상대 경로
|
|
2664
|
-
* @returns {string} 조합된 경로 (예: '/examples' + '/about' → '/examples/about')
|
|
2665
|
-
*/
|
|
2666
|
-
combinePaths(basePath, relativePath) {
|
|
2667
|
-
if (!basePath || basePath === "/") {
|
|
2668
|
-
return relativePath.replace(/\/+/g, "/");
|
|
2669
|
-
}
|
|
2670
|
-
const cleanBase = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
|
|
2671
|
-
const cleanRelative = relativePath.startsWith("/") ? relativePath : `/${relativePath}`;
|
|
2672
|
-
const combined = `${cleanBase}${cleanRelative}`;
|
|
2673
|
-
return combined.replace(/\/+/g, "/");
|
|
2674
|
-
}
|
|
2675
|
-
/**
|
|
2676
|
-
* 상대 경로를 완전한 절대 URL로 변환 (파일 시스템 전용)
|
|
2677
|
-
* @param {string} path - 변환할 경로
|
|
2678
|
-
* @param {string} basePath - 기본 경로 (옵션)
|
|
2679
|
-
* @returns {string} 완전한 절대 URL (예: 'http://localhost:3000/examples/about')
|
|
2680
|
-
*/
|
|
2681
|
-
resolvePath(path, basePath = null) {
|
|
2682
|
-
if (basePath === null) {
|
|
2683
|
-
basePath = this.config?.basePath || "/";
|
|
2684
|
-
}
|
|
2685
|
-
const currentOrigin = window.location.origin;
|
|
2686
|
-
if (path.startsWith("http")) {
|
|
2687
|
-
return path;
|
|
2688
|
-
}
|
|
2689
|
-
if (path.startsWith("/")) {
|
|
2690
|
-
const combinedPath = this.combinePaths(basePath, path);
|
|
2691
|
-
return `${currentOrigin}${combinedPath}`;
|
|
2692
|
-
}
|
|
2693
|
-
const currentPathname = window.location.pathname;
|
|
2694
|
-
const currentBase = currentPathname.endsWith("/") ? currentPathname : currentPathname.substring(0, currentPathname.lastIndexOf("/") + 1);
|
|
2695
|
-
const resolvedPath = this.combinePaths(currentBase, path);
|
|
2696
|
-
return `${currentOrigin}${resolvedPath}`;
|
|
2697
|
-
}
|
|
2698
|
-
/**
|
|
2699
|
-
* 로깅 래퍼 메서드
|
|
2700
|
-
*/
|
|
2701
|
-
log(level, ...args) {
|
|
2702
|
-
if (this.errorHandler) {
|
|
2703
|
-
this.errorHandler.log(level, "Router", ...args);
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
|
-
/**
|
|
2707
|
-
* 통합 초기화 - 매니저 생성 → 비동기 로딩 → 라우터 시작
|
|
2708
|
-
*/
|
|
2709
|
-
async initialize() {
|
|
2710
|
-
try {
|
|
2711
|
-
this.cacheManager = new CacheManager(this, this.config);
|
|
2712
|
-
this.stateHandler = new StateHandler(this);
|
|
2713
|
-
this.routeLoader = new RouteLoader(this, this.config);
|
|
2714
|
-
this.queryManager = new QueryManager(this);
|
|
2715
|
-
this.errorHandler = new ErrorHandler(this, this.config);
|
|
2716
|
-
if (this.config.useI18n) {
|
|
2717
|
-
try {
|
|
2718
|
-
this.i18nManager = new I18nManager(this, this.config);
|
|
2719
|
-
if (this.i18nManager.initPromise) {
|
|
2720
|
-
await this.i18nManager.initPromise;
|
|
2721
|
-
}
|
|
2722
|
-
this.log("info", "I18nManager initialized successfully");
|
|
2723
|
-
} catch (i18nError) {
|
|
2724
|
-
this.log("warn", "I18nManager initialization failed, continuing without i18n:", i18nError.message);
|
|
2725
|
-
this.i18nManager = null;
|
|
2726
|
-
this.config.useI18n = false;
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
if (this.config.authEnabled) {
|
|
2730
|
-
this.authManager = new AuthManager(this, this.config);
|
|
2731
|
-
}
|
|
2732
|
-
this.isReady = true;
|
|
2733
|
-
this.init();
|
|
2734
|
-
} catch (error) {
|
|
2735
|
-
this.log("error", "Router initialization failed:", error);
|
|
2736
|
-
this.isReady = true;
|
|
2737
|
-
this.init();
|
|
2738
|
-
}
|
|
2739
|
-
}
|
|
2740
|
-
/**
|
|
2741
|
-
* 라우터가 준비될 때까지 대기
|
|
2742
|
-
*/
|
|
2743
|
-
async waitForReady() {
|
|
2744
|
-
if (this.isReady) return true;
|
|
2745
|
-
if (this.readyPromise) {
|
|
2746
|
-
await this.readyPromise;
|
|
2747
|
-
}
|
|
2748
|
-
return this.isReady;
|
|
2749
|
-
}
|
|
2750
|
-
init() {
|
|
2751
|
-
const isHashMode = this.config.mode === "hash";
|
|
2752
|
-
window.addEventListener(
|
|
2753
|
-
isHashMode ? "hashchange" : "popstate",
|
|
2754
|
-
this._boundHandleRouteChange
|
|
2755
|
-
);
|
|
2756
|
-
const initRoute = () => {
|
|
2757
|
-
if (isHashMode && !window.location.hash) {
|
|
2758
|
-
window.location.hash = "#/";
|
|
2759
|
-
} else if (!isHashMode && window.location.pathname === "/") {
|
|
2760
|
-
this.navigateTo("home");
|
|
2761
|
-
} else {
|
|
2762
|
-
this.handleRouteChange();
|
|
2763
|
-
}
|
|
2764
|
-
};
|
|
2765
|
-
if (document.readyState === "loading") {
|
|
2766
|
-
document.addEventListener("DOMContentLoaded", initRoute);
|
|
2767
|
-
} else {
|
|
2768
|
-
requestAnimationFrame(initRoute);
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
handleRouteChange() {
|
|
2772
|
-
const { route, queryParams } = this._parseCurrentLocation();
|
|
2773
|
-
this.queryManager?.setCurrentQueryParams(queryParams);
|
|
2774
|
-
if (route !== this.currentHash || this.queryManager?.hasQueryParamsChanged(queryParams)) {
|
|
2775
|
-
this.currentHash = route;
|
|
2776
|
-
this.loadRoute(route);
|
|
2777
|
-
}
|
|
2778
|
-
}
|
|
2779
|
-
/**
|
|
2780
|
-
* 현재 위치 파싱 (분리하여 가독성 향상)
|
|
2781
|
-
*/
|
|
2782
|
-
_parseCurrentLocation() {
|
|
2783
|
-
if (this.config.mode === "hash") {
|
|
2784
|
-
const hashPath = window.location.hash.slice(1) || "/";
|
|
2785
|
-
const [pathPart, queryPart] = hashPath.split("?");
|
|
2786
|
-
let route = "home";
|
|
2787
|
-
if (pathPart && pathPart !== "/") {
|
|
2788
|
-
route = pathPart.startsWith("/") ? pathPart.slice(1) : pathPart;
|
|
2789
|
-
}
|
|
2790
|
-
return {
|
|
2791
|
-
route: route || "home",
|
|
2792
|
-
queryParams: this.queryManager?.parseQueryString(queryPart || window.location.search.slice(1)) || {}
|
|
2793
|
-
};
|
|
2794
|
-
} else {
|
|
2795
|
-
const fullPath = window.location.pathname;
|
|
2796
|
-
const basePath = this.config.basePath || "/";
|
|
2797
|
-
let route = fullPath;
|
|
2798
|
-
if (basePath !== "/" && fullPath.startsWith(basePath)) {
|
|
2799
|
-
route = fullPath.slice(basePath.length);
|
|
2800
|
-
}
|
|
2801
|
-
if (route.startsWith("/")) {
|
|
2802
|
-
route = route.slice(1);
|
|
2803
|
-
}
|
|
2804
|
-
return {
|
|
2805
|
-
route: route || "home",
|
|
2806
|
-
queryParams: this.queryManager?.parseQueryString(window.location.search.slice(1)) || {}
|
|
2807
|
-
};
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
async loadRoute(routeName) {
|
|
2811
|
-
const inProgress = this.transitionInProgress;
|
|
2812
|
-
if (inProgress) {
|
|
2813
|
-
return;
|
|
2814
|
-
}
|
|
2815
|
-
try {
|
|
2816
|
-
this.transitionInProgress = true;
|
|
2817
|
-
const authResult = this.authManager ? await this.authManager.checkAuthentication(routeName) : { allowed: true, reason: "auth_disabled" };
|
|
2818
|
-
if (!authResult.allowed) {
|
|
2819
|
-
if (this.authManager) {
|
|
2820
|
-
this.authManager.emitAuthEvent("auth_required", {
|
|
2821
|
-
originalRoute: routeName,
|
|
2822
|
-
loginRoute: this.config.loginRoute
|
|
2823
|
-
});
|
|
2824
|
-
if (routeName !== this.config.loginRoute) {
|
|
2825
|
-
this.navigateTo(this.config.loginRoute, { redirect: routeName });
|
|
2826
|
-
} else {
|
|
2827
|
-
this.navigateTo(this.config.loginRoute);
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
return;
|
|
2831
|
-
}
|
|
2832
|
-
const appElement = document.getElementById("app");
|
|
2833
|
-
if (!appElement) {
|
|
2834
|
-
throw new Error("App element not found");
|
|
2835
|
-
}
|
|
2836
|
-
const component = await this.routeLoader.createVueComponent(routeName);
|
|
2837
|
-
await this.renderComponentWithTransition(component, routeName);
|
|
2838
|
-
} catch (error) {
|
|
2839
|
-
this.log("error", `Route loading failed [${routeName}]:`, error.message);
|
|
2840
|
-
if (this.errorHandler) {
|
|
2841
|
-
await this.errorHandler.handleRouteError(routeName, error);
|
|
2842
|
-
} else {
|
|
2843
|
-
console.error("[Router] Critical: No error handler available for route error:", error);
|
|
2844
|
-
}
|
|
2845
|
-
} finally {
|
|
2846
|
-
this.transitionInProgress = false;
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
async renderComponentWithTransition(vueComponent, routeName) {
|
|
2850
|
-
const appElement = document.getElementById("app");
|
|
2851
|
-
if (!appElement) return;
|
|
2852
|
-
const newPageContainer = document.createElement("div");
|
|
2853
|
-
newPageContainer.className = "page-container page-entered";
|
|
2854
|
-
newPageContainer.id = `page-${routeName}-${Date.now()}`;
|
|
2855
|
-
const existingContainers = appElement.querySelectorAll(".page-container");
|
|
2856
|
-
existingContainers.forEach((container) => {
|
|
2857
|
-
container.classList.remove("page-entered");
|
|
2858
|
-
container.classList.add("page-exiting");
|
|
2859
|
-
});
|
|
2860
|
-
appElement.appendChild(newPageContainer);
|
|
2861
|
-
if (this.config.environment === "development" && vueComponent._style) {
|
|
2862
|
-
this.applyStyle(vueComponent._style, routeName);
|
|
2863
|
-
}
|
|
2864
|
-
const { createApp } = Vue;
|
|
2865
|
-
const newVueApp = createApp(vueComponent);
|
|
2866
|
-
newVueApp.config.globalProperties.$router = {
|
|
2867
|
-
navigateTo: (route, params) => this.navigateTo(route, params),
|
|
2868
|
-
getCurrentRoute: () => this.getCurrentRoute(),
|
|
2869
|
-
// 통합된 파라미터 관리 (라우팅 + 쿼리 파라미터)
|
|
2870
|
-
getParams: () => this.queryManager?.getAllParams() || {},
|
|
2871
|
-
getParam: (key, defaultValue) => this.queryManager?.getParam(key, defaultValue),
|
|
2872
|
-
// 쿼리 파라미터 전용 메서드 (하위 호환성)
|
|
2873
|
-
getQueryParams: () => this.queryManager?.getQueryParams() || {},
|
|
2874
|
-
getQueryParam: (key, defaultValue) => this.queryManager?.getQueryParam(key, defaultValue),
|
|
2875
|
-
setQueryParams: (params, replace) => this.queryManager?.setQueryParams(params, replace),
|
|
2876
|
-
removeQueryParams: (keys) => this.queryManager?.removeQueryParams(keys),
|
|
2877
|
-
// 라우팅 파라미터 전용 메서드
|
|
2878
|
-
getRouteParams: () => this.queryManager?.getRouteParams() || {},
|
|
2879
|
-
getRouteParam: (key, defaultValue) => this.queryManager?.getRouteParam(key, defaultValue),
|
|
2880
|
-
currentRoute: this.currentHash,
|
|
2881
|
-
currentQuery: this.queryManager?.getQueryParams() || {}
|
|
2882
|
-
};
|
|
2883
|
-
newVueApp.mount(`#${newPageContainer.id}`);
|
|
2884
|
-
window.scrollTo(0, 0);
|
|
2885
|
-
requestAnimationFrame(() => {
|
|
2886
|
-
this.cleanupPreviousPages();
|
|
2887
|
-
this.transitionInProgress = false;
|
|
2888
|
-
});
|
|
2889
|
-
if (this.currentVueApp) {
|
|
2890
|
-
this.previousVueApp = this.currentVueApp;
|
|
2891
|
-
}
|
|
2892
|
-
this.currentVueApp = newVueApp;
|
|
2893
|
-
}
|
|
2894
|
-
cleanupPreviousPages() {
|
|
2895
|
-
const appElement = document.getElementById("app");
|
|
2896
|
-
if (!appElement) return;
|
|
2897
|
-
const fragment = document.createDocumentFragment();
|
|
2898
|
-
const exitingContainers = appElement.querySelectorAll(".page-container.page-exiting");
|
|
2899
|
-
exitingContainers.forEach((container) => container.remove());
|
|
2900
|
-
if (this.previousVueApp) {
|
|
2901
|
-
try {
|
|
2902
|
-
this.previousVueApp.unmount();
|
|
2903
|
-
} catch (error) {
|
|
2904
|
-
}
|
|
2905
|
-
this.previousVueApp = null;
|
|
2906
|
-
}
|
|
2907
|
-
appElement.querySelector(".loading")?.remove();
|
|
2908
|
-
}
|
|
2909
|
-
applyStyle(css, routeName) {
|
|
2910
|
-
const existing = document.querySelector(`style[data-route="${routeName}"]`);
|
|
2911
|
-
if (existing) existing.remove();
|
|
2912
|
-
if (css) {
|
|
2913
|
-
const style = document.createElement("style");
|
|
2914
|
-
style.textContent = css;
|
|
2915
|
-
style.setAttribute("data-route", routeName);
|
|
2916
|
-
document.head.appendChild(style);
|
|
2917
|
-
}
|
|
2918
|
-
}
|
|
2919
|
-
navigateTo(routeName, params = null) {
|
|
2920
|
-
if (typeof routeName === "object") {
|
|
2921
|
-
params = routeName.params || null;
|
|
2922
|
-
routeName = routeName.route;
|
|
2923
|
-
}
|
|
2924
|
-
if (routeName !== this.currentHash && this.queryManager) {
|
|
2925
|
-
this.queryManager.clearQueryParams();
|
|
2926
|
-
}
|
|
2927
|
-
if (this.queryManager) {
|
|
2928
|
-
this.queryManager.setCurrentRouteParams(params);
|
|
2929
|
-
}
|
|
2930
|
-
this.updateURL(routeName, params);
|
|
2931
|
-
}
|
|
2932
|
-
getCurrentRoute() {
|
|
2933
|
-
return this.currentHash;
|
|
2934
|
-
}
|
|
2935
|
-
updateURL(route, params = null) {
|
|
2936
|
-
const queryParams = params || this.queryManager?.getQueryParams() || {};
|
|
2937
|
-
const queryString = this.queryManager?.buildQueryString(queryParams) || "";
|
|
2938
|
-
let base = route === "home" ? "/" : `/${route}`;
|
|
2939
|
-
if (this.config.mode === "hash") {
|
|
2940
|
-
const url = queryString ? `${base}?${queryString}` : base;
|
|
2941
|
-
const newHash = `#${url}`;
|
|
2942
|
-
if (window.location.hash !== newHash) {
|
|
2943
|
-
window.location.hash = newHash;
|
|
2944
|
-
}
|
|
2945
|
-
} else {
|
|
2946
|
-
base = this.combinePaths(this.config.basePath, base);
|
|
2947
|
-
const url = queryString ? `${base}?${queryString}` : base;
|
|
2948
|
-
window.history.pushState({}, "", url);
|
|
2949
|
-
this.handleRouteChange();
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
/**
|
|
2953
|
-
* 라우터 정리 (메모리 누수 방지)
|
|
2954
|
-
*/
|
|
2955
|
-
destroy() {
|
|
2956
|
-
window.removeEventListener(
|
|
2957
|
-
this.config.mode === "hash" ? "hashchange" : "popstate",
|
|
2958
|
-
this._boundHandleRouteChange
|
|
2959
|
-
);
|
|
2960
|
-
if (this.currentVueApp) {
|
|
2961
|
-
this.currentVueApp.unmount();
|
|
2962
|
-
this.currentVueApp = null;
|
|
2963
|
-
}
|
|
2964
|
-
if (this.previousVueApp) {
|
|
2965
|
-
this.previousVueApp.unmount();
|
|
2966
|
-
this.previousVueApp = null;
|
|
2967
|
-
}
|
|
2968
|
-
Object.values(this).forEach((manager) => {
|
|
2969
|
-
if (manager && typeof manager.destroy === "function") {
|
|
2970
|
-
manager.destroy();
|
|
2971
|
-
}
|
|
2972
|
-
});
|
|
2973
|
-
this.cacheManager?.clearAll();
|
|
2974
|
-
const appElement = document.getElementById("app");
|
|
2975
|
-
if (appElement) {
|
|
2976
|
-
appElement.innerHTML = "";
|
|
2977
|
-
}
|
|
2978
|
-
this.log("info", "Router destroyed");
|
|
2979
|
-
}
|
|
2980
|
-
};
|
|
2981
|
-
export {
|
|
2982
|
-
ViewLogicRouter
|
|
2983
|
-
};
|
|
2984
|
-
//# sourceMappingURL=viewlogic-router.js.map
|