@nine-lab/nine-mu 0.1.345 → 0.1.346
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/nine-mu.js +7 -195
- 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/src/components/exception/ExceptionHook.js +50 -184
- package/src/index.js +1 -1
package/package.json
CHANGED
|
@@ -1,211 +1,77 @@
|
|
|
1
1
|
// src/components/exception/ExceptionHook.js
|
|
2
2
|
import { nine } from '@nine-lab/nine-util';
|
|
3
3
|
import { trace } from '@nopeer';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
this.attachShadow({ mode: 'open' });
|
|
4
|
+
// npm 라이브러리용 패키지의 핵심 소스 (예: src/components/ContentErrorBoundary.jsx)
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
export class NineExceptionHook extends React.Component {
|
|
8
|
+
constructor(props) {
|
|
9
|
+
super(props);
|
|
10
|
+
this.state = { hasError: false, error: null };
|
|
11
|
+
this.lastPath = window.location.pathname;
|
|
12
|
+
this.handleLocationChange = this.handleLocationChange.bind(this);
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
this.#injectShadowStylesheet();
|
|
19
|
-
this.#setupGlobalSniffer();
|
|
20
|
-
|
|
21
|
-
window.addEventListener('unhandledrejection', this.#handlePromiseError, true);
|
|
22
|
-
window.addEventListener('popstate', this.#handleLocationChange);
|
|
23
|
-
document.addEventListener('click', this.#handleLocationChange, true);
|
|
15
|
+
static getDerivedStateFromError(error) {
|
|
16
|
+
return { hasError: true, error };
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
window.removeEventListener('unhandledrejection', this.#handlePromiseError, true);
|
|
29
|
-
window.removeEventListener('popstate', this.#handleLocationChange);
|
|
30
|
-
document.removeEventListener('click', this.#handleLocationChange, true);
|
|
19
|
+
componentDidCatch(error, errorInfo) {
|
|
20
|
+
console.error("🚨 [Nine-Library] 본문 렌더링 에러 포착:", error);
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
// 1. 외부 전역 복구 엔진(triggerAutoRecovery)이 있으면 호출
|
|
23
|
+
if (window.triggerAutoRecovery) {
|
|
24
|
+
window.triggerAutoRecovery(error.message || String(error));
|
|
34
25
|
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#setupGlobalSniffer() {
|
|
38
|
-
window.addEventListener('error', this.#handleRuntimeError, true);
|
|
39
|
-
|
|
40
|
-
this.#oldOnError = window.onerror;
|
|
41
|
-
window.onerror = (message, source, lineno, colno, error) => {
|
|
42
|
-
if (!this.#hasError) {
|
|
43
|
-
const errorMessage = error?.message || message || "알 수 없는 런타임 예외";
|
|
44
|
-
const errorStack = error?.stack || `${source}:${lineno}:${colno}`;
|
|
45
|
-
this.#renderError(errorMessage, errorStack);
|
|
46
|
-
}
|
|
47
|
-
if (this.#oldOnError) {
|
|
48
|
-
return this.#oldOnError(message, source, lineno, colno, error);
|
|
49
|
-
}
|
|
50
|
-
return false;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
#renderer = () => {
|
|
55
|
-
const customImport = nine.cssPath ? `@import "${nine.cssPath}/nine-mu.css";` : "";
|
|
56
|
-
|
|
57
|
-
// 🎯 [핵심 변경 1] :host 자체를 absolute의 기준점이 되는 block으로 잡고,
|
|
58
|
-
// 리액트 슬롯 내용물(메뉴 등)은 절대 숨기지 않고 100% 크기로 유지합니다.
|
|
59
|
-
this.shadowRoot.innerHTML = `
|
|
60
|
-
<style>
|
|
61
|
-
@import "https://cdn.jsdelivr.net/npm/@nine-lab/nine-mu@${__APP_VERSION__}/dist/css/nine-mu.css";
|
|
62
|
-
${customImport}
|
|
63
|
-
|
|
64
|
-
:host {
|
|
65
|
-
display: block;
|
|
66
|
-
width: 100%;
|
|
67
|
-
height: 100%;
|
|
68
|
-
position: relative !important; /* 내부 에러 보드의 기준점 기둥 생성 */
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.nine-hook-core {
|
|
72
|
-
width: 100%;
|
|
73
|
-
height: 100%;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/* 🎯 [핵심 변경 2] 리액트 슬롯을 가리지 않고, 오직 "이 컴포넌트 구역"만 가득 채우는 독립 팝업 레이어 */
|
|
77
|
-
.nine-hook-inner-cover {
|
|
78
|
-
position: absolute !important;
|
|
79
|
-
top: 0 !important;
|
|
80
|
-
left: 0 !important;
|
|
81
|
-
width: 100% !important;
|
|
82
|
-
height: 100% !important;
|
|
83
|
-
box-sizing: border-box !important;
|
|
84
|
-
background-color: #FFF5F5 !important;
|
|
85
|
-
z-index: 2147483647 !important; /* 리액트 내부 그 어떤 오버레이보다 무조건 위 */
|
|
86
|
-
overflow: auto !important;
|
|
87
|
-
display: none; /* 평소엔 숨김 */
|
|
88
|
-
}
|
|
89
|
-
</style>
|
|
90
|
-
|
|
91
|
-
<div class="nine-hook-core">
|
|
92
|
-
<slot></slot>
|
|
93
|
-
</div>
|
|
94
|
-
<div class="nine-exception-board class-target-board"></div>
|
|
95
|
-
`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
#injectShadowStylesheet() {
|
|
99
|
-
if (this.shadowRoot.querySelector(`link[href="${this.#cssPath}"]`)) return;
|
|
100
|
-
|
|
101
|
-
const link = document.createElement('link');
|
|
102
|
-
link.rel = 'stylesheet';
|
|
103
|
-
link.href = this.#cssPath;
|
|
104
|
-
this.shadowRoot.appendChild(link);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
#handleRuntimeError = (event) => {
|
|
108
|
-
if (this.#hasError) return;
|
|
109
26
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (errorMessage && errorMessage !== "Script error.") {
|
|
114
|
-
this.#renderError(errorMessage, errorStack);
|
|
27
|
+
// 2. 라이브러리 확장성: 컴포넌트 자체 Props로 콜백을 넘겨받았다면 실행
|
|
28
|
+
if (this.props.onCatch) {
|
|
29
|
+
this.props.onCatch(error, errorInfo);
|
|
115
30
|
}
|
|
116
31
|
}
|
|
117
32
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const errorMessage = reason?.message || String(reason || "Unhandled Promise Rejection");
|
|
123
|
-
const errorStack = reason?.stack || "No stack trace available";
|
|
33
|
+
componentDidMount() {
|
|
34
|
+
window.addEventListener('popstate', this.handleLocationChange);
|
|
35
|
+
document.addEventListener('click', this.handleLocationChange, true);
|
|
36
|
+
}
|
|
124
37
|
|
|
125
|
-
|
|
38
|
+
componentWillUnmount() {
|
|
39
|
+
window.removeEventListener('popstate', this.handleLocationChange);
|
|
40
|
+
document.removeEventListener('click', this.handleLocationChange, true);
|
|
126
41
|
}
|
|
127
42
|
|
|
128
|
-
|
|
43
|
+
handleLocationChange() {
|
|
129
44
|
setTimeout(() => {
|
|
130
|
-
if (this
|
|
131
|
-
|
|
132
|
-
this
|
|
133
|
-
|
|
134
|
-
const exceptionBoard = this.shadowRoot.querySelector('.nine-exception-board');
|
|
135
|
-
if (exceptionBoard) {
|
|
136
|
-
exceptionBoard.innerHTML = '';
|
|
137
|
-
exceptionBoard.className = 'nine-exception-board'; // 클래스 리셋
|
|
138
|
-
}
|
|
45
|
+
if (this.state.hasError && this.lastPath !== window.location.pathname) {
|
|
46
|
+
console.log("✨ [Nine-Library] 페이지 주소 변경 감지 -> 리액트 에러 상태 초기화");
|
|
47
|
+
this.lastPath = window.location.pathname;
|
|
48
|
+
this.setState({ hasError: false, error: null });
|
|
139
49
|
}
|
|
140
|
-
},
|
|
50
|
+
}, 10);
|
|
141
51
|
}
|
|
142
52
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
btn.disabled = true;
|
|
149
|
-
btn.innerText = "⚡ AI 관제탑에 소스 분석 요청 중...";
|
|
150
|
-
btn.classList.add('loading');
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
if (window.triggerAutoRecovery) {
|
|
154
|
-
await window.triggerAutoRecovery({
|
|
155
|
-
message: errorMessage,
|
|
156
|
-
stack: errorStack || "No stack trace available",
|
|
157
|
-
path: window.location.pathname
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
footer.innerHTML = `✅ <b>AI 관제탑 접수 완료:</b> 분석을 바탕으로 소스 코드를 교정하고 있습니다. 잠시만 기다려주세요.`;
|
|
161
|
-
btn.innerText = "🚀 전송 완료";
|
|
162
|
-
} else {
|
|
163
|
-
throw new Error("window.triggerAutoRecovery 관제탑 엔진이 로드되지 않았습니다.");
|
|
53
|
+
render() {
|
|
54
|
+
if (this.state.hasError) {
|
|
55
|
+
// 라이브러리 커스텀 UI 제공 (외부에서 커스텀 fallback UI를 주입하지 않았다면 기본 템플릿 사용)
|
|
56
|
+
if (this.props.fallback) {
|
|
57
|
+
return this.props.fallback(this.state.error);
|
|
164
58
|
}
|
|
165
|
-
} catch (err) {
|
|
166
|
-
btn.disabled = false;
|
|
167
|
-
btn.innerText = "❌ 재전송 시도";
|
|
168
|
-
footer.innerHTML = `<span style="color: #E53E3E;">⚠️ 전송 실패: ${err.message}</span>`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
59
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// 대신 에러 보드에 커버 클래스를 주입하여 "오직 본문 공간만" 물리적으로 완전히 덮어씌웁니다.
|
|
182
|
-
exceptionBoard.className = 'nine-hook-inner-cover';
|
|
183
|
-
|
|
184
|
-
const errorHTML = `
|
|
185
|
-
<div class="nine-error-container" style="padding: 30px; box-sizing: border-box; font-family: sans-serif; background: white; min-height: 100%; text-align: left;">
|
|
186
|
-
<h3 class="nine-error-title" style="color: #C53030; margin-top: 0; font-size: 20px; font-weight: 700; border-bottom: 2px solid #FED2D2; padding-bottom: 12px; font-family: sans-serif;">⚠️ 시스템 예외 감지 (${this.tagName.toLowerCase()})</h3>
|
|
187
|
-
<p class="nine-error-label" style="font-weight: bold; margin: 15px 0 6px 0; color: #4A5568; font-size: 14px; font-family: sans-serif;">[에러 원인 로그]:</p>
|
|
188
|
-
<pre class="nine-error-log" style="background: #1A202C; color: #EDF2F7; padding: 15px; border-radius: 6px; overflow-x: auto; white-space: pre-wrap; font-family: monospace; font-size: 13px; line-height: 1.5; margin-bottom: 20px;">${errorMessage}\n\n[Stack Trace]\n${errorStack}</pre>
|
|
189
|
-
|
|
190
|
-
<div class="nine-action-area" style="display: flex; align-items: center; gap: 15px; border-top: 1px solid #E2E8F0; padding-top: 15px;">
|
|
191
|
-
<button class="nine-ai-btn" style="background: #3182CE; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 14px;">🤖 AI에게 소스코드 수정 요청</button>
|
|
192
|
-
<div class="nine-error-footer" style="font-size: 13px; color: #718096; line-height: 1.4; font-family: sans-serif;">
|
|
193
|
-
버튼을 누르면 AI가 로그 및 컴포넌트 맵을 역추적하여 수정을 시작합니다.
|
|
194
|
-
</div>
|
|
60
|
+
return (
|
|
61
|
+
<div style={{ padding: '30px', margin: '20px', background: '#FFF5F5', border: '1px solid #FEB2B2', borderRadius: '8px', fontFamily: 'sans-serif', textAlign: 'left', ...this.props.containerStyle }}>
|
|
62
|
+
<h3 style={{ color: '#C53030', marginTop: 0, fontSize: '18px' }}>⚠️ 컴포넌트 빌드/렌더링 에러 감지</h3>
|
|
63
|
+
<p style={{ margin: '10px 0 5px 0', fontWeight: 'bold', color: '#4A5568' }}>[에러 원인 로그]:</p>
|
|
64
|
+
<pre style={{ background: '#1A202C', color: '#EDF2F7', padding: '15px', borderRadius: '6px', overflowX: 'auto', whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>
|
|
65
|
+
{this.state.error?.message || String(this.state.error)}
|
|
66
|
+
</pre>
|
|
67
|
+
<div style={{ marginTop: '15px', fontSize: '14px', color: '#718096' }}>
|
|
68
|
+
🔄 에러 로그가 관제탑으로 자동 피드백되었습니다. AI가 소스 코드를 재수정하는 동안 잠시만 기다려주세요...
|
|
195
69
|
</div>
|
|
196
70
|
</div>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const aiBtn = exceptionBoard.querySelector('.nine-ai-btn');
|
|
202
|
-
if (aiBtn) {
|
|
203
|
-
aiBtn.addEventListener('click', () => this.#sendToAiAction(errorMessage, errorStack));
|
|
204
|
-
}
|
|
205
|
-
}, 0);
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return this.props.children;
|
|
206
74
|
}
|
|
207
75
|
}
|
|
208
76
|
|
|
209
|
-
|
|
210
|
-
customElements.define('nine-exception-hook', ExceptionHook);
|
|
211
|
-
}
|
|
77
|
+
export default NineExceptionHook;
|
package/src/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { NineDiff } from './components/NineDiff.js';
|
|
|
4
4
|
import { NineDiffPopup } from './components/NineDiffPopup.js';
|
|
5
5
|
import { NineMenuDiffPopup } from './components/NineMenuDiffPopup.js';
|
|
6
6
|
import './components/ChatMessage.js';
|
|
7
|
-
import './components/exception/ExceptionHook.js';
|
|
7
|
+
//import { NineExceptionHook } from './components/exception/ExceptionHook.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Nine-Mu 엔진 메인 클래스
|