@blastlabs/utils 1.11.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.
Files changed (137) hide show
  1. package/README.md +267 -0
  2. package/dist/components/dev/ApiLogger.d.ts +136 -0
  3. package/dist/components/dev/ApiLogger.d.ts.map +1 -0
  4. package/dist/components/dev/ApiLogger.js +408 -0
  5. package/dist/components/dev/DevPanel.d.ts +32 -0
  6. package/dist/components/dev/DevPanel.d.ts.map +1 -0
  7. package/dist/components/dev/DevPanel.js +196 -0
  8. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +136 -0
  9. package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -0
  10. package/dist/components/dev/FormDevTools/FormDevTools.js +442 -0
  11. package/dist/components/dev/FormDevTools/index.d.ts +3 -0
  12. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -0
  13. package/dist/components/dev/FormDevTools/index.js +1 -0
  14. package/dist/components/dev/FormDevTools/styles.d.ts +45 -0
  15. package/dist/components/dev/FormDevTools/styles.d.ts.map +1 -0
  16. package/dist/components/dev/FormDevTools/styles.js +197 -0
  17. package/dist/components/dev/IdSelector.d.ts +50 -0
  18. package/dist/components/dev/IdSelector.d.ts.map +1 -0
  19. package/dist/components/dev/IdSelector.js +129 -0
  20. package/dist/components/dev/WindowSizeDisplay.d.ts +44 -0
  21. package/dist/components/dev/WindowSizeDisplay.d.ts.map +1 -0
  22. package/dist/components/dev/WindowSizeDisplay.js +74 -0
  23. package/dist/components/dev/ZIndexDebugger.d.ts +32 -0
  24. package/dist/components/dev/ZIndexDebugger.d.ts.map +1 -0
  25. package/dist/components/dev/ZIndexDebugger.js +184 -0
  26. package/dist/components/dev/index.d.ts +15 -0
  27. package/dist/components/dev/index.d.ts.map +1 -0
  28. package/dist/components/dev/index.js +12 -0
  29. package/dist/components/index.d.ts +8 -0
  30. package/dist/components/index.d.ts.map +1 -0
  31. package/dist/components/index.js +7 -0
  32. package/dist/date/index.d.ts +64 -0
  33. package/dist/date/index.d.ts.map +1 -0
  34. package/dist/date/index.js +92 -0
  35. package/dist/date/index.test.d.ts +2 -0
  36. package/dist/date/index.test.d.ts.map +1 -0
  37. package/dist/date/index.test.js +166 -0
  38. package/dist/form/__tests__/formatter.test.d.ts +2 -0
  39. package/dist/form/__tests__/formatter.test.d.ts.map +1 -0
  40. package/dist/form/__tests__/formatter.test.js +74 -0
  41. package/dist/form/__tests__/helpers.test.d.ts +2 -0
  42. package/dist/form/__tests__/helpers.test.d.ts.map +1 -0
  43. package/dist/form/__tests__/helpers.test.js +42 -0
  44. package/dist/form/__tests__/validation.test.d.ts +2 -0
  45. package/dist/form/__tests__/validation.test.d.ts.map +1 -0
  46. package/dist/form/__tests__/validation.test.js +67 -0
  47. package/dist/form/formatter.d.ts +34 -0
  48. package/dist/form/formatter.d.ts.map +1 -0
  49. package/dist/form/formatter.js +76 -0
  50. package/dist/form/helpers.d.ts +16 -0
  51. package/dist/form/helpers.d.ts.map +1 -0
  52. package/dist/form/helpers.js +34 -0
  53. package/dist/form/index.d.ts +9 -0
  54. package/dist/form/index.d.ts.map +1 -0
  55. package/dist/form/index.js +11 -0
  56. package/dist/form/validation.d.ts +33 -0
  57. package/dist/form/validation.d.ts.map +1 -0
  58. package/dist/form/validation.js +56 -0
  59. package/dist/hooks/index.d.ts +19 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/index.js +23 -0
  62. package/dist/hooks/useClickOutside.d.ts +49 -0
  63. package/dist/hooks/useClickOutside.d.ts.map +1 -0
  64. package/dist/hooks/useClickOutside.js +94 -0
  65. package/dist/hooks/useCopyToClipboard.d.ts +67 -0
  66. package/dist/hooks/useCopyToClipboard.d.ts.map +1 -0
  67. package/dist/hooks/useCopyToClipboard.js +79 -0
  68. package/dist/hooks/useDebounce.d.ts +47 -0
  69. package/dist/hooks/useDebounce.d.ts.map +1 -0
  70. package/dist/hooks/useDebounce.js +60 -0
  71. package/dist/hooks/useEventListener.d.ts +79 -0
  72. package/dist/hooks/useEventListener.d.ts.map +1 -0
  73. package/dist/hooks/useEventListener.js +33 -0
  74. package/dist/hooks/useIntersectionObserver.d.ts +109 -0
  75. package/dist/hooks/useIntersectionObserver.d.ts.map +1 -0
  76. package/dist/hooks/useIntersectionObserver.js +128 -0
  77. package/dist/hooks/useLocalStorage.d.ts +19 -0
  78. package/dist/hooks/useLocalStorage.d.ts.map +1 -0
  79. package/dist/hooks/useLocalStorage.js +91 -0
  80. package/dist/hooks/useMediaQuery.d.ts +56 -0
  81. package/dist/hooks/useMediaQuery.d.ts.map +1 -0
  82. package/dist/hooks/useMediaQuery.js +104 -0
  83. package/dist/hooks/usePrevious.d.ts +58 -0
  84. package/dist/hooks/usePrevious.d.ts.map +1 -0
  85. package/dist/hooks/usePrevious.js +67 -0
  86. package/dist/hooks/useSessionStorage.d.ts +19 -0
  87. package/dist/hooks/useSessionStorage.d.ts.map +1 -0
  88. package/dist/hooks/useSessionStorage.js +85 -0
  89. package/dist/hooks/useThrottle.d.ts +57 -0
  90. package/dist/hooks/useThrottle.d.ts.map +1 -0
  91. package/dist/hooks/useThrottle.js +80 -0
  92. package/dist/hooks/useToggle.d.ts +49 -0
  93. package/dist/hooks/useToggle.d.ts.map +1 -0
  94. package/dist/hooks/useToggle.js +56 -0
  95. package/dist/hooks/useWindowSize.d.ts +58 -0
  96. package/dist/hooks/useWindowSize.d.ts.map +1 -0
  97. package/dist/hooks/useWindowSize.js +79 -0
  98. package/dist/index.d.ts +6 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +17 -0
  101. package/dist/mock/form.d.ts +41 -0
  102. package/dist/mock/form.d.ts.map +1 -0
  103. package/dist/mock/form.js +195 -0
  104. package/dist/mock/generators.d.ts +112 -0
  105. package/dist/mock/generators.d.ts.map +1 -0
  106. package/dist/mock/generators.js +195 -0
  107. package/dist/mock/index.d.ts +8 -0
  108. package/dist/mock/index.d.ts.map +1 -0
  109. package/dist/mock/index.js +9 -0
  110. package/dist/number/format.d.ts +116 -0
  111. package/dist/number/format.d.ts.map +1 -0
  112. package/dist/number/format.js +165 -0
  113. package/dist/number/index.d.ts +7 -0
  114. package/dist/number/index.d.ts.map +1 -0
  115. package/dist/number/index.js +7 -0
  116. package/dist/string/__tests__/case.test.d.ts +2 -0
  117. package/dist/string/__tests__/case.test.d.ts.map +1 -0
  118. package/dist/string/__tests__/case.test.js +61 -0
  119. package/dist/string/__tests__/manipulation.test.d.ts +2 -0
  120. package/dist/string/__tests__/manipulation.test.d.ts.map +1 -0
  121. package/dist/string/__tests__/manipulation.test.js +109 -0
  122. package/dist/string/__tests__/validation.test.d.ts +2 -0
  123. package/dist/string/__tests__/validation.test.d.ts.map +1 -0
  124. package/dist/string/__tests__/validation.test.js +101 -0
  125. package/dist/string/case.d.ts +42 -0
  126. package/dist/string/case.d.ts.map +1 -0
  127. package/dist/string/case.js +71 -0
  128. package/dist/string/index.d.ts +9 -0
  129. package/dist/string/index.d.ts.map +1 -0
  130. package/dist/string/index.js +11 -0
  131. package/dist/string/manipulation.d.ts +61 -0
  132. package/dist/string/manipulation.d.ts.map +1 -0
  133. package/dist/string/manipulation.js +106 -0
  134. package/dist/string/validation.d.ts +79 -0
  135. package/dist/string/validation.d.ts.map +1 -0
  136. package/dist/string/validation.js +115 -0
  137. package/package.json +86 -0
