@blastlabs/utils 1.21.0 → 2.1.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 (44) hide show
  1. package/README.md +102 -8
  2. package/bin/Makefile +23 -0
  3. package/bin/init-routes.cjs +422 -0
  4. package/dist/components/dev/DevPanel.d.ts +16 -11
  5. package/dist/components/dev/DevPanel.d.ts.map +1 -1
  6. package/dist/components/dev/DevPanel.js +71 -77
  7. package/dist/components/dev/DevPanel.test.d.ts +2 -0
  8. package/dist/components/dev/DevPanel.test.d.ts.map +1 -0
  9. package/dist/components/dev/DevPanel.test.js +194 -0
  10. package/dist/components/dev/DevToolsProvider/DevToolsProvider.d.ts +97 -0
  11. package/dist/components/dev/DevToolsProvider/DevToolsProvider.d.ts.map +1 -0
  12. package/dist/components/dev/DevToolsProvider/DevToolsProvider.js +122 -0
  13. package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.d.ts +2 -0
  14. package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.d.ts.map +1 -0
  15. package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.js +104 -0
  16. package/dist/components/dev/DevToolsProvider/index.d.ts +3 -0
  17. package/dist/components/dev/DevToolsProvider/index.d.ts.map +1 -0
  18. package/dist/components/dev/DevToolsProvider/index.js +1 -0
  19. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +5 -70
  20. package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -1
  21. package/dist/components/dev/FormDevTools/FormDevTools.js +163 -236
  22. package/dist/components/dev/FormDevTools/FormDevToolsContent.d.ts +27 -0
  23. package/dist/components/dev/FormDevTools/FormDevToolsContent.d.ts.map +1 -0
  24. package/dist/components/dev/FormDevTools/FormDevToolsContent.js +298 -0
  25. package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.d.ts +29 -0
  26. package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.d.ts.map +1 -0
  27. package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.js +122 -0
  28. package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.d.ts +4 -0
  29. package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.d.ts.map +1 -0
  30. package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.js +121 -0
  31. package/dist/components/dev/TimezoneDevTools/index.d.ts +3 -0
  32. package/dist/components/dev/TimezoneDevTools/index.d.ts.map +1 -0
  33. package/dist/components/dev/TimezoneDevTools/index.js +1 -0
  34. package/dist/components/dev/TimezoneDevTools/styles.d.ts +12 -0
  35. package/dist/components/dev/TimezoneDevTools/styles.d.ts.map +1 -0
  36. package/dist/components/dev/TimezoneDevTools/styles.js +65 -0
  37. package/dist/components/dev/ZIndexDebugger.js +1 -1
  38. package/dist/components/dev/index.d.ts +4 -2
  39. package/dist/components/dev/index.d.ts.map +1 -1
  40. package/dist/components/dev/index.js +6 -1
  41. package/dist/date/index.d.ts +11 -0
  42. package/dist/date/index.d.ts.map +1 -1
  43. package/dist/date/index.js +43 -0
  44. package/package.json +4 -2
