viewlogic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,465 @@
1
+ // ViewLogic Router - ES6 Module
2
+ import { I18nManager } from './plugins/I18nManager.js';
3
+ import { AuthManager } from './plugins/AuthManager.js';
4
+ import { CacheManager } from './plugins/CacheManager.js';
5
+ import { QueryManager } from './plugins/QueryManager.js';
6
+ import { RouteLoader } from './core/RouteLoader.js';
7
+ import { ErrorHandler } from './core/ErrorHandler.js';
8
+ import { ComponentLoader } from './core/ComponentLoader.js';
9
+
10
+ export class ViewLogicRouter {
11
+ constructor(options = {}) {
12
+ // 버전 정보
13
+ this.version = options.version || '1.0.0';
14
+
15
+ // 기본 환경설정 최적화
16
+ this.config = this._buildConfig(options);
17
+
18
+ this.currentHash = '';
19
+ this.currentVueApp = null;
20
+ this.previousVueApp = null; // 이전 Vue 앱 (전환 효과를 위해 보관)
21
+ this.componentLoader = null; // 컴포넌트 로더 인스턴스
22
+
23
+ // LoadingManager가 없을 때를 위한 기본 전환 상태
24
+ this.transitionInProgress = false;
25
+
26
+ // 초기화 준비 상태
27
+ this.isReady = false;
28
+ this.readyPromise = null;
29
+
30
+ // 이벤트 리스너 바인딩 최적화
31
+ this._boundHandleRouteChange = this.handleRouteChange.bind(this);
32
+
33
+ // 모든 초기화를 한번에 처리
34
+ this.readyPromise = this.initialize();
35
+ }
36
+
37
+ /**
38
+ * 설정 빌드 (분리하여 가독성 향상)
39
+ */
40
+ _buildConfig(options) {
41
+ const defaults = {
42
+ basePath: '/src',
43
+ mode: 'hash',
44
+ cacheMode: 'memory',
45
+ cacheTTL: 300000,
46
+ maxCacheSize: 50,
47
+ useLayout: true,
48
+ defaultLayout: 'default',
49
+ environment: 'development',
50
+ routesPath: '/routes',
51
+ enableErrorReporting: true,
52
+ useComponents: true,
53
+ componentNames: ['Button', 'Modal', 'Card', 'Toast', 'Input', 'Tabs', 'Checkbox', 'Alert', 'DynamicInclude', 'HtmlInclude'],
54
+ useI18n: true,
55
+ defaultLanguage: 'ko',
56
+ logLevel: 'info',
57
+ authEnabled: false,
58
+ loginRoute: 'login',
59
+ protectedRoutes: [],
60
+ protectedPrefixes: [],
61
+ publicRoutes: ['login', 'register', 'home'],
62
+ checkAuthFunction: null,
63
+ redirectAfterLogin: 'home',
64
+ authCookieName: 'authToken',
65
+ authFallbackCookieNames: ['accessToken', 'token', 'jwt'],
66
+ authStorage: 'cookie',
67
+ authCookieOptions: {},
68
+ authSkipValidation: false,
69
+ enableParameterValidation: true,
70
+ maxParameterLength: 1000,
71
+ maxParameterCount: 50,
72
+ maxArraySize: 100,
73
+ allowedKeyPattern: /^[a-zA-Z0-9_-]+$/,
74
+ logSecurityWarnings: true
75
+ };
76
+
77
+ return { ...defaults, ...options };
78
+ }
79
+
80
+
81
+ /**
82
+ * 로깅 래퍼 메서드
83
+ */
84
+ log(level, ...args) {
85
+ if (this.errorHandler) {
86
+ this.errorHandler.log(level, 'Router', ...args);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * 통합 초기화 - 매니저 생성 → 비동기 로딩 → 라우터 시작
92
+ */
93
+ async initialize() {
94
+ try {
95
+ // 1. 매니저 초기화 (동기)
96
+ // 항상 필요한 매니저들
97
+ this.cacheManager = new CacheManager(this, this.config);
98
+ this.routeLoader = new RouteLoader(this, this.config);
99
+ this.queryManager = new QueryManager(this, this.config);
100
+ this.errorHandler = new ErrorHandler(this, this.config);
101
+
102
+ // 조건부 매니저들
103
+ if (this.config.useI18n) {
104
+ this.i18nManager = new I18nManager(this, this.config);
105
+ if (this.i18nManager.initPromise) {
106
+ await this.i18nManager.initPromise;
107
+ }
108
+ }
109
+
110
+ if (this.config.authEnabled) {
111
+ this.authManager = new AuthManager(this, this.config);
112
+ }
113
+
114
+ if (this.config.useComponents) {
115
+ this.componentLoader = new ComponentLoader(this, {
116
+ ...this.config,
117
+ basePath: this.config.basePath + '/components',
118
+ cache: true,
119
+ componentNames: this.config.componentNames
120
+ });
121
+ await this.componentLoader.loadAllComponents();
122
+ }
123
+
124
+ // 2. 라우터 시작
125
+ this.isReady = true;
126
+ this.init();
127
+
128
+ } catch (error) {
129
+ this.log('error', 'Router initialization failed:', error);
130
+ // 실패해도 라우터는 시작 (graceful degradation)
131
+ this.isReady = true;
132
+ this.init();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * 라우터가 준비될 때까지 대기
138
+ */
139
+ async waitForReady() {
140
+ if (this.isReady) return true;
141
+ if (this.readyPromise) {
142
+ await this.readyPromise;
143
+ }
144
+ return this.isReady;
145
+ }
146
+
147
+
148
+ init() {
149
+ const isHashMode = this.config.mode === 'hash';
150
+
151
+ // 이벤트 리스너 등록 (메모리 최적화)
152
+ window.addEventListener(
153
+ isHashMode ? 'hashchange' : 'popstate',
154
+ this._boundHandleRouteChange
155
+ );
156
+
157
+ // DOM 로드 처리 통합
158
+ const initRoute = () => {
159
+ if (isHashMode && !window.location.hash) {
160
+ window.location.hash = '#/';
161
+ } else if (!isHashMode && window.location.pathname === '/') {
162
+ this.navigateTo('home');
163
+ } else {
164
+ this.handleRouteChange();
165
+ }
166
+ };
167
+
168
+ if (document.readyState === 'loading') {
169
+ document.addEventListener('DOMContentLoaded', initRoute);
170
+ } else {
171
+ // requestAnimationFrame으로 성능 개선
172
+ requestAnimationFrame(initRoute);
173
+ }
174
+ }
175
+
176
+ handleRouteChange() {
177
+ const { route, queryParams } = this._parseCurrentLocation();
178
+
179
+ // Store current query parameters in QueryManager
180
+ this.queryManager?.setCurrentQueryParams(queryParams);
181
+
182
+ // 변경사항이 있을 때만 로드 (성능 최적화)
183
+ if (route !== this.currentHash || this.queryManager?.hasQueryParamsChanged(queryParams)) {
184
+ this.currentHash = route;
185
+ this.loadRoute(route);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * 현재 위치 파싱 (분리하여 가독성 향상)
191
+ */
192
+ _parseCurrentLocation() {
193
+ if (this.config.mode === 'hash') {
194
+ const hashPath = window.location.hash.slice(1) || '/';
195
+ const [pathPart, queryPart] = hashPath.split('?');
196
+
197
+ // 경로 파싱 최적화
198
+ let route = 'home';
199
+ if (pathPart && pathPart !== '/') {
200
+ route = pathPart.startsWith('/') ? pathPart.slice(1) : pathPart;
201
+ }
202
+
203
+ return {
204
+ route: route || 'home',
205
+ queryParams: this.queryManager?.parseQueryString(queryPart || window.location.search.slice(1)) || {}
206
+ };
207
+ } else {
208
+ return {
209
+ route: window.location.pathname.slice(1) || 'home',
210
+ queryParams: this.queryManager?.parseQueryString(window.location.search.slice(1)) || {}
211
+ };
212
+ }
213
+ }
214
+
215
+ async loadRoute(routeName) {
216
+ // 전환이 진행 중이면 무시
217
+ const inProgress = this.transitionInProgress;
218
+
219
+ if (inProgress) {
220
+ return;
221
+ }
222
+
223
+ try {
224
+ this.transitionInProgress = true;
225
+
226
+ // 인증 체크
227
+ const authResult = this.authManager ?
228
+ await this.authManager.checkAuthentication(routeName) :
229
+ { allowed: true, reason: 'auth_disabled' };
230
+ if (!authResult.allowed) {
231
+ // 인증 실패 시 로그인 페이지로 리다이렉트
232
+ if (this.authManager) {
233
+ this.authManager.emitAuthEvent('auth_required', {
234
+ originalRoute: routeName,
235
+ loginRoute: this.config.loginRoute
236
+ });
237
+ const redirectUrl = routeName !== this.config.loginRoute ?
238
+ `${this.config.loginRoute}?redirect=${encodeURIComponent(routeName)}` :
239
+ this.config.loginRoute;
240
+ this.navigateTo(redirectUrl);
241
+ }
242
+ return;
243
+ }
244
+
245
+ const appElement = document.getElementById('app');
246
+ if (!appElement) {
247
+ throw new Error('App element not found');
248
+ }
249
+
250
+ // Vue 컴포넌트 생성 (백그라운드에서)
251
+ const component = await this.routeLoader.createVueComponent(routeName);
252
+
253
+ // 새로운 페이지를 오버레이로 렌더링
254
+ await this.renderComponentWithTransition(component, routeName);
255
+
256
+ // 로딩 완료
257
+
258
+ } catch (error) {
259
+ this.log('error', `Route loading failed [${routeName}]:`, error.message);
260
+
261
+
262
+ // 에러 타입에 따른 처리
263
+ if (this.errorHandler) {
264
+ await this.errorHandler.handleRouteError(routeName, error);
265
+ } else {
266
+ console.error('[Router] No error handler available');
267
+ }
268
+ } finally {
269
+ // 모든 처리가 완료된 후 전환 상태 리셋
270
+ this.transitionInProgress = false;
271
+ }
272
+ }
273
+
274
+ async renderComponentWithTransition(vueComponent, routeName) {
275
+ const appElement = document.getElementById('app');
276
+ if (!appElement) return;
277
+
278
+ // 새로운 페이지 컨테이너 생성
279
+ const newPageContainer = document.createElement('div');
280
+ newPageContainer.className = 'page-container page-entered';
281
+ newPageContainer.id = `page-${routeName}-${Date.now()}`;
282
+
283
+ // 기존 컨테이너가 있다면 즉시 숨기기
284
+ const existingContainers = appElement.querySelectorAll('.page-container');
285
+ existingContainers.forEach(container => {
286
+ container.classList.remove('page-entered');
287
+ container.classList.add('page-exiting');
288
+ });
289
+
290
+ // 새 컨테이너를 앱에 추가
291
+ appElement.appendChild(newPageContainer);
292
+
293
+ // 개발 모드에서만 스타일 적용 (프로덕션 모드는 빌드된 JS에서 자동 처리)
294
+ if (this.config.environment === 'development' && vueComponent._style) {
295
+ this.applyStyle(vueComponent._style, routeName);
296
+ }
297
+
298
+ // 새로운 Vue 앱을 새 컨테이너에 마운트
299
+ const { createApp } = Vue;
300
+ const newVueApp = createApp(vueComponent);
301
+
302
+ // Vue 3 전역 속성 설정
303
+ newVueApp.config.globalProperties.$router = {
304
+ navigateTo: (route, params) => this.navigateTo(route, params),
305
+ getCurrentRoute: () => this.getCurrentRoute(),
306
+ getQueryParams: () => this.queryManager?.getQueryParams() || {},
307
+ getQueryParam: (key) => this.queryManager?.getQueryParam(key),
308
+ setQueryParams: (params, replace) => this.queryManager?.setQueryParams(params, replace),
309
+ removeQueryParams: (keys) => this.queryManager?.removeQueryParams(keys),
310
+ currentRoute: this.currentHash,
311
+ currentQuery: this.queryManager?.getQueryParams() || {}
312
+ };
313
+
314
+ // 모바일 메뉴 전역 함수 추가
315
+
316
+ newVueApp.mount(`#${newPageContainer.id}`);
317
+
318
+ // requestAnimationFrame으로 성능 개선
319
+ requestAnimationFrame(() => {
320
+ this.cleanupPreviousPages();
321
+ this.transitionInProgress = false;
322
+ });
323
+
324
+ // 이전 앱 정리 준비
325
+ if (this.currentVueApp) {
326
+ this.previousVueApp = this.currentVueApp;
327
+ }
328
+
329
+ this.currentVueApp = newVueApp;
330
+ }
331
+
332
+ cleanupPreviousPages() {
333
+ const appElement = document.getElementById('app');
334
+ if (!appElement) return;
335
+
336
+ // 배치 DOM 조작으로 성능 개선
337
+ const fragment = document.createDocumentFragment();
338
+ const exitingContainers = appElement.querySelectorAll('.page-container.page-exiting');
339
+
340
+ // 한번에 제거
341
+ exitingContainers.forEach(container => container.remove());
342
+
343
+ // 이전 Vue 앱 정리
344
+ if (this.previousVueApp) {
345
+ try {
346
+ this.previousVueApp.unmount();
347
+ } catch (error) {
348
+ // 무시 (이미 언마운트된 경우)
349
+ }
350
+ this.previousVueApp = null;
351
+ }
352
+
353
+ // 로딩 엘리먼트 제거
354
+
355
+ appElement.querySelector('.loading')?.remove();
356
+ }
357
+
358
+ applyStyle(css, routeName) {
359
+ // 기존 스타일 제거
360
+ const existing = document.querySelector(`style[data-route="${routeName}"]`);
361
+ if (existing) existing.remove();
362
+
363
+ if (css) {
364
+ const style = document.createElement('style');
365
+ style.textContent = css;
366
+ style.setAttribute('data-route', routeName);
367
+ document.head.appendChild(style);
368
+ }
369
+ }
370
+
371
+
372
+ navigateTo(routeName, params = null) {
373
+ // If routeName is an object, treat it as {route, params}
374
+ if (typeof routeName === 'object') {
375
+ params = routeName.params || null;
376
+ routeName = routeName.route;
377
+ }
378
+
379
+ // Clear current query params if navigating to a different route
380
+ if (routeName !== this.currentHash && this.queryManager) {
381
+ this.queryManager.clearQueryParams();
382
+ }
383
+
384
+ // Update URL with new route and params
385
+ this.updateURL(routeName, params);
386
+ }
387
+
388
+ getCurrentRoute() {
389
+ return this.currentHash;
390
+ }
391
+
392
+
393
+ updateURL(route, params = null) {
394
+ const queryParams = params || this.queryManager?.getQueryParams() || {};
395
+ const queryString = this.queryManager?.buildQueryString(queryParams) || '';
396
+
397
+ // URL 빌드 최적화
398
+ const buildURL = (route, queryString, isHash = true) => {
399
+ const base = route === 'home' ? '/' : `/${route}`;
400
+ const url = queryString ? `${base}?${queryString}` : base;
401
+ return isHash ? `#${url}` : url;
402
+ };
403
+
404
+ if (this.config.mode === 'hash') {
405
+ const newHash = buildURL(route, queryString);
406
+
407
+ // 동일한 URL이면 업데이트하지 않음 (성능 최적화)
408
+ if (window.location.hash !== newHash) {
409
+ window.location.hash = newHash;
410
+ }
411
+ } else {
412
+ const newPath = buildURL(route, queryString, false);
413
+ const isSameRoute = window.location.pathname === (route === 'home' ? '/' : `/${route}`);
414
+
415
+ if (isSameRoute) {
416
+ window.history.replaceState({}, '', newPath);
417
+ } else {
418
+ window.history.pushState({}, '', newPath);
419
+ }
420
+ this.handleRouteChange();
421
+ }
422
+ }
423
+
424
+ /**
425
+ * 라우터 정리 (메모리 누수 방지)
426
+ */
427
+ destroy() {
428
+ // 이벤트 리스너 제거
429
+ window.removeEventListener(
430
+ this.config.mode === 'hash' ? 'hashchange' : 'popstate',
431
+ this._boundHandleRouteChange
432
+ );
433
+
434
+ // 현재 Vue 앱 언마운트
435
+ if (this.currentVueApp) {
436
+ this.currentVueApp.unmount();
437
+ this.currentVueApp = null;
438
+ }
439
+
440
+ // 이전 Vue 앱 언마운트
441
+ if (this.previousVueApp) {
442
+ this.previousVueApp.unmount();
443
+ this.previousVueApp = null;
444
+ }
445
+
446
+ // 매니저 정리
447
+ Object.values(this).forEach(manager => {
448
+ if (manager && typeof manager.destroy === 'function') {
449
+ manager.destroy();
450
+ }
451
+ });
452
+
453
+ // 캐시 클리어
454
+ this.cacheManager?.clearAll();
455
+
456
+ // DOM 정리
457
+ const appElement = document.getElementById('app');
458
+ if (appElement) {
459
+ appElement.innerHTML = '';
460
+ }
461
+
462
+ this.log('info', 'Router destroyed');
463
+ }
464
+ }
465
+ // 전역 라우터는 index.html에서 환경설정과 함께 생성됨