@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
@@ -1,37 +1,47 @@
1
- import React, { useState, useRef } from 'react';
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
- * // Vite 프로젝트
14
+ * // Layout.tsx (한 번만)
10
15
  * import { DevPanel } from '@blastlabs/utils/components/dev';
11
16
  *
12
- * function App() {
17
+ * function Layout() {
13
18
  * return (
14
- * <div>
15
- * {import.meta.env.DEV && <DevPanel />}
16
- * </div>
19
+ * <>
20
+ * <Outlet />
21
+ * <DevPanel />
22
+ * </>
17
23
  * );
18
24
  * }
19
- * ```
20
25
  *
21
- * @example
22
- * ```tsx
23
- * // Create React App 프로젝트
24
- * {process.env.NODE_ENV === 'development' && <DevPanel position="top-left" />}
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 [isOpen, setIsOpen] = useState(false);
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
- renderCount.current += 1;
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 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
- }
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: () => 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 },
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: (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,
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=DevPanel.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=DevToolsProvider.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DevToolsProvider.test.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/DevToolsProvider/DevToolsProvider.test.tsx"],"names":[],"mappings":""}