UNPKG

viewlogic

Version:

A lightweight, file-based routing system for Vue 3 applications with zero build configuration

1,639 lines (1,634 loc) 98.9 kB
/** * ViewLogic v1.0.0 * (c) 2024 hopegiver * @license MIT */ // src/plugins/I18nManager.js var I18nManager = class { constructor(router, options = {}) { this.config = { enabled: options.useI18n !== void 0 ? options.useI18n : true, defaultLanguage: options.defaultLanguage || "ko", fallbackLanguage: options.defaultLanguage || "ko", cacheKey: options.cacheKey || "viewlogic_lang", dataCacheKey: options.dataCacheKey || "viewlogic_i18n_data", cacheVersion: options.cacheVersion || "1.0.0", enableDataCache: options.enableDataCache !== false, debug: options.debug || false }; this.router = router; this.messages = /* @__PURE__ */ new Map(); this.currentLanguage = this.config.defaultLanguage; this.isLoading = false; this.loadPromises = /* @__PURE__ */ new Map(); this.listeners = { languageChanged: [] }; this.initPromise = this.init(); } async init() { if (!this.config.enabled) { this.log("info", "I18n system disabled"); return; } this.loadLanguageFromCache(); if (this.config.debug) { this.config.enableDataCache = false; this.log("debug", "Data cache disabled in debug mode"); } if (!this.messages.has(this.currentLanguage)) { try { await this.loadMessages(this.currentLanguage); } catch (error) { this.log("error", "Failed to load initial language file:", error); this.messages.set(this.currentLanguage, {}); this.log("info", "Using empty message object as fallback"); } } else { this.log("debug", "Language messages already loaded:", this.currentLanguage); } } /** * 캐시에서 언어 설정 로드 */ loadLanguageFromCache() { try { const cachedLang = localStorage.getItem(this.config.cacheKey); if (cachedLang && this.isValidLanguage(cachedLang)) { this.currentLanguage = cachedLang; this.log("debug", "Language loaded from cache:", cachedLang); } } catch (error) { this.log("warn", "Failed to load language from cache:", error); } } /** * 언어 유효성 검사 */ isValidLanguage(lang) { return typeof lang === "string" && /^[a-z]{2}$/.test(lang); } /** * 현재 언어 반환 */ getCurrentLanguage() { return this.currentLanguage; } /** * 언어 변경 */ async setLanguage(language) { if (!this.isValidLanguage(language)) { this.log("warn", "Invalid language code:", language); return false; } if (this.currentLanguage === language) { this.log("debug", "Language already set to:", language); return true; } const oldLanguage = this.currentLanguage; this.currentLanguage = language; try { await this.loadMessages(language); this.saveLanguageToCache(language); this.emit("languageChanged", { from: oldLanguage, to: language, messages: this.messages.get(language) }); this.log("info", "Language changed successfully", { from: oldLanguage, to: language }); return true; } catch (error) { this.log("error", "Failed to load messages for language change, using empty messages:", error); this.messages.set(language, {}); this.saveLanguageToCache(language); this.emit("languageChanged", { from: oldLanguage, to: language, messages: {}, error: true }); this.log("warn", "Language changed with empty messages", { from: oldLanguage, to: language }); return true; } } /** * 언어를 캐시에 저장 */ saveLanguageToCache(language) { try { localStorage.setItem(this.config.cacheKey, language); this.log("debug", "Language saved to cache:", language); } catch (error) { this.log("warn", "Failed to save language to cache:", error); } } /** * 언어 메시지 파일 로드 */ async loadMessages(language) { if (this.messages.has(language)) { this.log("debug", "Messages already loaded for:", language); return this.messages.get(language); } if (this.loadPromises.has(language)) { this.log("debug", "Messages loading in progress for:", language); return await this.loadPromises.get(language); } const loadPromise = this._loadMessagesFromFile(language); this.loadPromises.set(language, loadPromise); try { const messages = await loadPromise; this.messages.set(language, messages); this.loadPromises.delete(language); this.log("debug", "Messages loaded successfully for:", language); return messages; } catch (error) { this.loadPromises.delete(language); this.log("error", "Failed to load messages, using empty fallback for:", language, error); const emptyMessages = {}; this.messages.set(language, emptyMessages); return emptyMessages; } } /** * 파일에서 메시지 로드 (캐싱 지원) */ async _loadMessagesFromFile(language) { if (this.config.enableDataCache) { const cachedData = this.getDataFromCache(language); if (cachedData) { this.log("debug", "Messages loaded from cache:", language); return cachedData; } } try { const i18nPath = `${this.router.config.i18nPath}/${language}.json`; const response = await fetch(i18nPath); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const messages = await response.json(); if (this.config.enableDataCache) { this.saveDataToCache(language, messages); } return messages; } catch (error) { this.log("error", "Failed to load messages file for:", language, error); if (language !== this.config.fallbackLanguage) { this.log("info", "Trying fallback language:", this.config.fallbackLanguage); try { return await this._loadMessagesFromFile(this.config.fallbackLanguage); } catch (fallbackError) { this.log("error", "Fallback language also failed:", fallbackError); return {}; } } this.log("warn", `No messages available for language: ${language}, using empty fallback`); return {}; } } /** * 언어 데이터를 캐시에서 가져오기 */ getDataFromCache(language) { try { const cacheKey = `${this.config.dataCacheKey}_${language}_${this.config.cacheVersion}`; const cachedItem = localStorage.getItem(cacheKey); if (cachedItem) { const { data, timestamp, version } = JSON.parse(cachedItem); if (version !== this.config.cacheVersion) { this.log("debug", "Cache version mismatch, clearing:", language); localStorage.removeItem(cacheKey); return null; } const now = Date.now(); const maxAge = 24 * 60 * 60 * 1e3; if (now - timestamp > maxAge) { this.log("debug", "Cache expired, removing:", language); localStorage.removeItem(cacheKey); return null; } return data; } } catch (error) { this.log("warn", "Failed to read from cache:", error); } return null; } /** * 언어 데이터를 캐시에 저장 */ saveDataToCache(language, data) { try { const cacheKey = `${this.config.dataCacheKey}_${language}_${this.config.cacheVersion}`; const cacheItem = { data, timestamp: Date.now(), version: this.config.cacheVersion }; localStorage.setItem(cacheKey, JSON.stringify(cacheItem)); this.log("debug", "Data saved to cache:", language); } catch (error) { this.log("warn", "Failed to save to cache:", error); } } /** * 메시지 번역 */ t(key, params = {}) { if (!this.config.enabled) { return key; } const messages = this.messages.get(this.currentLanguage); if (!messages) { this.log("warn", "No messages loaded for current language:", this.currentLanguage); return key; } const message = this.getNestedValue(messages, key); if (message === void 0) { this.log("warn", "Translation not found for key:", key); const fallbackMessages = this.messages.get(this.config.fallbackLanguage); if (fallbackMessages && this.currentLanguage !== this.config.fallbackLanguage) { const fallbackMessage = this.getNestedValue(fallbackMessages, key); if (fallbackMessage !== void 0) { return this.interpolate(fallbackMessage, params); } } return key; } return this.interpolate(message, params); } /** * 중첩된 객체에서 값 가져오기 */ getNestedValue(obj, path) { return path.split(".").reduce((current, key) => { return current && current[key] !== void 0 ? current[key] : void 0; }, obj); } /** * 문자열 보간 처리 */ interpolate(message, params) { if (typeof message !== "string") { return message; } return message.replace(/\{(\w+)\}/g, (match, key) => { return params.hasOwnProperty(key) ? params[key] : match; }); } /** * 복수형 처리 */ plural(key, count, params = {}) { const pluralKey = count === 1 ? `${key}.singular` : `${key}.plural`; return this.t(pluralKey, { ...params, count }); } /** * 사용 가능한 언어 목록 */ getAvailableLanguages() { return ["ko", "en"]; } /** * 언어 변경 이벤트 리스너 등록 */ on(event, callback) { if (this.listeners[event]) { this.listeners[event].push(callback); } } /** * 언어 변경 이벤트 리스너 제거 */ off(event, callback) { if (this.listeners[event]) { const index = this.listeners[event].indexOf(callback); if (index > -1) { this.listeners[event].splice(index, 1); } } } /** * 이벤트 발생 */ emit(event, data) { if (this.listeners[event]) { this.listeners[event].forEach((callback) => { try { callback(data); } catch (error) { this.log("error", "Error in event listener:", error); } }); } } /** * 현재 언어의 모든 메시지 반환 */ getMessages() { return this.messages.get(this.currentLanguage) || {}; } /** * 언어별 날짜 포맷팅 */ formatDate(date, options = {}) { const locale = this.currentLanguage === "ko" ? "ko-KR" : "en-US"; return new Intl.DateTimeFormat(locale, options).format(new Date(date)); } /** * 언어별 숫자 포맷팅 */ formatNumber(number, options = {}) { const locale = this.currentLanguage === "ko" ? "ko-KR" : "en-US"; return new Intl.NumberFormat(locale, options).format(number); } /** * 로깅 래퍼 메서드 */ log(level, ...args) { if (this.router?.errorHandler) { this.router.errorHandler.log(level, "I18nManager", ...args); } } /** * i18n 활성화 여부 확인 */ isEnabled() { return this.config.enabled; } /** * 초기 로딩이 완료되었는지 확인 */ async isReady() { if (!this.config.enabled) { return true; } try { await this.initPromise; return true; } catch (error) { this.log("error", "I18n initialization failed:", error); this.log("info", "I18n system ready with fallback behavior"); return true; } } /** * 캐시 초기화 (버전 변경 시 사용) */ clearCache() { try { const keys = Object.keys(localStorage); const cacheKeys = keys.filter((key) => key.startsWith(this.config.dataCacheKey)); cacheKeys.forEach((key) => { localStorage.removeItem(key); }); this.log("debug", "Cache cleared, removed", cacheKeys.length, "items"); } catch (error) { this.log("warn", "Failed to clear cache:", error); } } /** * 캐시 상태 확인 */ getCacheInfo() { const info = { enabled: this.config.enableDataCache, version: this.config.cacheVersion, languages: {} }; try { const keys = Object.keys(localStorage); const cacheKeys = keys.filter((key) => key.startsWith(this.config.dataCacheKey)); cacheKeys.forEach((key) => { const match = key.match(new RegExp(`${this.config.dataCacheKey}_(w+)_(.+)`)); if (match) { const [, language, version] = match; const cachedItem = JSON.parse(localStorage.getItem(key)); info.languages[language] = { version, timestamp: cachedItem.timestamp, age: Date.now() - cachedItem.timestamp }; } }); } catch (error) { this.log("warn", "Failed to get cache info:", error); } return info; } /** * 시스템 초기화 (현재 언어의 메시지 로드) */ async initialize() { if (!this.config.enabled) { this.log("info", "I18n system is disabled, skipping initialization"); return true; } try { await this.initPromise; this.log("info", "I18n system fully initialized"); return true; } catch (error) { this.log("error", "Failed to initialize I18n system:", error); this.log("info", "I18n system will continue with fallback behavior"); return true; } } }; // src/plugins/AuthManager.js var AuthManager = class { constructor(router, options = {}) { this.config = { enabled: options.authEnabled || false, loginRoute: options.loginRoute || "login", protectedRoutes: options.protectedRoutes || [], protectedPrefixes: options.protectedPrefixes || [], publicRoutes: options.publicRoutes || ["login", "register", "home"], checkAuthFunction: options.checkAuthFunction || null, redirectAfterLogin: options.redirectAfterLogin || "home", // 쿠키/스토리지 설정 authCookieName: options.authCookieName || "authToken", authFallbackCookieNames: options.authFallbackCookieNames || ["accessToken", "token", "jwt"], authStorage: options.authStorage || "cookie", authCookieOptions: options.authCookieOptions || {}, authSkipValidation: options.authSkipValidation || false, debug: options.debug || false }; this.router = router; this.eventListeners = /* @__PURE__ */ new Map(); this.log("info", "AuthManager initialized", { enabled: this.config.enabled }); } /** * 로깅 래퍼 메서드 */ log(level, ...args) { if (this.router?.errorHandler) { this.router.errorHandler.log(level, "AuthManager", ...args); } } /** * 라우트 인증 확인 */ async checkAuthentication(routeName) { if (!this.config.enabled) { return { allowed: true, reason: "auth_disabled" }; } this.log("debug", `\u{1F510} Checking authentication for route: ${routeName}`); if (this.isPublicRoute(routeName)) { return { allowed: true, reason: "public_route" }; } const isProtected = this.isProtectedRoute(routeName); if (!isProtected) { return { allowed: true, reason: "not_protected" }; } if (typeof this.config.checkAuthFunction === "function") { try { const isAuthenticated2 = await this.config.checkAuthFunction(routeName); return { allowed: isAuthenticated2, reason: isAuthenticated2 ? "custom_auth_success" : "custom_auth_failed", routeName }; } catch (error) { this.log("error", "Custom auth function failed:", error); return { allowed: false, reason: "custom_auth_error", error }; } } const isAuthenticated = this.isUserAuthenticated(); return { allowed: isAuthenticated, reason: isAuthenticated ? "authenticated" : "not_authenticated", routeName }; } /** * 사용자 인증 상태 확인 */ isUserAuthenticated() { this.log("debug", "\u{1F50D} Checking user authentication status"); const token = localStorage.getItem("authToken") || localStorage.getItem("accessToken"); if (token) { try { if (token.includes(".")) { const payload = JSON.parse(atob(token.split(".")[1])); if (payload.exp && Date.now() >= payload.exp * 1e3) { this.log("debug", "localStorage token expired, removing..."); localStorage.removeItem("authToken"); localStorage.removeItem("accessToken"); return false; } } this.log("debug", "\u2705 Valid token found in localStorage"); return true; } catch (error) { this.log("warn", "Invalid token in localStorage:", error); } } const sessionToken = sessionStorage.getItem("authToken") || sessionStorage.getItem("accessToken"); if (sessionToken) { this.log("debug", "\u2705 Token found in sessionStorage"); return true; } const authCookie = this.getAuthCookie(); if (authCookie) { try { if (authCookie.includes(".")) { const payload = JSON.parse(atob(authCookie.split(".")[1])); if (payload.exp && Date.now() >= payload.exp * 1e3) { this.log("debug", "Cookie token expired, removing..."); this.removeAuthCookie(); return false; } } this.log("debug", "\u2705 Valid token found in cookies"); return true; } catch (error) { this.log("warn", "Cookie token validation failed:", error); } } if (window.user || window.isAuthenticated) { this.log("debug", "\u2705 Global authentication variable found"); return true; } this.log("debug", "\u274C No valid authentication found"); return false; } /** * 공개 라우트인지 확인 */ isPublicRoute(routeName) { return this.config.publicRoutes.includes(routeName); } /** * 보호된 라우트인지 확인 */ isProtectedRoute(routeName) { if (this.config.protectedRoutes.includes(routeName)) { return true; } for (const prefix of this.config.protectedPrefixes) { if (routeName.startsWith(prefix)) { return true; } } return false; } /** * 인증 쿠키 가져오기 */ getAuthCookie() { const primaryCookie = this.getCookieValue(this.config.authCookieName); if (primaryCookie) { return primaryCookie; } for (const cookieName of this.config.authFallbackCookieNames) { const cookieValue = this.getCookieValue(cookieName); if (cookieValue) { this.log("debug", `Found auth token in fallback cookie: ${cookieName}`); return cookieValue; } } return null; } /** * 쿠키 값 가져오기 */ getCookieValue(name) { const value = `; ${document.cookie}`; const parts = value.split(`; ${name}=`); if (parts.length === 2) { return decodeURIComponent(parts.pop().split(";").shift()); } return null; } /** * 인증 쿠키 제거 */ removeAuthCookie() { const cookiesToRemove = [this.config.authCookieName, ...this.config.authFallbackCookieNames]; cookiesToRemove.forEach((cookieName) => { document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${window.location.pathname};`; }); this.log("debug", "Auth cookies removed"); } /** * 액세스 토큰 가져오기 */ getAccessToken() { let token = localStorage.getItem("authToken") || localStorage.getItem("accessToken"); if (token) return token; token = sessionStorage.getItem("authToken") || sessionStorage.getItem("accessToken"); if (token) return token; token = this.getAuthCookie(); if (token) return token; return null; } /** * 액세스 토큰 설정 */ setAccessToken(token, options = {}) { if (!token) { this.log("warn", "Empty token provided"); return false; } const { storage = this.config.authStorage, cookieOptions = this.config.authCookieOptions, skipValidation = this.config.authSkipValidation } = options; try { if (!skipValidation && token.includes(".")) { try { const payload = JSON.parse(atob(token.split(".")[1])); if (payload.exp && Date.now() >= payload.exp * 1e3) { this.log("warn", "\u274C Token is expired"); return false; } this.log("debug", "\u2705 JWT token validated"); } catch (error) { this.log("warn", "\u26A0\uFE0F JWT validation failed, but proceeding:", error.message); } } switch (storage) { case "localStorage": localStorage.setItem("authToken", token); this.log("debug", "Token saved to localStorage"); break; case "sessionStorage": sessionStorage.setItem("authToken", token); this.log("debug", "Token saved to sessionStorage"); break; case "cookie": this.setAuthCookie(token, cookieOptions); break; default: localStorage.setItem("authToken", token); this.log("debug", "Token saved to localStorage (default)"); } this.emitAuthEvent("token_set", { storage, tokenLength: token.length, hasExpiration: token.includes(".") }); return true; } catch (error) { this.log("Failed to set token:", error); return false; } } /** * 인증 쿠키 설정 */ setAuthCookie(token, options = {}) { const { cookieName = this.config.authCookieName, secure = window.location.protocol === "https:", sameSite = "Strict", path = "/", domain = null } = options; let cookieString = `${cookieName}=${encodeURIComponent(token)}; path=${path}`; if (secure) { cookieString += "; Secure"; } if (sameSite) { cookieString += `; SameSite=${sameSite}`; } if (domain) { cookieString += `; Domain=${domain}`; } try { if (token.includes(".")) { try { const payload = JSON.parse(atob(token.split(".")[1])); if (payload.exp) { const expireDate = new Date(payload.exp * 1e3); cookieString += `; Expires=${expireDate.toUTCString()}`; } } catch (error) { this.log("Could not extract expiration from JWT token"); } } } catch (error) { this.log("Token processing error:", error); } document.cookie = cookieString; this.log(`Auth cookie set: ${cookieName}`); } /** * 토큰 제거 */ removeAccessToken(storage = "all") { switch (storage) { case "localStorage": localStorage.removeItem("authToken"); localStorage.removeItem("accessToken"); break; case "sessionStorage": sessionStorage.removeItem("authToken"); sessionStorage.removeItem("accessToken"); break; case "cookie": this.removeAuthCookie(); break; case "all": default: localStorage.removeItem("authToken"); localStorage.removeItem("accessToken"); sessionStorage.removeItem("authToken"); sessionStorage.removeItem("accessToken"); this.removeAuthCookie(); break; } this.emitAuthEvent("token_removed", { storage }); this.log(`Token removed from: ${storage}`); } /** * 로그인 성공 처리 */ handleLoginSuccess(targetRoute = null) { const redirectRoute = targetRoute || this.config.redirectAfterLogin; this.log(`\u{1F389} Login success, redirecting to: ${redirectRoute}`); this.emitAuthEvent("login_success", { targetRoute: redirectRoute }); if (this.router && typeof this.router.navigateTo === "function") { this.router.navigateTo(redirectRoute); } return redirectRoute; } /** * 로그아웃 처리 */ handleLogout() { this.log("\u{1F44B} Logging out user"); this.removeAccessToken(); if (window.user) window.user = null; if (window.isAuthenticated) window.isAuthenticated = false; this.emitAuthEvent("logout", {}); if (this.router && typeof this.router.navigateTo === "function") { this.router.navigateTo(this.config.loginRoute); } return this.config.loginRoute; } /** * 인증 이벤트 발생 */ emitAuthEvent(eventType, data) { const event = new CustomEvent("router:auth", { detail: { type: eventType, timestamp: Date.now(), ...data } }); document.dispatchEvent(event); if (this.eventListeners.has(eventType)) { this.eventListeners.get(eventType).forEach((listener) => { try { listener(data); } catch (error) { this.log("Event listener error:", error); } }); } this.log(`\u{1F514} Auth event emitted: ${eventType}`, data); } /** * 이벤트 리스너 등록 */ on(eventType, listener) { if (!this.eventListeners.has(eventType)) { this.eventListeners.set(eventType, []); } this.eventListeners.get(eventType).push(listener); } /** * 이벤트 리스너 제거 */ off(eventType, listener) { if (this.eventListeners.has(eventType)) { const listeners = this.eventListeners.get(eventType); const index = listeners.indexOf(listener); if (index > -1) { listeners.splice(index, 1); } } } /** * 인증 상태 통계 */ getAuthStats() { return { enabled: this.config.enabled, isAuthenticated: this.isUserAuthenticated(), hasToken: !!this.getAccessToken(), protectedRoutesCount: this.config.protectedRoutes.length, protectedPrefixesCount: this.config.protectedPrefixes.length, publicRoutesCount: this.config.publicRoutes.length, storage: this.config.authStorage, loginRoute: this.config.loginRoute }; } /** * 정리 (메모리 누수 방지) */ destroy() { this.eventListeners.clear(); this.log("debug", "AuthManager destroyed"); } }; // src/plugins/CacheManager.js var CacheManager = class { constructor(router, options = {}) { this.config = { cacheMode: options.cacheMode || "memory", // 'memory' 또는 'lru' cacheTTL: options.cacheTTL || 3e5, // 5분 (밀리초) maxCacheSize: options.maxCacheSize || 50, // LRU 캐시 최대 크기 debug: options.debug || false }; this.router = router; this.cache = /* @__PURE__ */ new Map(); this.cacheTimestamps = /* @__PURE__ */ new Map(); this.lruOrder = []; this.log("info", "CacheManager initialized with config:", this.config); } /** * 로깅 래퍼 메서드 */ log(level, ...args) { if (this.router?.errorHandler) { this.router.errorHandler.log(level, "CacheManager", ...args); } } /** * 캐시에 값 저장 */ setCache(key, value) { const now = Date.now(); if (this.config.cacheMode === "lru") { if (this.cache.size >= this.config.maxCacheSize && !this.cache.has(key)) { const oldestKey = this.lruOrder.shift(); if (oldestKey) { this.cache.delete(oldestKey); this.cacheTimestamps.delete(oldestKey); this.log("debug", `\u{1F5D1}\uFE0F LRU evicted cache key: ${oldestKey}`); } } const existingIndex = this.lruOrder.indexOf(key); if (existingIndex > -1) { this.lruOrder.splice(existingIndex, 1); } this.lruOrder.push(key); } this.cache.set(key, value); this.cacheTimestamps.set(key, now); this.log("debug", `\u{1F4BE} Cached: ${key} (size: ${this.cache.size})`); } /** * 캐시에서 값 가져오기 */ getFromCache(key) { const now = Date.now(); const timestamp = this.cacheTimestamps.get(key); if (timestamp && now - timestamp > this.config.cacheTTL) { this.cache.delete(key); this.cacheTimestamps.delete(key); if (this.config.cacheMode === "lru") { const index = this.lruOrder.indexOf(key); if (index > -1) { this.lruOrder.splice(index, 1); } } this.log("debug", `\u23F0 Cache expired and removed: ${key}`); return null; } const value = this.cache.get(key); if (value && this.config.cacheMode === "lru") { const index = this.lruOrder.indexOf(key); if (index > -1) { this.lruOrder.splice(index, 1); this.lruOrder.push(key); } } if (value) { this.log("debug", `\u{1F3AF} Cache hit: ${key}`); } else { this.log("debug", `\u274C Cache miss: ${key}`); } return value; } /** * 캐시에 키가 있는지 확인 */ hasCache(key) { return this.cache.has(key) && this.getFromCache(key) !== null; } /** * 특정 키 패턴의 캐시 삭제 */ invalidateByPattern(pattern) { const keysToDelete = []; for (const key of this.cache.keys()) { if (key.includes(pattern) || key.startsWith(pattern)) { keysToDelete.push(key); } } keysToDelete.forEach((key) => { this.cache.delete(key); this.cacheTimestamps.delete(key); if (this.config.cacheMode === "lru") { const index = this.lruOrder.indexOf(key); if (index > -1) { this.lruOrder.splice(index, 1); } } }); this.log("debug", `\u{1F9F9} Invalidated ${keysToDelete.length} cache entries matching: ${pattern}`); return keysToDelete.length; } /** * 특정 컴포넌트 캐시 무효화 */ invalidateComponentCache(routeName) { const patterns = [ `component_${routeName}`, `script_${routeName}`, `template_${routeName}`, `style_${routeName}`, `layout_${routeName}` ]; let totalInvalidated = 0; patterns.forEach((pattern) => { totalInvalidated += this.invalidateByPattern(pattern); }); this.log(`\u{1F504} Invalidated component cache for route: ${routeName} (${totalInvalidated} entries)`); return totalInvalidated; } /** * 모든 컴포넌트 캐시 삭제 */ clearComponentCache() { const componentPatterns = ["component_", "script_", "template_", "style_", "layout_"]; let totalCleared = 0; componentPatterns.forEach((pattern) => { totalCleared += this.invalidateByPattern(pattern); }); this.log(`\u{1F9FD} Cleared all component caches (${totalCleared} entries)`); return totalCleared; } /** * 전체 캐시 삭제 */ clearCache() { const size = this.cache.size; this.cache.clear(); this.cacheTimestamps.clear(); this.lruOrder = []; this.log(`\u{1F525} Cleared all cache (${size} entries)`); return size; } /** * 만료된 캐시 항목들 정리 */ cleanExpiredCache() { const now = Date.now(); const expiredKeys = []; for (const [key, timestamp] of this.cacheTimestamps.entries()) { if (now - timestamp > this.config.cacheTTL) { expiredKeys.push(key); } } expiredKeys.forEach((key) => { this.cache.delete(key); this.cacheTimestamps.delete(key); if (this.config.cacheMode === "lru") { const index = this.lruOrder.indexOf(key); if (index > -1) { this.lruOrder.splice(index, 1); } } }); if (expiredKeys.length > 0) { this.log(`\u23F1\uFE0F Cleaned ${expiredKeys.length} expired cache entries`); } return expiredKeys.length; } /** * 캐시 통계 정보 */ getCacheStats() { return { size: this.cache.size, maxSize: this.config.maxCacheSize, mode: this.config.cacheMode, ttl: this.config.cacheTTL, memoryUsage: this.getMemoryUsage(), hitRatio: this.getHitRatio(), categories: this.getCategorizedStats() }; } /** * 메모리 사용량 추정 */ getMemoryUsage() { let estimatedBytes = 0; for (const [key, value] of this.cache.entries()) { estimatedBytes += key.length * 2; if (typeof value === "string") { estimatedBytes += value.length * 2; } else if (typeof value === "object" && value !== null) { estimatedBytes += JSON.stringify(value).length * 2; } else { estimatedBytes += 8; } } return { bytes: estimatedBytes, kb: Math.round(estimatedBytes / 1024 * 100) / 100, mb: Math.round(estimatedBytes / (1024 * 1024) * 100) / 100 }; } /** * 히트 비율 계산 (간단한 추정) */ getHitRatio() { const ratio = this.cache.size > 0 ? Math.min(this.cache.size / this.config.maxCacheSize, 1) : 0; return Math.round(ratio * 100); } /** * 카테고리별 캐시 통계 */ getCategorizedStats() { const categories = { components: 0, scripts: 0, templates: 0, styles: 0, layouts: 0, others: 0 }; for (const key of this.cache.keys()) { if (key.startsWith("component_")) categories.components++; else if (key.startsWith("script_")) categories.scripts++; else if (key.startsWith("template_")) categories.templates++; else if (key.startsWith("style_")) categories.styles++; else if (key.startsWith("layout_")) categories.layouts++; else categories.others++; } return categories; } /** * 캐시 키 목록 반환 */ getCacheKeys() { return Array.from(this.cache.keys()); } /** * 특정 패턴의 캐시 키들 반환 */ getCacheKeysByPattern(pattern) { return this.getCacheKeys().filter( (key) => key.includes(pattern) || key.startsWith(pattern) ); } /** * 자동 정리 시작 (백그라운드에서 만료된 캐시 정리) */ startAutoCleanup(interval = 6e4) { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.cleanupInterval = setInterval(() => { this.cleanExpiredCache(); }, interval); this.log(`\u{1F916} Auto cleanup started (interval: ${interval}ms)`); } /** * 자동 정리 중지 */ stopAutoCleanup() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; this.log("debug", "\u{1F6D1} Auto cleanup stopped"); } } /** * 정리 (메모리 누수 방지) */ destroy() { this.stopAutoCleanup(); this.clearCache(); this.log("debug", "CacheManager destroyed"); } }; // src/plugins/QueryManager.js var QueryManager = class { constructor(router, options = {}) { this.config = { enableParameterValidation: options.enableParameterValidation !== false, logSecurityWarnings: options.logSecurityWarnings !== false, maxParameterLength: options.maxParameterLength || 1e3, maxArraySize: options.maxArraySize || 100, maxParameterCount: options.maxParameterCount || 50, allowedKeyPattern: options.allowedKeyPattern || /^[a-zA-Z0-9_\-]+$/, debug: options.debug || false }; this.router = router; this.currentQueryParams = {}; this.currentRouteParams = {}; this.log("info", "QueryManager initialized with config:", this.config); } /** * 로깅 래퍼 메서드 */ log(level, ...args) { if (this.router?.errorHandler) { this.router.errorHandler.log(level, "QueryManager", ...args); } } /** * 파라미터 값 sanitize (XSS, SQL Injection 방어) */ sanitizeParameter(value) { if (typeof value !== "string") return value; let sanitized = value.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "").replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi, "").replace(/<embed\b[^<]*(?:(?!<\/embed>)<[^<]*)*<\/embed>/gi, "").replace(/<link\b[^<]*>/gi, "").replace(/<meta\b[^<]*>/gi, "").replace(/javascript:/gi, "").replace(/vbscript:/gi, "").replace(/data:/gi, "").replace(/on\w+\s*=/gi, "").replace(/expression\s*\(/gi, "").replace(/url\s*\(/gi, ""); const sqlPatterns = [ /(\b(union|select|insert|update|delete|drop|create|alter|exec|execute|sp_|xp_)\b)/gi, /(;|\||&|\*|%|<|>)/g, // 위험한 특수문자 /(--|\/\*|\*\/)/g, // SQL 주석 /(\bor\b.*\b=\b|\band\b.*\b=\b)/gi, // OR/AND 조건문 /('.*'|".*")/g, // 따옴표로 둘러싸인 문자열 /(\\\w+)/g // 백슬래시 이스케이프 ]; for (const pattern of sqlPatterns) { sanitized = sanitized.replace(pattern, ""); } sanitized = sanitized.replace(/[<>'"&]{2,}/g, ""); if (sanitized.length > this.config.maxParameterLength) { sanitized = sanitized.substring(0, this.config.maxParameterLength); } return sanitized.trim(); } /** * 파라미터 유효성 검증 */ validateParameter(key, value) { if (!this.config.enableParameterValidation) { return true; } if (typeof key !== "string" || key.length === 0) { return false; } if (!this.config.allowedKeyPattern.test(key)) { if (this.config.logSecurityWarnings) { console.warn(`Invalid parameter key format: ${key}`); } return false; } if (key.length > 50) { if (this.config.logSecurityWarnings) { console.warn(`Parameter key too long: ${key}`); } return false; } if (value !== null && value !== void 0) { if (typeof value === "string") { if (value.length > this.config.maxParameterLength) { if (this.config.logSecurityWarnings) { console.warn(`Parameter value too long for key: ${key}`); } return false; } const dangerousPatterns = [ /<script|<iframe|<object|<embed/gi, /javascript:|vbscript:|data:/gi, /union.*select|insert.*into|delete.*from/gi, /\.\.\//g, // 경로 탐색 공격 /[<>'"&]{3,}/g // 연속된 특수문자 ]; for (const pattern of dangerousPatterns) { if (pattern.test(value)) { if (this.config.logSecurityWarnings) { console.warn(`Dangerous pattern detected in parameter ${key}:`, value); } return false; } } } else if (Array.isArray(value)) { if (value.length > this.config.maxArraySize) { if (this.config.logSecurityWarnings) { console.warn(`Parameter array too large for key: ${key}`); } return false; } for (const item of value) { if (!this.validateParameter(`${key}[]`, item)) { return false; } } } } return true; } /** * 쿼리스트링 파싱 */ parseQueryString(queryString) { const params = {}; if (!queryString) return params; const pairs = queryString.split("&"); for (const pair of pairs) { try { const [rawKey, rawValue] = pair.split("="); if (!rawKey) continue; let key, value; try { key = decodeURIComponent(rawKey); value = rawValue ? decodeURIComponent(rawValue) : ""; } catch (e) { this.log("warn", "Failed to decode URI component:", pair); continue; } if (!this.validateParameter(key, value)) { this.log("warn", `Parameter rejected by security filter: ${key}`); continue; } const sanitizedValue = this.sanitizeParameter(value); if (key.endsWith("[]")) { const arrayKey = key.slice(0, -2); if (!this.validateParameter(arrayKey, [])) { continue; } if (!params[arrayKey]) params[arrayKey] = []; if (params[arrayKey].length < this.config.maxArraySize) { params[arrayKey].push(sanitizedValue); } else { if (this.config.logSecurityWarnings) { console.warn(`Array parameter ${arrayKey} size limit exceeded`); } } } else { params[key] = sanitizedValue; } } catch (error) { this.log("error", "Error parsing query parameter:", pair, error); } } const paramCount = Object.keys(params).length; if (paramCount > this.config.maxParameterCount) { if (this.config.logSecurityWarnings) { console.warn(`Too many parameters (${paramCount}). Limiting to first ${this.config.maxParameterCount}.`); } const limitedParams = {}; let count = 0; for (const [key, value] of Object.entries(params)) { if (count >= this.config.maxParameterCount) break; limitedParams[key] = value; count++; } return limitedParams; } return params; } /** * 쿼리스트링 생성 */ buildQueryString(params) { if (!params || Object.keys(params).length === 0) return ""; const pairs = []; for (const [key, value] of Object.entries(params)) { if (Array.isArray(value)) { for (const item of value) { pairs.push(`${encodeURIComponent(key)}[]=${encodeURIComponent(item)}`); } } else if (value !== void 0 && value !== null) { pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); } } return pairs.join("&"); } /** * 쿼리 파라미터 변경 감지 */ hasQueryParamsChanged(newParams) { if (!this.currentQueryParams && !newParams) return false; if (!this.currentQueryParams || !newParams) return true; const oldKeys = Object.keys(this.currentQueryParams); const newKeys = Object.keys(newParams); if (oldKeys.length !== newKeys.length) return true; for (const key of oldKeys) { if (JSON.stringify(this.currentQueryParams[key]) !== JSON.stringify(newParams[key])) { return true; } } return false; } /** * 현재 쿼리 파라미터 전체 가져오기 */ getQueryParams() { return { ...this.currentQueryParams }; } /** * 특정 쿼리 파라미터 가져오기 */ getQueryParam(key, defaultValue = void 0) { const value = this.currentQueryParams ? this.currentQueryParams[key] : void 0; return value !== void 0 ? value : defaultValue; } /** * 쿼리 파라미터 설정 */ setQueryParams(params, replace = false) { if (!params || typeof params !== "object") { console.warn("Invalid parameters object provided to setQueryParams"); return; } const currentParams = replace ? {} : { ...this.currentQueryParams }; const sanitizedParams = {}; for (const [key, value] of Object.entries(params)) { if (!this.validateParameter(key, value)) { console.warn(`Parameter ${key} rejected by security filter`); continue; } if (value !== void 0 && value !== null) { if (Array.isArray(value)) { sanitizedParams[key] = value.map((item) => this.sanitizeParameter(item)); } else { sanitizedParams[key] = this.sanitizeParameter(value); } } } Object.assign(currentParams, sanitizedParams); for (const [key, value] of Object.entries(currentParams)) { if (value === void 0 || value === null || value === "") { delete currentParams[key]; } } const paramCount = Object.keys(currentParams).length; if (paramCount > this.config.maxParameterCount) { if (this.config.logSecurityWarnings) { console.warn(`Too many parameters after update (${paramCount}). Some parameters may be dropped.`); } } this.currentQueryParams = currentParams; this.updateURL(); } /** * 쿼리 파라미터 제거 */ removeQueryParams(keys) { if (!keys) return; const keysToRemove = Array.isArray(keys) ? keys : [keys]; for (const key of keysToRemove) { delete this.currentQueryParams[key]; } this.updateURL(); } /** * 쿼리 파라미터 초기화 */ clearQueryParams() { this.currentQueryParams = {}; this.updateURL(); } /** * 현재 쿼리 파라미터 설정 (라우터에서 호출) */ setCurrentQueryParams(params) { this.currentQueryParams = params || {}; } /** * 현재 라우팅 파라미터 설정 (navigateTo에서 호출) */ setCurrentRouteParams(params) { this.currentRouteParams = params || {}; this.log("debug", "Route params set:", this.currentRouteParams); } /** * 통합된 파라미터 반환 (라우팅 파라미터 + 쿼리 파라미터) */ getAllParams() { return { ...this.currentRouteParams, ...this.currentQueryParams }; } /** * 통합된 파라미터에서 특정 키 값 반환 */ getParam(key, defaultValue = void 0) { const value = this.currentQueryParams[key] !== void 0 ? this.currentQueryParams[key] : this.currentRouteParams[key]; return value !== void 0 ? value : defaultValue; } /** * 라우팅 파라미터만 반환 */ getRouteParams() { return { ...this.currentRouteParams }; } /** * 라우팅 파라미터에서 특정 키 값 반환 */ getRouteParam(key, defaultValue = void 0) { const value = this.currentRouteParams[key]; return value !== void 0 ? value : defaultValue; } /** * URL 업데이트 (라우터의 updateURL 메소드 호출) */ updateURL() { if (this.router && typeof this.router.updateURL === "function") { const route = this.router.currentHash || "home"; this.router.updateURL(route, this.currentQueryParams); } } /** * 쿼리 파라미터 통계 */ getStats() { return { currentParams: Object.keys(this.currentQueryParams).length, maxAllowed: this.config.maxParameterCount, validationEnabled: this.config.enableParameterValidation, currentQueryString: this.buildQueryString(this.currentQueryParams) }; } /** * 정리 (메모리 누수 방지) */ destroy() { this.currentQueryParams = {}; this.currentRouteParams = {}; this.router = null; this.log("debug", "QueryManager destroyed"); } }; // src/core/RouteLoader.js var RouteLoader = class { constructor(router, options = {}) { this.config = { srcPath: options.srcPath || router.config.srcPath || "/src", // 소스 파일 경로 routesPath: options.routesPath || router.config.routesPath || "/routes", // 프로덕션 라우트 경로 environment: options.environment || "development", useLayout: options.useLayout !== false, defaultLayout: options.defaultLayout || "default", useComponents: options.useComponents !== false, debug: options.debug || false }; this.router = router; this.log("debug", "RouteLoader initialized with config:", this.config); } /** * 스크립트 파일 로드 */ async loadScript(routeName) { let script; try { if (this.config.environment === "production") { const importPath = `${this.config.routesPath}/${routeName}.js`; this.log("debug", `Loading production route: ${importPath}`); const module = await import(importPath); script = module.default; } else { const importPath = `${this.config.srcPath}/logic/${routeName}.js`; this.log("debug", `Loading development route: ${importPath}`); const module = await import(importPath); script = module.default; } if (!script) { throw new Error(`Route '${routeName}' not found - no default export`); } } catch (error) { if (error.message.includes("Failed to resolve") || error.message.includes("Failed to fetch") || error.message.includes("not found") || error.name === "TypeError") { throw new Error(`Route '${routeName}' not found - 404`); } throw error; } return script; } /** * 템플릿 파일 로드 (실패시 기본값 반환) */ async loadTemplate(routeName) { try { const templatePath = `${this.config.srcPath}/views/${routeName}.html`; const response = await fetch(templatePath); if (!response.ok) throw new Error(`Template not found: ${response.status}`); const template = await response.text(); this.log("debug", `Template '${routeName}' loaded successfully`); return template; } catch (error) { this.log("warn", `Template '${routeName}' not found, using default:`, error.message); return this.generateDefaultTemplate(routeName); } } /** * 스타일 파일 로드 (실패시 빈 문자열 반환) */ async loadStyle(routeName) { try { const stylePath = `${this.config.srcPath}/styles/${routeName}.css`; const response = await fetch(stylePath); if (!response.ok) throw new Error(`Style not found: ${response.status}`); const style = await response.text(); this.log("debug", `Style '${routeName}' loaded successfully`); return style; } catch (error) { this.log("debug", `Style '${routeName}' not found, no styles applied:`, error.message); return ""; } } /** * 레이아웃 파일 로드 (실패시 null 반환) */ async loadLayout(layoutName) { try { const layoutPath = `${this.config.srcPath}/layouts/${layoutName}.html`; const response = await fetch(layoutPath); if (!response.ok) throw new Error(`Layout not found: ${response.status}`); const layout = await response.text(); this.log("debug", `Layout '${layoutName}' loaded successfully`); return layout; } catch (error) { this.log("debug", `Layout '${layoutName}' not found, no layout applied:`, error.message); return null; } } /** * 레이아웃과 템플릿 병합 */ mergeLayoutWithTemplate(routeName, layout, template) { let result; if (layout.includes("{{ content }}")) { result = layout.replace( /{{ content }}/s, template ); } else if (layout.includes('class="main-content"')) { this.log("debug", "Using main-content replacement"); result = layout.replace( /(<div class="container">).*?(<\/div>\s*<\/main>)/s, `$1${template}$2` ); } else { this.log("debug", "Wrapping template with layout"); result = `${layout} ${template}`; } return result; } /** * Vue 컴포넌트 생성 */ async createVueComponent(routeName) { const cacheKey = `component_${routeName}`; const cached = this.router.cacheManager?.getFromCache(cacheKey); if (cached) { return cached; } const script = await this.loadScript(routeName); const router = this.router; const isProduction = this.config.environment === "production"; let template, style = "", layout = null; if (isProduction) { template = script.template || this.generateDefaultTemplate(routeName); } else { template = await this.loadTemplate(routeName); style = await this.loadStyle(routeName); layout = this.config.useLayout && script.layout !== null ? await this.loadLayout(script.layout || this.config.defaultLayout) : null; if (layout) { template = this.mergeLayoutWithTemplate(routeName, layout, template); } } let loadedComponents = {}; if (this.config.useComponents && router.componentLoader) { try { loadedComponents = await router.componentLoader.loadAllComponents(); this.log("debug", `Components loaded successfully for route: ${routeName}`); } catch (error) { this.log("warn", `Component loading failed for route '${routeName}', continuing without components:`, error.message); loadedComponents = {}; } } const component = { ...script, name: script.name || this.toPascalCase(routeName), template, components: loadedComponents, data