viewlogic 1.1.2 → 1.2.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.
@@ -42,6 +42,8 @@ var I18nManager = class {
42
42
  await this.loadMessages(this.currentLanguage);
43
43
  } catch (error) {
44
44
  this.log("error", "Failed to load initial language file:", error);
45
+ this.messages.set(this.currentLanguage, {});
46
+ this.log("info", "Using empty message object as fallback");
45
47
  }
46
48
  } else {
47
49
  this.log("debug", "Language messages already loaded:", this.currentLanguage);
@@ -98,9 +100,17 @@ var I18nManager = class {
98
100
  this.log("info", "Language changed successfully", { from: oldLanguage, to: language });
99
101
  return true;
100
102
  } catch (error) {
101
- this.currentLanguage = oldLanguage;
102
- this.log("error", "Failed to change language:", error);
103
- return false;
103
+ this.log("error", "Failed to load messages for language change, using empty messages:", error);
104
+ this.messages.set(language, {});
105
+ this.saveLanguageToCache(language);
106
+ this.emit("languageChanged", {
107
+ from: oldLanguage,
108
+ to: language,
109
+ messages: {},
110
+ error: true
111
+ });
112
+ this.log("warn", "Language changed with empty messages", { from: oldLanguage, to: language });
113
+ return true;
104
114
  }
105
115
  }
106
116
  /**
@@ -136,7 +146,10 @@ var I18nManager = class {
136
146
  return messages;
137
147
  } catch (error) {
138
148
  this.loadPromises.delete(language);
139
- throw error;
149
+ this.log("error", "Failed to load messages, using empty fallback for:", language, error);
150
+ const emptyMessages = {};
151
+ this.messages.set(language, emptyMessages);
152
+ return emptyMessages;
140
153
  }
141
154
  }
142
155
  /**
@@ -165,9 +178,15 @@ var I18nManager = class {
165
178
  this.log("error", "Failed to load messages file for:", language, error);
166
179
  if (language !== this.config.fallbackLanguage) {
167
180
  this.log("info", "Trying fallback language:", this.config.fallbackLanguage);
168
- return await this._loadMessagesFromFile(this.config.fallbackLanguage);
181
+ try {
182
+ return await this._loadMessagesFromFile(this.config.fallbackLanguage);
183
+ } catch (fallbackError) {
184
+ this.log("error", "Fallback language also failed:", fallbackError);
185
+ return {};
186
+ }
169
187
  }
170
- throw new Error(`Failed to load messages for language: ${language}`);
188
+ this.log("warn", `No messages available for language: ${language}, using empty fallback`);
189
+ return {};
171
190
  }
172
191
  }
173
192
  /**
@@ -352,7 +371,8 @@ var I18nManager = class {
352
371
  return true;
353
372
  } catch (error) {
354
373
  this.log("error", "I18n initialization failed:", error);
355
- return false;
374
+ this.log("info", "I18n system ready with fallback behavior");
375
+ return true;
356
376
  }
357
377
  }
358
378
  /**
@@ -413,7 +433,8 @@ var I18nManager = class {
413
433
  return true;
414
434
  } catch (error) {
415
435
  this.log("error", "Failed to initialize I18n system:", error);
416
- return false;
436
+ this.log("info", "I18n system will continue with fallback behavior");
437
+ return true;
417
438
  }
418
439
  }
419
440
  };
@@ -972,6 +993,7 @@ var CacheManager = class {
972
993
  this.cacheTimestamps.clear();
973
994
  this.lruOrder = [];
974
995
  this.log(`\u{1F525} Cleared all cache (${size} entries)`);
996
+ return size;
975
997
  }
976
998
  /**
977
999
  * 만료된 캐시 항목들 정리
@@ -1460,449 +1482,884 @@ var QueryManager = class {
1460
1482
  }
1461
1483
  };
1462
1484
 
1463
- // src/core/RouteLoader.js
1464
- var RouteLoader = class {
1485
+ // src/core/FormHandler.js
1486
+ var FormHandler = class {
1465
1487
  constructor(router, options = {}) {
1488
+ this.router = router;
1466
1489
  this.config = {
1467
- basePath: options.basePath || "/src",
1468
- routesPath: options.routesPath || "/routes",
1469
- environment: options.environment || "development",
1470
- useLayout: options.useLayout !== false,
1471
- defaultLayout: options.defaultLayout || "default",
1472
- useComponents: options.useComponents !== false,
1473
- debug: options.debug || false
1490
+ debug: options.debug || false,
1491
+ ...options
1474
1492
  };
1475
- this.router = router;
1476
- this.log("debug", "RouteLoader initialized with config:", this.config);
1493
+ this.log("debug", "FormHandler initialized");
1477
1494
  }
1478
1495
  /**
1479
- * 스크립트 파일 로드
1496
+ * 로깅 래퍼 메서드
1480
1497
  */