@@ -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"}
@@ -0,0 +1,196 @@
1
+ import React, { useState, useRef } from 'react';
2
+ import { useWindowSize } from '../../hooks/useWindowSize';
3
+ /**
4
+ * 개발자 도구 패널
5
+ * 여러 개발용 도구를 하나의 패널에서 관리할 수 있습니다.
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * // Vite 프로젝트
10
+ * import { DevPanel } from 'goodchuck-utils/components/dev';
11
+ *
12
+ * function App() {
13
+ * return (
14
+ * <div>
15
+ * {import.meta.env.DEV && <DevPanel />}
16
+ * </div>
17
+ * );
18
+ * }
19
+ * ```
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * // Create React App 프로젝트
24
+ * {process.env.NODE_ENV === 'development' && <DevPanel position="top-left" />}
25
+ * ```
26
+ */
27
+ export default function DevPanel({ position = 'bottom-right' }) {
28
+ const [isOpen, setIsOpen] = useState(false);
29
+ const [showWindowSize, setShowWindowSize] = useState(false);
30
+ const [showRenderCount, setShowRenderCount] = useState(false);
31
+ const renderCount = useRef(0);
32
+ const { width, height } = useWindowSize();
33
+ // 렌더 카운트 증가
34
+ renderCount.current += 1;
35
+ const positionStyles = {
36
+ 'top-left': { top: 16, left: 16 },
37
+ 'top-right': { top: 16, right: 16 },
38
+ 'bottom-left': { bottom: 16, left: 16 },
39
+ 'bottom-right': { bottom: 16, right: 16 },
40
+ };
41
+ const containerStyle = {
42
+ position: 'fixed',
43
+ ...positionStyles[position],
44
+ zIndex: 99999,
45
+ fontFamily: 'system-ui, -apple-system, sans-serif',
46
+ };
47
+ const toggleButtonStyle = {
48
+ width: '48px',
49
+ height: '48px',
50
+ borderRadius: '50%',
51
+ backgroundColor: '#3b82f6',
52
+ color: 'white',
53
+ border: 'none',
54
+ cursor: 'pointer',
55
+ fontSize: '20px',
56
+ fontWeight: 'bold',
57
+ boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ justifyContent: 'center',
61
+ transition: 'background-color 0.2s',
62
+ };
63
+ const panelStyle = {
64
+ position: 'absolute',
65
+ bottom: position.includes('bottom') ? '60px' : undefined,
66
+ top: position.includes('top') ? '60px' : undefined,
67
+ right: position.includes('right') ? '0' : undefined,
68
+ left: position.includes('left') ? '0' : undefined,
69
+ backgroundColor: 'white',
70
+ borderRadius: '12px',
71
+ boxShadow: '0 10px 25px rgba(0, 0, 0, 0.15)',
72
+ border: '1px solid #e5e7eb',
73
+ minWidth: '280px',
74
+ overflow: 'hidden',
75
+ color: '#111827',
76
+ };
77
+ const headerStyle = {
78
+ padding: '12px 16px',
79
+ borderBottom: '1px solid #e5e7eb',
80
+ fontWeight: 'bold',
81
+ fontSize: '14px',
82
+ color: '#111827',
83
+ backgroundColor: '#f9fafb',
84
+ };
85
+ const contentStyle = {
86
+ padding: '8px',
87
+ display: 'flex',
88
+ flexDirection: 'column',
89
+ gap: '4px',
90
+ };
91
+ const toggleItemStyle = {
92
+ display: 'flex',
93
+ alignItems: 'center',
94
+ justifyContent: 'space-between',
95
+ padding: '8px 12px',
96
+ borderRadius: '6px',
97
+ fontSize: '13px',
98
+ cursor: 'pointer',
99
+ transition: 'background-color 0.15s',
100
+ backgroundColor: 'transparent',
101
+ };
102
+ const getSwitchStyle = (isOn) => ({
103
+ width: '36px',
104
+ height: '20px',
105
+ backgroundColor: isOn ? '#3b82f6' : '#d1d5db',
106
+ borderRadius: '10px',
107
+ position: 'relative',
108
+ transition: 'background-color 0.2s',
109
+ cursor: 'pointer',
110
+ });
111
+ const getSwitchKnobStyle = (isOn) => ({
112
+ width: '16px',
113
+ height: '16px',
114
+ backgroundColor: 'white',
115
+ borderRadius: '50%',
116
+ position: 'absolute',
117
+ top: '2px',
118
+ left: isOn ? '18px' : '2px',
119
+ transition: 'left 0.2s',
120
+ });
121
+ const overlayStyle = {
122
+ position: 'fixed',
123
+ padding: '8px 12px',
124
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
125
+ color: 'white',
126
+ fontSize: '14px',
127
+ fontFamily: 'monospace',
128
+ borderRadius: '8px',
129
+ zIndex: 99998,
130
+ };
131
+ const handleClearLocalStorage = () => {
132
+ if (confirm('LocalStorage를 모두 삭제하시겠습니까?')) {
133
+ localStorage.clear();
134
+ alert('LocalStorage가 삭제되었습니다.');
135
+ }
136
+ };
137
+ const handleClearSessionStorage = () => {
138
+ if (confirm('SessionStorage를 모두 삭제하시겠습니까?')) {
139
+ sessionStorage.clear();
140
+ alert('SessionStorage가 삭제되었습니다.');
141
+ }
142
+ };
143
+ return (React.createElement(React.Fragment, null,
144
+ React.createElement("div", { style: containerStyle },
145
+ React.createElement("button", { onClick: () => setIsOpen(!isOpen), style: toggleButtonStyle, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#2563eb'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = '#3b82f6') }, isOpen ? '✕' : '🛠'),
146
+ isOpen && (React.createElement("div", { style: panelStyle },
147
+ React.createElement("div", { style: headerStyle }, "\u2699\uFE0F \uAC1C\uBC1C\uC790 \uB3C4\uAD6C"),
148
+ React.createElement("div", { style: contentStyle },
149
+ React.createElement("div", { style: toggleItemStyle, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => setShowWindowSize(!showWindowSize) },
150
+ React.createElement("span", null, "\uC708\uB3C4\uC6B0 \uD06C\uAE30 \uD45C\uC2DC"),
151
+ React.createElement("div", { style: getSwitchStyle(showWindowSize) },
152
+ React.createElement("div", { style: getSwitchKnobStyle(showWindowSize) }))),
153
+ React.createElement("div", { style: toggleItemStyle, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => setShowRenderCount(!showRenderCount) },
154
+ React.createElement("span", null, "\uB80C\uB354 \uCE74\uC6B4\uD2B8 \uD45C\uC2DC"),
155
+ React.createElement("div", { style: getSwitchStyle(showRenderCount) },
156
+ React.createElement("div", { style: getSwitchKnobStyle(showRenderCount) }))),
157
+ React.createElement("div", { style: { height: '1px', backgroundColor: '#e5e7eb', margin: '8px 0' } }),
158
+ React.createElement("button", { onClick: handleClearLocalStorage, style: {
159
+ ...toggleItemStyle,
160
+ border: '1px solid #e5e7eb',
161
+ backgroundColor: 'white',
162
+ color: '#dc2626',
163
+ fontWeight: 500,
164
+ }, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#fef2f2'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = 'white') }, "\uD83D\uDDD1\uFE0F LocalStorage \uC0AD\uC81C"),
165
+ React.createElement("button", { onClick: handleClearSessionStorage, style: {
166
+ ...toggleItemStyle,
167
+ border: '1px solid #e5e7eb',
168
+ backgroundColor: 'white',
169
+ color: '#dc2626',
170
+ fontWeight: 500,
171
+ }, onMouseEnter: (e) => (e.currentTarget.style.backgroundColor = '#fef2f2'), onMouseLeave: (e) => (e.currentTarget.style.backgroundColor = 'white') }, "\uD83D\uDDD1\uFE0F SessionStorage \uC0AD\uC81C"))))),
172
+ showWindowSize && (React.createElement("div", { style: {
173
+ ...overlayStyle,
174
+ top: 16,
175
+ left: 16,
176
+ } },
177
+ React.createElement("span", { style: { color: '#60a5fa' } }, "W:"),
178
+ ' ',
179
+ React.createElement("span", { style: { fontWeight: 'bold' } },
180
+ width,
181
+ "px"),
182
+ React.createElement("span", { style: { color: '#9ca3af', margin: '0 4px' } }, "\u00D7"),
183
+ React.createElement("span", { style: { color: '#4ade80' } }, "H:"),
184
+ ' ',
185
+ React.createElement("span", { style: { fontWeight: 'bold' } },
186
+ height,
187
+ "px"))),
188
+ showRenderCount && (React.createElement("div", { style: {
189
+ ...overlayStyle,
190
+ top: 16,
191
+ right: 16,
192
+ } },
193
+ React.createElement("span", { style: { color: '#fbbf24' } }, "Renders:"),
194
+ ' ',
195
+ React.createElement("span", { style: { fontWeight: 'bold' } }, renderCount.current)))));
196
+ }