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
JavaScript
/**
* 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