@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.
- package/README.md +102 -8
- package/bin/Makefile +23 -0
- package/bin/init-routes.cjs +422 -0
- package/dist/components/dev/DevPanel.d.ts +16 -11
- package/dist/components/dev/DevPanel.d.ts.map +1 -1
- package/dist/components/dev/DevPanel.js +71 -77
- package/dist/components/dev/DevPanel.test.d.ts +2 -0
- package/dist/components/dev/DevPanel.test.d.ts.map +1 -0
- package/dist/components/dev/DevPanel.test.js +194 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.d.ts +97 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.d.ts.map +1 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.js +122 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.d.ts +2 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.d.ts.map +1 -0
- package/dist/components/dev/DevToolsProvider/DevToolsProvider.test.js +104 -0
- package/dist/components/dev/DevToolsProvider/index.d.ts +3 -0
- package/dist/components/dev/DevToolsProvider/index.d.ts.map +1 -0
- package/dist/components/dev/DevToolsProvider/index.js +1 -0
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts +5 -70
- package/dist/components/dev/FormDevTools/FormDevTools.d.ts.map +1 -1
- package/dist/components/dev/FormDevTools/FormDevTools.js +163 -236
- package/dist/components/dev/FormDevTools/FormDevToolsContent.d.ts +27 -0
- package/dist/components/dev/FormDevTools/FormDevToolsContent.d.ts.map +1 -0
- package/dist/components/dev/FormDevTools/FormDevToolsContent.js +298 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.d.ts +29 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.d.ts.map +1 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevTools.js +122 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.d.ts +4 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.d.ts.map +1 -0
- package/dist/components/dev/TimezoneDevTools/TimezoneDevToolsContent.js +121 -0
- package/dist/components/dev/TimezoneDevTools/index.d.ts +3 -0
- package/dist/components/dev/TimezoneDevTools/index.d.ts.map +1 -0
- package/dist/components/dev/TimezoneDevTools/index.js +1 -0
- package/dist/components/dev/TimezoneDevTools/styles.d.ts +12 -0
- package/dist/components/dev/TimezoneDevTools/styles.d.ts.map +1 -0
- package/dist/components/dev/TimezoneDevTools/styles.js +65 -0
- package/dist/components/dev/ZIndexDebugger.js +1 -1
- package/dist/components/dev/index.d.ts +4 -2
- package/dist/components/dev/index.d.ts.map +1 -1
- package/dist/components/dev/index.js +6 -1
- package/dist/date/index.d.ts +11 -0
- package/dist/date/index.d.ts.map +1 -1
- package/dist/date/index.js +43 -0
- package/package.json +4 -2
|
@@ -1,37 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
2
3
|
import { useWindowSize } from '../../hooks';
|
|
4
|
+
import { useDevTools } from './DevToolsProvider';
|
|
5
|
+
import FormDevToolsContent from './FormDevTools/FormDevToolsContent';
|
|
6
|
+
import TimezoneDevToolsContent from './TimezoneDevTools/TimezoneDevToolsContent';
|
|
7
|
+
import ZIndexDebugger from './ZIndexDebugger';
|
|
3
8
|
/**
|
|
4
9
|
* 개발자 도구 패널
|
|
5
|
-
*
|
|
10
|
+
* Layout에 한 번만 배치하고, 페이지에서 hook으로 form을 주입받습니다.
|
|
6
11
|
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```tsx
|
|
9
|
-
* //
|
|
14
|
+
* // Layout.tsx (한 번만)
|
|
10
15
|
* import { DevPanel } from '@blastlabs/utils/components/dev';
|
|
11
16
|
*
|
|
12
|
-
* function
|
|
17
|
+
* function Layout() {
|
|
13
18
|
* return (
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* <>
|
|
20
|
+
* <Outlet />
|
|
21
|
+
* <DevPanel />
|
|
22
|
+
* </>
|
|
17
23
|
* );
|
|
18
24
|
* }
|
|
19
|
-
* ```
|
|
20
25
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
26
|
+
* // 페이지에서 form 주입
|
|
27
|
+
* import { useRegisterForm } from '@blastlabs/utils/components/dev';
|
|
28
|
+
*
|
|
29
|
+
* function MyPage() {
|
|
30
|
+
* const form = useForm();
|
|
31
|
+
* useRegisterForm(form);
|
|
32
|
+
*
|
|
33
|
+
* return <form>...</form>;
|
|
34
|
+
* }
|
|
25
35
|
* ```
|
|
26
36
|
*/
|
|
27
37
|
export default function DevPanel({ position = 'bottom-right' }) {
|
|
28
|
-
const
|
|
29
|
-
const [showWindowSize, setShowWindowSize] = useState(false);
|
|
30
|
-
const [showRenderCount, setShowRenderCount] = useState(false);
|
|
31
|
-
const renderCount = useRef(0);
|
|
38
|
+
const { tools, toggleTool, getForm, isMenuOpen, toggleMenu, showWindowOverlay, toggleWindowOverlay, showZIndexDebugger, toggleZIndexDebugger, } = useDevTools();
|
|
32
39
|
const { width, height } = useWindowSize();
|
|
33
|
-
|
|
34
|
-
|
|
40
|
+
const [mounted, setMounted] = useState(false);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
setMounted(true);
|
|
43
|
+
}, []);
|
|
44
|
+
const form = getForm();
|
|
35
45
|
const positionStyles = {
|
|
36
46
|
'top-left': { top: 16, left: 16 },
|
|
37
47
|
'top-right': { top: 16, right: 16 },
|
|
@@ -118,79 +128,63 @@ export default function DevPanel({ position = 'bottom-right' }) {
|
|
|
118
128
|
left: isOn ? '18px' : '2px',
|
|
119
129
|
transition: 'left 0.2s',
|
|
120
130
|
});
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
}
|
|
131
|
+
const disabledItemStyle = {
|
|
132
|
+
...toggleItemStyle,
|
|
133
|
+
opacity: 0.5,
|
|
134
|
+
cursor: 'not-allowed',
|
|
142
135
|
};
|
|
143
136
|
return (React.createElement(React.Fragment, null,
|
|
144
137
|
React.createElement("div", { style: containerStyle },
|
|
145
|
-
React.createElement("button", { onClick: () =>
|
|
146
|
-
|
|
138
|
+
React.createElement("button", { onClick: () => toggleMenu(), style: toggleButtonStyle, onMouseEnter: e => (e.currentTarget.style.backgroundColor = '#2563eb'), onMouseLeave: e => (e.currentTarget.style.backgroundColor = '#3b82f6') }, isMenuOpen ? '✕' : '🛠'),
|
|
139
|
+
isMenuOpen && (React.createElement("div", { style: panelStyle },
|
|
147
140
|
React.createElement("div", { style: headerStyle }, "\u2699\uFE0F \uAC1C\uBC1C\uC790 \uB3C4\uAD6C"),
|
|
148
141
|
React.createElement("div", { style: contentStyle },
|
|
149
|
-
React.createElement("div", { style: toggleItemStyle, onMouseEnter:
|
|
150
|
-
React.createElement("span", null, "\
|
|
151
|
-
React.createElement("div", { style: getSwitchStyle(
|
|
152
|
-
React.createElement("div", { style: getSwitchKnobStyle(
|
|
153
|
-
|
|
154
|
-
React.createElement("
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
142
|
+
form ? (React.createElement("div", { style: toggleItemStyle, onMouseEnter: e => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: e => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => toggleTool('formDevTools') },
|
|
143
|
+
React.createElement("span", null, "\uD83D\uDCDD Form DevTools"),
|
|
144
|
+
React.createElement("div", { style: getSwitchStyle(tools.formDevTools) },
|
|
145
|
+
React.createElement("div", { style: getSwitchKnobStyle(tools.formDevTools) })))) : (React.createElement("div", { style: disabledItemStyle },
|
|
146
|
+
React.createElement("span", null, "\uD83D\uDCDD Form DevTools"),
|
|
147
|
+
React.createElement("div", { style: getSwitchStyle(false) },
|
|
148
|
+
React.createElement("div", { style: getSwitchKnobStyle(false) })))),
|
|
149
|
+
React.createElement("div", { style: toggleItemStyle, onMouseEnter: e => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: e => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => toggleTool('timezoneDevTools') },
|
|
150
|
+
React.createElement("span", null, "\uD83C\uDF0D Timezone DevTools"),
|
|
151
|
+
React.createElement("div", { style: getSwitchStyle(tools.timezoneDevTools) },
|
|
152
|
+
React.createElement("div", { style: getSwitchKnobStyle(tools.timezoneDevTools) }))),
|
|
153
|
+
React.createElement("div", { style: toggleItemStyle, onMouseEnter: e => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: e => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => toggleWindowOverlay() },
|
|
154
|
+
React.createElement("span", null, "\uD83D\uDCD0 Window Size"),
|
|
155
|
+
React.createElement("div", { style: getSwitchStyle(showWindowOverlay) },
|
|
156
|
+
React.createElement("div", { style: getSwitchKnobStyle(showWindowOverlay) }))),
|
|
157
|
+
React.createElement("div", { style: toggleItemStyle, onMouseEnter: e => (e.currentTarget.style.backgroundColor = '#f3f4f6'), onMouseLeave: e => (e.currentTarget.style.backgroundColor = 'transparent'), onClick: () => toggleZIndexDebugger() },
|
|
158
|
+
React.createElement("span", null, "\uD83D\uDD0D Z-Index Debugger"),
|
|
159
|
+
React.createElement("div", { style: getSwitchStyle(showZIndexDebugger) },
|
|
160
|
+
React.createElement("div", { style: getSwitchKnobStyle(showZIndexDebugger) }))))))),
|
|
161
|
+
tools.formDevTools && form && (React.createElement("div", { style: { position: 'fixed', top: 80, right: 16, zIndex: 99998 } },
|
|
162
|
+
React.createElement(FormDevToolsContent, { form: form }))),
|
|
163
|
+
tools.timezoneDevTools && (React.createElement("div", { style: { position: 'fixed', top: 80, left: 16, zIndex: 99998 } },
|
|
164
|
+
React.createElement(TimezoneDevToolsContent, null))),
|
|
165
|
+
showZIndexDebugger && (React.createElement("div", { style: { position: 'fixed', top: 80, right: 16, zIndex: 99997 } },
|
|
166
|
+
React.createElement(ZIndexDebugger, { position: "top-right" }))),
|
|
167
|
+
mounted && showWindowOverlay && (React.createElement("div", { "data-testid": "window-overlay", style: {
|
|
168
|
+
position: 'fixed',
|
|
169
|
+
padding: '8px 12px',
|
|
170
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
171
|
+
color: 'white',
|
|
172
|
+
fontSize: '14px',
|
|
173
|
+
fontFamily: 'monospace',
|
|
174
|
+
borderRadius: '8px',
|
|
175
|
+
zIndex: 99997,
|
|
174
176
|
top: 16,
|
|
175
177
|
left: 16,
|
|
176
178
|
} },
|
|
177
179
|
React.createElement("span", { style: { color: '#60a5fa' } }, "W:"),
|
|
178
|
-
|
|
180
|
+
" ",
|
|
179
181
|
React.createElement("span", { style: { fontWeight: 'bold' } },
|
|
180
182
|
width,
|
|
181
183
|
"px"),
|
|
182
184
|
React.createElement("span", { style: { color: '#9ca3af', margin: '0 4px' } }, "\u00D7"),
|
|
183
185
|
React.createElement("span", { style: { color: '#4ade80' } }, "H:"),
|
|
184
|
-
|
|
186
|
+
" ",
|
|
185
187
|
React.createElement("span", { style: { fontWeight: 'bold' } },
|
|
186
188
|
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)))));
|
|
189
|
+
"px")))));
|
|
196
190
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevPanel.test.d.ts","sourceRoot":"","sources":["../../../src/components/dev/DevPanel.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import userEvent from '@testing-library/user-event';
|
|
5
|
+
import DevPanel from './DevPanel';
|
|
6
|
+
import { DevToolsProvider, useRegisterForm } from './DevToolsProvider';
|
|
7
|
+
// Mock useWindowSize
|
|
8
|
+
vi.mock('../../hooks', () => ({
|
|
9
|
+
useWindowSize: () => ({ width: 1920, height: 1080 }),
|
|
10
|
+
}));
|
|
11
|
+
// Mock FormDevToolsContent and TimezoneDevToolsContent
|
|
12
|
+
vi.mock('./FormDevTools/FormDevToolsContent', () => ({
|
|
13
|
+
default: () => React.createElement("div", { "data-testid": "form-devtools-panel" }, "Form DevTools Panel"),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock('./TimezoneDevTools/TimezoneDevToolsContent', () => ({
|
|
16
|
+
default: () => React.createElement("div", { "data-testid": "timezone-devtools-panel" }, "Timezone DevTools Panel"),
|
|
17
|
+
}));
|
|
18
|
+
// Note: ZIndexDebugger is not mocked to test actual toggle behavior
|
|
19
|
+
// Mock form
|
|
20
|
+
const createMockForm = () => ({
|
|
21
|
+
formState: {
|
|
22
|
+
errors: {},
|
|
23
|
+
dirtyFields: {},
|
|
24
|
+
touchedFields: {},
|
|
25
|
+
isValid: true,
|
|
26
|
+
isSubmitting: false,
|
|
27
|
+
submitCount: 0,
|
|
28
|
+
defaultValues: { username: '', email: '' },
|
|
29
|
+
},
|
|
30
|
+
watch: vi.fn(() => ({ username: '', email: '' })),
|
|
31
|
+
setValue: vi.fn(),
|
|
32
|
+
trigger: vi.fn(),
|
|
33
|
+
});
|
|
34
|
+
function TestPage({ hasForm = true }) {
|
|
35
|
+
const form = createMockForm();
|
|
36
|
+
if (hasForm) {
|
|
37
|
+
useRegisterForm(form);
|
|
38
|
+
}
|
|
39
|
+
return React.createElement("div", null, "Test Page");
|
|
40
|
+
}
|
|
41
|
+
describe('DevPanel', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
describe('기본 동작', () => {
|
|
46
|
+
it('메인 토글 버튼이 렌더링된다', () => {
|
|
47
|
+
render(React.createElement(DevToolsProvider, null,
|
|
48
|
+
React.createElement(DevPanel, null)));
|
|
49
|
+
const toggleButton = screen.getByRole('button');
|
|
50
|
+
expect(toggleButton).toBeTruthy();
|
|
51
|
+
expect(toggleButton.textContent).toBe('🛠');
|
|
52
|
+
});
|
|
53
|
+
it('메뉴가 닫혀있을 때는 메뉴 패널이 보이지 않는다', () => {
|
|
54
|
+
render(React.createElement(DevToolsProvider, null,
|
|
55
|
+
React.createElement(DevPanel, null)));
|
|
56
|
+
expect(screen.queryByText('⚙️ 개발자 도구')).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('윈도우 사이즈가 항상 표시된다', () => {
|
|
59
|
+
render(React.createElement(DevToolsProvider, null,
|
|
60
|
+
React.createElement(DevPanel, null)));
|
|
61
|
+
expect(screen.getByText(/W:/)).toBeTruthy();
|
|
62
|
+
expect(screen.getByText(/H:/)).toBeTruthy();
|
|
63
|
+
expect(screen.getByText('1920px')).toBeTruthy();
|
|
64
|
+
expect(screen.getByText('1080px')).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('메뉴 토글', () => {
|
|
68
|
+
it('메인 버튼 클릭 시 메뉴가 열린다', async () => {
|
|
69
|
+
const user = userEvent.setup();
|
|
70
|
+
render(React.createElement(DevToolsProvider, null,
|
|
71
|
+
React.createElement(DevPanel, null)));
|
|
72
|
+
const toggleButton = screen.getByRole('button');
|
|
73
|
+
expect(toggleButton.textContent).toBe('🛠');
|
|
74
|
+
await user.click(toggleButton);
|
|
75
|
+
expect(screen.getByText('⚙️ 개발자 도구')).toBeTruthy();
|
|
76
|
+
expect(toggleButton.textContent).toBe('✕');
|
|
77
|
+
});
|
|
78
|
+
it('메인 버튼 다시 클릭 시 메뉴가 닫힌다', async () => {
|
|
79
|
+
const user = userEvent.setup();
|
|
80
|
+
render(React.createElement(DevToolsProvider, null,
|
|
81
|
+
React.createElement(DevPanel, null)));
|
|
82
|
+
const toggleButton = screen.getByRole('button');
|
|
83
|
+
// 메뉴 열기
|
|
84
|
+
await user.click(toggleButton);
|
|
85
|
+
expect(screen.getByText('⚙️ 개발자 도구')).toBeTruthy();
|
|
86
|
+
// 메뉴 닫기
|
|
87
|
+
await user.click(toggleButton);
|
|
88
|
+
expect(screen.queryByText('⚙️ 개발자 도구')).toBeNull();
|
|
89
|
+
expect(toggleButton.textContent).toBe('🛠');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('툴 토글', () => {
|
|
93
|
+
it('메뉴에서 Form DevTools 토글 클릭 시 패널이 표시된다', async () => {
|
|
94
|
+
const user = userEvent.setup();
|
|
95
|
+
render(React.createElement(DevToolsProvider, null,
|
|
96
|
+
React.createElement(TestPage, { hasForm: true }),
|
|
97
|
+
React.createElement(DevPanel, null)));
|
|
98
|
+
const toggleButton = screen.getByRole('button');
|
|
99
|
+
await user.click(toggleButton);
|
|
100
|
+
const formDevToolsToggle = screen.getByText('📝 Form DevTools');
|
|
101
|
+
await user.click(formDevToolsToggle);
|
|
102
|
+
expect(screen.getByTestId('form-devtools-panel')).toBeTruthy();
|
|
103
|
+
});
|
|
104
|
+
it('메뉴에서 Timezone DevTools 토글 클릭 시 패널이 표시된다', async () => {
|
|
105
|
+
const user = userEvent.setup();
|
|
106
|
+
render(React.createElement(DevToolsProvider, null,
|
|
107
|
+
React.createElement(DevPanel, null)));
|
|
108
|
+
const toggleButton = screen.getByRole('button');
|
|
109
|
+
await user.click(toggleButton);
|
|
110
|
+
const timezoneDevToolsToggle = screen.getByText('🌍 Timezone DevTools');
|
|
111
|
+
await user.click(timezoneDevToolsToggle);
|
|
112
|
+
expect(screen.getByTestId('timezone-devtools-panel')).toBeTruthy();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('독립적 토글 동작', () => {
|
|
116
|
+
it('메뉴를 닫아도 각 툴 패널은 유지된다', async () => {
|
|
117
|
+
const user = userEvent.setup();
|
|
118
|
+
render(React.createElement(DevToolsProvider, null,
|
|
119
|
+
React.createElement(TestPage, { hasForm: true }),
|
|
120
|
+
React.createElement(DevPanel, null)));
|
|
121
|
+
const toggleButton = screen.getByRole('button');
|
|
122
|
+
// 메뉴 열기
|
|
123
|
+
await user.click(toggleButton);
|
|
124
|
+
// Form DevTools 켜기
|
|
125
|
+
const formToggle = screen.getByText('📝 Form DevTools');
|
|
126
|
+
await user.click(formToggle);
|
|
127
|
+
// 메뉴 닫기
|
|
128
|
+
await user.click(toggleButton);
|
|
129
|
+
// 메뉴는 닫혀야 함
|
|
130
|
+
expect(screen.queryByText('⚙️ 개발자 도구')).toBeNull();
|
|
131
|
+
// Form DevTools 패널은 유지되어야 함
|
|
132
|
+
expect(screen.getByTestId('form-devtools-panel')).toBeTruthy();
|
|
133
|
+
});
|
|
134
|
+
it('각 툴이 독립적으로 토글된다', async () => {
|
|
135
|
+
const user = userEvent.setup();
|
|
136
|
+
render(React.createElement(DevToolsProvider, null,
|
|
137
|
+
React.createElement(TestPage, { hasForm: true }),
|
|
138
|
+
React.createElement(DevPanel, null)));
|
|
139
|
+
const toggleButton = screen.getByRole('button');
|
|
140
|
+
// 메뉴 열기
|
|
141
|
+
await user.click(toggleButton);
|
|
142
|
+
// Form DevTools만 켜기
|
|
143
|
+
const formToggle = screen.getByText('📝 Form DevTools');
|
|
144
|
+
await user.click(formToggle);
|
|
145
|
+
// 메뉴 닫기
|
|
146
|
+
await user.click(toggleButton);
|
|
147
|
+
// Form DevTools 패널만 있어야 함
|
|
148
|
+
expect(screen.getByTestId('form-devtools-panel')).toBeTruthy();
|
|
149
|
+
expect(screen.queryByTestId('timezone-devtools-panel')).toBeNull();
|
|
150
|
+
// 메뉴 다시 열기
|
|
151
|
+
await user.click(toggleButton);
|
|
152
|
+
// Timezone DevTools 켜기
|
|
153
|
+
const timezoneToggle = screen.getByText('🌍 Timezone DevTools');
|
|
154
|
+
await user.click(timezoneToggle);
|
|
155
|
+
// 메뉴 닫기
|
|
156
|
+
await user.click(toggleButton);
|
|
157
|
+
// 두 패널 모두 있어야 함
|
|
158
|
+
expect(screen.getByTestId('form-devtools-panel')).toBeTruthy();
|
|
159
|
+
expect(screen.getByTestId('timezone-devtools-panel')).toBeTruthy();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('패널 토글', () => {
|
|
163
|
+
it('Window Size Overlay 토글', async () => {
|
|
164
|
+
const user = userEvent.setup();
|
|
165
|
+
render(React.createElement(DevToolsProvider, null,
|
|
166
|
+
React.createElement(DevPanel, null)));
|
|
167
|
+
// 윈도우 사이즈가 항상 표시됨
|
|
168
|
+
expect(screen.getByText(/W:/)).toBeTruthy();
|
|
169
|
+
expect(screen.getByText(/H:/)).toBeTruthy();
|
|
170
|
+
expect(screen.getByText('1920px')).toBeTruthy();
|
|
171
|
+
expect(screen.getByText('1080px')).toBeTruthy();
|
|
172
|
+
const toggleButton = screen.getByRole('button');
|
|
173
|
+
await user.click(toggleButton);
|
|
174
|
+
const windowToggle = screen.getByText('📐 Window Size');
|
|
175
|
+
await user.click(windowToggle);
|
|
176
|
+
// 숨겨졸 때 윈도우 사이즈가 사라짐
|
|
177
|
+
expect(screen.queryByText(/W:/)).toBeNull();
|
|
178
|
+
expect(screen.queryByText(/H:/)).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
it('ZIndex Debugger 토글', async () => {
|
|
181
|
+
const user = userEvent.setup();
|
|
182
|
+
render(React.createElement(DevToolsProvider, null,
|
|
183
|
+
React.createElement(DevPanel, null)));
|
|
184
|
+
// 초기 상태에서는 렌더링되지 않음
|
|
185
|
+
expect(screen.queryByTestId('zindex-debugger')).toBeNull();
|
|
186
|
+
const toggleButton = screen.getByRole('button');
|
|
187
|
+
await user.click(toggleButton);
|
|
188
|
+
const zIndexToggle = screen.getByText('🔍 Z-Index Debugger');
|
|
189
|
+
await user.click(zIndexToggle);
|
|
190
|
+
// 토글 후 ZIndexDebugger가 렌더링됨
|
|
191
|
+
expect(screen.getByTestId('zindex-debugger')).toBeTruthy();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
export type DevToolForm = {
|
|
3
|
+
form: {
|
|
4
|
+
formState: {
|
|
5
|
+
errors?: Record<string, any>;
|
|
6
|
+
dirtyFields?: Record<string, any>;
|
|
7
|
+
touchedFields?: Record<string, any>;
|
|
8
|
+
isValid?: boolean;
|
|
9
|
+
isSubmitting?: boolean;
|
|
10
|
+
submitCount?: number;
|
|
11
|
+
defaultValues?: Record<string, any>;
|
|
12
|
+
};
|
|
13
|
+
watch: () => any;
|
|
14
|
+
setValue: (name: any, value: any, options?: any) => void;
|
|
15
|
+
trigger?: () => Promise<boolean>;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
type DevToolsContextType = {
|
|
19
|
+
forms: Map<string, DevToolForm['form']>;
|
|
20
|
+
registerForm: (form: DevToolForm['form']) => () => void;
|
|
21
|
+
getForm: () => DevToolForm['form'] | null;
|
|
22
|
+
tools: {
|
|
23
|
+
formDevTools: boolean;
|
|
24
|
+
timezoneDevTools: boolean;
|
|
25
|
+
};
|
|
26
|
+
toggleTool: (tool: 'formDevTools' | 'timezoneDevTools') => void;
|
|
27
|
+
isMenuOpen: boolean;
|
|
28
|
+
toggleMenu: () => void;
|
|
29
|
+
openMenu: () => void;
|
|
30
|
+
closeMenu: () => void;
|
|
31
|
+
showWindowOverlay: boolean;
|
|
32
|
+
toggleWindowOverlay: () => void;
|
|
33
|
+
showZIndexDebugger: boolean;
|
|
34
|
+
toggleZIndexDebugger: () => void;
|
|
35
|
+
};
|
|
36
|
+
type ProviderProps = {
|
|
37
|
+
children: ReactNode;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* DevTools Provider 컴포넌트
|
|
41
|
+
* 앱 상단에서 한 번만 감싸면 됩니다.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { DevToolsProvider } from '@blastlabs/utils/components/dev';
|
|
46
|
+
*
|
|
47
|
+
* function App() {
|
|
48
|
+
* return (
|
|
49
|
+
* <DevToolsProvider>
|
|
50
|
+
* <YourApp />
|
|
51
|
+
* </DevToolsProvider>
|
|
52
|
+
* );
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export default function DevToolsProvider({ children }: ProviderProps): React.JSX.Element;
|
|
57
|
+
/**
|
|
58
|
+
* Form을 DevTools에 등록하는 Hook
|
|
59
|
+
* 페이지에서 form을 사용할 때 호출합니다.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { useRegisterForm } from '@blastlabs/utils/components/dev';
|
|
64
|
+
*
|
|
65
|
+
* function MyPage() {
|
|
66
|
+
* const form = useForm();
|
|
67
|
+
*
|
|
68
|
+
* useRegisterForm(form);
|
|
69
|
+
*
|
|
70
|
+
* return <form>...</form>;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare function useRegisterForm(form: DevToolForm['form']): void;
|
|
75
|
+
/**
|
|
76
|
+
* DevTools 상태를 조회하는 Hook
|
|
77
|
+
*/
|
|
78
|
+
export declare function useDevTools(): DevToolsContextType | {
|
|
79
|
+
forms: Map<any, any>;
|
|
80
|
+
registerForm: () => () => void;
|
|
81
|
+
getForm: () => null;
|
|
82
|
+
tools: {
|
|
83
|
+
formDevTools: boolean;
|
|
84
|
+
timezoneDevTools: boolean;
|
|
85
|
+
};
|
|
86
|
+
toggleTool: () => void;
|
|
87
|
+
isMenuOpen: boolean;
|
|
88
|
+
toggleMenu: () => void;
|
|
89
|
+
openMenu: () => void;
|
|
90
|
+
closeMenu: () => void;
|
|
91
|
+
showWindowOverlay: boolean;
|
|
92
|
+
toggleWindowOverlay: () => void;
|
|
93
|
+
showZIndexDebugger: boolean;
|
|
94
|
+
toggleZIndexDebugger: () => void;
|
|
95
|
+
};
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=DevToolsProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevToolsProvider.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/DevToolsProvider/DevToolsProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAuC,SAAS,EAAqB,MAAM,OAAO,CAAC;AAGjG,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE;QACJ,SAAS,EAAE;YACT,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC7B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAClC,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YACpC,OAAO,CAAC,EAAE,OAAO,CAAC;YAClB,YAAY,CAAC,EAAE,OAAO,CAAC;YACvB,WAAW,CAAC,EAAE,MAAM,CAAC;YACrB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;SACrC,CAAC;QACF,KAAK,EAAE,MAAM,GAAG,CAAC;QACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;QACzD,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;KAClC,CAAC;CACH,CAAC;AAGF,KAAK,mBAAmB,GAAG;IAEzB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;IACxC,YAAY,EAAE,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,MAAM,IAAI,CAAC;IACxD,OAAO,EAAE,MAAM,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAG1C,KAAK,EAAE;QACL,YAAY,EAAE,OAAO,CAAC;QACtB,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;IACF,UAAU,EAAE,CAAC,IAAI,EAAE,cAAc,GAAG,kBAAkB,KAAK,IAAI,CAAC;IAGhE,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;IAGtB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,oBAAoB,EAAE,MAAM,IAAI,CAAC;CAClC,CAAC;AAKF,KAAK,aAAa,GAAG;IACnB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,qBA4DnE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,QAYxD;AAED;;GAEG;AACH,wBAAgB,WAAW;;;;;;;;;;;;;;;;;EAsB1B"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
|
|
3
|
+
const DevToolsContext = createContext(null);
|
|
4
|
+
/**
|
|
5
|
+
* DevTools Provider 컴포넌트
|
|
6
|
+
* 앱 상단에서 한 번만 감싸면 됩니다.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { DevToolsProvider } from '@blastlabs/utils/components/dev';
|
|
11
|
+
*
|
|
12
|
+
* function App() {
|
|
13
|
+
* return (
|
|
14
|
+
* <DevToolsProvider>
|
|
15
|
+
* <YourApp />
|
|
16
|
+
* </DevToolsProvider>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export default function DevToolsProvider({ children }) {
|
|
22
|
+
const formsRef = useRef(new Map());
|
|
23
|
+
const [tools, setTools] = useState({
|
|
24
|
+
formDevTools: false,
|
|
25
|
+
timezoneDevTools: false,
|
|
26
|
+
});
|
|
27
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
28
|
+
const [showWindowOverlay, setShowWindowOverlay] = useState(true);
|
|
29
|
+
const [showZIndexDebugger, setShowZIndexDebugger] = useState(false);
|
|
30
|
+
const registerForm = (form) => {
|
|
31
|
+
const id = `form-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
32
|
+
formsRef.current.set(id, form);
|
|
33
|
+
// 컴포넌트 unmount 시 정리
|
|
34
|
+
return () => {
|
|
35
|
+
formsRef.current.delete(id);
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const getForm = () => {
|
|
39
|
+
if (formsRef.current.size === 0)
|
|
40
|
+
return null;
|
|
41
|
+
// 첫 번째 form 반환 (보통 페이지당 하나의 form)
|
|
42
|
+
const firstKey = formsRef.current.keys().next().value;
|
|
43
|
+
if (!firstKey)
|
|
44
|
+
return null;
|
|
45
|
+
return formsRef.current.get(firstKey) || null;
|
|
46
|
+
};
|
|
47
|
+
const toggleTool = (tool) => {
|
|
48
|
+
setTools(prev => ({ ...prev, [tool]: !prev[tool] }));
|
|
49
|
+
};
|
|
50
|
+
const toggleMenu = () => setIsMenuOpen(prev => !prev);
|
|
51
|
+
const openMenu = () => setIsMenuOpen(true);
|
|
52
|
+
const closeMenu = () => setIsMenuOpen(false);
|
|
53
|
+
const toggleWindowOverlay = () => setShowWindowOverlay(prev => !prev);
|
|
54
|
+
const toggleZIndexDebugger = () => setShowZIndexDebugger(prev => !prev);
|
|
55
|
+
return (React.createElement(DevToolsContext.Provider, { value: {
|
|
56
|
+
forms: formsRef.current,
|
|
57
|
+
registerForm,
|
|
58
|
+
getForm,
|
|
59
|
+
tools,
|
|
60
|
+
toggleTool,
|
|
61
|
+
isMenuOpen,
|
|
62
|
+
toggleMenu,
|
|
63
|
+
openMenu,
|
|
64
|
+
closeMenu,
|
|
65
|
+
showWindowOverlay,
|
|
66
|
+
toggleWindowOverlay,
|
|
67
|
+
showZIndexDebugger,
|
|
68
|
+
toggleZIndexDebugger,
|
|
69
|
+
} }, children));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Form을 DevTools에 등록하는 Hook
|
|
73
|
+
* 페이지에서 form을 사용할 때 호출합니다.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```tsx
|
|
77
|
+
* import { useRegisterForm } from '@blastlabs/utils/components/dev';
|
|
78
|
+
*
|
|
79
|
+
* function MyPage() {
|
|
80
|
+
* const form = useForm();
|
|
81
|
+
*
|
|
82
|
+
* useRegisterForm(form);
|
|
83
|
+
*
|
|
84
|
+
* return <form>...</form>;
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export function useRegisterForm(form) {
|
|
89
|
+
const context = useContext(DevToolsContext);
|
|
90
|
+
if (!context) {
|
|
91
|
+
console.warn('useRegisterForm must be used within DevToolsProvider');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const unregister = context.registerForm(form);
|
|
96
|
+
return unregister;
|
|
97
|
+
}, [form, context]);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* DevTools 상태를 조회하는 Hook
|
|
101
|
+
*/
|
|
102
|
+
export function useDevTools() {
|
|
103
|
+
const context = useContext(DevToolsContext);
|
|
104
|
+
if (!context) {
|
|
105
|
+
return {
|
|
106
|
+
forms: new Map(),
|
|
107
|
+
registerForm: () => () => { },
|
|
108
|
+
getForm: () => null,
|
|
109
|
+
tools: { formDevTools: false, timezoneDevTools: false },
|
|
110
|
+
toggleTool: () => { },
|
|
111
|
+
isMenuOpen: false,
|
|
112
|
+
toggleMenu: () => { },
|
|
113
|
+
openMenu: () => { },
|
|
114
|
+
closeMenu: () => { },
|
|
115
|
+
showWindowOverlay: true,
|
|
116
|
+
toggleWindowOverlay: () => { },
|
|
117
|
+
showZIndexDebugger: false,
|
|
118
|
+
toggleZIndexDebugger: () => { },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return context;
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevToolsProvider.test.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/DevToolsProvider/DevToolsProvider.test.tsx"],"names":[],"mappings":""}
|