viewlogic 1.1.3 → 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.
package/dist/viewlogic-router.js
CHANGED
|
@@ -1482,6 +1482,604 @@ var QueryManager = class {
|
|
|
1482
1482
|
}
|
|
1483
1483
|
};
|
|
1484
1484
|
|
|
1485
|
+
// src/core/FormHandler.js
|
|
1486
|
+
var FormHandler = class {
|
|
1487
|
+
constructor(router, options = {}) {
|
|
1488
|
+
this.router = router;
|
|
1489
|
+
this.config = {
|
|
1490
|
+
debug: options.debug || false,
|
|
1491
|
+
...options
|
|
1492
|
+
};
|
|
1493
|
+
this.log("debug", "FormHandler initialized");
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* 로깅 래퍼 메서드
|
|
1497
|
+
*/
|
|
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");
|
|
1528
|
+
try {
|
|
1529
|
+
if (loadingHandler && component[loadingHandler]) {
|
|
1530
|
+
component[loadingHandler](true, form);
|
|
1531
|
+
}
|
|
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);
|
|
1547
|
+
}
|
|
1548
|
+
} catch (error) {
|
|
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);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* 액션 파라미터 처리 (ApiHandler 재사용)
|
|
1563
|
+
*/
|
|
1564
|
+
processActionParams(actionTemplate, component) {
|
|
1565
|
+
return this.router.routeLoader.apiHandler.processURLParameters(actionTemplate, component);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* 폼 데이터 서브밋 (ApiHandler 활용)
|
|
1569
|
+
*/
|
|
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";
|
|
1581
|
+
}
|
|
1582
|
+
return await this.router.routeLoader.apiHandler.fetchData(action, component, options);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* 클라이언트 사이드 폼 검증
|
|
1586
|
+
*/
|
|
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;
|
|
1610
|
+
}
|
|
1611
|
+
/**
|
|
1612
|
+
* 개별 입력 검증
|
|
1613
|
+
*/
|
|
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
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
this.log("warn", `Validation function '${validationFunction}' not found`);
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* 정리 (메모리 누수 방지)
|
|
1629
|
+
*/
|
|
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;
|
|
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);
|
|
1661
|
+
}
|
|
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
|
+
}
|
|
1702
|
+
try {
|
|
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 };
|
|
1710
|
+
}
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
this.log("error", "Failed to fetch data:", error);
|
|
1713
|
+
throw error;
|
|
1714
|
+
}
|
|
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;
|
|
1734
|
+
}
|
|
1735
|
+
if (url) {
|
|
1736
|
+
const data = await this.fetchData(url, component, options);
|
|
1737
|
+
results[key] = data;
|
|
1738
|
+
}
|
|
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);
|
|
1761
|
+
}
|
|
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];
|
|
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
|
+
|
|
1485
2083
|
// src/core/RouteLoader.js
|
|
1486
2084
|
var RouteLoader = class {
|
|
1487
2085
|
constructor(router, options = {}) {
|
|
@@ -1493,10 +2091,12 @@ var RouteLoader = class {
|
|
|
1493
2091
|
environment: options.environment || "development",
|
|
1494
2092
|
useLayout: options.useLayout !== false,
|
|
1495
2093
|
defaultLayout: options.defaultLayout || "default",
|
|
1496
|
-
useComponents: options.useComponents !== false,
|
|
1497
2094
|
debug: options.debug || false
|
|
1498
2095
|
};
|
|
1499
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);
|
|
1500
2100
|
this.log("debug", "RouteLoader initialized with config:", this.config);
|
|
1501
2101
|
}
|
|
1502
2102
|
/**
|
|
@@ -1614,17 +2214,33 @@ ${template}`;
|
|
|
1614
2214
|
if (isProduction) {
|
|
1615
2215
|
template = script.template || this.generateDefaultTemplate(routeName);
|
|
1616
2216
|
} else {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
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;
|
|
1620
2230
|
if (layout) {
|
|
1621
2231
|
template = this.mergeLayoutWithTemplate(routeName, layout, template);
|
|
1622
2232
|
}
|
|
1623
2233
|
}
|
|
1624
2234
|
let loadedComponents = {};
|
|
1625
|
-
if (this.
|
|
2235
|
+
if (this.componentLoader) {
|
|
1626
2236
|
try {
|
|
1627
|
-
|
|
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);
|
|
1628
2244
|
this.log("debug", `Components loaded successfully for route: ${routeName}`);
|
|
1629
2245
|
} catch (error) {
|
|
1630
2246
|
this.log("warn", `Component loading failed for route '${routeName}', continuing without components:`, error.message);
|
|
@@ -1666,14 +2282,10 @@ ${template}`;
|
|
|
1666
2282
|
await script.mounted.call(this);
|
|
1667
2283
|
}
|
|
1668
2284
|
if (script.dataURL) {
|
|
1669
|
-
|
|
1670
|
-
await this.$fetchData();
|
|
1671
|
-
} else if (typeof script.dataURL === "object") {
|
|
1672
|
-
await this.$fetchMultipleData();
|
|
1673
|
-
}
|
|
2285
|
+
await this.$fetchData();
|
|
1674
2286
|
}
|
|
1675
2287
|
await this.$nextTick();
|
|
1676
|
-
|
|
2288
|
+
router.routeLoader.formHandler.bindAutoForms(this);
|
|
1677
2289
|
},
|
|
1678
2290
|
methods: {
|
|
1679
2291
|
...script.methods,
|
|
@@ -1702,245 +2314,52 @@ ${template}`;
|
|
|
1702
2314
|
$removeToken: (storage) => router.authManager?.removeAccessToken(storage) || null,
|
|
1703
2315
|
$getAuthCookie: () => router.authManager?.getAuthCookie() || null,
|
|
1704
2316
|
$getCookie: (name) => router.authManager?.getCookieValue(name) || null,
|
|
1705
|
-
// 데이터 fetch (
|
|
1706
|
-
async $fetchData(
|
|
1707
|
-
|
|
2317
|
+
// 데이터 fetch (ApiHandler 래퍼)
|
|
2318
|
+
async $fetchData(dataConfig = null) {
|
|
2319
|
+
const configToUse = dataConfig || script.dataURL;
|
|
2320
|
+
if (!configToUse) return null;
|
|
1708
2321
|
this.$dataLoading = true;
|
|
1709
2322
|
try {
|
|
1710
|
-
if (typeof
|
|
1711
|
-
const data = await router.routeLoader.
|
|
1712
|
-
if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Data fetched for ${routeName}:`, data);
|
|
2323
|
+
if (typeof configToUse === "string") {
|
|
2324
|
+
const data = await router.routeLoader.apiHandler.fetchData(configToUse, this);
|
|
1713
2325
|
Object.assign(this, data);
|
|
1714
2326
|
this.$emit("data-loaded", data);
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
this
|
|
1721
|
-
this.$emit("data-loaded", { [apiName]: data });
|
|
1722
|
-
}
|
|
1723
|
-
} else {
|
|
1724
|
-
await this.$fetchMultipleData();
|
|
1725
|
-
}
|
|
1726
|
-
} catch (error) {
|
|
1727
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch data for ${routeName}:`, error);
|
|
1728
|
-
this.$emit("data-error", error);
|
|
1729
|
-
} finally {
|
|
1730
|
-
this.$dataLoading = false;
|
|
1731
|
-
}
|
|
1732
|
-
},
|
|
1733
|
-
// 다중 API 데이터 fetch
|
|
1734
|
-
async $fetchMultipleData() {
|
|
1735
|
-
if (!script.dataURL || typeof script.dataURL !== "object") return;
|
|
1736
|
-
const dataURLs = script.dataURL;
|
|
1737
|
-
this.$dataLoading = true;
|
|
1738
|
-
try {
|
|
1739
|
-
const promises = Object.entries(dataURLs).map(async ([key, url]) => {
|
|
1740
|
-
try {
|
|
1741
|
-
const data = await router.routeLoader.fetchComponentData(url);
|
|
1742
|
-
return { key, data, success: true };
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch ${key} for ${routeName}:`, error);
|
|
1745
|
-
return { key, error, success: false };
|
|
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);
|
|
1746
2333
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
const successfulResults = {};
|
|
1750
|
-
const errors = {};
|
|
1751
|
-
results.forEach(({ key, data, error, success }) => {
|
|
1752
|
-
if (success) {
|
|
1753
|
-
this[key] = data;
|
|
1754
|
-
successfulResults[key] = data;
|
|
1755
|
-
} else {
|
|
1756
|
-
errors[key] = error;
|
|
2334
|
+
if (Object.keys(errors).length > 0) {
|
|
2335
|
+
this.$emit("data-error", errors);
|
|
1757
2336
|
}
|
|
1758
|
-
|
|
1759
|
-
if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Multiple data fetched for ${routeName}:`, successfulResults);
|
|
1760
|
-
if (Object.keys(successfulResults).length > 0) {
|
|
1761
|
-
this.$emit("data-loaded", successfulResults);
|
|
1762
|
-
}
|
|
1763
|
-
if (Object.keys(errors).length > 0) {
|
|
1764
|
-
this.$emit("data-error", errors);
|
|
2337
|
+
return results;
|
|
1765
2338
|
}
|
|
2339
|
+
return null;
|
|
1766
2340
|
} catch (error) {
|
|
1767
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch
|
|
2341
|
+
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Failed to fetch data for ${routeName}:`, error);
|
|
1768
2342
|
this.$emit("data-error", error);
|
|
2343
|
+
throw error;
|
|
1769
2344
|
} finally {
|
|
1770
2345
|
this.$dataLoading = false;
|
|
1771
2346
|
}
|
|
1772
2347
|
},
|
|
1773
|
-
//
|
|
1774
|
-
async $
|
|
1775
|
-
|
|
1776
|
-
await this.$fetchData();
|
|
1777
|
-
} else if (typeof script.dataURL === "object") {
|
|
1778
|
-
await this.$fetchMultipleData();
|
|
1779
|
-
}
|
|
1780
|
-
},
|
|
1781
|
-
// 🆕 자동 폼 바인딩 메서드
|
|
1782
|
-
$bindAutoForms() {
|
|
1783
|
-
const forms = document.querySelectorAll("form.auto-form, form[action]");
|
|
1784
|
-
forms.forEach((form) => {
|
|
1785
|
-
form.removeEventListener("submit", form._boundSubmitHandler);
|
|
1786
|
-
const boundHandler = (e) => this.$handleFormSubmit(e);
|
|
1787
|
-
form._boundSubmitHandler = boundHandler;
|
|
1788
|
-
form.addEventListener("submit", boundHandler);
|
|
1789
|
-
if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Form auto-bound: ${form.getAttribute("action")}`);
|
|
1790
|
-
});
|
|
1791
|
-
},
|
|
1792
|
-
// 🆕 폼 서브밋 핸들러
|
|
1793
|
-
async $handleFormSubmit(event) {
|
|
1794
|
-
event.preventDefault();
|
|
1795
|
-
const form = event.target;
|
|
1796
|
-
let action = form.getAttribute("action");
|
|
1797
|
-
const method = form.getAttribute("method") || "POST";
|
|
1798
|
-
const successHandler = form.getAttribute("data-success-handler");
|
|
1799
|
-
const errorHandler = form.getAttribute("data-error-handler");
|
|
1800
|
-
const loadingHandler = form.getAttribute("data-loading-handler");
|
|
1801
|
-
const redirectTo = form.getAttribute("data-redirect");
|
|
1802
|
-
try {
|
|
1803
|
-
if (loadingHandler && this[loadingHandler]) {
|
|
1804
|
-
this[loadingHandler](true, form);
|
|
1805
|
-
}
|
|
1806
|
-
action = this.$processActionParams(action);
|
|
1807
|
-
if (!this.$validateForm(form)) {
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
const formData = new FormData(form);
|
|
1811
|
-
const data = Object.fromEntries(formData.entries());
|
|
1812
|
-
if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Form submitting to: ${action}`, data);
|
|
1813
|
-
const response = await this.$submitFormData(action, method, data, form);
|
|
1814
|
-
if (successHandler && this[successHandler]) {
|
|
1815
|
-
this[successHandler](response, form);
|
|
1816
|
-
}
|
|
1817
|
-
if (redirectTo) {
|
|
1818
|
-
setTimeout(() => {
|
|
1819
|
-
this.navigateTo(redirectTo);
|
|
1820
|
-
}, 1e3);
|
|
1821
|
-
}
|
|
1822
|
-
} catch (error) {
|
|
1823
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Form submission error:`, error);
|
|
1824
|
-
if (errorHandler && this[errorHandler]) {
|
|
1825
|
-
this[errorHandler](error, form);
|
|
1826
|
-
} else {
|
|
1827
|
-
console.error("Form submission error:", error);
|
|
1828
|
-
}
|
|
1829
|
-
} finally {
|
|
1830
|
-
if (loadingHandler && this[loadingHandler]) {
|
|
1831
|
-
this[loadingHandler](false, form);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
2348
|
+
// HTTP 메서드 래퍼들 (ApiHandler 직접 접근)
|
|
2349
|
+
async $get(url, options = {}) {
|
|
2350
|
+
return await router.routeLoader.apiHandler.get(url, this, options);
|
|
1834
2351
|
},
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
let processedAction = actionTemplate;
|
|
1838
|
-
const paramMatches = actionTemplate.match(/\{([^}]+)\}/g);
|
|
1839
|
-
if (paramMatches) {
|
|
1840
|
-
paramMatches.forEach((match) => {
|
|
1841
|
-
const paramName = match.slice(1, -1);
|
|
1842
|
-
try {
|
|
1843
|
-
let paramValue = null;
|
|
1844
|
-
paramValue = this.getParam(paramName);
|
|
1845
|
-
if (paramValue === null || paramValue === void 0) {
|
|
1846
|
-
paramValue = this[paramName];
|
|
1847
|
-
}
|
|
1848
|
-
if (paramValue === null || paramValue === void 0) {
|
|
1849
|
-
if (this.$options.computed && this.$options.computed[paramName]) {
|
|
1850
|
-
paramValue = this[paramName];
|
|
1851
|
-
}
|
|
1852
|
-
}
|
|
1853
|
-
if (paramValue !== null && paramValue !== void 0) {
|
|
1854
|
-
processedAction = processedAction.replace(
|
|
1855
|
-
match,
|
|
1856
|
-
encodeURIComponent(paramValue)
|
|
1857
|
-
);
|
|
1858
|
-
if (router.errorHandler) router.errorHandler.debug("RouteLoader", `Parameter resolved: ${paramName} = ${paramValue}`);
|
|
1859
|
-
} else {
|
|
1860
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Parameter '${paramName}' not found in component data, computed, or route params`);
|
|
1861
|
-
}
|
|
1862
|
-
} catch (error) {
|
|
1863
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Error processing parameter '${paramName}':`, error);
|
|
1864
|
-
}
|
|
1865
|
-
});
|
|
1866
|
-
}
|
|
1867
|
-
return processedAction;
|
|
2352
|
+
async $post(url, data, options = {}) {
|
|
2353
|
+
return await router.routeLoader.apiHandler.post(url, data, this, options);
|
|
1868
2354
|
},
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
const hasFile = Array.from(form.elements).some((el) => el.type === "file" && el.files.length > 0);
|
|
1872
|
-
const headers = {
|
|
1873
|
-
"Accept": "application/json",
|
|
1874
|
-
// 인증 토큰 자동 추가
|
|
1875
|
-
...this.$getToken() && {
|
|
1876
|
-
"Authorization": `Bearer ${this.$getToken()}`
|
|
1877
|
-
}
|
|
1878
|
-
};
|
|
1879
|
-
let body;
|
|
1880
|
-
if (hasFile) {
|
|
1881
|
-
body = new FormData(form);
|
|
1882
|
-
} else {
|
|
1883
|
-
headers["Content-Type"] = "application/json";
|
|
1884
|
-
body = JSON.stringify(data);
|
|
1885
|
-
}
|
|
1886
|
-
const response = await fetch(action, {
|
|
1887
|
-
method: method.toUpperCase(),
|
|
1888
|
-
headers,
|
|
1889
|
-
body
|
|
1890
|
-
});
|
|
1891
|
-
if (!response.ok) {
|
|
1892
|
-
let error;
|
|
1893
|
-
try {
|
|
1894
|
-
error = await response.json();
|
|
1895
|
-
} catch (e) {
|
|
1896
|
-
error = { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
1897
|
-
}
|
|
1898
|
-
throw new Error(error.message || `HTTP ${response.status}`);
|
|
1899
|
-
}
|
|
1900
|
-
try {
|
|
1901
|
-
return await response.json();
|
|
1902
|
-
} catch (e) {
|
|
1903
|
-
return { success: true };
|
|
1904
|
-
}
|
|
2355
|
+
async $put(url, data, options = {}) {
|
|
2356
|
+
return await router.routeLoader.apiHandler.put(url, data, this, options);
|
|
1905
2357
|
},
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
let isValid = true;
|
|
1909
|
-
const inputs = form.querySelectorAll("input, textarea, select");
|
|
1910
|
-
inputs.forEach((input) => {
|
|
1911
|
-
if (!input.checkValidity()) {
|
|
1912
|
-
isValid = false;
|
|
1913
|
-
input.classList.add("error");
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
const validationFunction = input.getAttribute("data-validation");
|
|
1917
|
-
if (validationFunction) {
|
|
1918
|
-
const isInputValid = this.$validateInput(input, validationFunction);
|
|
1919
|
-
if (!isInputValid) {
|
|
1920
|
-
isValid = false;
|
|
1921
|
-
input.classList.add("error");
|
|
1922
|
-
} else {
|
|
1923
|
-
input.classList.remove("error");
|
|
1924
|
-
}
|
|
1925
|
-
} else {
|
|
1926
|
-
input.classList.remove("error");
|
|
1927
|
-
}
|
|
1928
|
-
});
|
|
1929
|
-
return isValid;
|
|
2358
|
+
async $patch(url, data, options = {}) {
|
|
2359
|
+
return await router.routeLoader.apiHandler.patch(url, data, this, options);
|
|
1930
2360
|
},
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
const value = input.value;
|
|
1934
|
-
if (typeof this[validationFunction] === "function") {
|
|
1935
|
-
try {
|
|
1936
|
-
return this[validationFunction](value, input);
|
|
1937
|
-
} catch (error) {
|
|
1938
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Validation function '${validationFunction}' error:`, error);
|
|
1939
|
-
return false;
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
if (router.errorHandler) router.errorHandler.warn("RouteLoader", `Validation function '${validationFunction}' not found`);
|
|
1943
|
-
return true;
|
|
2361
|
+
async $delete(url, options = {}) {
|
|
2362
|
+
return await router.routeLoader.apiHandler.delete(url, this, options);
|
|
1944
2363
|
}
|
|
1945
2364
|
},
|
|
1946
2365
|
_routeName: routeName
|
|
@@ -1963,61 +2382,6 @@ ${template}`;
|
|
|
1963
2382
|
generateDefaultTemplate(routeName) {
|
|
1964
2383
|
return `<div class="route-${routeName}"><h1>Route: ${routeName}</h1></div>`;
|
|
1965
2384
|
}
|
|
1966
|
-
/**
|
|
1967
|
-
* 컴포넌트 데이터 가져오기
|
|
1968
|
-
*/
|
|
1969
|
-
async fetchComponentData(dataURL) {
|
|
1970
|
-
try {
|
|
1971
|
-
const queryString = this.router.queryManager?.buildQueryString(this.router.queryManager?.getQueryParams()) || "";
|
|
1972
|
-
const fullURL = queryString ? `${dataURL}?${queryString}` : dataURL;
|
|
1973
|
-
this.log("debug", `Fetching data from: ${fullURL}`);
|
|
1974
|
-
const response = await fetch(fullURL, {
|
|
1975
|
-
method: "GET",
|
|
1976
|
-
headers: {
|
|
1977
|
-
"Content-Type": "application/json",
|
|
1978
|
-
"Accept": "application/json"
|
|
1979
|
-
}
|
|
1980
|
-
});
|
|
1981
|
-
if (!response.ok) {
|
|
1982
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
1983
|
-
}
|
|
1984
|
-
const data = await response.json();
|
|
1985
|
-
if (typeof data !== "object" || data === null) {
|
|
1986
|
-
throw new Error("Invalid data format: expected object");
|
|
1987
|
-
}
|
|
1988
|
-
return data;
|
|
1989
|
-
} catch (error) {
|
|
1990
|
-
this.log("error", "Failed to fetch component data:", error);
|
|
1991
|
-
throw error;
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
/**
|
|
1995
|
-
* 캐시 무효화
|
|
1996
|
-
*/
|
|
1997
|
-
invalidateCache(routeName) {
|
|
1998
|
-
if (this.router.cacheManager) {
|
|
1999
|
-
this.router.cacheManager.invalidateComponentCache(routeName);
|
|
2000
|
-
}
|
|
2001
|
-
this.log("debug", `Cache invalidated for route: ${routeName}`);
|
|
2002
|
-
}
|
|
2003
|
-
/**
|
|
2004
|
-
* 통계 정보 반환
|
|
2005
|
-
*/
|
|
2006
|
-
getStats() {
|
|
2007
|
-
return {
|
|
2008
|
-
environment: this.config.environment,
|
|
2009
|
-
srcPath: this.config.srcPath,
|
|
2010
|
-
routesPath: this.config.routesPath,
|
|
2011
|
-
useLayout: this.config.useLayout,
|
|
2012
|
-
useComponents: this.config.useComponents
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
/**
|
|
2016
|
-
* 페이지 제목 생성
|
|
2017
|
-
*/
|
|
2018
|
-
generatePageTitle(routeName) {
|
|
2019
|
-
return this.toPascalCase(routeName).replace(/([A-Z])/g, " $1").trim();
|
|
2020
|
-
}
|
|
2021
2385
|
/**
|
|
2022
2386
|
* 로깅 래퍼 메서드
|
|
2023
2387
|
*/
|
|
@@ -2030,6 +2394,18 @@ ${template}`;
|
|
|
2030
2394
|
* 정리 (메모리 누수 방지)
|
|
2031
2395
|
*/
|
|
2032
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
|
+
}
|
|
2033
2409
|
this.log("debug", "RouteLoader destroyed");
|
|
2034
2410
|
this.router = null;
|
|
2035
2411
|
}
|
|
@@ -2306,176 +2682,6 @@ var ErrorHandler = class {
|
|
|
2306
2682
|
}
|
|
2307
2683
|
};
|
|
2308
2684
|
|
|
2309
|
-
// src/core/ComponentLoader.js
|
|
2310
|
-
var ComponentLoader = class {
|
|
2311
|
-
constructor(router = null, options = {}) {
|
|
2312
|
-
this.config = {
|
|
2313
|
-
componentsPath: options.componentsPath || "/components",
|
|
2314
|
-
// srcPath 기준 상대 경로
|
|
2315
|
-
debug: options.debug || false,
|
|
2316
|
-
environment: options.environment || "development",
|
|
2317
|
-
...options
|
|
2318
|
-
};
|
|
2319
|
-
this.router = router;
|
|
2320
|
-
this.loadingPromises = /* @__PURE__ */ new Map();
|
|
2321
|
-
this.unifiedComponents = null;
|
|
2322
|
-
}
|
|
2323
|
-
/**
|
|
2324
|
-
* 로깅 래퍼 메서드
|
|
2325
|
-
*/
|
|
2326
|
-
log(level, ...args) {
|
|
2327
|
-
if (this.router?.errorHandler) {
|
|
2328
|
-
this.router.errorHandler.log(level, "ComponentLoader", ...args);
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
/**
|
|
2332
|
-
* 컴포넌트를 비동기로 로드
|
|
2333
|
-
*/
|
|
2334
|
-
async loadComponent(componentName) {
|
|
2335
|
-
if (!componentName || typeof componentName !== "string") {
|
|
2336
|
-
throw new Error("Component name must be a non-empty string");
|
|
2337
|
-
}
|
|
2338
|
-
if (this.loadingPromises.has(componentName)) {
|
|
2339
|
-
return this.loadingPromises.get(componentName);
|
|
2340
|
-
}
|
|
2341
|
-
const loadPromise = this._loadComponentFromFile(componentName);
|
|
2342
|
-
this.loadingPromises.set(componentName, loadPromise);
|
|
2343
|
-
try {
|
|
2344
|
-
const component = await loadPromise;
|
|
2345
|
-
return component;
|
|
2346
|
-
} catch (error) {
|
|
2347
|
-
throw error;
|
|
2348
|
-
} finally {
|
|
2349
|
-
this.loadingPromises.delete(componentName);
|
|
2350
|
-
}
|
|
2351
|
-
}
|
|
2352
|
-
/**
|
|
2353
|
-
* 파일에서 컴포넌트 로드
|
|
2354
|
-
*/
|
|
2355
|
-
async _loadComponentFromFile(componentName) {
|
|
2356
|
-
const componentRelativePath = `${this.config.componentsPath}/${componentName}.js`;
|
|
2357
|
-
let componentPath;
|
|
2358
|
-
if (this.router && this.router.config.srcPath) {
|
|
2359
|
-
const srcPath = this.router.config.srcPath;
|
|
2360
|
-
if (srcPath.startsWith("http")) {
|
|
2361
|
-
const cleanSrcPath = srcPath.endsWith("/") ? srcPath.slice(0, -1) : srcPath;
|
|
2362
|
-
const cleanComponentPath = componentRelativePath.startsWith("/") ? componentRelativePath : `/${componentRelativePath}`;
|
|
2363
|
-
componentPath = `${cleanSrcPath}${cleanComponentPath}`;
|
|
2364
|
-
} else {
|
|
2365
|
-
componentPath = this.router.resolvePath(`${srcPath}${componentRelativePath}`);
|
|
2366
|
-
}
|
|
2367
|
-
} else {
|
|
2368
|
-
componentPath = this.router ? this.router.resolvePath(`/src${componentRelativePath}`) : `/src${componentRelativePath}`;
|
|
2369
|
-
}
|
|
2370
|
-
try {
|
|
2371
|
-
const module = await import(componentPath);
|
|
2372
|
-
const component = module.default;
|
|
2373
|
-
if (!component) {
|
|
2374
|
-
throw new Error(`Component '${componentName}' has no default export`);
|
|
2375
|
-
}
|
|
2376
|
-
if (!component.name) {
|
|
2377
|
-
component.name = componentName;
|
|
2378
|
-
}
|
|
2379
|
-
this.log("debug", `Component '${componentName}' loaded successfully`);
|
|
2380
|
-
return component;
|
|
2381
|
-
} catch (error) {
|
|
2382
|
-
this.log("error", `Failed to load component '${componentName}':`, error);
|
|
2383
|
-
throw new Error(`Component '${componentName}' not found: ${error.message}`);
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
/**
|
|
2387
|
-
* 컴포넌트 모듈 클리어
|
|
2388
|
-
*/
|
|
2389
|
-
clearComponents() {
|
|
2390
|
-
this.loadingPromises.clear();
|
|
2391
|
-
this.unifiedComponents = null;
|
|
2392
|
-
this.log("debug", "All components cleared");
|
|
2393
|
-
}
|
|
2394
|
-
/**
|
|
2395
|
-
* 환경에 따른 모든 컴포넌트 로딩 (캐싱 지원)
|
|
2396
|
-
*/
|
|
2397
|
-
async loadAllComponents() {
|
|
2398
|
-
if (this.unifiedComponents) {
|
|
2399
|
-
this.log("debug", "Using existing unified components");
|
|
2400
|
-
return this.unifiedComponents;
|
|
2401
|
-
}
|
|
2402
|
-
if (this.config.environment === "production") {
|
|
2403
|
-
return await this._loadProductionComponents();
|
|
2404
|
-
}
|
|
2405
|
-
return await this._loadDevelopmentComponents();
|
|
2406
|
-
}
|
|
2407
|
-
/**
|
|
2408
|
-
* 운영 모드: 통합 컴포넌트 로딩
|
|
2409
|
-
*/
|
|
2410
|
-
async _loadProductionComponents() {
|
|
2411
|
-
try {
|
|
2412
|
-
const componentsPath = `${this.router?.config?.routesPath || "/routes"}/_components.js`;
|
|
2413
|
-
this.log("info", "[PRODUCTION] Loading unified components from:", componentsPath);
|
|
2414
|
-
const componentsModule = await import(componentsPath);
|
|
2415
|
-
if (typeof componentsModule.registerComponents === "function") {
|
|
2416
|
-
this.unifiedComponents = componentsModule.components || {};
|
|
2417
|
-
this.log("info", `[PRODUCTION] Unified components loaded: ${Object.keys(this.unifiedComponents).length} components`);
|
|
2418
|
-
return this.unifiedComponents;
|
|
2419
|
-
} else {
|
|
2420
|
-
throw new Error("registerComponents function not found in components module");
|
|
2421
|
-
}
|
|
2422
|
-
} catch (error) {
|
|
2423
|
-
this.log("warn", "[PRODUCTION] Failed to load unified components:", error.message);
|
|
2424
|
-
this.unifiedComponents = {};
|
|
2425
|
-
return {};
|
|
2426
|
-
}
|
|
2427
|
-
}
|
|
2428
|
-
/**
|
|
2429
|
-
* 개발 모드: 개별 컴포넌트 로딩
|
|
2430
|
-
*/
|
|
2431
|
-
async _loadDevelopmentComponents() {
|
|
2432
|
-
const componentNames = this._getComponentNames();
|
|
2433
|
-
const components = {};
|
|
2434
|
-
this.log("info", `[DEVELOPMENT] Loading individual components: ${componentNames.join(", ")}`);
|
|
2435
|
-
for (const name of componentNames) {
|
|
2436
|
-
try {
|
|
2437
|
-
const component = await this.loadComponent(name);
|
|
2438
|
-
if (component) {
|
|
2439
|
-
components[name] = component;
|
|
2440
|
-
}
|
|
2441
|
-
} catch (loadError) {
|
|
2442
|
-
this.log("warn", `[DEVELOPMENT] Failed to load component ${name}:`, loadError.message);
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
this.unifiedComponents = components;
|
|
2446
|
-
this.log("info", `[DEVELOPMENT] Individual components loaded: ${Object.keys(components).length} components`);
|
|
2447
|
-
return components;
|
|
2448
|
-
}
|
|
2449
|
-
/**
|
|
2450
|
-
* 컴포넌트 이름 목록 가져오기
|
|
2451
|
-
*/
|
|
2452
|
-
_getComponentNames() {
|
|
2453
|
-
if (Array.isArray(this.config.componentNames) && this.config.componentNames.length > 0) {
|
|
2454
|
-
return [...this.config.componentNames];
|
|
2455
|
-
}
|
|
2456
|
-
return [
|
|
2457
|
-
"Button",
|
|
2458
|
-
"Modal",
|
|
2459
|
-
"Card",
|
|
2460
|
-
"Toast",
|
|
2461
|
-
"Input",
|
|
2462
|
-
"Tabs",
|
|
2463
|
-
"Checkbox",
|
|
2464
|
-
"Alert",
|
|
2465
|
-
"DynamicInclude",
|
|
2466
|
-
"HtmlInclude"
|
|
2467
|
-
];
|
|
2468
|
-
}
|
|
2469
|
-
/**
|
|
2470
|
-
* 메모리 정리
|
|
2471
|
-
*/
|
|
2472
|
-
dispose() {
|
|
2473
|
-
this.clearComponents();
|
|
2474
|
-
this.log("debug", "ComponentLoader disposed");
|
|
2475
|
-
this.router = null;
|
|
2476
|
-
}
|
|
2477
|
-
};
|
|
2478
|
-
|
|
2479
2685
|
// src/viewlogic-router.js
|
|
2480
2686
|
var ViewLogicRouter = class {
|
|
2481
2687
|
constructor(options = {}) {
|
|
@@ -2484,7 +2690,6 @@ var ViewLogicRouter = class {
|
|
|
2484
2690
|
this.currentHash = "";
|
|
2485
2691
|
this.currentVueApp = null;
|
|
2486
2692
|
this.previousVueApp = null;
|
|
2487
|
-
this.componentLoader = null;
|
|
2488
2693
|
this.transitionInProgress = false;
|
|
2489
2694
|
this.isReady = false;
|
|
2490
2695
|
this.readyPromise = null;
|
|
@@ -2511,8 +2716,6 @@ var ViewLogicRouter = class {
|
|
|
2511
2716
|
routesPath: "/routes",
|
|
2512
2717
|
// 프로덕션 라우트 경로
|
|
2513
2718
|
enableErrorReporting: true,
|
|
2514
|
-
useComponents: true,
|
|
2515
|
-
componentNames: ["Button", "Modal", "Card", "Toast", "Input", "Tabs", "Checkbox", "Alert", "DynamicInclude", "HtmlInclude"],
|
|
2516
2719
|
useI18n: false,
|
|
2517
2720
|
defaultLanguage: "ko",
|
|
2518
2721
|
i18nPath: "/i18n",
|
|
@@ -2621,21 +2824,6 @@ var ViewLogicRouter = class {
|
|
|
2621
2824
|
if (this.config.authEnabled) {
|
|
2622
2825
|
this.authManager = new AuthManager(this, this.config);
|
|
2623
2826
|
}
|
|
2624
|
-
if (this.config.useComponents) {
|
|
2625
|
-
try {
|
|
2626
|
-
this.componentLoader = new ComponentLoader(this, {
|
|
2627
|
-
...this.config,
|
|
2628
|
-
basePath: `${this.config.basePath}/components`,
|
|
2629
|
-
cache: true,
|
|
2630
|
-
componentNames: this.config.componentNames
|
|
2631
|
-
});
|
|
2632
|
-
await this.componentLoader.loadAllComponents();
|
|
2633
|
-
this.log("info", "ComponentLoader initialized successfully");
|
|
2634
|
-
} catch (componentError) {
|
|
2635
|
-
this.log("warn", "ComponentLoader initialization failed, continuing without components:", componentError.message);
|
|
2636
|
-
this.componentLoader = null;
|
|
2637
|
-
}
|
|
2638
|
-
}
|
|
2639
2827
|
this.isReady = true;
|
|
2640
2828
|
this.init();
|
|
2641
2829
|
} catch (error) {
|