@@ -0,0 +1,298 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { useCopyToClipboard } from '../../../hooks';
3
+ import { getPanelStyle, headerStyle, headerTitleStyle, getStatusBadgeStyle, getCopyButtonStyle, tabContainerStyle, getTabStyle, contentStyle, sectionTitleStyle, codeBlockStyle, errorItemStyle, errorLabelStyle, errorMessageStyle, statsContainerStyle, statCardStyle, statLabelStyle, statValueStyle, resizeHandleStyle, resizeHandleIndicatorStyle, } from './styles';
4
+ export default function FormDevToolsContent({ form, validationSchema, generateMock, title = 'Form DevTools' }) {
5
+ const { formState, watch, setValue, trigger } = form;
6
+ const values = watch();
7
+ const originalValues = formState.defaultValues;
8
+ const [activeTab, setActiveTab] = useState('all');
9
+ const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 });
10
+ const [panelSize, setPanelSize] = useState({ width: 500, height: 400 });
11
+ const [isDragging, setIsDragging] = useState(false);
12
+ const [isResizing, setIsResizing] = useState(false);
13
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
14
+ const [resizeStart, setResizeStart] = useState({ x: 0, y: 0, width: 0, height: 0 });
15
+ const [isGenerating, setIsGenerating] = useState(false);
16
+ const [generateError, setGenerateError] = useState(null);
17
+ const [mounted, setMounted] = useState(false);
18
+ const panelRef = useRef(null);
19
+ const { copy, copiedText } = useCopyToClipboard();
20
+ useEffect(() => {
21
+ setMounted(true);
22
+ }, []);
23
+ const handleCopy = () => {
24
+ const data = {
25
+ values,
26
+ errors: formState.errors,
27
+ changedFields,
28
+ dirtyFields: formState.dirtyFields,
29
+ touchedFields: formState.touchedFields,
30
+ isValid: formState.isValid,
31
+ isSubmitting: formState.isSubmitting,
32
+ submitCount: formState.submitCount,
33
+ };
34
+ copy(JSON.stringify(data, null, 2));
35
+ };
36
+ const isCopied = copiedText !== null;
37
+ const errorCount = Object.keys(formState.errors || {}).length;
38
+ const dirtyFieldsCount = Object.keys(formState.dirtyFields || {}).length;
39
+ const touchedFieldsCount = Object.keys(formState.touchedFields || {}).length;
40
+ const getChangedFields = () => {
41
+ if (!formState.dirtyFields || !values)
42
+ return {};
43
+ const changed = {};
44
+ const getNestedValue = (obj, path) => {
45
+ return path.split('.').reduce((acc, key) => acc?.[key], obj);
46
+ };
47
+ const processDirtyFields = (dirty, prefix = '') => {
48
+ Object.keys(dirty).forEach((key) => {
49
+ const fullPath = prefix ? `${prefix}.${key}` : key;
50
+ const dirtyValue = dirty[key];
51
+ if (dirtyValue === true) {
52
+ const currentValue = getNestedValue(values, fullPath);
53
+ const originalValue = originalValues ? getNestedValue(originalValues, fullPath) : undefined;
54
+ if (JSON.stringify(currentValue) !== JSON.stringify(originalValue)) {
55
+ changed[fullPath] = {
56
+ from: originalValue,
57
+ to: currentValue,
58
+ };
59
+ }
60
+ }
61
+ else if (typeof dirtyValue === 'object' && dirtyValue !== null) {
62
+ processDirtyFields(dirtyValue, fullPath);
63
+ }
64
+ });
65
+ };
66
+ processDirtyFields(formState.dirtyFields);
67
+ return changed;
68
+ };
69
+ const changedFields = getChangedFields();
70
+ const changedFieldsCount = Object.keys(changedFields).length;
71
+ useEffect(() => {
72
+ const handleMouseMove = (e) => {
73
+ if (isDragging) {
74
+ const deltaX = e.clientX - dragStart.x;
75
+ const deltaY = e.clientY - dragStart.y;
76
+ setPanelPosition((prev) => ({
77
+ x: prev.x + deltaX,
78
+ y: prev.y + deltaY,
79
+ }));
80
+ setDragStart({ x: e.clientX, y: e.clientY });
81
+ }
82
+ if (isResizing) {
83
+ const deltaX = e.clientX - resizeStart.x;
84
+ const deltaY = e.clientY - resizeStart.y;
85
+ const maxHeight = window.innerHeight * 0.85;
86
+ setPanelSize({
87
+ width: Math.max(300, resizeStart.width + deltaX),
88
+ height: Math.min(maxHeight, Math.max(200, resizeStart.height + deltaY)),
89
+ });
90
+ }
91
+ };
92
+ const handleMouseUp = () => {
93
+ setIsDragging(false);
94
+ setIsResizing(false);
95
+ };
96
+ if (isDragging || isResizing) {
97
+ document.addEventListener('mousemove', handleMouseMove);
98
+ document.addEventListener('mouseup', handleMouseUp);
99
+ return () => {
100
+ document.removeEventListener('mousemove', handleMouseMove);
101
+ document.removeEventListener('mouseup', handleMouseUp);
102
+ };
103
+ }
104
+ }, [isDragging, isResizing, dragStart, resizeStart]);
105
+ const handleHeaderMouseDown = (e) => {
106
+ if (panelRef.current) {
107
+ const rect = panelRef.current.getBoundingClientRect();
108
+ setIsDragging(true);
109
+ setDragStart({
110
+ x: e.clientX,
111
+ y: e.clientY,
112
+ });
113
+ setPanelPosition({
114
+ x: rect.left,
115
+ y: rect.top,
116
+ });
117
+ }
118
+ };
119
+ const handleResizeMouseDown = (e) => {
120
+ e.stopPropagation();
121
+ setIsResizing(true);
122
+ setResizeStart({
123
+ x: e.clientX,
124
+ y: e.clientY,
125
+ width: panelSize.width,
126
+ height: panelSize.height,
127
+ });
128
+ };
129
+ const renderErrors = () => {
130
+ if (!formState.errors || Object.keys(formState.errors).length === 0) {
131
+ return (React.createElement("div", { style: { textAlign: 'center', color: '#9ca3af', padding: '20px', fontSize: '13px' } }, "No validation errors"));
132
+ }
133
+ return Object.entries(formState.errors).map(([field, error]) => (React.createElement("div", { key: field, style: errorItemStyle },
134
+ React.createElement("div", { style: errorLabelStyle }, field),
135
+ React.createElement("div", { style: errorMessageStyle }, error?.message || 'Invalid value'))));
136
+ };
137
+ const handleGenerateMock = async () => {
138
+ if (!generateMock) {
139
+ setGenerateError('generateMock 함수가 필요합니다');
140
+ return;
141
+ }
142
+ setIsGenerating(true);
143
+ setGenerateError(null);
144
+ try {
145
+ const mockData = await generateMock({
146
+ values,
147
+ originalValues,
148
+ });
149
+ const setNestedValue = (data, prefix = '') => {
150
+ Object.keys(data).forEach((key) => {
151
+ const fullPath = prefix ? `${prefix}.${key}` : key;
152
+ const value = data[key];
153
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
154
+ setNestedValue(value, fullPath);
155
+ }
156
+ else {
157
+ setValue(fullPath, value, {
158
+ shouldDirty: true,
159
+ shouldValidate: true,
160
+ });
161
+ }
162
+ });
163
+ };
164
+ setNestedValue(mockData);
165
+ if (trigger) {
166
+ await trigger();
167
+ }
168
+ }
169
+ catch (error) {
170
+ setGenerateError(error.message || 'Mock 데이터 생성 실패');
171
+ }
172
+ finally {
173
+ setIsGenerating(false);
174
+ }
175
+ };
176
+ return (React.createElement("div", { ref: panelRef, style: getPanelStyle('bottom-right', panelPosition, panelSize, isDragging) },
177
+ React.createElement("div", { style: headerStyle, onMouseDown: handleHeaderMouseDown },
178
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
179
+ React.createElement("div", { style: headerTitleStyle },
180
+ "\uD83D\uDCDD ",
181
+ title),
182
+ React.createElement("div", { style: getStatusBadgeStyle(formState.isValid) }, formState.isValid ? '✓ Valid' : `✗ ${errorCount} Error${errorCount > 1 ? 's' : ''}`)),
183
+ React.createElement("div", { style: { display: 'flex', gap: '8px', alignItems: 'center' } },
184
+ generateMock && (React.createElement("button", { type: "button", onClick: handleGenerateMock, disabled: isGenerating, style: {
185
+ ...getCopyButtonStyle(false),
186
+ backgroundColor: isGenerating ? '#9ca3af' : '#10b981',
187
+ opacity: isGenerating ? 0.6 : 1,
188
+ cursor: isGenerating ? 'not-allowed' : 'pointer',
189
+ }, onMouseEnter: (e) => {
190
+ if (!isGenerating)
191
+ e.currentTarget.style.backgroundColor = '#059669';
192
+ }, onMouseLeave: (e) => {
193
+ if (!isGenerating)
194
+ e.currentTarget.style.backgroundColor = '#10b981';
195
+ } }, isGenerating ? '⏳ Generating...' : '🤖 Generate Mock')),
196
+ React.createElement("button", { type: "button", onClick: handleCopy, style: getCopyButtonStyle(isCopied), onMouseEnter: (e) => {
197
+ if (!isCopied)
198
+ e.currentTarget.style.backgroundColor = '#2563eb';
199
+ }, onMouseLeave: (e) => {
200
+ if (!isCopied)
201
+ e.currentTarget.style.backgroundColor = '#3b82f6';
202
+ } }, isCopied ? '✓ Copied' : 'Copy All'))),
203
+ generateError && (React.createElement("div", { style: {
204
+ padding: '8px 16px',
205
+ backgroundColor: '#fef2f2',
206
+ borderBottom: '1px solid #fecaca',
207
+ fontSize: '12px',
208
+ color: '#991b1b',
209
+ } }, generateError)),
210
+ mounted && (React.createElement(React.Fragment, null,
211
+ React.createElement("div", { style: tabContainerStyle },
212
+ React.createElement("button", { type: "button", onClick: () => setActiveTab('all'), style: getTabStyle(activeTab === 'all') }, "All"),
213
+ React.createElement("button", { type: "button", onClick: () => setActiveTab('values'), style: getTabStyle(activeTab === 'values') }, "Values"),
214
+ React.createElement("button", { type: "button", onClick: () => setActiveTab('errors'), style: getTabStyle(activeTab === 'errors') },
215
+ "Errors ",
216
+ errorCount > 0 && `(${errorCount})`),
217
+ React.createElement("button", { type: "button", onClick: () => setActiveTab('changed'), style: getTabStyle(activeTab === 'changed') },
218
+ "Changed ",
219
+ changedFieldsCount > 0 && `(${changedFieldsCount})`),
220
+ React.createElement("button", { type: "button", onClick: () => setActiveTab('state'), style: getTabStyle(activeTab === 'state') }, "State"),
221
+ validationSchema && React.createElement("button", { type: "button", onClick: () => setActiveTab('validation'), style: getTabStyle(activeTab === 'validation') }, "Validation")),
222
+ React.createElement("div", { style: contentStyle },
223
+ activeTab === 'all' && (React.createElement(React.Fragment, null,
224
+ React.createElement("div", { style: statsContainerStyle },
225
+ React.createElement("div", { style: statCardStyle },
226
+ React.createElement("div", { style: statLabelStyle }, "Dirty Fields"),
227
+ React.createElement("div", { style: statValueStyle }, dirtyFieldsCount)),
228
+ React.createElement("div", { style: statCardStyle },
229
+ React.createElement("div", { style: statLabelStyle }, "Touched Fields"),
230
+ React.createElement("div", { style: statValueStyle }, touchedFieldsCount)),
231
+ React.createElement("div", { style: statCardStyle },
232
+ React.createElement("div", { style: statLabelStyle }, "Submit Count"),
233
+ React.createElement("div", { style: statValueStyle }, formState.submitCount || 0)),
234
+ React.createElement("div", { style: statCardStyle },
235
+ React.createElement("div", { style: statLabelStyle }, "Submitting"),
236
+ React.createElement("div", { style: statValueStyle }, formState.isSubmitting ? 'Yes' : 'No'))),
237
+ React.createElement("div", { style: sectionTitleStyle }, "Form Values"),
238
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(values || {}, null, 2)),
239
+ errorCount > 0 && (React.createElement(React.Fragment, null,
240
+ React.createElement("div", { style: sectionTitleStyle },
241
+ "Validation Errors (",
242
+ errorCount,
243
+ ")"),
244
+ renderErrors())),
245
+ changedFieldsCount > 0 && (React.createElement(React.Fragment, null,
246
+ React.createElement("div", { style: sectionTitleStyle },
247
+ "Changed Fields (",
248
+ changedFieldsCount,
249
+ ")"),
250
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(changedFields, null, 2)))),
251
+ dirtyFieldsCount > 0 && (React.createElement(React.Fragment, null,
252
+ React.createElement("div", { style: sectionTitleStyle }, "Dirty Fields"),
253
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(formState.dirtyFields || {}, null, 2)))),
254
+ touchedFieldsCount > 0 && (React.createElement(React.Fragment, null,
255
+ React.createElement("div", { style: sectionTitleStyle }, "Touched Fields"),
256
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(formState.touchedFields || {}, null, 2)))))),
257
+ activeTab === 'values' && (values && Object.keys(values).length > 0 ? (React.createElement(React.Fragment, null,
258
+ React.createElement("div", { style: sectionTitleStyle }, "Form Values"),
259
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(values, null, 2)))) : (React.createElement("div", { style: { textAlign: 'center', color: '#9ca3af', padding: '40px 20px', fontSize: '13px' } },
260
+ React.createElement("div", null, "No form values")))),
261
+ activeTab === 'errors' && (React.createElement(React.Fragment, null,
262
+ React.createElement("div", { style: sectionTitleStyle },
263
+ "Validation Errors ",
264
+ errorCount > 0 && `(${errorCount})`),
265
+ errorCount > 0 ? renderErrors() : React.createElement("div", { style: { textAlign: 'center', color: '#9ca3af', padding: '20px', fontSize: '13px' } }, "No validation errors"))),
266
+ activeTab === 'changed' && (changedFieldsCount > 0 ? (React.createElement(React.Fragment, null,
267
+ React.createElement("div", { style: sectionTitleStyle },
268
+ "Changed Fields (",
269
+ changedFieldsCount,
270
+ ")"),
271
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(changedFields, null, 2)))) : (React.createElement("div", { style: { textAlign: 'center', color: '#9ca3af', padding: '40px 20px', fontSize: '13px' } },
272
+ React.createElement("div", null, "No changed fields")))),
273
+ activeTab === 'state' && (React.createElement(React.Fragment, null,
274
+ React.createElement("div", { style: statsContainerStyle },
275
+ React.createElement("div", { style: statCardStyle },
276
+ React.createElement("div", { style: statLabelStyle }, "Dirty Fields"),
277
+ React.createElement("div", { style: statValueStyle }, dirtyFieldsCount)),
278
+ React.createElement("div", { style: statCardStyle },
279
+ React.createElement("div", { style: statLabelStyle }, "Touched Fields"),
280
+ React.createElement("div", { style: statValueStyle }, touchedFieldsCount)),
281
+ React.createElement("div", { style: statCardStyle },
282
+ React.createElement("div", { style: statLabelStyle }, "Submit Count"),
283
+ React.createElement("div", { style: statValueStyle }, formState.submitCount || 0)),
284
+ React.createElement("div", { style: statCardStyle },
285
+ React.createElement("div", { style: statLabelStyle }, "Submitting"),
286
+ React.createElement("div", { style: statValueStyle }, formState.isSubmitting ? 'Yes' : 'No'))),
287
+ dirtyFieldsCount > 0 && (React.createElement(React.Fragment, null,
288
+ React.createElement("div", { style: sectionTitleStyle }, "Dirty Fields"),
289
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(formState.dirtyFields || {}, null, 2)))),
290
+ touchedFieldsCount > 0 && (React.createElement(React.Fragment, null,
291
+ React.createElement("div", { style: sectionTitleStyle }, "Touched Fields"),
292
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(formState.touchedFields || {}, null, 2)))))),
293
+ activeTab === 'validation' && validationSchema && (React.createElement(React.Fragment, null,
294
+ React.createElement("div", { style: sectionTitleStyle }, "Validation Schema"),
295
+ React.createElement("pre", { style: codeBlockStyle }, JSON.stringify(validationSchema, null, 2))))))),
296
+ React.createElement("div", { onMouseDown: handleResizeMouseDown, style: resizeHandleStyle }),
297
+ React.createElement("div", { onMouseDown: handleResizeMouseDown, style: resizeHandleIndicatorStyle })));
298
+ }
@@ -0,0 +1,29 @@
1
+ import React from 'react';
2
+ import 'dayjs/locale/ko';
3
+ export type Props = {
4
+ /** 패널 초기 위치 (기본값: 'bottom-right') */
5
+ position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
+ };
7
+ /**
8
+ * 타임존 개발용 도구 컴포넌트
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * import { DevToolsPanel } from '@blastlabs/utils/components/dev';
13
+ * import { TimezoneDevTools } from '@blastlabs/utils/components/dev';
14
+ *
15
+ * function MyPage() {
16
+ * return (
17
+ * <>
18
+ * <YourContent />
19
+ *
20
+ * <DevToolsPanel>
21
+ * <TimezoneDevTools />
22
+ * </DevToolsPanel>
23
+ * </>
24
+ * );
25
+ * }
26
+ * ```
27
+ */
28
+ export default function TimezoneDevTools({ position }: Props): React.JSX.Element;
29
+ //# sourceMappingURL=TimezoneDevTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimezoneDevTools.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/TimezoneDevTools/TimezoneDevTools.tsx"],"names":[],"mappings":"AAEA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAInD,OAAO,iBAAiB,CAAC;AAuCzB,MAAM,MAAM,KAAK,GAAG;IAClB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,UAAU,GAAG,WAAW,GAAG,aAAa,GAAG,cAAc,CAAC;CACtE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EAAE,QAAyB,EAAE,EAAE,KAAK,qBAwG5E"}
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+ import React, { useState, useEffect } from 'react';
3
+ import dayjs from 'dayjs';
4
+ import utc from 'dayjs/plugin/utc';
5
+ import timezone from 'dayjs/plugin/timezone';
6
+ import 'dayjs/locale/ko';
7
+ import { getUserTimezone, getTimezoneOffset } from '../../../date';
8
+ import { headerStyle, contentStyle, sectionTitleStyle, cardStyle, cityNameStyle, timezoneStyle, timeStyle, offsetStyle, highlightStyle, } from './styles';
9
+ import { getPanelStyle } from '../FormDevTools/styles';
10
+ dayjs.extend(utc);
11
+ dayjs.extend(timezone);
12
+ const CITIES = [
13
+ { name: '서울', timezone: 'Asia/Seoul', flag: '🇰🇷' },
14
+ { name: '뉴욕', timezone: 'America/New_York', flag: '🇺🇸' },
15
+ { name: '런던', timezone: 'Europe/London', flag: '🇬🇧' },
16
+ { name: '도쿄', timezone: 'Asia/Tokyo', flag: '🇯🇵' },
17
+ { name: '파리', timezone: 'Europe/Paris', flag: '🇫🇷' },
18
+ { name: '시드니', timezone: 'Australia/Sydney', flag: '🇦🇺' },
19
+ { name: '베를린', timezone: 'Europe/Berlin', flag: '🇩🇪' },
20
+ { name: '상하이', timezone: 'Asia/Shanghai', flag: '🇨🇳' },
21
+ ];
22
+ /**
23
+ * 타임존 개발용 도구 컴포넌트
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { DevToolsPanel } from '@blastlabs/utils/components/dev';
28
+ * import { TimezoneDevTools } from '@blastlabs/utils/components/dev';
29
+ *
30
+ * function MyPage() {
31
+ * return (
32
+ * <>
33
+ * <YourContent />
34
+ *
35
+ * <DevToolsPanel>
36
+ * <TimezoneDevTools />
37
+ * </DevToolsPanel>
38
+ * </>
39
+ * );
40
+ * }
41
+ * ```
42
+ */
43
+ export default function TimezoneDevTools({ position = 'bottom-right' }) {
44
+ const [selectedTimezone, setSelectedTimezone] = useState('');
45
+ const [userTimezone, setUserTimezone] = useState('');
46
+ const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 });
47
+ const [isDragging, setIsDragging] = useState(false);
48
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
49
+ const [now, setNow] = useState(dayjs());
50
+ useEffect(() => {
51
+ const tz = getUserTimezone();
52
+ setUserTimezone(tz);
53
+ setSelectedTimezone(tz);
54
+ setNow(dayjs());
55
+ }, []);
56
+ const handleMouseDown = (e) => {
57
+ if (e.button !== 0)
58
+ return;
59
+ setIsDragging(true);
60
+ setDragStart({ x: e.clientX - panelPosition.x, y: e.clientY - panelPosition.y });
61
+ };
62
+ useEffect(() => {
63
+ const handleMouseMove = (e) => {
64
+ if (!isDragging)
65
+ return;
66
+ setPanelPosition({
67
+ x: e.clientX - dragStart.x,
68
+ y: e.clientY - dragStart.y,
69
+ });
70
+ };
71
+ const handleMouseUp = () => {
72
+ setIsDragging(false);
73
+ };
74
+ if (isDragging) {
75
+ document.addEventListener('mousemove', handleMouseMove);
76
+ document.addEventListener('mouseup', handleMouseUp);
77
+ return () => {
78
+ document.removeEventListener('mousemove', handleMouseMove);
79
+ document.removeEventListener('mouseup', handleMouseUp);
80
+ };
81
+ }
82
+ }, [isDragging, dragStart]);
83
+ return (
84
+ // <DevTool icon="🌍" title="Timezone DevTools">
85
+ React.createElement("div", { style: getPanelStyle(position, panelPosition, { width: 360, height: 500 }, isDragging), onMouseDown: handleMouseDown },
86
+ React.createElement("div", { style: headerStyle },
87
+ React.createElement("span", { style: { fontWeight: 'bold', color: '#111827' } }, "\uD83C\uDF0D \uD0C0\uC784\uC874 \uB3C4\uAD6C")),
88
+ React.createElement("div", { style: contentStyle },
89
+ React.createElement("div", { style: { marginBottom: '16px' } },
90
+ React.createElement("div", { style: sectionTitleStyle }, "\uD83D\uDCCD \uB0B4 \uD0C0\uC784\uC874"),
91
+ React.createElement("div", { style: cardStyle },
92
+ React.createElement("div", null,
93
+ React.createElement("div", { style: cityNameStyle }, userTimezone),
94
+ React.createElement("div", { style: timezoneStyle },
95
+ "UTC",
96
+ getTimezoneOffset(userTimezone))),
97
+ React.createElement("div", { style: timeStyle }, now.tz(userTimezone).format('HH:mm:ss')))),
98
+ React.createElement("div", null,
99
+ React.createElement("div", { style: sectionTitleStyle }, "\uD83C\uDF06 \uC8FC\uC694 \uB3C4\uC2DC \uD604\uC9C0 \uC2DC\uAC04"),
100
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, CITIES.map(city => {
101
+ const isSelected = city.timezone === selectedTimezone;
102
+ const cityTime = now.tz(city.timezone);
103
+ const offset = getTimezoneOffset(city.timezone);
104
+ return (React.createElement("div", { key: city.timezone, style: { ...cardStyle, ...(isSelected ? highlightStyle : {}) } },
105
+ React.createElement("div", null,
106
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
107
+ React.createElement("span", { style: { fontSize: '18px' } }, city.flag),
108
+ React.createElement("div", null,
109
+ React.createElement("div", { style: cityNameStyle }, city.name),
110
+ React.createElement("div", { style: timezoneStyle },
111
+ city.timezone,
112
+ React.createElement("span", { style: offsetStyle },
113
+ "UTC",
114
+ offset))))),
115
+ React.createElement("div", { style: timeStyle }, cityTime.format('HH:mm:ss'))));
116
+ }))),
117
+ React.createElement("div", { style: { marginTop: '16px', padding: '12px', backgroundColor: '#f3f4f6', borderRadius: '8px' } },
118
+ React.createElement("div", { style: { fontSize: '11px', color: '#6b7280', marginBottom: '4px' } }, "UTC \uAE30\uC900"),
119
+ React.createElement("div", { style: { fontSize: '20px', fontWeight: 'bold', color: '#111827', fontFamily: 'monospace' } }, now.utc().format('YYYY-MM-DD HH:mm:ss')))))
120
+ // </DevTool>
121
+ );
122
+ }
@@ -0,0 +1,4 @@
1
+ import React from 'react';
2
+ import 'dayjs/locale/ko';
3
+ export default function TimezoneDevToolsContent(): React.JSX.Element;
4
+ //# sourceMappingURL=TimezoneDevToolsContent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TimezoneDevToolsContent.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/TimezoneDevTools/TimezoneDevToolsContent.tsx"],"names":[],"mappings":"AACA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAI3D,OAAO,iBAAiB,CAAC;AAuCzB,MAAM,CAAC,OAAO,UAAU,uBAAuB,sBAoI9C"}
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+ import dayjs from 'dayjs';
4
+ import utc from 'dayjs/plugin/utc';
5
+ import timezone from 'dayjs/plugin/timezone';
6
+ import 'dayjs/locale/ko';
7
+ import { getUserTimezone, getTimezoneOffset } from '../../../date';
8
+ import { headerStyle, contentStyle, sectionTitleStyle, cardStyle, cityNameStyle, timezoneStyle, timeStyle, offsetStyle, highlightStyle, } from './styles';
9
+ import { getPanelStyle } from '../FormDevTools/styles';
10
+ dayjs.extend(utc);
11
+ dayjs.extend(timezone);
12
+ const CITIES = [
13
+ { name: '서울', timezone: 'Asia/Seoul', flag: '🇰🇷' },
14
+ { name: '뉴욕', timezone: 'America/New_York', flag: '🇺🇸' },
15
+ { name: '런던', timezone: 'Europe/London', flag: '🇬🇧' },
16
+ { name: '도쿄', timezone: 'Asia/Tokyo', flag: '🇯🇵' },
17
+ { name: '파리', timezone: 'Europe/Paris', flag: '🇫🇷' },
18
+ { name: '시드니', timezone: 'Australia/Sydney', flag: '🇦🇺' },
19
+ { name: '베를린', timezone: 'Europe/Berlin', flag: '🇩🇪' },
20
+ { name: '상하이', timezone: 'Asia/Shanghai', flag: '🇨🇳' },
21
+ ];
22
+ export default function TimezoneDevToolsContent() {
23
+ const [selectedTimezone, setSelectedTimezone] = useState('');
24
+ const [userTimezone, setUserTimezone] = useState('');
25
+ const [panelPosition, setPanelPosition] = useState({ x: 0, y: 0 });
26
+ const [isDragging, setIsDragging] = useState(false);
27
+ const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
28
+ const [now, setNow] = useState(dayjs('2000-01-01T00:00:00Z')); // 고정된 초기값
29
+ const [mounted, setMounted] = useState(false);
30
+ const panelRef = useRef(null);
31
+ useEffect(() => {
32
+ const tz = getUserTimezone();
33
+ setUserTimezone(tz);
34
+ setSelectedTimezone(tz);
35
+ setNow(dayjs()); // 클라이언트에서만 실제 시간 설정
36
+ setMounted(true);
37
+ }, []);
38
+ // 1초마다 시간 업데이트 (클라이언트 사이드에서만)
39
+ useEffect(() => {
40
+ if (!mounted)
41
+ return;
42
+ const timer = setInterval(() => {
43
+ setNow(dayjs());
44
+ }, 1000);
45
+ return () => clearInterval(timer);
46
+ }, [mounted]);
47
+ // 드래그 핸들러 (FormDevTools와 동일한 방식)
48
+ useEffect(() => {
49
+ const handleMouseMove = (e) => {
50
+ if (isDragging) {
51
+ const deltaX = e.clientX - dragStart.x;
52
+ const deltaY = e.clientY - dragStart.y;
53
+ setPanelPosition((prev) => ({
54
+ x: prev.x + deltaX,
55
+ y: prev.y + deltaY,
56
+ }));
57
+ setDragStart({ x: e.clientX, y: e.clientY });
58
+ }
59
+ };
60
+ const handleMouseUp = () => {
61
+ setIsDragging(false);
62
+ };
63
+ if (isDragging) {
64
+ document.addEventListener('mousemove', handleMouseMove);
65
+ document.addEventListener('mouseup', handleMouseUp);
66
+ return () => {
67
+ document.removeEventListener('mousemove', handleMouseMove);
68
+ document.removeEventListener('mouseup', handleMouseUp);
69
+ };
70
+ }
71
+ }, [isDragging, dragStart]);
72
+ const handleMouseDown = (e) => {
73
+ if (panelRef.current) {
74
+ const rect = panelRef.current.getBoundingClientRect();
75
+ setIsDragging(true);
76
+ setDragStart({
77
+ x: e.clientX,
78
+ y: e.clientY,
79
+ });
80
+ setPanelPosition({
81
+ x: rect.left,
82
+ y: rect.top,
83
+ });
84
+ }
85
+ };
86
+ return (React.createElement("div", { ref: panelRef, style: getPanelStyle('bottom-right', panelPosition, { width: 360, height: 500 }, isDragging), onMouseDown: handleMouseDown },
87
+ React.createElement("div", { style: headerStyle },
88
+ React.createElement("span", { style: { fontWeight: 'bold', color: '#111827' } }, "\uD83C\uDF0D \uD0C0\uC784\uC874 \uB3C4\uAD6C")),
89
+ mounted && (React.createElement("div", { style: contentStyle },
90
+ React.createElement("div", { style: { marginBottom: '16px' } },
91
+ React.createElement("div", { style: sectionTitleStyle }, "\uD83D\uDCCD \uB0B4 \uD0C0\uC784\uC874"),
92
+ React.createElement("div", { style: cardStyle },
93
+ React.createElement("div", null,
94
+ React.createElement("div", { style: cityNameStyle }, userTimezone),
95
+ React.createElement("div", { style: timezoneStyle },
96
+ "UTC",
97
+ getTimezoneOffset(userTimezone))),
98
+ React.createElement("div", { style: timeStyle }, userTimezone ? now.tz(userTimezone).format('HH:mm:ss') : '--:--:--'))),
99
+ React.createElement("div", null,
100
+ React.createElement("div", { style: sectionTitleStyle }, "\uD83C\uDF06 \uC8FC\uC694 \uB3C4\uC2DC \uD604\uC9C0 \uC2DC\uAC04"),
101
+ React.createElement("div", { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, CITIES.map(city => {
102
+ const isSelected = city.timezone === selectedTimezone;
103
+ const cityTime = now.tz(city.timezone);
104
+ const offset = getTimezoneOffset(city.timezone);
105
+ return (React.createElement("div", { key: city.timezone, style: { ...cardStyle, ...(isSelected ? highlightStyle : {}) } },
106
+ React.createElement("div", null,
107
+ React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '8px' } },
108
+ React.createElement("span", { style: { fontSize: '18px' } }, city.flag),
109
+ React.createElement("div", null,
110
+ React.createElement("div", { style: cityNameStyle }, city.name),
111
+ React.createElement("div", { style: timezoneStyle },
112
+ city.timezone,
113
+ React.createElement("span", { style: offsetStyle },
114
+ "UTC",
115
+ offset))))),
116
+ React.createElement("div", { style: timeStyle }, cityTime.format('HH:mm:ss'))));
117
+ }))),
118
+ React.createElement("div", { style: { marginTop: '16px', padding: '12px', backgroundColor: '#f3f4f6', borderRadius: '8px' } },
119
+ React.createElement("div", { style: { fontSize: '11px', color: '#6b7280', marginBottom: '4px' } }, "UTC \uAE30\uC900"),
120
+ React.createElement("div", { style: { fontSize: '20px', fontWeight: 'bold', color: '#111827', fontFamily: 'monospace' } }, now.utc().format('YYYY-MM-DD HH:mm:ss')))))));
121
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './TimezoneDevTools';
2
+ export type { Props as TimezoneDevToolsProps } from './TimezoneDevTools';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/TimezoneDevTools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,YAAY,EAAE,KAAK,IAAI,qBAAqB,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1 @@
1
+ export { default } from './TimezoneDevTools';
@@ -0,0 +1,12 @@
1
+ import { CSSProperties } from 'react';
2
+ export declare const containerStyle: CSSProperties;
3
+ export declare const headerStyle: CSSProperties;
4
+ export declare const contentStyle: CSSProperties;
5
+ export declare const sectionTitleStyle: CSSProperties;
6
+ export declare const cardStyle: CSSProperties;
7
+ export declare const cityNameStyle: CSSProperties;
8
+ export declare const timezoneStyle: CSSProperties;
9
+ export declare const timeStyle: CSSProperties;
10
+ export declare const offsetStyle: CSSProperties;
11
+ export declare const highlightStyle: CSSProperties;
12
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/TimezoneDevTools/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtC,eAAO,MAAM,cAAc,EAAE,aAM5B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,aAOzB,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,aAM1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,aAO/B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,aAQvB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,aAG3B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,aAI3B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,aAKvB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,aAMzB,CAAC;AAEF,eAAO,MAAM,cAAc,EAAE,aAG5B,CAAC"}