@nine-lab/nine-mu 0.1.334 → 0.1.336
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/css/nine-mu.css +16 -6
- package/dist/nine-mu.js +92 -75
- package/dist/nine-mu.js.map +1 -1
- package/dist/nine-mu.umd.js +3 -3
- package/dist/nine-mu.umd.js.map +1 -1
- package/package.json +1 -1
- package/public/css/nine-mu.css +16 -6
- package/src/components/exception/ExceptionHook.js +54 -44
package/package.json
CHANGED
package/public/css/nine-mu.css
CHANGED
|
@@ -943,9 +943,19 @@
|
|
|
943
943
|
}
|
|
944
944
|
|
|
945
945
|
:host(nine-exception-hook) {
|
|
946
|
-
display:
|
|
946
|
+
display: flex;
|
|
947
947
|
width: 100%;
|
|
948
948
|
height: 100%;
|
|
949
|
+
color: inherit;
|
|
950
|
+
font-size: inherit;
|
|
951
|
+
line-height: inherit;
|
|
952
|
+
|
|
953
|
+
/* 내부 모든 엘리먼트의 여백 계산법을 어드민 기본형에 동기화 */
|
|
954
|
+
.nine-error-container *,
|
|
955
|
+
.nine-error-container *::before,
|
|
956
|
+
.nine-error-container *::after {
|
|
957
|
+
box-sizing: border-box;
|
|
958
|
+
}
|
|
949
959
|
|
|
950
960
|
.nine-error-container {
|
|
951
961
|
padding: 30px;
|
|
@@ -1012,10 +1022,10 @@
|
|
|
1012
1022
|
font-size: 14px;
|
|
1013
1023
|
color: #718096;
|
|
1014
1024
|
}
|
|
1025
|
+
}
|
|
1015
1026
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
}
|
|
1027
|
+
@keyframes pulse {
|
|
1028
|
+
0% { opacity: 0.6; }
|
|
1029
|
+
50% { opacity: 1; }
|
|
1030
|
+
100% { opacity: 0.6; }
|
|
1021
1031
|
}
|
|
@@ -1,38 +1,36 @@
|
|
|
1
1
|
// src/components/exception/ExceptionHook.js
|
|
2
2
|
export class ExceptionHook extends HTMLElement {
|
|
3
|
+
// 💡 [핵심] 비공개 인스턴스 필드는 클래스 최상단에 먼저 선언해야 합니다.
|
|
4
|
+
#lastPath = window.location.pathname;
|
|
5
|
+
#hasError = false;
|
|
6
|
+
#cssPath = "/css/nine-exception-hook.css";
|
|
7
|
+
|
|
3
8
|
constructor() {
|
|
4
9
|
super();
|
|
5
|
-
|
|
6
|
-
this.hasError = false;
|
|
7
|
-
this.cssPath = "/css/nine-exception-hook.css";
|
|
8
|
-
|
|
9
|
-
// 💡 1. Shadow DOM(open 모드) 활성화 및 슬롯 공간 준비
|
|
10
|
+
// Shadow DOM(open 모드) 활성화
|
|
10
11
|
this.attachShadow({ mode: 'open' });
|
|
11
|
-
|
|
12
|
-
// 기본 구조 선언: 자식 엘리먼트들이 위치할 <slot>을 배치합니다.
|
|
13
|
-
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
// src/components/exception/ExceptionHook.js 내부 수정
|
|
17
15
|
|
|
16
|
+
connectedCallback() {
|
|
18
17
|
this.#renderer();
|
|
19
|
-
|
|
20
|
-
// Shadow DOM 내부에 캡슐화된 스타일시트 링크 주입
|
|
21
18
|
this.#injectShadowStylesheet();
|
|
22
19
|
|
|
23
|
-
window.addEventListener('error', this.#handleRuntimeError
|
|
24
|
-
window.addEventListener('unhandledrejection', this.#handlePromiseError
|
|
25
|
-
window.addEventListener('popstate', this.#handleLocationChange
|
|
26
|
-
document.addEventListener('click', this.#handleLocationChange
|
|
20
|
+
window.addEventListener('error', this.#handleRuntimeError, true);
|
|
21
|
+
window.addEventListener('unhandledrejection', this.#handlePromiseError, true);
|
|
22
|
+
window.addEventListener('popstate', this.#handleLocationChange);
|
|
23
|
+
document.addEventListener('click', this.#handleLocationChange, true);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
disconnectedCallback() {
|
|
30
|
-
window.removeEventListener('error', this.#handleRuntimeError);
|
|
31
|
-
window.removeEventListener('unhandledrejection', this.#handlePromiseError);
|
|
27
|
+
window.removeEventListener('error', this.#handleRuntimeError, true);
|
|
28
|
+
window.removeEventListener('unhandledrejection', this.#handlePromiseError, true);
|
|
32
29
|
window.removeEventListener('popstate', this.#handleLocationChange);
|
|
33
30
|
document.removeEventListener('click', this.#handleLocationChange, true);
|
|
34
31
|
}
|
|
35
32
|
|
|
33
|
+
|
|
36
34
|
#renderer = () => {
|
|
37
35
|
const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
|
|
38
36
|
|
|
@@ -45,52 +43,65 @@ export class ExceptionHook extends HTMLElement {
|
|
|
45
43
|
<div class="nine-hook-core">
|
|
46
44
|
<slot></slot>
|
|
47
45
|
</div>
|
|
46
|
+
<div class="nine-exception-board"></div>
|
|
48
47
|
`;
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
// 💡 2. Shadow Root 내부 전용 스타일 링크 주입 함수
|
|
52
50
|
#injectShadowStylesheet() {
|
|
53
|
-
|
|
51
|
+
// 💡 내부 필드 호출 시에도 모두 this.#형식으로 매핑합니다.
|
|
52
|
+
if (this.shadowRoot.querySelector(`link[href="${this.#cssPath}"]`)) return;
|
|
54
53
|
|
|
55
54
|
const link = document.createElement('link');
|
|
56
55
|
link.rel = 'stylesheet';
|
|
57
|
-
link.href = this
|
|
58
|
-
// head가 아닌 shadowRoot에 집어넣어 글로벌 오염을 완벽히 격리합니다.
|
|
56
|
+
link.href = this.#cssPath;
|
|
59
57
|
this.shadowRoot.appendChild(link);
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
60
|
+
// src/components/exception/ExceptionHook.js
|
|
61
|
+
|
|
62
|
+
#handleRuntimeError = (event) => {
|
|
63
|
+
if (this.#hasError) return;
|
|
64
|
+
|
|
65
|
+
// 🎯 핵심 1: 리액트 에러는 내부 가상 DOM에서 발생하므로
|
|
66
|
+
// event.error가 존재하거나, 에러 타겟이 이 컴포넌트 하위에 속해있는지 확인합니다.
|
|
67
|
+
const isReactError = event.error || (event.target && this.contains(event.target));
|
|
68
|
+
|
|
69
|
+
if (isReactError) {
|
|
70
|
+
// 에러 스택과 메시지 추출
|
|
71
|
+
const message = event.error?.message || event.message || "알 수 없는 런타임 에러";
|
|
72
|
+
const stack = event.error?.stack || "Stack trace unavailable";
|
|
73
|
+
|
|
74
|
+
// 🎯 핵심 2: 리액트가 에러 때문에 화면을 통째로 날려버리기 전에
|
|
75
|
+
// 기본 렌더링 슬롯(<slot>)을 즉시 숨기고 Web Component 자체 에러 보드를 띄웁니다.
|
|
76
|
+
this.#renderError(message, stack);
|
|
77
|
+
|
|
78
|
+
// 브라우저 기본 에러 동작(콘솔 중복 출력 등)을 필요에 따라 제어하려면 추가
|
|
79
|
+
// event.preventDefault();
|
|
66
80
|
}
|
|
67
81
|
}
|
|
68
82
|
|
|
69
|
-
|
|
70
|
-
|
|
83
|
+
#handlePromiseError = (event) => {
|
|
84
|
+
if (this.#hasError) return;
|
|
71
85
|
this.#renderError(event.reason?.message || String(event.reason), event.reason?.stack);
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
#handleLocationChange() {
|
|
88
|
+
#handleLocationChange = () => {
|
|
75
89
|
setTimeout(() => {
|
|
76
|
-
if (this
|
|
90
|
+
if (this.#hasError && this.#lastPath !== window.location.pathname) {
|
|
77
91
|
console.log("✨ [nine-exception-hook] Shadow DOM 내부 예외 청소 및 정상 복구");
|
|
78
|
-
this
|
|
79
|
-
this
|
|
92
|
+
this.#lastPath = window.location.pathname;
|
|
93
|
+
this.#hasError = false;
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (errorBox) errorBox.remove();
|
|
95
|
+
const exceptionBoard = this.shadowRoot.querySelector('.nine-exception-board');
|
|
96
|
+
if (exceptionBoard) exceptionBoard.innerHTML = '';
|
|
84
97
|
|
|
85
|
-
// 숨겨두었던 원래 본문 슬롯 구역을 다시 노출
|
|
86
98
|
const coreContainer = this.shadowRoot.querySelector('.nine-hook-core');
|
|
87
99
|
if (coreContainer) coreContainer.style.display = '';
|
|
88
100
|
}
|
|
89
101
|
}, 10);
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
async
|
|
93
|
-
// 💡 4. DOM 쿼리 탐색을 모두 this가 아닌 this.shadowRoot로 전환
|
|
104
|
+
#sendToAiAction = async (errorMessage, errorStack) => {
|
|
94
105
|
const btn = this.shadowRoot.querySelector('.nine-ai-btn');
|
|
95
106
|
const footer = this.shadowRoot.querySelector('.nine-error-footer');
|
|
96
107
|
if (!btn) return;
|
|
@@ -121,14 +132,15 @@ export class ExceptionHook extends HTMLElement {
|
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
#renderError(errorMessage, errorStack) {
|
|
124
|
-
if (this
|
|
125
|
-
this
|
|
135
|
+
if (this.#hasError) return;
|
|
136
|
+
this.#hasError = true;
|
|
126
137
|
|
|
127
|
-
// 💡 5. 리액트의 원본 마크업을 건드리지 않고, 슬롯이 감싸진 껍데기만 숨김 처리
|
|
128
138
|
const coreContainer = this.shadowRoot.querySelector('.nine-hook-core');
|
|
129
139
|
if (coreContainer) coreContainer.style.display = 'none';
|
|
130
140
|
|
|
131
|
-
|
|
141
|
+
const exceptionBoard = this.shadowRoot.querySelector('.nine-exception-board');
|
|
142
|
+
if (!exceptionBoard) return;
|
|
143
|
+
|
|
132
144
|
const errorHTML = `
|
|
133
145
|
<div class="nine-error-container">
|
|
134
146
|
<h3 class="nine-error-title">⚠️ 시스템 예외 감지 (${this.tagName.toLowerCase()})</h3>
|
|
@@ -144,10 +156,8 @@ export class ExceptionHook extends HTMLElement {
|
|
|
144
156
|
</div>
|
|
145
157
|
`;
|
|
146
158
|
|
|
147
|
-
|
|
148
|
-
this.shadowRoot.insertAdjacentHTML('beforeend', errorHTML);
|
|
159
|
+
exceptionBoard.innerHTML = errorHTML;
|
|
149
160
|
|
|
150
|
-
// 버튼 이벤트 바인딩도 shadowRoot 기준 처리
|
|
151
161
|
const aiBtn = this.shadowRoot.querySelector('.nine-ai-btn');
|
|
152
162
|
if (aiBtn) {
|
|
153
163
|
aiBtn.addEventListener('click', () => this.#sendToAiAction(errorMessage, errorStack));
|