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.
- package/README.md +70 -9
- package/dist/viewlogic-router.js +950 -643
- package/dist/viewlogic-router.js.map +4 -4
- package/dist/viewlogic-router.min.js +3 -3
- package/dist/viewlogic-router.min.js.map +4 -4
- package/package.json +2 -1
package/dist/viewlogic-router.js
CHANGED
|
@@ -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.
|
|
102
|
-
this.
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1464
|
-
var
|
|
1485
|
+
// src/core/FormHandler.js
|
|
1486
|
+
var FormHandler = class {
|
|
1465
1487
|
constructor(router, options = {}) {
|
|
1488
|
+
this.router = router;
|
|
1466
1489
|
this.config = {
|
|
1467
|
-
|
|
1468
|
-
|
|
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.
|
|
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
|
-
|
|
1482
|
-
|
|
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 (
|
|
1485
|
-
|
|
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
|
-
|
|
1496
|
-
|
|
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
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
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
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
|
|
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
|
-
*
|
|
1585
|
+
* 클라이언트 사이드 폼 검증
|
|
1540
1586
|
*/
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
if (!
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
1558
|
-
|
|
1559
|
-
if (
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
1624
|
+
this.log("warn", `Validation function '${validationFunction}' not found`);
|
|
1625
|
+
return true;
|
|
1576
1626
|
}
|
|
1577
1627
|
/**
|
|
1578
|
-
*
|
|
1628
|
+
* 정리 (메모리 누수 방지)
|
|
1579
1629
|
*/
|
|
1580
|
-
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
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
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
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
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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",
|
|
1690
|
-
|
|
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
|
-
//
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
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
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
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
|
-
|
|
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
|
|
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 $
|
|
1737
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
2716
|
+
routesPath: "/routes",
|
|
2717
|
+
// 프로덕션 라우트 경로
|
|
2457
2718
|
enableErrorReporting: true,
|
|
2458
|
-
|
|
2459
|
-
componentNames: ["Button", "Modal", "Card", "Toast", "Input", "Tabs", "Checkbox", "Alert", "DynamicInclude", "HtmlInclude"],
|
|
2460
|
-
useI18n: true,
|
|
2719
|
+
useI18n: false,
|
|
2461
2720
|
defaultLanguage: "ko",
|
|
2462
|
-
i18nPath:
|
|
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
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|