1481
- async loadScript(routeName) {
1482
- let script;
1498
+ log(level, ...args) {
1499
+ if (this.router?.errorHandler) {
1500
+ this.router.errorHandler.log(level, "FormHandler", ...args);
1501
+ }
1502
+ }
1503
+ /**
1504
+ * 자동 폼 바인딩
1505
+ */
1506
+ bindAutoForms(component) {
1507
+ const forms = document.querySelectorAll("form.auto-form, form[action]");
1508
+ forms.forEach((form) => {
1509
+ form.removeEventListener("submit", form._boundSubmitHandler);
1510
+ const boundHandler = (e) => this.handleFormSubmit(e, component);
1511
+ form._boundSubmitHandler = boundHandler;
1512
+ form.addEventListener("submit", boundHandler);
1513
+ this.log("debug", `Form auto-bound: ${form.getAttribute("action")}`);
1514
+ });
1515
+ }
1516
+ /**
1517
+ * 폼 서브밋 핸들러
1518
+ */
1519
+ async handleFormSubmit(event, component) {
1520
+ event.preventDefault();
1521
+ const form = event.target;
1522
+ let action = form.getAttribute("action");
1523
+ const method = form.getAttribute("method") || "POST";
1524
+ const successHandler = form.getAttribute("data-success-handler");
1525
+ const errorHandler = form.getAttribute("data-error-handler");
1526
+ const loadingHandler = form.getAttribute("data-loading-handler");
1527
+ const redirectTo = form.getAttribute("data-redirect");
1483
1528
  try {
1484
- if (this.config.environment === "production") {
1485
- const importPath = `${this.config.routesPath}/${routeName}.js`;
1486
- this.log("debug", `Loading production route: ${importPath}`);
1487
- const module = await import(importPath);
1488
- script = module.default;
1489
- } else {
1490
- const importPath = `${this.config.basePath}/logic/${routeName}.js`;
1491
- this.log("debug", `Loading development route: ${importPath}`);
1492
- const module = await import(importPath);
1493
- script = module.default;
1529
+ if (loadingHandler && component[loadingHandler]) {
1530
+ component[loadingHandler](true, form);
1494
1531
  }
1495
- if (!script) {
1496
- throw new Error(`Route '${routeName}' not found - no default export`);
1532
+ action = this.processActionParams(action, component);
1533
+ if (!this.validateForm(form, component)) {
1534
+ return;
1535
+ }
1536
+ const formData = new FormData(form);
1537
+ const data = Object.fromEntries(formData.entries());
1538
+ this.log("debug", `Form submitting to: ${action}`, data);
1539
+ const response = await this.submitFormData(action, method, data, form, component);
1540
+ if (successHandler && component[successHandler]) {
1541
+ component[successHandler](response, form);
1542
+ }
1543
+ if (redirectTo) {
1544
+ setTimeout(() => {
1545
+ component.navigateTo(redirectTo);
1546
+ }, 1e3);
1497
1547
  }
1498
1548
  } catch (error) {
1499
- if (error.message.includes("Failed to resolve") || error.message.includes("Failed to fetch") || error.message.includes("not found") || error.name === "TypeError") {
1500
- throw new Error(`Route '${routeName}' not found - 404`);
1549
+ this.log("warn", `Form submission error:`, error);
1550
+ if (errorHandler && component[errorHandler]) {
1551
+ component[errorHandler](error, form);
1552
+ } else {
1553
+ console.error("Form submission error:", error);
1554
+ }
1555
+ } finally {
1556
+ if (loadingHandler && component[loadingHandler]) {
1557
+ component[loadingHandler](false, form);
1501
1558
  }
1502
- throw error;
1503
1559
  }
1504
- return script;
1505
1560
  }
1506
1561
  /**
1507
- * 템플릿 파일 로드 (실패시 기본값 반환)
1562
+ * 액션 파라미터 처리 (ApiHandler 재사용)
1508
1563
  */
1509
- async loadTemplate(routeName) {
1510
- try {
1511
- const templatePath = `${this.config.basePath}/views/${routeName}.html`;
1512
- const response = await fetch(templatePath);
1513
- if (!response.ok) throw new Error(`Template not found: ${response.status}`);
1514
- const template = await response.text();
1515
- this.log("debug", `Template '${routeName}' loaded successfully`);
1516
- return template;
1517
- } catch (error) {
1518
- this.log("warn", `Template '${routeName}' not found, using default:`, error.message);
1519
- return this.generateDefaultTemplate(routeName);
1520
- }
1564
+ processActionParams(actionTemplate, component) {
1565
+ return this.router.routeLoader.apiHandler.processURLParameters(actionTemplate, component);
1521
1566
  }
1522
1567
  /**
1523
- * 스타일 파일 로드 (실패시 빈 문자열 반환)
1568
+ * 데이터 서브밋 (ApiHandler 활용)
1524
1569
  */
1525
- async loadStyle(routeName) {
1526
- try {
1527
- const stylePath = `${this.config.basePath}/styles/${routeName}.css`;
1528
- const response = await fetch(stylePath);
1529
- if (!response.ok) throw new Error(`Style not found: ${response.status}`);
1530
- const style = await response.text();
1531
- this.log("debug", `Style '${routeName}' loaded successfully`);
1532
- return style;
1533
- } catch (error) {
1534
- this.log("debug", `Style '${routeName}' not found, no styles applied:`, error.message);
1535
- return "";
1570
+ async submitFormData(action, method, data, form, component) {
1571
+ const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
1572
+ const options = {
1573
+ method: method.toUpperCase(),
1574
+ headers: {}
1575
+ };
1576
+ if (hasFile) {
1577
+ options.data = new FormData(form);
1578
+ } else {
1579
+ options.data = data;
1580
+ options.headers["Content-Type"] = "application/json";
1536
1581
  }
1582
+ return await this.router.routeLoader.apiHandler.fetchData(action, component, options);
1537
1583
  }
1538
1584
  /**
1539
- * 레이아웃 파일 로드 (실패시 null 반환)
1585
+ * 클라이언트 사이드 검증
1540
1586
  */
1541
- async loadLayout(layoutName) {
1542
- try {
1543
- const layoutPath = `${this.config.basePath}/layouts/${layoutName}.html`;
1544
- const response = await fetch(layoutPath);
1545
- if (!response.ok) throw new Error(`Layout not found: ${response.status}`);
1546
- const layout = await response.text();
1547
- this.log("debug", `Layout '${layoutName}' loaded successfully`);
1548
- return layout;
1549
- } catch (error) {
1550
- this.log("debug", `Layout '${layoutName}' not found, no layout applied:`, error.message);
1551
- return null;
1552
- }
1587
+ validateForm(form, component) {
1588
+ let isValid = true;
1589
+ const inputs = form.querySelectorAll("input, textarea, select");
1590
+ inputs.forEach((input) => {
1591
+ if (!input.checkValidity()) {
1592
+ isValid = false;
1593
+ input.classList.add("error");
1594
+ return;
1595
+ }
1596
+ const validationFunction = input.getAttribute("data-validation");
1597
+ if (validationFunction) {
1598
+ const isInputValid = this.validateInput(input, validationFunction, component);
1599
+ if (!isInputValid) {
1600
+ isValid = false;
1601
+ input.classList.add("error");
1602
+ } else {
1603
+ input.classList.remove("error");
1604
+ }
1605
+ } else {
1606
+ input.classList.remove("error");
1607
+ }
1608
+ });
1609
+ return isValid;
1553
1610
  }
1554
1611
  /**
1555
- * 레이아웃과 템플릿 병합
1612
+ * 개별 입력 검증
1556
1613
  */
1557
- mergeLayoutWithTemplate(routeName, layout, template) {
1558
- let result;
1559
- if (layout.includes("{{ content }}")) {
1560
- result = layout.replace(
1561
- /{{ content }}/s,
1562
- template
1563
- );
1564
- } else if (layout.includes('class="main-content"')) {
1565
- this.log("debug", "Using main-content replacement");
1566
- result = layout.replace(
1567
- /(<div class="container">).*?(<\/div>\s*<\/main>)/s,
1568
- `$1${template}$2`
1569
- );
1570
- } else {
1571
- this.log("debug", "Wrapping template with layout");
1572
- result = `${layout}
1573
- ${template}`;
1614
+ validateInput(input, validationFunction, component) {
1615
+ const value = input.value;
1616
+ if (typeof component[validationFunction] === "function") {
1617
+ try {
1618
+ return component[validationFunction](value, input);
1619
+ } catch (error) {
1620
+ this.log("warn", `Validation function '${validationFunction}' error:`, error);
1621
+ return false;
1622
+ }
1574
1623
  }
1575
- return result;
1624
+ this.log("warn", `Validation function '${validationFunction}' not found`);
1625
+ return true;
1576
1626
  }
1577
1627
  /**
1578
- * Vue 컴포넌트 생성
1628
+ * 정리 (메모리 누수 방지)
1579
1629
  */
1580
- async createVueComponent(routeName) {
1581
- const cacheKey = `component_${routeName}`;
1582
- const cached = this.router.cacheManager?.getFromCache(cacheKey);
1583
- if (cached) {
1584
- return cached;
1585
- }
1586
- const script = await this.loadScript(routeName);
1587
- const router = this.router;
1588
- const isProduction = this.config.environment === "production";
1589
- let template, style = "", layout = null;
1590
- if (isProduction) {
1591
- template = script.template || this.generateDefaultTemplate(routeName);
1592
- } else {
1593
- template = await this.loadTemplate(routeName);
1594
- style = await this.loadStyle(routeName);
1595
- layout = this.config.useLayout && script.layout !== null ? await this.loadLayout(script.layout || this.config.defaultLayout) : null;
1596
- if (layout) {
1597
- template = this.mergeLayoutWithTemplate(routeName, layout, template);
1630
+ destroy() {
1631
+ const forms = document.querySelectorAll("form.auto-form, form[action]");
1632
+ forms.forEach((form) => {
1633
+ if (form._boundSubmitHandler) {
1634
+ form.removeEventListener("submit", form._boundSubmitHandler);
1635
+ delete form._boundSubmitHandler;
1598
1636
  }
1637
+ });
1638
+ this.log("debug", "FormHandler destroyed");
1639
+ this.router = null;
1640
+ }
1641
+ };
1642
+
1643
+ // src/core/ApiHandler.js
1644
+ var ApiHandler = class {
1645
+ constructor(router, options = {}) {
1646
+ this.router = router;
1647
+ this.config = {
1648
+ debug: options.debug || false,
1649
+ timeout: options.timeout || 1e4,
1650
+ retries: options.retries || 1,
1651
+ ...options
1652
+ };
1653
+ this.log("debug", "ApiHandler initialized");
1654
+ }
1655
+ /**
1656
+ * 로깅 래퍼 메서드
1657
+ */
1658
+ log(level, ...args) {
1659
+ if (this.router?.errorHandler) {
1660
+ this.router.errorHandler.log(level, "ApiHandler", ...args);
1599
1661
  }
1600
- let loadedComponents = {};
1601
- if (this.config.useComponents && router.componentLoader) {
1662
+ }
1663
+ /**
1664
+ * 컴포넌트 데이터 가져오기 (파라미터 치환 지원)
1665
+ */
1666
+ async fetchData(dataURL, component = null, options = {}) {
1667
+ try {
1668
+ let processedURL = this.processURLParameters(dataURL, component);
1669
+ const queryString = this.router.queryManager?.buildQueryString(this.router.queryManager?.getQueryParams()) || "";
1670
+ const fullURL = queryString ? `${processedURL}?${queryString}` : processedURL;
1671
+ this.log("debug", `Fetching data from: ${fullURL}`);
1672
+ const requestOptions = {
1673
+ method: options.method || "GET",
1674
+ headers: {
1675
+ "Content-Type": "application/json",
1676
+ "Accept": "application/json",
1677
+ ...options.headers
1678
+ },
1679
+ ...options
1680
+ };
1681
+ if (component?.$getToken && component.$getToken()) {
1682
+ requestOptions.headers["Authorization"] = `Bearer ${component.$getToken()}`;
1683
+ }
1684
+ if (options.data && ["POST", "PUT", "PATCH"].includes(requestOptions.method.toUpperCase())) {
1685
+ if (options.data instanceof FormData) {
1686
+ requestOptions.body = options.data;
1687
+ delete requestOptions.headers["Content-Type"];
1688
+ } else {
1689
+ requestOptions.body = JSON.stringify(options.data);
1690
+ }
1691
+ }
1692
+ const response = await fetch(fullURL, requestOptions);
1693
+ if (!response.ok) {
1694
+ let error;
1695
+ try {
1696
+ error = await response.json();
1697
+ } catch (e) {
1698
+ error = { message: `HTTP ${response.status}: ${response.statusText}` };
1699
+ }
1700
+ throw new Error(error.message || `HTTP ${response.status}`);
1701
+ }
1602
1702
  try {
1603
- loadedComponents = await router.componentLoader.loadAllComponents();
1604
- this.log("debug", `Components loaded successfully for route: ${routeName}`);
1605
- } catch (error) {
1606
- this.log("warn", `Component loading failed for route '${routeName}', continuing without components:`, error.message);
1607
- loadedComponents = {};
1703
+ const data = await response.json();
1704
+ if (typeof data !== "object" || data === null) {
1705
+ throw new Error("Invalid data format: expected object");
1706
+ }
1707
+ return data;
1708
+ } catch (e) {
1709
+ return { success: true };
1608
1710
  }
1711
+ } catch (error) {
1712
+ this.log("error", "Failed to fetch data:", error);
1713
+ throw error;
1609
1714
  }
1610
- const component = {
1611
- ...script,
1612
- name: script.name || this.toPascalCase(routeName),
1613
- template,
1614
- components: loadedComponents,
1615
- data() {
1616
- const originalData = script.data ? script.data() : {};
1617
- const commonData = {
1618
- ...originalData,
1619
- currentRoute: routeName,
1620
- $query: router.queryManager?.getQueryParams() || {},
1621
- $lang: router.i18nManager?.getCurrentLanguage() || router.config.i18nDefaultLanguage,
1622
- $dataLoading: false
1623
- };
1624
- return commonData;
1625
- },
1626
- computed: {
1627
- ...script.computed || {},
1628
- // 하위 호환성을 위해 params는 유지하되 getAllParams 사용
1629
- params() {
1630
- return router.queryManager?.getAllParams() || {};
1715
+ }
1716
+ /**
1717
+ * 여러 API 엔드포인트에서 데이터 가져오기
1718
+ */
1719
+ async fetchMultipleData(dataConfig, component = null) {
1720
+ if (!dataConfig || typeof dataConfig !== "object") {
1721
+ return {};
1722
+ }
1723
+ const results = {};
1724
+ const errors = {};
1725
+ const promises = Object.entries(dataConfig).map(async ([key, config]) => {
1726
+ try {
1727
+ let url, options = {};
1728
+ if (typeof config === "string") {
1729
+ url = config;
1730
+ } else if (typeof config === "object") {
1731
+ url = config.url;
1732
+ options = { ...config };
1733
+ delete options.url;
1631
1734
  }
1632
- },
1633
- async mounted() {
1634
- if (script.mounted) {
1635
- await script.mounted.call(this);
1735
+ if (url) {
1736
+ const data = await this.fetchData(url, component, options);
1737
+ results[key] = data;
1636
1738
  }
1637
- if (script.dataURL) {
1638
- if (typeof script.dataURL === "string") {
1639
- await this.$fetchData();
1640
- } else if (typeof script.dataURL === "object") {
1641
- await this.$fetchMultipleData();
1739
+ } catch (error) {
1740
+ errors[key] = error;
1741
+ this.log("warn", `Failed to fetch data for '${key}':`, error);
1742
+ }
1743
+ });
1744
+ await Promise.all(promises);
1745
+ return { results, errors };
1746
+ }
1747
+ /**
1748
+ * URL에서 파라미터 치환 처리 ({param} 형식)
1749
+ */
1750
+ processURLParameters(url, component = null) {
1751
+ if (!url || typeof url !== "string") return url;
1752
+ let processedURL = url;
1753
+ const paramMatches = url.match(/\{([^}]+)\}/g);
1754
+ if (paramMatches && component) {
1755
+ paramMatches.forEach((match) => {
1756
+ const paramName = match.slice(1, -1);
1757
+ try {
1758
+ let paramValue = null;
1759
+ if (component.getParam) {
1760
+ paramValue = component.getParam(paramName);
1642
1761
  }
1643
- }
1644
- await this.$nextTick();
1645
- this.$bindAutoForms();
1646
- },
1647
- methods: {
1648
- ...script.methods,
1649
- // 라우팅 관련
1650
- navigateTo: (route, params) => router.navigateTo(route, params),
1651
- getCurrentRoute: () => router.getCurrentRoute(),
1652
- // 통합된 파라미터 관리 (라우팅 + 쿼리 파라미터)
1653
- getParams: () => router.queryManager?.getAllParams() || {},
1654
- getParam: (key, defaultValue) => router.queryManager?.getParam(key, defaultValue),
1655
- // i18n 관련
1656
- $t: (key, params) => router.i18nManager?.t(key, params) || key,
1657
- // 인증 관련
1658
- $isAuthenticated: () => router.authManager?.isUserAuthenticated() || false,
1659
- $logout: () => router.authManager ? router.navigateTo(router.authManager.handleLogout()) : null,
1660
- $loginSuccess: (target) => router.authManager ? router.navigateTo(router.authManager.handleLoginSuccess(target)) : null,
1661
- $checkAuth: (route) => router.authManager ? router.authManager.checkAuthentication(route) : Promise.resolve({ allowed: true, reason: "auth_disabled" }),
1662
- $getToken: () => router.authManager?.getAccessToken() || null,
1663
- $setToken: (token, options) => router.authManager?.setAccessToken(token, options) || false,
1664
- $removeToken: (storage) => router.authManager?.removeAccessToken(storage) || null,
1665
- $getAuthCookie: () => router.authManager?.getAuthCookie() || null,
1666
- $getCookie: (name) => router.authManager?.getCookieValue(name) || null,
1667
- // 데이터 fetch (단일 API 또는 특정 API)
1668
- async $fetchData(apiName) {
1669
- if (!script.dataURL) return;
1670
- this.$dataLoading = true;
1671
- try {
1672
- if (typeof script.dataURL === "string") {
1673
- const data = await router.routeLoader.fetchComponentData(script.dataURL);
1674
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Data fetched for ${routeName}:`, data);
1675
- Object.assign(this, data);
1676
- this.$emit("data-loaded", data);
1677
- } else if (typeof script.dataURL === "object" && apiName) {
1678
- const url = script.dataURL[apiName];
1679
- if (url) {
1680
- const data = await router.routeLoader.fetchComponentData(url);
1681
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Data fetched for ${routeName}.${apiName}:`, data);
1682
- this[apiName] = data;
1683
- this.$emit("data-loaded", { [apiName]: data });
1684
- }
1685
- } else {
1686
- await this.$fetchMultipleData();
1762
+ if (paramValue === null || paramValue === void 0) {
1763
+ paramValue = component[paramName];
1764
+ }
1765
+ if (paramValue === null || paramValue === void 0) {
1766
+ if (component.$options?.computed?.[paramName]) {
1767
+ paramValue = component[paramName];
1687
1768
  }
1769
+ }
1770
+ if (paramValue === null || paramValue === void 0) {
1771
+ paramValue = this.router.queryManager?.getParam(paramName);
1772
+ }
1773
+ if (paramValue !== null && paramValue !== void 0) {
1774
+ processedURL = processedURL.replace(
1775
+ match,
1776
+ encodeURIComponent(paramValue)
1777
+ );
1778
+ this.log("debug", `URL parameter resolved: ${paramName} = ${paramValue}`);
1779
+ } else {
1780
+ this.log("warn", `URL parameter '${paramName}' not found, keeping original: ${match}`);
1781
+ }
1782
+ } catch (error) {
1783
+ this.log("warn", `Error processing URL parameter '${paramName}':`, error);
1784
+ }
1785
+ });
1786
+ }
1787
+ return processedURL;
1788
+ }
1789
+ /**
1790
+ * HTTP 메서드별 헬퍼 함수들
1791
+ */
1792
+ async get(url, component = null, options = {}) {
1793
+ return this.fetchData(url, component, { ...options, method: "GET" });
1794
+ }
1795
+ async post(url, data, component = null, options = {}) {
1796
+ return this.fetchData(url, component, { ...options, method: "POST", data });
1797
+ }
1798
+ async put(url, data, component = null, options = {}) {
1799
+ return this.fetchData(url, component, { ...options, method: "PUT", data });
1800
+ }
1801
+ async patch(url, data, component = null, options = {}) {
1802
+ return this.fetchData(url, component, { ...options, method: "PATCH", data });
1803
+ }
1804
+ async delete(url, component = null, options = {}) {
1805
+ return this.fetchData(url, component, { ...options, method: "DELETE" });
1806
+ }
1807
+ /**
1808
+ * 정리 (메모리 누수 방지)
1809
+ */
1810
+ destroy() {
1811
+ this.log("debug", "ApiHandler destroyed");
1812
+ this.router = null;
1813
+ }
1814
+ };
1815
+
1816
+ // src/core/ComponentLoader.js
1817
+ var ComponentLoader = class {
1818
+ constructor(router = null, options = {}) {
1819
+ this.config = {
1820
+ componentsPath: options.componentsPath || "/components",
1821
+ // srcPath 기준 상대 경로
1822
+ debug: options.debug || false,
1823
+ environment: options.environment || "development",
1824
+ ...options
1825
+ };
1826
+ this.router = router;
1827
+ this.loadingPromises = /* @__PURE__ */ new Map();
1828
+ this.unifiedComponents = null;
1829
+ }
1830
+ /**
1831
+ * 로깅 래퍼 메서드
1832
+ */
1833
+ log(level, ...args) {
1834
+ if (this.router?.errorHandler) {
1835
+ this.router.errorHandler.log(level, "ComponentLoader", ...args);
1836
+ }
1837
+ }
1838
+ /**
1839
+ * 컴포넌트를 비동기로 로드 (캐시 지원)
1840
+ */
1841
+ async loadComponent(componentName) {
1842
+ if (!componentName || typeof componentName !== "string") {
1843
+ throw new Error("Component name must be a non-empty string");
1844
+ }
1845
+ const cacheKey = `component_${componentName}`;
1846
+ const cachedComponent = this.router?.cacheManager?.getFromCache(cacheKey);
1847
+ if (cachedComponent) {
1848
+ this.log("debug", `Component '${componentName}' loaded from cache`);
1849
+ return cachedComponent;
1850
+ }
1851
+ if (this.loadingPromises.has(componentName)) {
1852
+ return this.loadingPromises.get(componentName);
1853
+ }
1854
+ const loadPromise = this._loadComponentFromFile(componentName);
1855
+ this.loadingPromises.set(componentName, loadPromise);
1856
+ try {
1857
+ const component = await loadPromise;
1858
+ if (component && this.router?.cacheManager) {
1859
+ this.router.cacheManager.setCache(cacheKey, component);
1860
+ this.log("debug", `Component '${componentName}' cached successfully`);
1861
+ }
1862
+ return component;
1863
+ } catch (error) {
1864
+ throw error;
1865
+ } finally {
1866
+ this.loadingPromises.delete(componentName);
1867
+ }
1868
+ }
1869
+ /**
1870
+ * 파일에서 컴포넌트 로드
1871
+ */
1872
+ async _loadComponentFromFile(componentName) {
1873
+ const componentRelativePath = `${this.config.componentsPath}/${componentName}.js`;
1874
+ let componentPath;
1875
+ if (this.router && this.router.config.srcPath) {
1876
+ const srcPath = this.router.config.srcPath;
1877
+ if (srcPath.startsWith("http")) {
1878
+ const cleanSrcPath = srcPath.endsWith("/") ? srcPath.slice(0, -1) : srcPath;
1879
+ const cleanComponentPath = componentRelativePath.startsWith("/") ? componentRelativePath : `/${componentRelativePath}`;
1880
+ componentPath = `${cleanSrcPath}${cleanComponentPath}`;
1881
+ } else {
1882
+ componentPath = this.router.resolvePath(`${srcPath}${componentRelativePath}`);
1883
+ }
1884
+ } else {
1885
+ componentPath = this.router ? this.router.resolvePath(`/src${componentRelativePath}`) : `/src${componentRelativePath}`;
1886
+ }
1887
+ try {
1888
+ const module = await import(componentPath);
1889
+ const component = module.default;
1890
+ if (!component) {
1891
+ throw new Error(`Component '${componentName}' has no default export`);
1892
+ }
1893
+ if (!component.name) {
1894
+ component.name = componentName;
1895
+ }
1896
+ this.log("debug", `Component '${componentName}' loaded successfully`);
1897
+ return component;
1898
+ } catch (error) {
1899
+ this.log("error", `Failed to load component '${componentName}':`, error);
1900
+ throw new Error(`Component '${componentName}' not found: ${error.message}`);
1901
+ }
1902
+ }
1903
+ /**
1904
+ * 컴포넌트 모듈 클리어
1905
+ */
1906
+ clearComponents() {
1907
+ this.loadingPromises.clear();
1908
+ this.unifiedComponents = null;
1909
+ this.log("debug", "All components cleared");
1910
+ }
1911
+ /**
1912
+ * 환경에 따른 모든 컴포넌트 로딩 (캐싱 지원)
1913
+ */
1914
+ async loadAllComponents(componentNames = null) {
1915
+ let components;
1916
+ if (this.config.environment === "production") {
1917
+ if (this.unifiedComponents) {
1918
+ this.log("debug", "Using existing unified components");
1919
+ return this.unifiedComponents;
1920
+ }
1921
+ components = await this._loadProductionComponents();
1922
+ } else {
1923
+ components = await this._loadDevelopmentComponents(componentNames);
1924
+ }
1925
+ return components;
1926
+ }
1927
+ /**
1928
+ * 운영 모드: 통합 컴포넌트 로딩
1929
+ */
1930
+ async _loadProductionComponents() {
1931
+ try {
1932
+ const componentsPath = `${this.router?.config?.routesPath || "/routes"}/_components.js`;
1933
+ this.log("info", "[PRODUCTION] Loading unified components from:", componentsPath);
1934
+ const componentsModule = await import(componentsPath);
1935
+ if (typeof componentsModule.registerComponents === "function") {
1936
+ this.unifiedComponents = componentsModule.components || {};
1937
+ this.log("info", `[PRODUCTION] Unified components loaded: ${Object.keys(this.unifiedComponents).length} components`);
1938
+ return this.unifiedComponents;
1939
+ } else {
1940
+ throw new Error("registerComponents function not found in components module");
1941
+ }
1942
+ } catch (error) {
1943
+ this.log("warn", "[PRODUCTION] Failed to load unified components:", error.message);
1944
+ this.unifiedComponents = {};
1945
+ return {};
1946
+ }
1947
+ }
1948
+ /**
1949
+ * 개발 모드: 개별 컴포넌트 로딩
1950
+ */
1951
+ async _loadDevelopmentComponents(componentNames = null) {
1952
+ const namesToLoad = componentNames || [];
1953
+ const components = {};
1954
+ if (namesToLoad.length === 0) {
1955
+ this.log("info", "[DEVELOPMENT] No components to load");
1956
+ return components;
1957
+ }
1958
+ this.log("info", `[DEVELOPMENT] Loading individual components: ${namesToLoad.join(", ")}`);
1959
+ for (const name of namesToLoad) {
1960
+ try {
1961
+ const component = await this.loadComponent(name);
1962
+ if (component) {
1963
+ components[name] = component;
1964
+ }
1965
+ } catch (loadError) {
1966
+ this.log("warn", `[DEVELOPMENT] Failed to load component ${name}:`, loadError.message);
1967
+ }
1968
+ }
1969
+ this.log("info", `[DEVELOPMENT] Individual components loaded: ${Object.keys(components).length} components`);
1970
+ return components;
1971
+ }
1972
+ /**
1973
+ * 템플릿과 레이아웃에서 사용된 컴포넌트 추출
1974
+ */
1975
+ getComponentNames(template, layout = null, layoutName = null) {
1976
+ const componentSet = layout ? this._getLayoutComponents(layout, layoutName) : /* @__PURE__ */ new Set();
1977
+ if (template) {
1978
+ this._extractComponentsFromContent(template, componentSet);
1979
+ }
1980
+ const components = Array.from(componentSet);
1981
+ this.log("debug", `Discovered ${components.length} components:`, components);
1982
+ return components;
1983
+ }
1984
+ /**
1985
+ * 레이아웃에서 컴포넌트 추출 (캐시 활용)
1986
+ */
1987
+ _getLayoutComponents(layout, layoutName) {
1988
+ if (!layout || typeof layout !== "string") return /* @__PURE__ */ new Set();
1989
+ if (!layoutName || typeof layoutName !== "string") return /* @__PURE__ */ new Set();
1990
+ const cacheKey = `layout_components_${layoutName}`;
1991
+ const cachedComponents = this.router?.cacheManager?.getFromCache(cacheKey);
1992
+ if (cachedComponents) {
1993
+ this.log("debug", `Using cached layout components for '${layoutName}'`);
1994
+ return cachedComponents;
1995
+ }
1996
+ const componentSet = /* @__PURE__ */ new Set();
1997
+ this._extractComponentsFromContent(layout, componentSet);
1998
+ if (this.router?.cacheManager) {
1999
+ this.router.cacheManager.setCache(cacheKey, componentSet);
2000
+ this.log("debug", `Cached layout components for '${layoutName}': ${Array.from(componentSet).join(", ")}`);
2001
+ }
2002
+ return componentSet;
2003
+ }
2004
+ /**
2005
+ * HTML 컨텐츠에서 Vue 컴포넌트 추출
2006
+ */
2007
+ _extractComponentsFromContent(content, componentSet) {
2008
+ if (!content || typeof content !== "string") return;
2009
+ const componentPattern = /<([A-Z][a-zA-Z0-9]*)(?:\s[^>]*)?\/?>|<\/([A-Z][a-zA-Z0-9]*)\s*>/gs;
2010
+ let match;
2011
+ while ((match = componentPattern.exec(content)) !== null) {
2012
+ const componentName = match[1] || match[2];
2013
+ if (componentName && !this._isHtmlTag(componentName)) {
2014
+ componentSet.add(componentName);
2015
+ this.log("debug", `Found component: ${componentName}`);
2016
+ }
2017
+ }
2018
+ }
2019
+ /**
2020
+ * HTML 기본 태그인지 확인
2021
+ */
2022
+ _isHtmlTag(tagName) {
2023
+ const htmlTags = [
2024
+ "div",
2025
+ "span",
2026
+ "p",
2027
+ "a",
2028
+ "img",
2029
+ "ul",
2030
+ "ol",
2031
+ "li",
2032
+ "h1",
2033
+ "h2",
2034
+ "h3",
2035
+ "h4",
2036
+ "h5",
2037
+ "h6",
2038
+ "table",
2039
+ "tr",
2040
+ "td",
2041
+ "th",
2042
+ "form",
2043
+ "select",
2044
+ "option",
2045
+ "textarea",
2046
+ "nav",
2047
+ "header",
2048
+ "footer",
2049
+ "main",
2050
+ "section",
2051
+ "article",
2052
+ "aside",
2053
+ "figure",
2054
+ "figcaption",
2055
+ "video",
2056
+ "audio",
2057
+ "canvas",
2058
+ "svg",
2059
+ "iframe",
2060
+ "script",
2061
+ "style",
2062
+ "link",
2063
+ "meta",
2064
+ "title",
2065
+ "body",
2066
+ "html",
2067
+ "head",
2068
+ "template",
2069
+ "slot"
2070
+ ];
2071
+ return htmlTags.includes(tagName);
2072
+ }
2073
+ /**
2074
+ * 메모리 정리
2075
+ */
2076
+ dispose() {
2077
+ this.clearComponents();
2078
+ this.log("debug", "ComponentLoader disposed");
2079
+ this.router = null;
2080
+ }
2081
+ };
2082
+
2083
+ // src/core/RouteLoader.js
2084
+ var RouteLoader = class {
2085
+ constructor(router, options = {}) {
2086
+ this.config = {
2087
+ srcPath: options.srcPath || router.config.srcPath || "/src",
2088
+ // 소스 파일 경로
2089
+ routesPath: options.routesPath || router.config.routesPath || "/routes",
2090
+ // 프로덕션 라우트 경로
2091
+ environment: options.environment || "development",
2092
+ useLayout: options.useLayout !== false,
2093
+ defaultLayout: options.defaultLayout || "default",
2094
+ debug: options.debug || false
2095
+ };
2096
+ this.router = router;
2097
+ this.formHandler = new FormHandler(router, this.config);
2098
+ this.apiHandler = new ApiHandler(router, this.config);
2099
+ this.componentLoader = new ComponentLoader(router, this.config);
2100
+ this.log("debug", "RouteLoader initialized with config:", this.config);
2101
+ }
2102
+ /**
2103
+ * 스크립트 파일 로드
2104
+ */
2105
+ async loadScript(routeName) {
2106
+ let script;
2107
+ try {
2108
+ if (this.config.environment === "production") {
2109
+ const importPath = `${this.config.routesPath}/${routeName}.js`;
2110
+ this.log("debug", `Loading production route: ${importPath}`);
2111
+ const module = await import(importPath);
2112
+ script = module.default;
2113
+ } else {
2114
+ const importPath = `${this.config.srcPath}/logic/${routeName}.js`;
2115
+ this.log("debug", `Loading development route: ${importPath}`);
2116
+ const module = await import(importPath);
2117
+ script = module.default;
2118
+ }
2119
+ if (!script) {
2120
+ throw new Error(`Route '${routeName}' not found - no default export`);
2121
+ }
2122
+ } catch (error) {
2123
+ if (error.message.includes("Failed to resolve") || error.message.includes("Failed to fetch") || error.message.includes("not found") || error.name === "TypeError") {
2124
+ throw new Error(`Route '${routeName}' not found - 404`);
2125
+ }
2126
+ throw error;
2127
+ }
2128
+ return script;
2129
+ }
2130
+ /**
2131
+ * 템플릿 파일 로드 (실패시 기본값 반환)
2132
+ */
2133
+ async loadTemplate(routeName) {
2134
+ try {
2135
+ const templatePath = `${this.config.srcPath}/views/${routeName}.html`;
2136
+ const response = await fetch(templatePath);
2137
+ if (!response.ok) throw new Error(`Template not found: ${response.status}`);
2138
+ const template = await response.text();
2139
+ this.log("debug", `Template '${routeName}' loaded successfully`);
2140
+ return template;
2141
+ } catch (error) {
2142
+ this.log("warn", `Template '${routeName}' not found, using default:`, error.message);
2143
+ return this.generateDefaultTemplate(routeName);
2144
+ }
2145
+ }
2146
+ /**
2147
+ * 스타일 파일 로드 (실패시 빈 문자열 반환)
2148
+ */
2149
+ async loadStyle(routeName) {
2150
+ try {
2151
+ const stylePath = `${this.config.srcPath}/styles/${routeName}.css`;
2152
+ const response = await fetch(stylePath);
2153
+ if (!response.ok) throw new Error(`Style not found: ${response.status}`);
2154
+ const style = await response.text();
2155
+ this.log("debug", `Style '${routeName}' loaded successfully`);
2156
+ return style;
2157
+ } catch (error) {
2158
+ this.log("debug", `Style '${routeName}' not found, no styles applied:`, error.message);
2159
+ return "";
2160
+ }
2161
+ }
2162
+ /**
2163
+ * 레이아웃 파일 로드 (실패시 null 반환)
2164
+ */
2165
+ async loadLayout(layoutName) {
2166
+ try {
2167
+ const layoutPath = `${this.config.srcPath}/layouts/${layoutName}.html`;
2168
+ const response = await fetch(layoutPath);
2169
+ if (!response.ok) throw new Error(`Layout not found: ${response.status}`);
2170
+ const layout = await response.text();
2171
+ this.log("debug", `Layout '${layoutName}' loaded successfully`);
2172
+ return layout;
2173
+ } catch (error) {
2174
+ this.log("debug", `Layout '${layoutName}' not found, no layout applied:`, error.message);
2175
+ return null;
2176
+ }
2177
+ }
2178
+ /**
2179
+ * 레이아웃과 템플릿 병합
2180
+ */
2181
+ mergeLayoutWithTemplate(routeName, layout, template) {
2182
+ let result;
2183
+ if (layout.includes("{{ content }}")) {
2184
+ result = layout.replace(
2185
+ /{{ content }}/s,
2186
+ template
2187
+ );
2188
+ } else if (layout.includes('class="main-content"')) {
2189
+ this.log("debug", "Using main-content replacement");
2190
+ result = layout.replace(
2191
+ /(<div class="container">).*?(<\/div>\s*<\/main>)/s,
2192
+ `$1${template}$2`
2193
+ );
2194
+ } else {
2195
+ this.log("debug", "Wrapping template with layout");
2196
+ result = `${layout}
2197
+ ${template}`;
2198
+ }
2199
+ return result;
2200
+ }
2201
+ /**
2202
+ * Vue 컴포넌트 생성
2203
+ */
2204
+ async createVueComponent(routeName) {
2205
+ const cacheKey = `component_${routeName}`;
2206
+ const cached = this.router.cacheManager?.getFromCache(cacheKey);
2207
+ if (cached) {
2208
+ return cached;
2209
+ }
2210
+ const script = await this.loadScript(routeName);
2211
+ const router = this.router;
2212
+ const isProduction = this.config.environment === "production";
2213
+ let template, style = "", layout = null;
2214
+ if (isProduction) {
2215
+ template = script.template || this.generateDefaultTemplate(routeName);
2216
+ } else {
2217
+ const loadPromises = [
2218
+ this.loadTemplate(routeName),
2219
+ this.loadStyle(routeName)
2220
+ ];
2221
+ if (this.config.useLayout && script.layout !== null) {
2222
+ loadPromises.push(this.loadLayout(script.layout || this.config.defaultLayout));
2223
+ } else {
2224
+ loadPromises.push(Promise.resolve(null));
2225
+ }
2226
+ const [loadedTemplate, loadedStyle, loadedLayout] = await Promise.all(loadPromises);
2227
+ template = loadedTemplate;
2228
+ style = loadedStyle;
2229
+ layout = loadedLayout;
2230
+ if (layout) {
2231
+ template = this.mergeLayoutWithTemplate(routeName, layout, template);
2232
+ }
2233
+ }
2234
+ let loadedComponents = {};
2235
+ if (this.componentLoader) {
2236
+ try {
2237
+ let componentNames = null;
2238
+ if (!isProduction) {
2239
+ const layoutName = script.layout || this.config.defaultLayout;
2240
+ componentNames = this.componentLoader.getComponentNames(template, layout, layoutName);
2241
+ this.log("info", `[DEVELOPMENT] Discovered components for route '${routeName}':`, componentNames);
2242
+ }
2243
+ loadedComponents = await this.componentLoader.loadAllComponents(componentNames);
2244
+ this.log("debug", `Components loaded successfully for route: ${routeName}`);
2245
+ } catch (error) {
2246
+ this.log("warn", `Component loading failed for route '${routeName}', continuing without components:`, error.message);
2247
+ loadedComponents = {};
2248
+ }
2249
+ }
2250
+ const component = {
2251
+ ...script,
2252
+ name: script.name || this.toPascalCase(routeName),
2253
+ template,
2254
+ components: loadedComponents,
2255
+ data() {
2256
+ const originalData = script.data ? script.data() : {};
2257
+ const commonData = {
2258
+ ...originalData,
2259
+ currentRoute: routeName,
2260
+ $query: router.queryManager?.getQueryParams() || {},
2261
+ $lang: (() => {
2262
+ try {
2263
+ return router.i18nManager?.getCurrentLanguage() || router.config.i18nDefaultLanguage || router.config.defaultLanguage || "ko";
2264
+ } catch (error) {
2265
+ if (router.errorHandler) router.errorHandler.warn("RouteLoader", "Failed to get current language:", error);
2266
+ return router.config.defaultLanguage || "ko";
2267
+ }
2268
+ })(),
2269
+ $dataLoading: false
2270
+ };
2271
+ return commonData;
2272
+ },
2273
+ computed: {
2274
+ ...script.computed || {},
2275
+ // 하위 호환성을 위해 params는 유지하되 getAllParams 사용
2276
+ params() {
2277
+ return router.queryManager?.getAllParams() || {};
2278
+ }
2279
+ },
2280
+ async mounted() {
2281
+ if (script.mounted) {
2282
+ await script.mounted.call(this);
2283
+ }
2284
+ if (script.dataURL) {
2285
+ await this.$fetchData();
2286
+ }
2287
+ await this.$nextTick();
2288
+ router.routeLoader.formHandler.bindAutoForms(this);
2289
+ },
2290
+ methods: {
2291
+ ...script.methods,
2292
+ // 라우팅 관련
2293
+ navigateTo: (route, params) => router.navigateTo(route, params),
2294
+ getCurrentRoute: () => router.getCurrentRoute(),
2295
+ // 통합된 파라미터 관리 (라우팅 + 쿼리 파라미터)
2296
+ getParams: () => router.queryManager?.getAllParams() || {},
2297
+ getParam: (key, defaultValue) => router.queryManager?.getParam(key, defaultValue),
2298
+ // i18n 관련 (resilient - i18n 실패해도 key 반환)
2299
+ $t: (key, params) => {
2300
+ try {
2301
+ return router.i18nManager?.t(key, params) || key;
1688
2302
  } catch (error) {
1689
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch data for ${routeName}:`, error);
1690
- this.$emit("data-error", error);
1691
- } finally {
1692
- this.$dataLoading = false;
2303
+ if (router.errorHandler) router.errorHandler.warn("RouteLoader", "i18n translation failed, returning key:", error);
2304
+ return key;
1693
2305
  }
1694
2306
  },
1695
- // 다중 API 데이터 fetch
1696
- async $fetchMultipleData() {
1697
- if (!script.dataURL || typeof script.dataURL !== "object") return;
1698
- const dataURLs = script.dataURL;
2307
+ // 인증 관련
2308
+ $isAuthenticated: () => router.authManager?.isUserAuthenticated() || false,
2309
+ $logout: () => router.authManager ? router.navigateTo(router.authManager.handleLogout()) : null,
2310
+ $loginSuccess: (target) => router.authManager ? router.navigateTo(router.authManager.handleLoginSuccess(target)) : null,
2311
+ $checkAuth: (route) => router.authManager ? router.authManager.checkAuthentication(route) : Promise.resolve({ allowed: true, reason: "auth_disabled" }),
2312
+ $getToken: () => router.authManager?.getAccessToken() || null,
2313
+ $setToken: (token, options) => router.authManager?.setAccessToken(token, options) || false,
2314
+ $removeToken: (storage) => router.authManager?.removeAccessToken(storage) || null,
2315
+ $getAuthCookie: () => router.authManager?.getAuthCookie() || null,
2316
+ $getCookie: (name) => router.authManager?.getCookieValue(name) || null,
2317
+ // 데이터 fetch (ApiHandler 래퍼)
2318
+ async $fetchData(dataConfig = null) {
2319
+ const configToUse = dataConfig || script.dataURL;
2320
+ if (!configToUse) return null;
1699
2321
  this.$dataLoading = true;
1700
2322
  try {
1701
- const promises = Object.entries(dataURLs).map(async ([key, url]) => {
1702
- try {
1703
- const data = await router.routeLoader.fetchComponentData(url);
1704
- return { key, data, success: true };
1705
- } catch (error) {
1706
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch ${key} for ${routeName}:`, error);
1707
- return { key, error, success: false };
2323
+ if (typeof configToUse === "string") {
2324
+ const data = await router.routeLoader.apiHandler.fetchData(configToUse, this);
2325
+ Object.assign(this, data);
2326
+ this.$emit("data-loaded", data);
2327
+ return data;
2328
+ } else if (typeof configToUse === "object") {
2329
+ const { results, errors } = await router.routeLoader.apiHandler.fetchMultipleData(configToUse, this);
2330
+ Object.assign(this, results);
2331
+ if (Object.keys(results).length > 0) {
2332
+ this.$emit("data-loaded", results);
1708
2333
  }
1709
- });
1710
- const results = await Promise.all(promises);
1711
- const successfulResults = {};
1712
- const errors = {};
1713
- results.forEach(({ key, data, error, success }) => {
1714
- if (success) {
1715
- this[key] = data;
1716
- successfulResults[key] = data;
1717
- } else {
1718
- errors[key] = error;
2334
+ if (Object.keys(errors).length > 0) {
2335
+ this.$emit("data-error", errors);
1719
2336
  }
1720
- });
1721
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Multiple data fetched for ${routeName}:`, successfulResults);
1722
- if (Object.keys(successfulResults).length > 0) {
1723
- this.$emit("data-loaded", successfulResults);
1724
- }
1725
- if (Object.keys(errors).length > 0) {
1726
- this.$emit("data-error", errors);
2337
+ return results;
1727
2338
  }
2339
+ return null;
1728
2340
  } catch (error) {
1729
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch multiple data for ${routeName}:`, error);
2341
+ if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch data for ${routeName}:`, error);
1730
2342
  this.$emit("data-error", error);
2343
+ throw error;
1731
2344
  } finally {
1732
2345
  this.$dataLoading = false;
1733
2346
  }
1734
2347
  },
1735
- // 전체 데이터 새로고침 (명시적 메서드)
1736
- async $fetchAllData() {
1737
- if (typeof script.dataURL === "string") {
1738
- await this.$fetchData();
1739
- } else if (typeof script.dataURL === "object") {
1740
- await this.$fetchMultipleData();
1741
- }
1742
- },
1743
- // 🆕 자동 폼 바인딩 메서드
1744
- $bindAutoForms() {
1745
- const forms = document.querySelectorAll("form.auto-form, form[action]");
1746
- forms.forEach((form) => {
1747
- form.removeEventListener("submit", form._boundSubmitHandler);
1748
- const boundHandler = (e) => this.$handleFormSubmit(e);
1749
- form._boundSubmitHandler = boundHandler;
1750
- form.addEventListener("submit", boundHandler);
1751
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Form auto-bound: ${form.getAttribute("action")}`);
1752
- });
1753
- },
1754
- // 🆕 폼 서브밋 핸들러
1755
- async $handleFormSubmit(event) {
1756
- event.preventDefault();
1757
- const form = event.target;
1758
- let action = form.getAttribute("action");
1759
- const method = form.getAttribute("method") || "POST";
1760
- const successHandler = form.getAttribute("data-success-handler");
1761
- const errorHandler = form.getAttribute("data-error-handler");
1762
- const loadingHandler = form.getAttribute("data-loading-handler");
1763
- const redirectTo = form.getAttribute("data-redirect");
1764
- try {
1765
- if (loadingHandler && this[loadingHandler]) {
1766
- this[loadingHandler](true, form);
1767
- }
1768
- action = this.$processActionParams(action);
1769
- if (!this.$validateForm(form)) {
1770
- return;
1771
- }
1772
- const formData = new FormData(form);
1773
- const data = Object.fromEntries(formData.entries());
1774
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Form submitting to: ${action}`, data);
1775
- const response = await this.$submitFormData(action, method, data, form);
1776
- if (successHandler && this[successHandler]) {
1777
- this[successHandler](response, form);
1778
- }
1779
- if (redirectTo) {
1780
- setTimeout(() => {
1781
- this.navigateTo(redirectTo);
1782
- }, 1e3);
1783
- }
1784
- } catch (error) {
1785
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Form submission error:`, error);
1786
- if (errorHandler && this[errorHandler]) {
1787
- this[errorHandler](error, form);
1788
- } else {
1789
- console.error("Form submission error:", error);
1790
- }
1791
- } finally {
1792
- if (loadingHandler && this[loadingHandler]) {
1793
- this[loadingHandler](false, form);
1794
- }
1795
- }
2348
+ // HTTP 메서드 래퍼들 (ApiHandler 직접 접근)
2349
+ async $get(url, options = {}) {
2350
+ return await router.routeLoader.apiHandler.get(url, this, options);
1796
2351
  },
1797
- // 🆕 액션 파라미터 처리 메서드 (간단한 템플릿 치환)
1798
- $processActionParams(actionTemplate) {
1799
- let processedAction = actionTemplate;
1800
- const paramMatches = actionTemplate.match(/\{([^}]+)\}/g);
1801
- if (paramMatches) {
1802
- paramMatches.forEach((match) => {
1803
- const paramName = match.slice(1, -1);
1804
- try {
1805
- let paramValue = null;
1806
- paramValue = this.getParam(paramName);
1807
- if (paramValue === null || paramValue === void 0) {
1808
- paramValue = this[paramName];
1809
- }
1810
- if (paramValue === null || paramValue === void 0) {
1811
- if (this.$options.computed && this.$options.computed[paramName]) {
1812
- paramValue = this[paramName];
1813
- }
1814
- }
1815
- if (paramValue !== null && paramValue !== void 0) {
1816
- processedAction = processedAction.replace(
1817
- match,
1818
- encodeURIComponent(paramValue)
1819
- );
1820
- if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Parameter resolved: ${paramName} = ${paramValue}`);
1821
- } else {
1822
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Parameter '${paramName}' not found in component data, computed, or route params`);
1823
- }
1824
- } catch (error) {
1825
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Error processing parameter '${paramName}':`, error);
1826
- }
1827
- });
1828
- }
1829
- return processedAction;
2352
+ async $post(url, data, options = {}) {
2353
+ return await router.routeLoader.apiHandler.post(url, data, this, options);
1830
2354
  },
1831
- // 🆕 데이터 서브밋
1832
- async $submitFormData(action, method, data, form) {
1833
- const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
1834
- const headers = {
1835
- "Accept": "application/json",
1836
- // 인증 토큰 자동 추가
1837
- ...this.$getToken() && {
1838
- "Authorization": `Bearer ${this.$getToken()}`
1839
- }
1840
- };
1841
- let body;
1842
- if (hasFile) {
1843
- body = new FormData(form);
1844
- } else {
1845
- headers["Content-Type"] = "application/json";
1846
- body = JSON.stringify(data);
1847
- }
1848
- const response = await fetch(action, {
1849
- method: method.toUpperCase(),
1850
- headers,
1851
- body
1852
- });
1853
- if (!response.ok) {
1854
- let error;
1855
- try {
1856
- error = await response.json();
1857
- } catch (e) {
1858
- error = { message: `HTTP ${response.status}: ${response.statusText}` };
1859
- }
1860
- throw new Error(error.message || `HTTP ${response.status}`);
1861
- }
1862
- try {
1863
- return await response.json();
1864
- } catch (e) {
1865
- return { success: true };
1866
- }
2355
+ async $put(url, data, options = {}) {
2356
+ return await router.routeLoader.apiHandler.put(url, data, this, options);
1867
2357
  },
1868
- // 🆕 클라이언트 사이드 검증
1869
- $validateForm(form) {
1870
- let isValid = true;
1871
- const inputs = form.querySelectorAll("input, textarea, select");
1872
- inputs.forEach((input) => {
1873
- if (!input.checkValidity()) {
1874
- isValid = false;
1875
- input.classList.add("error");
1876
- return;
1877
- }
1878
- const validationFunction = input.getAttribute("data-validation");
1879
- if (validationFunction) {
1880
- const isInputValid = this.$validateInput(input, validationFunction);
1881
- if (!isInputValid) {
1882
- isValid = false;
1883
- input.classList.add("error");
1884
- } else {
1885
- input.classList.remove("error");
1886
- }
1887
- } else {
1888
- input.classList.remove("error");
1889
- }
1890
- });
1891
- return isValid;
2358
+ async $patch(url, data, options = {}) {
2359
+ return await router.routeLoader.apiHandler.patch(url, data, this, options);
1892
2360
  },
1893
- // 🆕 개별 입력 검증
1894
- $validateInput(input, validationFunction) {
1895
- const value = input.value;
1896
- if (typeof this[validationFunction] === "function") {
1897
- try {
1898
- return this[validationFunction](value, input);
1899
- } catch (error) {
1900
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Validation function '${validationFunction}' error:`, error);
1901
- return false;
1902
- }
1903
- }
1904
- if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Validation function '${validationFunction}' not found`);
1905
- return true;
2361
+ async $delete(url, options = {}) {
2362
+ return await router.routeLoader.apiHandler.delete(url, this, options);
1906
2363
  }
1907
2364
  },
1908
2365
  _routeName: routeName
@@ -1925,61 +2382,6 @@ ${template}`;
1925
2382
  generateDefaultTemplate(routeName) {
1926
2383
  return `<div class="route-${routeName}"><h1>Route: ${routeName}</h1></div>`;
1927
2384
  }
1928
- /**
1929
- * 컴포넌트 데이터 가져오기
1930
- */
1931
- async fetchComponentData(dataURL) {
1932
- try {
1933
- const queryString = this.router.queryManager?.buildQueryString(this.router.queryManager?.getQueryParams()) || "";
1934
- const fullURL = queryString ? `${dataURL}?${queryString}` : dataURL;
1935
- this.log("debug", `Fetching data from: ${fullURL}`);
1936
- const response = await fetch(fullURL, {
1937
- method: "GET",
1938
- headers: {
1939
- "Content-Type": "application/json",
1940
- "Accept": "application/json"
1941
- }
1942
- });
1943
- if (!response.ok) {
1944
- throw new Error(`HTTP error! status: ${response.status}`);
1945
- }
1946
- const data = await response.json();
1947
- if (typeof data !== "object" || data === null) {
1948
- throw new Error("Invalid data format: expected object");
1949
- }
1950
- return data;
1951
- } catch (error) {
1952
- this.log("error", "Failed to fetch component data:", error);
1953
- throw error;
1954
- }
1955
- }
1956
- /**
1957
- * 캐시 무효화
1958
- */
1959
- invalidateCache(routeName) {
1960
- if (this.router.cacheManager) {
1961
- this.router.cacheManager.invalidateComponentCache(routeName);
1962
- }
1963
- this.log("debug", `Cache invalidated for route: ${routeName}`);
1964
- }
1965
- /**
1966
- * 통계 정보 반환
1967
- */
1968
- getStats() {
1969
- return {
1970
- environment: this.config.environment,
1971
- basePath: this.config.basePath,
1972
- routesPath: this.config.routesPath,
1973
- useLayout: this.config.useLayout,
1974
- useComponents: this.config.useComponents
1975
- };
1976
- }
1977
- /**
1978
- * 페이지 제목 생성
1979
- */
1980
- generatePageTitle(routeName) {
1981
- return this.toPascalCase(routeName).replace(/([A-Z])/g, " $1").trim();
1982
- }
1983
2385
  /**
1984
2386
  * 로깅 래퍼 메서드
1985
2387
  */
@@ -1992,6 +2394,18 @@ ${template}`;
1992
2394
  * 정리 (메모리 누수 방지)
1993
2395
  */
1994
2396
  destroy() {
2397
+ if (this.formHandler) {
2398
+ this.formHandler.destroy();
2399
+ this.formHandler = null;
2400
+ }
2401
+ if (this.apiHandler) {
2402
+ this.apiHandler.destroy();
2403
+ this.apiHandler = null;
2404
+ }
2405
+ if (this.componentLoader) {
2406
+ this.componentLoader.dispose();
2407
+ this.componentLoader = null;
2408
+ }
1995
2409
  this.log("debug", "RouteLoader destroyed");
1996
2410
  this.router = null;
1997
2411
  }
@@ -2268,162 +2682,6 @@ var ErrorHandler = class {
2268
2682
  }
2269
2683
  };
2270
2684
 
2271
- // src/core/ComponentLoader.js
2272
- var ComponentLoader = class {
2273
- constructor(router = null, options = {}) {
2274
- this.config = {
2275
- basePath: options.basePath || "/src/components",
2276
- debug: options.debug || false,
2277
- environment: options.environment || "development",
2278
- ...options
2279
- };
2280
- this.router = router;
2281
- this.loadingPromises = /* @__PURE__ */ new Map();
2282
- this.unifiedComponents = null;
2283
- }
2284
- /**
2285
- * 로깅 래퍼 메서드
2286
- */
2287
- log(level, ...args) {
2288
- if (this.router?.errorHandler) {
2289
- this.router.errorHandler.log(level, "ComponentLoader", ...args);
2290
- }
2291
- }
2292
- /**
2293
- * 컴포넌트를 비동기로 로드
2294
- */
2295
- async loadComponent(componentName) {
2296
- if (!componentName || typeof componentName !== "string") {
2297
- throw new Error("Component name must be a non-empty string");
2298
- }
2299
- if (this.loadingPromises.has(componentName)) {
2300
- return this.loadingPromises.get(componentName);
2301
- }
2302
- const loadPromise = this._loadComponentFromFile(componentName);
2303
- this.loadingPromises.set(componentName, loadPromise);
2304
- try {
2305
- const component = await loadPromise;
2306
- return component;
2307
- } catch (error) {
2308
- throw error;
2309
- } finally {
2310
- this.loadingPromises.delete(componentName);
2311
- }
2312
- }
2313
- /**
2314
- * 파일에서 컴포넌트 로드
2315
- */
2316
- async _loadComponentFromFile(componentName) {
2317
- const componentPath = `${this.config.basePath}/${componentName}.js`;
2318
- try {
2319
- const module = await import(componentPath);
2320
- const component = module.default;
2321
- if (!component) {
2322
- throw new Error(`Component '${componentName}' has no default export`);
2323
- }
2324
- if (!component.name) {
2325
- component.name = componentName;
2326
- }
2327
- this.log("debug", `Component '${componentName}' loaded successfully`);
2328
- return component;
2329
- } catch (error) {
2330
- this.log("error", `Failed to load component '${componentName}':`, error);
2331
- throw new Error(`Component '${componentName}' not found: ${error.message}`);
2332
- }
2333
- }
2334
- /**
2335
- * 컴포넌트 모듈 클리어
2336
- */
2337
- clearComponents() {
2338
- this.loadingPromises.clear();
2339
- this.unifiedComponents = null;
2340
- this.log("debug", "All components cleared");
2341
- }
2342
- /**
2343
- * 환경에 따른 모든 컴포넌트 로딩 (캐싱 지원)
2344
- */
2345
- async loadAllComponents() {
2346
- if (this.unifiedComponents) {
2347
- this.log("debug", "Using existing unified components");
2348
- return this.unifiedComponents;
2349
- }
2350
- if (this.config.environment === "production") {
2351
- return await this._loadProductionComponents();
2352
- }
2353
- return await this._loadDevelopmentComponents();
2354
- }
2355
- /**
2356
- * 운영 모드: 통합 컴포넌트 로딩
2357
- */
2358
- async _loadProductionComponents() {
2359
- try {
2360
- const componentsPath = `${this.config.routesPath}/_components.js`;
2361
- this.log("info", "[PRODUCTION] Loading unified components from:", componentsPath);
2362
- const componentsModule = await import(componentsPath);
2363
- if (typeof componentsModule.registerComponents === "function") {
2364
- this.unifiedComponents = componentsModule.components || {};
2365
- this.log("info", `[PRODUCTION] Unified components loaded: ${Object.keys(this.unifiedComponents).length} components`);
2366
- return this.unifiedComponents;
2367
- } else {
2368
- throw new Error("registerComponents function not found in components module");
2369
- }
2370
- } catch (error) {
2371
- this.log("warn", "[PRODUCTION] Failed to load unified components:", error.message);
2372
- this.unifiedComponents = {};
2373
- return {};
2374
- }
2375
- }
2376
- /**
2377
- * 개발 모드: 개별 컴포넌트 로딩
2378
- */
2379
- async _loadDevelopmentComponents() {
2380
- const componentNames = this._getComponentNames();
2381
- const components = {};
2382
- this.log("info", `[DEVELOPMENT] Loading individual components: ${componentNames.join(", ")}`);
2383
- for (const name of componentNames) {
2384
- try {
2385
- const component = await this.loadComponent(name);
2386
- if (component) {
2387
- components[name] = component;
2388
- }
2389
- } catch (loadError) {
2390
- this.log("warn", `[DEVELOPMENT] Failed to load component ${name}:`, loadError.message);
2391
- }
2392
- }
2393
- this.unifiedComponents = components;
2394
- this.log("info", `[DEVELOPMENT] Individual components loaded: ${Object.keys(components).length} components`);
2395
- return components;
2396
- }
2397
- /**
2398
- * 컴포넌트 이름 목록 가져오기
2399
- */
2400
- _getComponentNames() {
2401
- if (Array.isArray(this.config.componentNames) && this.config.componentNames.length > 0) {
2402
- return [...this.config.componentNames];
2403
- }
2404
- return [
2405
- "Button",
2406
- "Modal",
2407
- "Card",
2408
- "Toast",
2409
- "Input",
2410
- "Tabs",
2411
- "Checkbox",
2412
- "Alert",
2413
- "DynamicInclude",
2414
- "HtmlInclude"
2415
- ];
2416
- }
2417
- /**
2418
- * 메모리 정리
2419
- */
2420
- dispose() {
2421
- this.clearComponents();
2422
- this.log("debug", "ComponentLoader disposed");
2423
- this.router = null;
2424
- }
2425
- };
2426
-
2427
2685
  // src/viewlogic-router.js
2428
2686
  var ViewLogicRouter = class {
2429
2687
  constructor(options = {}) {
@@ -2432,7 +2690,6 @@ var ViewLogicRouter = class {
2432
2690
  this.currentHash = "";
2433
2691
  this.currentVueApp = null;
2434
2692
  this.previousVueApp = null;
2435
- this.componentLoader = null;
2436
2693
  this.transitionInProgress = false;
2437
2694
  this.isReady = false;
2438
2695
  this.readyPromise = null;
@@ -2445,7 +2702,10 @@ var ViewLogicRouter = class {
2445
2702
  _buildConfig(options) {
2446
2703
  const currentOrigin = window.location.origin;
2447
2704
  const defaults = {
2448
- basePath: `${currentOrigin}/src`,
2705
+ basePath: "/",
2706
+ // 애플리케이션 기본 경로 (서브폴더 배포용)
2707
+ srcPath: "/src",
2708
+ // 소스 파일 경로
2449
2709
  mode: "hash",
2450
2710
  cacheMode: "memory",
2451
2711
  cacheTTL: 3e5,
@@ -2453,13 +2713,13 @@ var ViewLogicRouter = class {
2453
2713
  useLayout: true,
2454
2714
  defaultLayout: "default",
2455
2715
  environment: "development",
2456
- routesPath: `${currentOrigin}/routes`,
2716
+ routesPath: "/routes",
2717
+ // 프로덕션 라우트 경로
2457
2718
  enableErrorReporting: true,
2458
- useComponents: true,
2459
- componentNames: ["Button", "Modal", "Card", "Toast", "Input", "Tabs", "Checkbox", "Alert", "DynamicInclude", "HtmlInclude"],
2460
- useI18n: true,
2719
+ useI18n: false,
2461
2720
  defaultLanguage: "ko",
2462
- i18nPath: `${currentOrigin}/i18n`,
2721
+ i18nPath: "/i18n",
2722
+ // 다국어 파일 경로
2463
2723
  logLevel: "info",
2464
2724
  authEnabled: false,
2465
2725
  loginRoute: "login",
@@ -2481,17 +2741,56 @@ var ViewLogicRouter = class {
2481
2741
  logSecurityWarnings: true
2482
2742
  };
2483
2743
  const config = { ...defaults, ...options };
2484
- if (options.basePath && !options.basePath.startsWith("http")) {
2485
- config.basePath = `${currentOrigin}${options.basePath}`;
2486
- }
2487
- if (options.routesPath && !options.routesPath.startsWith("http")) {
2488
- config.routesPath = `${currentOrigin}${options.routesPath}`;
2489
- }
2490
- if (options.i18nPath && !options.i18nPath.startsWith("http")) {
2491
- config.i18nPath = `${currentOrigin}${options.i18nPath}`;
2492
- }
2744
+ config.srcPath = this.resolvePath(config.srcPath, config.basePath);
2745
+ config.routesPath = this.resolvePath(config.routesPath, config.basePath);
2746
+ config.i18nPath = this.resolvePath(config.i18nPath, config.basePath);
2493
2747
  return config;
2494
2748
  }
2749
+ /**
2750
+ * 통합 경로 해결 - 서브폴더 배포 및 basePath 지원
2751
+ */
2752
+ resolvePath(path, basePath = null) {
2753
+ const currentOrigin = window.location.origin;
2754
+ if (path.startsWith("http")) {
2755
+ return path;
2756
+ }
2757
+ if (path.startsWith("/")) {
2758
+ if (basePath && basePath !== "/") {
2759
+ const cleanBasePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
2760
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
2761
+ const fullPath = `${cleanBasePath}${cleanPath}`;
2762
+ const fullUrl2 = `${currentOrigin}${fullPath}`;
2763
+ return fullUrl2.replace(/([^:])\/{2,}/g, "$1/");
2764
+ }
2765
+ return `${currentOrigin}${path}`;
2766
+ }
2767
+ const currentPathname = window.location.pathname;
2768
+ const currentBase = currentPathname.endsWith("/") ? currentPathname : currentPathname.substring(0, currentPathname.lastIndexOf("/") + 1);
2769
+ const resolvedPath = this.normalizePath(currentBase + path);
2770
+ const fullUrl = `${currentOrigin}${resolvedPath}`;
2771
+ return fullUrl.replace(/([^:])\/{2,}/g, "$1/");
2772
+ }
2773
+ /**
2774
+ * URL 경로 정규화 (이중 슬래시 제거 및 ../, ./ 처리)
2775
+ */
2776
+ normalizePath(path) {
2777
+ path = path.replace(/\/+/g, "/");
2778
+ const parts = path.split("/").filter((part) => part !== "" && part !== ".");
2779
+ const stack = [];
2780
+ for (const part of parts) {
2781
+ if (part === "..") {
2782
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") {
2783
+ stack.pop();
2784
+ } else if (!path.startsWith("/")) {
2785
+ stack.push(part);
2786
+ }
2787
+ } else {
2788
+ stack.push(part);
2789
+ }
2790
+ }
2791
+ const normalized = "/" + stack.join("/");
2792
+ return normalized === "/" ? "/" : normalized;
2793
+ }
2495
2794
  /**
2496
2795
  * 로깅 래퍼 메서드
2497
2796
  */
@@ -2510,29 +2809,21 @@ var ViewLogicRouter = class {
2510
2809
  this.queryManager = new QueryManager(this, this.config);
2511
2810
  this.errorHandler = new ErrorHandler(this, this.config);
2512
2811
  if (this.config.useI18n) {
2513
- this.i18nManager = new I18nManager(this, this.config);
2514
- if (this.i18nManager.initPromise) {
2515
- await this.i18nManager.initPromise;
2812
+ try {
2813
+ this.i18nManager = new I18nManager(this, this.config);
2814
+ if (this.i18nManager.initPromise) {
2815
+ await this.i18nManager.initPromise;
2816
+ }
2817
+ this.log("info", "I18nManager initialized successfully");
2818
+ } catch (i18nError) {
2819
+ this.log("warn", "I18nManager initialization failed, continuing without i18n:", i18nError.message);
2820
+ this.i18nManager = null;
2821
+ this.config.useI18n = false;
2516
2822
  }
2517
2823
  }
2518
2824
  if (this.config.authEnabled) {
2519
2825
  this.authManager = new AuthManager(this, this.config);
2520
2826
  }
2521
- if (this.config.useComponents) {
2522
- try {
2523
- this.componentLoader = new ComponentLoader(this, {
2524
- ...this.config,
2525
- basePath: `${this.config.basePath}/components`,
2526
- cache: true,
2527
- componentNames: this.config.componentNames
2528
- });
2529
- await this.componentLoader.loadAllComponents();
2530
- this.log("info", "ComponentLoader initialized successfully");
2531
- } catch (componentError) {
2532
- this.log("warn", "ComponentLoader initialization failed, continuing without components:", componentError.message);
2533
- this.componentLoader = null;
2534
- }
2535
- }
2536
2827
  this.isReady = true;
2537
2828
  this.init();
2538
2829
  } catch (error) {
@@ -2596,8 +2887,17 @@ var ViewLogicRouter = class {
2596
2887
  queryParams: this.queryManager?.parseQueryString(queryPart || window.location.search.slice(1)) || {}
2597
2888
  };
2598
2889
  } else {
2890
+ const fullPath = window.location.pathname;
2891
+ const basePath = this.config.basePath || "/";
2892
+ let route = fullPath;
2893
+ if (basePath !== "/" && fullPath.startsWith(basePath)) {
2894
+ route = fullPath.slice(basePath.length);
2895
+ }
2896
+ if (route.startsWith("/")) {
2897
+ route = route.slice(1);
2898
+ }
2599
2899
  return {
2600
- route: window.location.pathname.slice(1) || "home",
2900
+ route: route || "home",
2601
2901
  queryParams: this.queryManager?.parseQueryString(window.location.search.slice(1)) || {}
2602
2902
  };
2603
2903
  }
@@ -2730,7 +3030,10 @@ var ViewLogicRouter = class {
2730
3030
  const queryParams = params || this.queryManager?.getQueryParams() || {};
2731
3031
  const queryString = this.queryManager?.buildQueryString(queryParams) || "";
2732
3032
  const buildURL = (route2, queryString2, isHash = true) => {
2733
- const base = route2 === "home" ? "/" : `/${route2}`;
3033
+ let base = route2 === "home" ? "/" : `/${route2}`;
3034
+ if (!isHash && this.config.basePath && this.config.basePath !== "/") {
3035
+ base = `${this.config.basePath}${base}`;
3036
+ }
2734
3037
  const url = queryString2 ? `${base}?${queryString2}` : base;
2735
3038
  return isHash ? `#${url}` : url;
2736
3039
  };
@@ -2741,7 +3044,11 @@ var ViewLogicRouter = class {
2741
3044
  }
2742
3045
  } else {
2743
3046
  const newPath = buildURL(route, queryString, false);
2744
- const isSameRoute = window.location.pathname === (route === "home" ? "/" : `/${route}`);
3047
+ let expectedPath = route === "home" ? "/" : `/${route}`;
3048
+ if (this.config.basePath && this.config.basePath !== "/") {
3049
+ expectedPath = `${this.config.basePath}${expectedPath}`;
3050
+ }
3051
+ const isSameRoute = window.location.pathname === expectedPath;
2745
3052
  if (isSameRoute) {
2746
3053
  window.history.replaceState({}, "", newPath);
2747
3054
  } else {