goodchuck-utils 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/dev/ApiLogger.d.ts +136 -0
- package/dist/components/dev/ApiLogger.d.ts.map +1 -0
- package/dist/components/dev/ApiLogger.js +408 -0
- package/dist/components/dev/DevPanel.d.ts +32 -0
- package/dist/components/dev/DevPanel.d.ts.map +1 -0
- package/dist/components/dev/DevPanel.js +196 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts +75 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/FormDevTools.js +196 -0
- package/dist/components/dev/FormDevTools/index.d.ts +3 -0
- package/dist/components/dev/FormDevTools/index.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/index.js +1 -0
- package/dist/components/dev/FormDevTools/styles.d.ts +43 -0
- package/dist/components/dev/FormDevTools/styles.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/styles.js +176 -0
- package/dist/components/dev/IdSelector.d.ts +10 -1
- package/dist/components/dev/IdSelector.d.ts.map +1 -1
- package/dist/components/dev/IdSelector.js +89 -12
- package/dist/components/dev/WindowSizeDisplay.d.ts +44 -0
- package/dist/components/dev/WindowSizeDisplay.d.ts.map +1 -0
- package/dist/components/dev/WindowSizeDisplay.js +74 -0
- package/dist/components/dev/ZIndexDebugger.d.ts +32 -0
- package/dist/components/dev/ZIndexDebugger.d.ts.map +1 -0
- package/dist/components/dev/ZIndexDebugger.js +184 -0
- package/dist/components/dev/index.d.ts +7 -0
- package/dist/components/dev/index.d.ts.map +1 -1
- package/dist/components/dev/index.js +5 -0
- package/package.json +4 -2
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type ApiLogEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
timestamp: Date;
|
|
5
|
+
method: string;
|
|
6
|
+
url: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
statusText?: string;
|
|
9
|
+
duration?: number;
|
|
10
|
+
requestBody?: unknown;
|
|
11
|
+
responseBody?: unknown;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
type Props = {
|
|
15
|
+
/** 표시 위치 (기본값: 'bottom-right') */
|
|
16
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
17
|
+
/** 최대 로그 개수 (기본값: 50) */
|
|
18
|
+
maxLogs?: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* API 로그를 추가하는 함수
|
|
22
|
+
* fetch나 axios interceptor에서 호출하여 사용합니다.
|
|
23
|
+
*/
|
|
24
|
+
export declare function addApiLog(log: Omit<ApiLogEntry, 'id' | 'timestamp'>): void;
|
|
25
|
+
/**
|
|
26
|
+
* 모든 API 로그를 초기화하는 함수
|
|
27
|
+
*/
|
|
28
|
+
export declare function clearApiLogs(): void;
|
|
29
|
+
/**
|
|
30
|
+
* API 요청/응답을 로깅하는 개발용 컴포넌트
|
|
31
|
+
* Axios interceptor 또는 fetch wrapper와 함께 사용하여 API 호출을 모니터링합니다.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* // Axios interceptor 사용 (가장 일반적)
|
|
36
|
+
* import axios from 'axios';
|
|
37
|
+
* import { ApiLogger, addApiLog } from 'goodchuck-utils/components/dev';
|
|
38
|
+
*
|
|
39
|
+
* // Request interceptor
|
|
40
|
+
* axios.interceptors.request.use(
|
|
41
|
+
* (config) => {
|
|
42
|
+
* config.metadata = { startTime: Date.now() };
|
|
43
|
+
* return config;
|
|
44
|
+
* },
|
|
45
|
+
* (error) => Promise.reject(error)
|
|
46
|
+
* );
|
|
47
|
+
*
|
|
48
|
+
* // Response interceptor
|
|
49
|
+
* axios.interceptors.response.use(
|
|
50
|
+
* (response) => {
|
|
51
|
+
* const duration = Date.now() - response.config.metadata?.startTime;
|
|
52
|
+
*
|
|
53
|
+
* addApiLog({
|
|
54
|
+
* method: response.config.method?.toUpperCase() || 'GET',
|
|
55
|
+
* url: response.config.url || '',
|
|
56
|
+
* status: response.status,
|
|
57
|
+
* statusText: response.statusText,
|
|
58
|
+
* duration,
|
|
59
|
+
* requestBody: response.config.data,
|
|
60
|
+
* responseBody: response.data,
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* return response;
|
|
64
|
+
* },
|
|
65
|
+
* (error) => {
|
|
66
|
+
* const duration = Date.now() - error.config?.metadata?.startTime;
|
|
67
|
+
*
|
|
68
|
+
* addApiLog({
|
|
69
|
+
* method: error.config?.method?.toUpperCase() || 'GET',
|
|
70
|
+
* url: error.config?.url || '',
|
|
71
|
+
* status: error.response?.status,
|
|
72
|
+
* statusText: error.response?.statusText,
|
|
73
|
+
* duration,
|
|
74
|
+
* requestBody: error.config?.data,
|
|
75
|
+
* responseBody: error.response?.data,
|
|
76
|
+
* error: error.message,
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* return Promise.reject(error);
|
|
80
|
+
* }
|
|
81
|
+
* );
|
|
82
|
+
*
|
|
83
|
+
* function App() {
|
|
84
|
+
* return (
|
|
85
|
+
* <div>
|
|
86
|
+
* {import.meta.env.DEV && <ApiLogger />}
|
|
87
|
+
* </div>
|
|
88
|
+
* );
|
|
89
|
+
* }
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```tsx
|
|
94
|
+
* // fetch wrapper 사용
|
|
95
|
+
* import { addApiLog } from 'goodchuck-utils/components/dev';
|
|
96
|
+
*
|
|
97
|
+
* const originalFetch = window.fetch;
|
|
98
|
+
* window.fetch = async (...args) => {
|
|
99
|
+
* const startTime = Date.now();
|
|
100
|
+
* const [url, options] = args;
|
|
101
|
+
*
|
|
102
|
+
* try {
|
|
103
|
+
* const response = await originalFetch(...args);
|
|
104
|
+
* const duration = Date.now() - startTime;
|
|
105
|
+
*
|
|
106
|
+
* addApiLog({
|
|
107
|
+
* method: options?.method || 'GET',
|
|
108
|
+
* url: url.toString(),
|
|
109
|
+
* status: response.status,
|
|
110
|
+
* statusText: response.statusText,
|
|
111
|
+
* duration,
|
|
112
|
+
* requestBody: options?.body,
|
|
113
|
+
* });
|
|
114
|
+
*
|
|
115
|
+
* return response;
|
|
116
|
+
* } catch (error) {
|
|
117
|
+
* addApiLog({
|
|
118
|
+
* method: options?.method || 'GET',
|
|
119
|
+
* url: url.toString(),
|
|
120
|
+
* error: (error as Error).message,
|
|
121
|
+
* duration: Date.now() - startTime,
|
|
122
|
+
* });
|
|
123
|
+
* throw error;
|
|
124
|
+
* }
|
|
125
|
+
* };
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```tsx
|
|
130
|
+
* // Create React App 프로젝트
|
|
131
|
+
* {process.env.NODE_ENV === 'development' && <ApiLogger position="bottom-left" />}
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export default function ApiLogger({ position, maxLogs }: Props): React.JSX.Element;
|
|
135
|
+
export {};
|
|
136
|
+
//# sourceMappingURL=ApiLogger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ApiLogger.d.ts","sourceRoot":"","sources":["../../../src/components/dev/ApiLogger.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6C,MAAM,OAAO,CAAC;AAGlE,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,IAAI,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,KAAK,GAAG;IACX,kCAAkC;IAClC,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;IACrE,yBAAyB;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAMF;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,GAAG,WAAW,CAAC,QASnE;AAED;;GAEG;AACH,wBAAgB,YAAY,SAG3B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwGG;AACH,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,EAAE,QAAyB,EAAE,OAAY,EAAE,EAAE,KAAK,qBAgXnF"}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useCopyToClipboard } from '../../hooks/useCopyToClipboard';
|
|
3
|
+
// 전역 로그 저장소
|
|
4
|
+
let globalLogs = [];
|
|
5
|
+
let logListeners = [];
|
|
6
|
+
/**
|
|
7
|
+
* API 로그를 추가하는 함수
|
|
8
|
+
* fetch나 axios interceptor에서 호출하여 사용합니다.
|
|
9
|
+
*/
|
|
10
|
+
export function addApiLog(log) {
|
|
11
|
+
const newLog = {
|
|
12
|
+
...log,
|
|
13
|
+
id: `${Date.now()}-${Math.random()}`,
|
|
14
|
+
timestamp: new Date(),
|
|
15
|
+
};
|
|
16
|
+
globalLogs = [newLog, ...globalLogs].slice(0, 100);
|
|
17
|
+
logListeners.forEach((listener) => listener([...globalLogs]));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 모든 API 로그를 초기화하는 함수
|
|
21
|
+
*/
|
|
22
|
+
export function clearApiLogs() {
|
|
23
|
+
globalLogs = [];
|
|
24
|
+
logListeners.forEach((listener) => listener([]));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* API 요청/응답을 로깅하는 개발용 컴포넌트
|
|
28
|
+
* Axios interceptor 또는 fetch wrapper와 함께 사용하여 API 호출을 모니터링합니다.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* // Axios interceptor 사용 (가장 일반적)
|
|
33
|
+
* import axios from 'axios';
|
|
34
|
+
* import { ApiLogger, addApiLog } from 'goodchuck-utils/components/dev';
|
|
35
|
+
*
|
|
36
|
+
* // Request interceptor
|
|
37
|
+
* axios.interceptors.request.use(
|
|
38
|
+
* (config) => {
|
|
39
|
+
* config.metadata = { startTime: Date.now() };
|
|
40
|
+
* return config;
|
|
41
|
+
* },
|
|
42
|
+
* (error) => Promise.reject(error)
|
|
43
|
+
* );
|
|
44
|
+
*
|
|
45
|
+
* // Response interceptor
|
|
46
|
+
* axios.interceptors.response.use(
|
|
47
|
+
* (response) => {
|
|
48
|
+
* const duration = Date.now() - response.config.metadata?.startTime;
|
|
49
|
+
*
|
|
50
|
+
* addApiLog({
|
|
51
|
+
* method: response.config.method?.toUpperCase() || 'GET',
|
|
52
|
+
* url: response.config.url || '',
|
|
53
|
+
* status: response.status,
|
|
54
|
+
* statusText: response.statusText,
|
|
55
|
+
* duration,
|
|
56
|
+
* requestBody: response.config.data,
|
|
57
|
+
* responseBody: response.data,
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* return response;
|
|
61
|
+
* },
|
|
62
|
+
* (error) => {
|
|
63
|
+
* const duration = Date.now() - error.config?.metadata?.startTime;
|
|
64
|
+
*
|
|
65
|
+
* addApiLog({
|
|
66
|
+
* method: error.config?.method?.toUpperCase() || 'GET',
|
|
67
|
+
* url: error.config?.url || '',
|
|
68
|
+
* status: error.response?.status,
|
|
69
|
+
* statusText: error.response?.statusText,
|
|
70
|
+
* duration,
|
|
71
|
+
* requestBody: error.config?.data,
|
|
72
|
+
* responseBody: error.response?.data,
|
|
73
|
+
* error: error.message,
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* return Promise.reject(error);
|
|
77
|
+
* }
|
|
78
|
+
* );
|
|
79
|
+
*
|
|
80
|
+
* function App() {
|
|
81
|
+
* return (
|
|
82
|
+
* <div>
|
|
83
|
+
* {import.meta.env.DEV && <ApiLogger />}
|
|
84
|
+
* </div>
|
|
85
|
+
* );
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* // fetch wrapper 사용
|
|
92
|
+
* import { addApiLog } from 'goodchuck-utils/components/dev';
|
|
93
|
+
*
|
|
94
|
+
* const originalFetch = window.fetch;
|
|
95
|
+
* window.fetch = async (...args) => {
|
|
96
|
+
* const startTime = Date.now();
|
|
97
|
+
* const [url, options] = args;
|
|
98
|
+
*
|
|
99
|
+
* try {
|
|
100
|
+
* const response = await originalFetch(...args);
|
|
101
|
+
* const duration = Date.now() - startTime;
|
|
102
|
+
*
|
|
103
|
+
* addApiLog({
|
|
104
|
+
* method: options?.method || 'GET',
|
|
105
|
+
* url: url.toString(),
|
|
106
|
+
* status: response.status,
|
|
107
|
+
* statusText: response.statusText,
|
|
108
|
+
* duration,
|
|
109
|
+
* requestBody: options?.body,
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* return response;
|
|
113
|
+
* } catch (error) {
|
|
114
|
+
* addApiLog({
|
|
115
|
+
* method: options?.method || 'GET',
|
|
116
|
+
* url: url.toString(),
|
|
117
|
+
* error: (error as Error).message,
|
|
118
|
+
* duration: Date.now() - startTime,
|
|
119
|
+
* });
|
|
120
|
+
* throw error;
|
|
121
|
+
* }
|
|
122
|
+
* };
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```tsx
|
|
127
|
+
* // Create React App 프로젝트
|
|
128
|
+
* {process.env.NODE_ENV === 'development' && <ApiLogger position="bottom-left" />}
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export default function ApiLogger({ position = 'bottom-right', maxLogs = 50 }) {
|
|
132
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
133
|
+
const [logs, setLogs] = useState([]);
|
|
134
|
+
const [selectedLog, setSelectedLog] = useState(null);
|
|
135
|
+
const { copy, copiedText } = useCopyToClipboard();
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
const listener = (newLogs) => {
|
|
138
|
+
setLogs(newLogs.slice(0, maxLogs));
|
|
139
|
+
};
|
|
140
|
+
logListeners.push(listener);
|
|
141
|
+
listener([...globalLogs]);
|
|
142
|
+
return () => {
|
|
143
|
+
logListeners = logListeners.filter((l) => l !== listener);
|
|
144
|
+
};
|
|
145
|
+
}, [maxLogs]);
|
|
146
|
+
const handleClearLogs = () => {
|
|
147
|
+
if (confirm('모든 API 로그를 삭제하시겠습니까?')) {
|
|
148
|
+
clearApiLogs();
|
|
149
|
+
setSelectedLog(null);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const handleCopyLog = (log) => {
|
|
153
|
+
const logText = JSON.stringify(log, null, 2);
|
|
154
|
+
copy(logText);
|
|
155
|
+
};
|
|
156
|
+
const isCopied = copiedText !== null;
|
|
157
|
+
const getStatusColor = (status) => {
|
|
158
|
+
if (!status)
|
|
159
|
+
return '#6b7280';
|
|
160
|
+
if (status >= 200 && status < 300)
|
|
161
|
+
return '#10b981';
|
|
162
|
+
if (status >= 300 && status < 400)
|
|
163
|
+
return '#3b82f6';
|
|
164
|
+
if (status >= 400 && status < 500)
|
|
165
|
+
return '#f59e0b';
|
|
166
|
+
return '#ef4444';
|
|
167
|
+
};
|
|
168
|
+
const getMethodColor = (method) => {
|
|
169
|
+
switch (method.toUpperCase()) {
|
|
170
|
+
case 'GET':
|
|
171
|
+
return '#10b981';
|
|
172
|
+
case 'POST':
|
|
173
|
+
return '#3b82f6';
|
|
174
|
+
case 'PUT':
|
|
175
|
+
return '#f59e0b';
|
|
176
|
+
case 'PATCH':
|
|
177
|
+
return '#8b5cf6';
|
|
178
|
+
case 'DELETE':
|
|
179
|
+
return '#ef4444';
|
|
180
|
+
default:
|
|
181
|
+
return '#6b7280';
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const formatTime = (date) => {
|
|
185
|
+
return date.toLocaleTimeString('ko-KR', { hour12: false });
|
|
186
|
+
};
|
|
187
|
+
const formatUrl = (url) => {
|
|
188
|
+
try {
|
|
189
|
+
const urlObj = new URL(url);
|
|
190
|
+
return urlObj.pathname + urlObj.search;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return url;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const positionStyles = {
|
|
197
|
+
'top-left': { top: 16, left: 16 },
|
|
198
|
+
'top-right': { top: 16, right: 16 },
|
|
199
|
+
'bottom-left': { bottom: 16, left: 16 },
|
|
200
|
+
'bottom-right': { bottom: 16, right: 16 },
|
|
201
|
+
};
|
|
202
|
+
const containerStyle = {
|
|
203
|
+
position: 'fixed',
|
|
204
|
+
...positionStyles[position],
|
|
205
|
+
zIndex: 99999,
|
|
206
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
207
|
+
};
|
|
208
|
+
const toggleButtonStyle = {
|
|
209
|
+
width: '48px',
|
|
210
|
+
height: '48px',
|
|
211
|
+
borderRadius: '50%',
|
|
212
|
+
backgroundColor: logs.length > 0 ? '#10b981' : '#6b7280',
|
|
213
|
+
color: 'white',
|
|
214
|
+
border: 'none',
|
|
215
|
+
cursor: 'pointer',
|
|
216
|
+
fontSize: '20px',
|
|
217
|
+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
|
218
|
+
display: 'flex',
|
|
219
|
+
alignItems: 'center',
|
|
220
|
+
justifyContent: 'center',
|
|
221
|
+
position: 'relative',
|
|
222
|
+
transition: 'background-color 0.2s',
|
|
223
|
+
};
|
|
224
|
+
const badgeStyle = {
|
|
225
|
+
position: 'absolute',
|
|
226
|
+
top: '-4px',
|
|
227
|
+
right: '-4px',
|
|
228
|
+
backgroundColor: '#ef4444',
|
|
229
|
+
color: 'white',
|
|
230
|
+
borderRadius: '10px',
|
|
231
|
+
padding: '2px 6px',
|
|
232
|
+
fontSize: '11px',
|
|
233
|
+
fontWeight: 'bold',
|
|
234
|
+
minWidth: '20px',
|
|
235
|
+
textAlign: 'center',
|
|
236
|
+
};
|
|
237
|
+
const panelStyle = {
|
|
238
|
+
position: 'absolute',
|
|
239
|
+
bottom: position.includes('bottom') ? '60px' : undefined,
|
|
240
|
+
top: position.includes('top') ? '60px' : undefined,
|
|
241
|
+
right: position.includes('right') ? '0' : undefined,
|
|
242
|
+
left: position.includes('left') ? '0' : undefined,
|
|
243
|
+
backgroundColor: 'white',
|
|
244
|
+
borderRadius: '12px',
|
|
245
|
+
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.15)',
|
|
246
|
+
border: '1px solid #e5e7eb',
|
|
247
|
+
width: '600px',
|
|
248
|
+
maxHeight: '500px',
|
|
249
|
+
overflow: 'hidden',
|
|
250
|
+
display: 'flex',
|
|
251
|
+
flexDirection: 'column',
|
|
252
|
+
};
|
|
253
|
+
const headerStyle = {
|
|
254
|
+
padding: '12px 16px',
|
|
255
|
+
borderBottom: '1px solid #e5e7eb',
|
|
256
|
+
display: 'flex',
|
|
257
|
+
justifyContent: 'space-between',
|
|
258
|
+
alignItems: 'center',
|
|
259
|
+
backgroundColor: '#f9fafb',
|
|
260
|
+
};
|
|
261
|
+
const headerTitleStyle = {
|
|
262
|
+
fontWeight: 'bold',
|
|
263
|
+
fontSize: '14px',
|
|
264
|
+
color: '#111827',
|
|
265
|
+
};
|
|
266
|
+
const clearButtonStyle = {
|
|
267
|
+
padding: '4px 12px',
|
|
268
|
+
backgroundColor: 'white',
|
|
269
|
+
color: '#ef4444',
|
|
270
|
+
border: '1px solid #fecaca',
|
|
271
|
+
borderRadius: '6px',
|
|
272
|
+
cursor: 'pointer',
|
|
273
|
+
fontSize: '12px',
|
|
274
|
+
fontWeight: 600,
|
|
275
|
+
transition: 'background-color 0.15s',
|
|
276
|
+
};
|
|
277
|
+
const contentStyle = {
|
|
278
|
+
display: 'flex',
|
|
279
|
+
flex: 1,
|
|
280
|
+
overflow: 'hidden',
|
|
281
|
+
};
|
|
282
|
+
const logListStyle = {
|
|
283
|
+
flex: selectedLog ? '0 0 300px' : '1',
|
|
284
|
+
overflowY: 'auto',
|
|
285
|
+
borderRight: selectedLog ? '1px solid #e5e7eb' : 'none',
|
|
286
|
+
};
|
|
287
|
+
const logItemStyle = (isSelected) => ({
|
|
288
|
+
padding: '12px 16px',
|
|
289
|
+
borderBottom: '1px solid #f3f4f6',
|
|
290
|
+
cursor: 'pointer',
|
|
291
|
+
backgroundColor: isSelected ? '#eff6ff' : 'white',
|
|
292
|
+
transition: 'background-color 0.15s',
|
|
293
|
+
});
|
|
294
|
+
const detailStyle = {
|
|
295
|
+
flex: 1,
|
|
296
|
+
overflowY: 'auto',
|
|
297
|
+
padding: '16px',
|
|
298
|
+
fontSize: '13px',
|
|
299
|
+
};
|
|
300
|
+
const methodBadgeStyle = (method) => ({
|
|
301
|
+
display: 'inline-block',
|
|
302
|
+
padding: '2px 6px',
|
|
303
|
+
backgroundColor: getMethodColor(method),
|
|
304
|
+
color: 'white',
|
|
305
|
+
borderRadius: '4px',
|
|
306
|
+
fontSize: '11px',
|
|
307
|
+
fontWeight: 'bold',
|
|
308
|
+
marginRight: '8px',
|
|
309
|
+
});
|
|
310
|
+
const statusBadgeStyle = (status) => ({
|
|
311
|
+
display: 'inline-block',
|
|
312
|
+
padding: '2px 6px',
|
|
313
|
+
backgroundColor: getStatusColor(status),
|
|
314
|
+
color: 'white',
|
|
315
|
+
borderRadius: '4px',
|
|
316
|
+
fontSize: '11px',
|
|
317
|
+
fontWeight: 'bold',
|
|
318
|
+
marginLeft: '8px',
|
|
319
|
+
});
|
|
320
|
+
return (React.createElement("div", { style: containerStyle },
|
|
321
|
+
React.createElement("button", { onClick: () => setIsOpen(!isOpen), style: toggleButtonStyle, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = logs.length > 0 ? '#059669' : '#4b5563'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = logs.length > 0 ? '#10b981' : '#6b7280') },
|
|
322
|
+
isOpen ? '✕' : '📡',
|
|
323
|
+
logs.length > 0 && React.createElement("div", { style: badgeStyle }, logs.length)),
|
|
324
|
+
isOpen && (React.createElement("div", { style: panelStyle },
|
|
325
|
+
React.createElement("div", { style: headerStyle },
|
|
326
|
+
React.createElement("div", { style: headerTitleStyle },
|
|
327
|
+
"\uD83D\uDCE1 API Logger (",
|
|
328
|
+
logs.length,
|
|
329
|
+
")"),
|
|
330
|
+
React.createElement("button", { onClick: handleClearLogs, style: clearButtonStyle, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#fef2f2'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = 'white') }, "Clear")),
|
|
331
|
+
React.createElement("div", { style: contentStyle },
|
|
332
|
+
React.createElement("div", { style: logListStyle }, logs.length === 0 ? (React.createElement("div", { style: { padding: '20px', textAlign: 'center', color: '#9ca3af', fontSize: '13px' } }, "API \uB85C\uADF8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.")) : (logs.map((log) => (React.createElement("div", { key: log.id, style: logItemStyle(selectedLog?.id === log.id), onClick: () => setSelectedLog(log), onMouseEnter: (e) => {
|
|
333
|
+
if (selectedLog?.id !== log.id) {
|
|
334
|
+
e.currentTarget.style.backgroundColor = '#f9fafb';
|
|
335
|
+
}
|
|
336
|
+
}, onMouseLeave: (e) => {
|
|
337
|
+
if (selectedLog?.id !== log.id) {
|
|
338
|
+
e.currentTarget.style.backgroundColor = 'white';
|
|
339
|
+
}
|
|
340
|
+
} },
|
|
341
|
+
React.createElement("div", { style: { fontSize: '11px', color: '#6b7280', marginBottom: '4px' } },
|
|
342
|
+
formatTime(log.timestamp),
|
|
343
|
+
log.duration && React.createElement("span", { style: { marginLeft: '8px' } },
|
|
344
|
+
log.duration,
|
|
345
|
+
"ms")),
|
|
346
|
+
React.createElement("div", null,
|
|
347
|
+
React.createElement("span", { style: methodBadgeStyle(log.method) }, log.method),
|
|
348
|
+
log.status && React.createElement("span", { style: statusBadgeStyle(log.status) }, log.status)),
|
|
349
|
+
React.createElement("div", { style: {
|
|
350
|
+
fontSize: '12px',
|
|
351
|
+
color: '#374151',
|
|
352
|
+
marginTop: '4px',
|
|
353
|
+
overflow: 'hidden',
|
|
354
|
+
textOverflow: 'ellipsis',
|
|
355
|
+
whiteSpace: 'nowrap',
|
|
356
|
+
} }, formatUrl(log.url)),
|
|
357
|
+
log.error && (React.createElement("div", { style: { fontSize: '11px', color: '#ef4444', marginTop: '4px' } },
|
|
358
|
+
"Error: ",
|
|
359
|
+
log.error))))))),
|
|
360
|
+
selectedLog && (React.createElement("div", { style: detailStyle },
|
|
361
|
+
React.createElement("div", { style: { marginBottom: '16px', display: 'flex', justifyContent: 'space-between' } },
|
|
362
|
+
React.createElement("h3", { style: { margin: 0, fontSize: '14px', fontWeight: 'bold' } }, "\uC0C1\uC138 \uC815\uBCF4"),
|
|
363
|
+
React.createElement("button", { onClick: () => handleCopyLog(selectedLog), style: {
|
|
364
|
+
padding: '4px 8px',
|
|
365
|
+
backgroundColor: isCopied ? '#10b981' : '#3b82f6',
|
|
366
|
+
color: 'white',
|
|
367
|
+
border: 'none',
|
|
368
|
+
borderRadius: '4px',
|
|
369
|
+
cursor: 'pointer',
|
|
370
|
+
fontSize: '11px',
|
|
371
|
+
transition: 'background-color 0.2s',
|
|
372
|
+
} }, isCopied ? '✓ Copied' : 'Copy JSON')),
|
|
373
|
+
React.createElement("div", { style: { marginBottom: '12px' } },
|
|
374
|
+
React.createElement("strong", null, "URL:"),
|
|
375
|
+
React.createElement("div", { style: {
|
|
376
|
+
marginTop: '4px',
|
|
377
|
+
padding: '8px',
|
|
378
|
+
backgroundColor: '#f3f4f6',
|
|
379
|
+
borderRadius: '6px',
|
|
380
|
+
fontSize: '12px',
|
|
381
|
+
fontFamily: 'monospace',
|
|
382
|
+
wordBreak: 'break-all',
|
|
383
|
+
} }, selectedLog.url)),
|
|
384
|
+
selectedLog.requestBody !== undefined && (React.createElement("div", { style: { marginBottom: '12px' } },
|
|
385
|
+
React.createElement("strong", null, "Request Body:"),
|
|
386
|
+
React.createElement("pre", { style: {
|
|
387
|
+
marginTop: '4px',
|
|
388
|
+
padding: '8px',
|
|
389
|
+
backgroundColor: '#f3f4f6',
|
|
390
|
+
borderRadius: '6px',
|
|
391
|
+
fontSize: '11px',
|
|
392
|
+
fontFamily: 'monospace',
|
|
393
|
+
overflow: 'auto',
|
|
394
|
+
maxHeight: '150px',
|
|
395
|
+
} }, JSON.stringify(selectedLog.requestBody, null, 2)))),
|
|
396
|
+
selectedLog.responseBody !== undefined && (React.createElement("div", { style: { marginBottom: '12px' } },
|
|
397
|
+
React.createElement("strong", null, "Response Body:"),
|
|
398
|
+
React.createElement("pre", { style: {
|
|
399
|
+
marginTop: '4px',
|
|
400
|
+
padding: '8px',
|
|
401
|
+
backgroundColor: '#f3f4f6',
|
|
402
|
+
borderRadius: '6px',
|
|
403
|
+
fontSize: '11px',
|
|
404
|
+
fontFamily: 'monospace',
|
|
405
|
+
overflow: 'auto',
|
|
406
|
+
maxHeight: '150px',
|
|
407
|
+
} }, JSON.stringify(selectedLog.responseBody, null, 2)))))))))));
|
|
408
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** 패널 초기 위치 (기본값: 'bottom-right') */
|
|
4
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* 개발자 도구 패널
|
|
8
|
+
* 여러 개발용 도구를 하나의 패널에서 관리할 수 있습니다.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* // Vite 프로젝트
|
|
13
|
+
* import { DevPanel } from 'goodchuck-utils/components/dev';
|
|
14
|
+
*
|
|
15
|
+
* function App() {
|
|
16
|
+
* return (
|
|
17
|
+
* <div>
|
|
18
|
+
* {import.meta.env.DEV && <DevPanel />}
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // Create React App 프로젝트
|
|
27
|
+
* {process.env.NODE_ENV === 'development' && <DevPanel position="top-left" />}
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export default function DevPanel({ position }: Props): React.JSX.Element;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=DevPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevPanel.d.ts","sourceRoot":"","sources":["../../../src/components/dev/DevPanel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0C,MAAM,OAAO,CAAC;AAG/D,KAAK,KAAK,GAAG;IACX,qCAAqC;IACrC,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;CACtE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,CAAC,OAAO,UAAU,QAAQ,CAAC,EAAE,QAAyB,EAAE,EAAE,KAAK,qBA6OpE"}
|