@blastlabs/utils 1.11.1 → 1.12.1

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 (151) hide show
  1. package/README.md +20 -13
  2. package/dist/components/auth/AuthGuard.d.ts +95 -0
  3. package/dist/components/auth/AuthGuard.d.ts.map +1 -0
  4. package/dist/components/auth/AuthGuard.js +103 -0
  5. package/dist/components/auth/index.d.ts +5 -0
  6. package/dist/components/auth/index.d.ts.map +1 -0
  7. package/dist/components/auth/index.js +4 -0
  8. package/dist/components/dev/ApiLogger.d.ts +2 -2
  9. package/dist/components/dev/ApiLogger.js +3 -3
  10. package/dist/components/dev/DevPanel.d.ts +1 -1
  11. package/dist/components/dev/DevPanel.js +2 -2
  12. package/dist/components/dev/FormDevTools/FormDevTools.d.ts +1 -1
  13. package/dist/components/dev/FormDevTools/FormDevTools.js +2 -2
  14. package/dist/components/dev/FormDevTools/index.d.ts +1 -1
  15. package/dist/components/dev/FormDevTools/index.d.ts.map +1 -1
  16. package/dist/components/dev/{IdSelector.d.ts → IdSelector/IdSelector.d.ts} +3 -4
  17. package/dist/components/dev/IdSelector/IdSelector.d.ts.map +1 -0
  18. package/dist/components/dev/IdSelector/IdSelector.js +60 -0
  19. package/dist/components/dev/IdSelector/IdSelector.test.d.ts +2 -0
  20. package/dist/components/dev/IdSelector/IdSelector.test.d.ts.map +1 -0
  21. package/dist/components/dev/IdSelector/IdSelector.test.js +203 -0
  22. package/dist/components/dev/IdSelector/LoginCard.d.ts +17 -0
  23. package/dist/components/dev/IdSelector/LoginCard.d.ts.map +1 -0
  24. package/dist/components/dev/IdSelector/LoginCard.js +16 -0
  25. package/dist/components/dev/IdSelector/index.d.ts +3 -0
  26. package/dist/components/dev/IdSelector/index.d.ts.map +1 -0
  27. package/dist/components/dev/IdSelector/index.js +1 -0
  28. package/dist/components/dev/IdSelector/styles.d.ts +16 -0
  29. package/dist/components/dev/IdSelector/styles.d.ts.map +1 -0
  30. package/dist/components/dev/IdSelector/styles.js +66 -0
  31. package/dist/components/dev/WindowSizeDisplay.d.ts +1 -1
  32. package/dist/components/dev/WindowSizeDisplay.js +2 -2
  33. package/dist/components/dev/ZIndexDebugger.d.ts +1 -1
  34. package/dist/components/dev/ZIndexDebugger.js +1 -1
  35. package/dist/components/dev/index.d.ts +2 -1
  36. package/dist/components/dev/index.d.ts.map +1 -1
  37. package/dist/components/index.d.ts +1 -0
  38. package/dist/components/index.d.ts.map +1 -1
  39. package/dist/components/index.js +1 -0
  40. package/dist/hooks/auth/__tests__/useAuth.test.d.ts +2 -0
  41. package/dist/hooks/auth/__tests__/useAuth.test.d.ts.map +1 -0
  42. package/dist/hooks/auth/__tests__/useAuth.test.js +139 -0
  43. package/dist/hooks/auth/index.d.ts +5 -0
  44. package/dist/hooks/auth/index.d.ts.map +1 -0
  45. package/dist/hooks/auth/index.js +4 -0
  46. package/dist/hooks/auth/useAuth.d.ts +275 -0
  47. package/dist/hooks/auth/useAuth.d.ts.map +1 -0
  48. package/dist/hooks/auth/useAuth.js +384 -0
  49. package/dist/hooks/event/index.d.ts +6 -0
  50. package/dist/hooks/event/index.d.ts.map +1 -0
  51. package/dist/hooks/event/index.js +5 -0
  52. package/dist/hooks/event/useClickOutside.d.ts.map +1 -0
  53. package/dist/hooks/event/useEventListener.d.ts.map +1 -0
  54. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts +2 -0
  55. package/dist/hooks/form/__tests__/useCRUDForm.test.d.ts.map +1 -0
  56. package/dist/hooks/form/__tests__/useCRUDForm.test.js +487 -0
  57. package/dist/hooks/form/index.d.ts +5 -0
  58. package/dist/hooks/form/index.d.ts.map +1 -0
  59. package/dist/hooks/form/index.js +4 -0
  60. package/dist/hooks/form/useCRUDForm.d.ts +211 -0
  61. package/dist/hooks/form/useCRUDForm.d.ts.map +1 -0
  62. package/dist/hooks/form/useCRUDForm.js +287 -0
  63. package/dist/hooks/index.d.ts +10 -14
  64. package/dist/hooks/index.d.ts.map +1 -1
  65. package/dist/hooks/index.js +18 -19
  66. package/dist/hooks/performance/index.d.ts +7 -0
  67. package/dist/hooks/performance/index.d.ts.map +1 -0
  68. package/dist/hooks/performance/index.js +6 -0
  69. package/dist/hooks/performance/useDebounce.d.ts.map +1 -0
  70. package/dist/hooks/performance/useIntersectionObserver.d.ts.map +1 -0
  71. package/dist/hooks/performance/useThrottle.d.ts.map +1 -0
  72. package/dist/hooks/state/index.d.ts +6 -0
  73. package/dist/hooks/state/index.d.ts.map +1 -0
  74. package/dist/hooks/state/index.js +5 -0
  75. package/dist/hooks/state/usePrevious.d.ts.map +1 -0
  76. package/dist/hooks/state/useToggle.d.ts.map +1 -0
  77. package/dist/hooks/storage/index.d.ts +7 -0
  78. package/dist/hooks/storage/index.d.ts.map +1 -0
  79. package/dist/hooks/storage/index.js +6 -0
  80. package/dist/hooks/storage/useCopyToClipboard.d.ts.map +1 -0
  81. package/dist/hooks/storage/useLocalStorage.d.ts.map +1 -0
  82. package/dist/hooks/storage/useSessionStorage.d.ts.map +1 -0
  83. package/dist/hooks/time/__tests__/useCountdown.test.d.ts +2 -0
  84. package/dist/hooks/time/__tests__/useCountdown.test.d.ts.map +1 -0
  85. package/dist/hooks/time/__tests__/useCountdown.test.js +150 -0
  86. package/dist/hooks/time/__tests__/useInterval.test.d.ts +2 -0
  87. package/dist/hooks/time/__tests__/useInterval.test.d.ts.map +1 -0
  88. package/dist/hooks/time/__tests__/useInterval.test.js +39 -0
  89. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts +2 -0
  90. package/dist/hooks/time/__tests__/useStopwatch.test.d.ts.map +1 -0
  91. package/dist/hooks/time/__tests__/useStopwatch.test.js +149 -0
  92. package/dist/hooks/time/index.d.ts +7 -0
  93. package/dist/hooks/time/index.d.ts.map +1 -0
  94. package/dist/hooks/time/index.js +6 -0
  95. package/dist/hooks/time/useCountdown.d.ts +116 -0
  96. package/dist/hooks/time/useCountdown.d.ts.map +1 -0
  97. package/dist/hooks/time/useCountdown.js +152 -0
  98. package/dist/hooks/time/useInterval.d.ts +40 -0
  99. package/dist/hooks/time/useInterval.d.ts.map +1 -0
  100. package/dist/hooks/time/useInterval.js +61 -0
  101. package/dist/hooks/time/useStopwatch.d.ts +142 -0
  102. package/dist/hooks/time/useStopwatch.d.ts.map +1 -0
  103. package/dist/hooks/time/useStopwatch.js +179 -0
  104. package/dist/hooks/ui/index.d.ts +7 -0
  105. package/dist/hooks/ui/index.d.ts.map +1 -0
  106. package/dist/hooks/ui/index.js +6 -0
  107. package/dist/hooks/ui/useMediaQuery.d.ts.map +1 -0
  108. package/dist/hooks/ui/useTabs.d.ts +33 -0
  109. package/dist/hooks/ui/useTabs.d.ts.map +1 -0
  110. package/dist/hooks/ui/useTabs.js +117 -0
  111. package/dist/hooks/ui/useWindowSize.d.ts.map +1 -0
  112. package/dist/index.js +1 -1
  113. package/package.json +14 -4
  114. package/dist/components/dev/IdSelector.d.ts.map +0 -1
  115. package/dist/components/dev/IdSelector.js +0 -129
  116. package/dist/hooks/useClickOutside.d.ts.map +0 -1
  117. package/dist/hooks/useCopyToClipboard.d.ts.map +0 -1
  118. package/dist/hooks/useDebounce.d.ts.map +0 -1
  119. package/dist/hooks/useEventListener.d.ts.map +0 -1
  120. package/dist/hooks/useIntersectionObserver.d.ts.map +0 -1
  121. package/dist/hooks/useLocalStorage.d.ts.map +0 -1
  122. package/dist/hooks/useMediaQuery.d.ts.map +0 -1
  123. package/dist/hooks/usePrevious.d.ts.map +0 -1
  124. package/dist/hooks/useSessionStorage.d.ts.map +0 -1
  125. package/dist/hooks/useThrottle.d.ts.map +0 -1
  126. package/dist/hooks/useToggle.d.ts.map +0 -1
  127. package/dist/hooks/useWindowSize.d.ts.map +0 -1
  128. /package/dist/hooks/{useClickOutside.d.ts → event/useClickOutside.d.ts} +0 -0
  129. /package/dist/hooks/{useClickOutside.js → event/useClickOutside.js} +0 -0
  130. /package/dist/hooks/{useEventListener.d.ts → event/useEventListener.d.ts} +0 -0
  131. /package/dist/hooks/{useEventListener.js → event/useEventListener.js} +0 -0
  132. /package/dist/hooks/{useDebounce.d.ts → performance/useDebounce.d.ts} +0 -0
  133. /package/dist/hooks/{useDebounce.js → performance/useDebounce.js} +0 -0
  134. /package/dist/hooks/{useIntersectionObserver.d.ts → performance/useIntersectionObserver.d.ts} +0 -0
  135. /package/dist/hooks/{useIntersectionObserver.js → performance/useIntersectionObserver.js} +0 -0
  136. /package/dist/hooks/{useThrottle.d.ts → performance/useThrottle.d.ts} +0 -0
  137. /package/dist/hooks/{useThrottle.js → performance/useThrottle.js} +0 -0
  138. /package/dist/hooks/{usePrevious.d.ts → state/usePrevious.d.ts} +0 -0
  139. /package/dist/hooks/{usePrevious.js → state/usePrevious.js} +0 -0
  140. /package/dist/hooks/{useToggle.d.ts → state/useToggle.d.ts} +0 -0
  141. /package/dist/hooks/{useToggle.js → state/useToggle.js} +0 -0
  142. /package/dist/hooks/{useCopyToClipboard.d.ts → storage/useCopyToClipboard.d.ts} +0 -0
  143. /package/dist/hooks/{useCopyToClipboard.js → storage/useCopyToClipboard.js} +0 -0
  144. /package/dist/hooks/{useLocalStorage.d.ts → storage/useLocalStorage.d.ts} +0 -0
  145. /package/dist/hooks/{useLocalStorage.js → storage/useLocalStorage.js} +0 -0
  146. /package/dist/hooks/{useSessionStorage.d.ts → storage/useSessionStorage.d.ts} +0 -0
  147. /package/dist/hooks/{useSessionStorage.js → storage/useSessionStorage.js} +0 -0
  148. /package/dist/hooks/{useMediaQuery.d.ts → ui/useMediaQuery.d.ts} +0 -0
  149. /package/dist/hooks/{useMediaQuery.js → ui/useMediaQuery.js} +0 -0
  150. /package/dist/hooks/{useWindowSize.d.ts → ui/useWindowSize.d.ts} +0 -0
  151. /package/dist/hooks/{useWindowSize.js → ui/useWindowSize.js} +0 -0
@@ -0,0 +1,203 @@
1
+ import React from 'react';
2
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
3
+ import { render, screen, waitFor } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
+ import IdSelector from './IdSelector';
6
+ describe('IdSelector', () => {
7
+ const mockOnLogin = vi.fn();
8
+ const defaultInfos = [
9
+ { id: 'admin@test.com', pw: 'admin123', memo: '관리자' },
10
+ { id: 'user@test.com', pw: 'user123', memo: '일반 사용자' },
11
+ ];
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+ afterEach(() => {
16
+ vi.restoreAllMocks();
17
+ });
18
+ describe('렌더링', () => {
19
+ it('컴포넌트가 정상적으로 렌더링된다', () => {
20
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
21
+ expect(screen.getByText('🚀 개발용 빠른 로그인')).toBeTruthy();
22
+ });
23
+ it('전달된 모든 로그인 정보를 렌더링한다', () => {
24
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
25
+ expect(screen.getByText('관리자')).toBeTruthy();
26
+ expect(screen.getByText('일반 사용자')).toBeTruthy();
27
+ expect(screen.getByText('admin@test.com')).toBeTruthy();
28
+ expect(screen.getByText('user@test.com')).toBeTruthy();
29
+ });
30
+ it('각 카드에 ID와 PW 정보를 표시한다', () => {
31
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
32
+ // ID와 PW는 여러 카드에 나타나므로 getAllByText 사용
33
+ const idLabels = screen.getAllByText('ID');
34
+ const pwLabels = screen.getAllByText('PW');
35
+ expect(idLabels).toHaveLength(2);
36
+ expect(pwLabels).toHaveLength(2);
37
+ defaultInfos.forEach((info) => {
38
+ expect(screen.getByText(info.id)).toBeTruthy();
39
+ expect(screen.getByText(info.pw)).toBeTruthy();
40
+ });
41
+ });
42
+ it('빈 배열이 전달되면 카드가 렌더링되지 않는다', () => {
43
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: [] }));
44
+ expect(screen.getByText('🚀 개발용 빠른 로그인')).toBeTruthy();
45
+ expect(screen.queryByRole('button')).toBeNull();
46
+ });
47
+ });
48
+ describe('로그인 기능', () => {
49
+ it('로그인 버튼 클릭 시 onLogin이 올바른 인자로 호출된다', async () => {
50
+ const user = userEvent.setup();
51
+ mockOnLogin.mockResolvedValue(undefined);
52
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
53
+ const adminButton = screen.getByRole('button', { name: '관리자' });
54
+ await user.click(adminButton);
55
+ await waitFor(() => {
56
+ expect(mockOnLogin).toHaveBeenCalledWith('admin@test.com', 'admin123');
57
+ expect(mockOnLogin).toHaveBeenCalledTimes(1);
58
+ });
59
+ });
60
+ it('여러 버튼을 순차적으로 클릭하면 각각의 정보로 onLogin이 호출된다', async () => {
61
+ const user = userEvent.setup();
62
+ mockOnLogin.mockResolvedValue(undefined);
63
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
64
+ const adminButton = screen.getByRole('button', { name: '관리자' });
65
+ const userButton = screen.getByRole('button', { name: '일반 사용자' });
66
+ await user.click(adminButton);
67
+ await waitFor(() => {
68
+ expect(mockOnLogin).toHaveBeenCalledWith('admin@test.com', 'admin123');
69
+ });
70
+ await user.click(userButton);
71
+ await waitFor(() => {
72
+ expect(mockOnLogin).toHaveBeenCalledWith('user@test.com', 'user123');
73
+ });
74
+ expect(mockOnLogin).toHaveBeenCalledTimes(2);
75
+ });
76
+ it('로그인 중일 때 버튼이 비활성화되고 텍스트가 변경된다', async () => {
77
+ const user = userEvent.setup();
78
+ let resolveLogin;
79
+ const loginPromise = new Promise((resolve) => {
80
+ resolveLogin = resolve;
81
+ });
82
+ mockOnLogin.mockReturnValue(loginPromise);
83
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
84
+ const adminButton = screen.getByRole('button', { name: '관리자' });
85
+ await user.click(adminButton);
86
+ await waitFor(() => {
87
+ expect(screen.getByText('로그인 중...')).toBeTruthy();
88
+ expect(adminButton.disabled).toBe(true);
89
+ });
90
+ resolveLogin();
91
+ await waitFor(() => {
92
+ expect(screen.getByText('관리자')).toBeTruthy();
93
+ expect(adminButton.disabled).toBe(false);
94
+ });
95
+ });
96
+ it('로그인 실패 시에도 로딩 상태가 해제된다', async () => {
97
+ const user = userEvent.setup();
98
+ const error = new Error('로그인 실패');
99
+ mockOnLogin.mockRejectedValue(error);
100
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
101
+ const adminButton = screen.getByRole('button', { name: '관리자' });
102
+ // 에러가 발생하더라도 catch 블록에서 처리되고 finally에서 상태가 해제됨
103
+ await user.click(adminButton);
104
+ // 에러가 발생하더라도 catch 블록에서 처리되고 상태가 해제되므로 기다림
105
+ await waitFor(() => {
106
+ expect(screen.getByText('관리자')).toBeTruthy();
107
+ expect(adminButton.disabled).toBe(false);
108
+ }, { timeout: 3000 });
109
+ // 에러가 발생했는지 확인
110
+ expect(mockOnLogin).toHaveBeenCalled();
111
+ });
112
+ it('한 카드가 로딩 중일 때 다른 카드의 버튼도 클릭 가능하다', async () => {
113
+ const user = userEvent.setup();
114
+ let resolveFirstLogin;
115
+ const firstLoginPromise = new Promise((resolve) => {
116
+ resolveFirstLogin = resolve;
117
+ });
118
+ mockOnLogin.mockImplementation(async (id) => {
119
+ if (id === 'admin@test.com') {
120
+ return firstLoginPromise;
121
+ }
122
+ return Promise.resolve();
123
+ });
124
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
125
+ const adminButton = screen.getByRole('button', { name: '관리자' });
126
+ const userButton = screen.getByRole('button', { name: '일반 사용자' });
127
+ await user.click(adminButton);
128
+ await waitFor(() => {
129
+ expect(adminButton.disabled).toBe(true);
130
+ });
131
+ // 다른 버튼은 여전히 클릭 가능해야 함
132
+ expect(userButton.disabled).toBe(false);
133
+ await user.click(userButton);
134
+ await waitFor(() => {
135
+ expect(mockOnLogin).toHaveBeenCalledWith('user@test.com', 'user123');
136
+ });
137
+ resolveFirstLogin();
138
+ });
139
+ });
140
+ describe('호버 상태', () => {
141
+ it('카드에 마우스를 올리면 hover 상태가 적용된다', async () => {
142
+ const user = userEvent.setup();
143
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: defaultInfos }));
144
+ const adminCard = screen.getByText('admin@test.com').closest('div[style*="border"]');
145
+ expect(adminCard).toBeTruthy();
146
+ // hover 이벤트는 CSS 스타일 변경이므로 스타일 속성을 직접 확인하기 어려움
147
+ // 대신 이벤트 핸들러가 정상 작동하는지 확인
148
+ const adminButton = screen.getByRole('button', { name: '관리자' });
149
+ await user.hover(adminButton);
150
+ // hover 이벤트가 발생했는지 확인 (에러가 없으면 성공)
151
+ expect(adminButton).toBeTruthy();
152
+ });
153
+ });
154
+ describe('에지 케이스', () => {
155
+ it('메모가 비어있는 경우에도 정상 동작한다', async () => {
156
+ const user = userEvent.setup();
157
+ mockOnLogin.mockResolvedValue(undefined);
158
+ const infosWithEmptyMemo = [
159
+ { id: 'test@test.com', pw: 'test123', memo: '' },
160
+ ];
161
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: infosWithEmptyMemo }));
162
+ const button = screen.getByRole('button');
163
+ expect(button).toBeTruthy();
164
+ expect(button.textContent).toBe('');
165
+ await user.click(button);
166
+ await waitFor(() => {
167
+ expect(mockOnLogin).toHaveBeenCalledWith('test@test.com', 'test123');
168
+ });
169
+ });
170
+ it('동일한 ID가 여러 개 있어도 각각 독립적으로 동작한다', async () => {
171
+ const user = userEvent.setup();
172
+ mockOnLogin.mockResolvedValue(undefined);
173
+ const duplicateInfos = [
174
+ { id: 'same@test.com', pw: 'pw1', memo: '첫 번째' },
175
+ { id: 'same@test.com', pw: 'pw2', memo: '두 번째' },
176
+ ];
177
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: duplicateInfos }));
178
+ const firstButton = screen.getByRole('button', { name: '첫 번째' });
179
+ const secondButton = screen.getByRole('button', { name: '두 번째' });
180
+ await user.click(firstButton);
181
+ await waitFor(() => {
182
+ expect(mockOnLogin).toHaveBeenCalledWith('same@test.com', 'pw1');
183
+ });
184
+ await user.click(secondButton);
185
+ await waitFor(() => {
186
+ expect(mockOnLogin).toHaveBeenCalledWith('same@test.com', 'pw2');
187
+ });
188
+ });
189
+ it('매우 긴 텍스트가 있어도 정상 렌더링된다', () => {
190
+ const longTextInfos = [
191
+ {
192
+ id: 'verylongemailaddressthatmightoverflow@example.com',
193
+ pw: 'verylongpasswordthatexceedsnormallimits123456789',
194
+ memo: '매우 긴 메모 텍스트가 있는 계정입니다. 이 텍스트는 매우 길 수 있습니다.',
195
+ },
196
+ ];
197
+ render(React.createElement(IdSelector, { onLogin: mockOnLogin, infos: longTextInfos }));
198
+ expect(screen.getByText(/매우 긴 메모/)).toBeTruthy();
199
+ expect(screen.getByText(/verylongemail/)).toBeTruthy();
200
+ expect(screen.getByText(/verylongpassword/)).toBeTruthy();
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { LoginInfo } from './IdSelector';
3
+ type LoginCardProps = {
4
+ info: LoginInfo;
5
+ index: number;
6
+ loading: number | null;
7
+ hoveredIndex: number | null;
8
+ hoveredButton: number | null;
9
+ onMouseEnter: () => void;
10
+ onMouseLeave: () => void;
11
+ onButtonMouseEnter: () => void;
12
+ onButtonMouseLeave: () => void;
13
+ onLogin: (info: LoginInfo, index: number) => Promise<void>;
14
+ };
15
+ export declare const LoginCard: React.FC<LoginCardProps>;
16
+ export {};
17
+ //# sourceMappingURL=LoginCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LoginCard.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/IdSelector/LoginCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAUzC,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,YAAY,EAAE,MAAM,IAAI,CAAC;IACzB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,OAAO,EAAE,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAyC9C,CAAC"}
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { getCardStyle, getButtonStyle, infoContainerStyle, infoRowStyle, labelStyle, valueStyle, } from './styles';
3
+ export const LoginCard = ({ info, index, loading, hoveredIndex, hoveredButton, onMouseEnter, onMouseLeave, onButtonMouseEnter, onButtonMouseLeave, onLogin, }) => {
4
+ const handleClick = () => {
5
+ onLogin(info, index);
6
+ };
7
+ return (React.createElement("div", { style: getCardStyle(hoveredIndex, index), onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave },
8
+ React.createElement("button", { onClick: handleClick, disabled: loading === index, style: getButtonStyle(hoveredButton, loading, index), onMouseEnter: onButtonMouseEnter, onMouseLeave: onButtonMouseLeave }, loading === index ? '로그인 중...' : info.memo),
9
+ React.createElement("div", { style: infoContainerStyle },
10
+ React.createElement("div", { style: infoRowStyle },
11
+ React.createElement("span", { style: labelStyle }, "ID"),
12
+ React.createElement("span", { style: valueStyle }, info.id)),
13
+ React.createElement("div", { style: infoRowStyle },
14
+ React.createElement("span", { style: labelStyle }, "PW"),
15
+ React.createElement("span", { style: valueStyle }, info.pw)))));
16
+ };
@@ -0,0 +1,3 @@
1
+ export { default } from './IdSelector';
2
+ export type { Props as IdSelectorProps, LoginInfo } from './IdSelector';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/IdSelector/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EAAE,KAAK,IAAI,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1 @@
1
+ export { default } from './IdSelector';
@@ -0,0 +1,16 @@
1
+ import { CSSProperties } from 'react';
2
+ export type StyleProps = {
3
+ hoveredIndex: number | null;
4
+ hoveredButton: number | null;
5
+ loading: number | null;
6
+ index: number;
7
+ };
8
+ export declare const getContainerStyle: () => CSSProperties;
9
+ export declare const getHeaderStyle: () => CSSProperties;
10
+ export declare const getCardStyle: (hoveredIndex: number | null, index: number) => CSSProperties;
11
+ export declare const getButtonStyle: (hoveredButton: number | null, loading: number | null, index: number) => CSSProperties;
12
+ export declare const infoContainerStyle: CSSProperties;
13
+ export declare const infoRowStyle: CSSProperties;
14
+ export declare const labelStyle: CSSProperties;
15
+ export declare const valueStyle: CSSProperties;
16
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../../src/components/dev/IdSelector/styles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAEtC,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,iBAAiB,QAAO,aAenC,CAAC;AAEH,eAAO,MAAM,cAAc,QAAO,aAMhC,CAAC;AAEH,eAAO,MAAM,YAAY,GAAI,cAAc,MAAM,GAAG,IAAI,EAAE,OAAO,MAAM,KAAG,aASxE,CAAC;AAEH,eAAO,MAAM,cAAc,GACzB,eAAe,MAAM,GAAG,IAAI,EAC5B,SAAS,MAAM,GAAG,IAAI,EACtB,OAAO,MAAM,KACZ,aAUD,CAAC;AAEH,eAAO,MAAM,kBAAkB,EAAE,aAQhC,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,aAI1B,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,aAGxB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,aAGxB,CAAC"}
@@ -0,0 +1,66 @@
1
+ export const getContainerStyle = () => ({
2
+ position: 'fixed',
3
+ top: '50%',
4
+ right: '16px',
5
+ transform: 'translateY(-50%)',
6
+ display: 'flex',
7
+ flexDirection: 'column',
8
+ gap: '12px',
9
+ padding: '16px',
10
+ backgroundColor: '#ffffff',
11
+ borderRadius: '12px',
12
+ boxShadow: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
13
+ border: '1px solid #e5e7eb',
14
+ minWidth: '280px',
15
+ zIndex: 9999,
16
+ });
17
+ export const getHeaderStyle = () => ({
18
+ color: '#111827',
19
+ fontSize: '14px',
20
+ fontWeight: 'bold',
21
+ paddingBottom: '8px',
22
+ borderBottom: '1px solid #e5e7eb',
23
+ });
24
+ export const getCardStyle = (hoveredIndex, index) => ({
25
+ display: 'flex',
26
+ flexDirection: 'column',
27
+ gap: '8px',
28
+ padding: '12px',
29
+ backgroundColor: '#f9fafb',
30
+ borderRadius: '8px',
31
+ border: hoveredIndex === index ? '1px solid #60a5fa' : '1px solid #e5e7eb',
32
+ transition: 'all 0.2s',
33
+ });
34
+ export const getButtonStyle = (hoveredButton, loading, index) => ({
35
+ width: '100%',
36
+ padding: '8px 16px',
37
+ backgroundColor: loading === index ? '#9ca3af' : hoveredButton === index ? '#2563eb' : '#3b82f6',
38
+ color: 'white',
39
+ fontWeight: 600,
40
+ borderRadius: '8px',
41
+ border: 'none',
42
+ cursor: loading === index ? 'not-allowed' : 'pointer',
43
+ transition: 'background-color 0.2s',
44
+ });
45
+ export const infoContainerStyle = {
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ gap: '4px',
49
+ fontSize: '12px',
50
+ color: '#6b7280',
51
+ paddingLeft: '4px',
52
+ paddingRight: '4px',
53
+ };
54
+ export const infoRowStyle = {
55
+ display: 'flex',
56
+ alignItems: 'center',
57
+ gap: '8px',
58
+ };
59
+ export const labelStyle = {
60
+ fontWeight: 600,
61
+ minWidth: '24px',
62
+ };
63
+ export const valueStyle = {
64
+ fontFamily: 'monospace',
65
+ color: '#374151',
66
+ };
@@ -13,7 +13,7 @@ type Props = {
13
13
  * @example
14
14
  * ```tsx
15
15
  * // Vite 프로젝트
16
- * import { WindowSizeDisplay } from 'goodchuck-utils/components/dev';
16
+ * import { WindowSizeDisplay } from '@blastlabs/utils/components/dev';
17
17
  *
18
18
  * function App() {
19
19
  * return (
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { useWindowSize } from '../../hooks/useWindowSize';
2
+ import { useWindowSize } from '../../hooks';
3
3
  const positionStyles = {
4
4
  'top-left': { top: 16, left: 16 },
5
5
  'top-right': { top: 16, right: 16 },
@@ -13,7 +13,7 @@ const positionStyles = {
13
13
  * @example
14
14
  * ```tsx
15
15
  * // Vite 프로젝트
16
- * import { WindowSizeDisplay } from 'goodchuck-utils/components/dev';
16
+ * import { WindowSizeDisplay } from '@blastlabs/utils/components/dev';
17
17
  *
18
18
  * function App() {
19
19
  * return (
@@ -10,7 +10,7 @@ type Props = {
10
10
  * @example
11
11
  * ```tsx
12
12
  * // Vite 프로젝트
13
- * import { ZIndexDebugger } from 'goodchuck-utils/components/dev';
13
+ * import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
14
14
  *
15
15
  * function App() {
16
16
  * return (
@@ -6,7 +6,7 @@ import React, { useState, useEffect } from 'react';
6
6
  * @example
7
7
  * ```tsx
8
8
  * // Vite 프로젝트
9
- * import { ZIndexDebugger } from 'goodchuck-utils/components/dev';
9
+ * import { ZIndexDebugger } from '@blastlabs/utils/components/dev';
10
10
  *
11
11
  * function App() {
12
12
  * return (
@@ -5,11 +5,12 @@
5
5
  * production 환경에서는 제외하는 것을 권장합니다.
6
6
  */
7
7
  export { default as IdSelector } from './IdSelector';
8
+ export type { IdSelectorProps, LoginInfo } from './IdSelector';
8
9
  export { default as WindowSizeDisplay } from './WindowSizeDisplay';
9
10
  export { default as DevPanel } from './DevPanel';
10
11
  export { default as ZIndexDebugger } from './ZIndexDebugger';
11
12
  export { default as ApiLogger, addApiLog, clearApiLogs } from './ApiLogger';
12
13
  export { default as FormDevTools } from './FormDevTools';
13
- export type { FormDevToolsProps, UseFormReturn as FormDevToolsUseFormReturn } from './FormDevTools';
14
+ export type { FormDevToolsProps } from './FormDevTools';
14
15
  export type { ApiLogEntry } from './ApiLogger';
15
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,aAAa,IAAI,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AACpG,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/dev/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC/D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -5,4 +5,5 @@
5
5
  * Note: Uses Tailwind CSS classes - ensure Tailwind is configured in your project.
6
6
  */
7
7
  export * from './dev';
8
+ export * from './auth';
8
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,OAAO,CAAC;AACtB,cAAc,QAAQ,CAAC"}
@@ -5,3 +5,4 @@
5
5
  * Note: Uses Tailwind CSS classes - ensure Tailwind is configured in your project.
6
6
  */
7
7
  export * from './dev';
8
+ export * from './auth';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useAuth.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAuth.test.d.ts","sourceRoot":"","sources":["../../../../src/hooks/auth/__tests__/useAuth.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { renderHook, act, waitFor } from '@testing-library/react';
3
+ import { useAuth } from '../useAuth';
4
+ describe('useAuth', () => {
5
+ beforeEach(() => {
6
+ localStorage.clear();
7
+ sessionStorage.clear();
8
+ vi.clearAllMocks();
9
+ });
10
+ afterEach(() => {
11
+ localStorage.clear();
12
+ sessionStorage.clear();
13
+ });
14
+ describe('Initialization', () => {
15
+ it('should initialize with no user', () => {
16
+ const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
17
+ expect(result.current.user).toBeNull();
18
+ expect(result.current.isAuthenticated).toBe(false);
19
+ expect(result.current.isLoading).toBe(false);
20
+ expect(result.current.token).toBeNull();
21
+ });
22
+ it('should load user from localStorage on mount', async () => {
23
+ const mockUser = { id: '1', name: 'Test User' };
24
+ localStorage.setItem('auth_token', 'test-token');
25
+ localStorage.setItem('auth_user', JSON.stringify(mockUser));
26
+ const { result } = renderHook(() => useAuth({ autoLoadUser: true }));
27
+ await waitFor(() => {
28
+ expect(result.current.user).toEqual(mockUser);
29
+ expect(result.current.isAuthenticated).toBe(true);
30
+ });
31
+ });
32
+ });
33
+ describe('Login', () => {
34
+ it('should login successfully', async () => {
35
+ const mockUser = { id: '1', name: 'Test User' };
36
+ const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'test-token' });
37
+ const onLoginSuccess = vi.fn();
38
+ const { result } = renderHook(() => useAuth({ loginFn, onLoginSuccess, autoLoadUser: false }));
39
+ await act(async () => {
40
+ await result.current.login({ email: 'test@example.com', password: 'password' });
41
+ });
42
+ expect(result.current.user).toEqual(mockUser);
43
+ expect(result.current.isAuthenticated).toBe(true);
44
+ expect(result.current.token).toBe('test-token');
45
+ expect(onLoginSuccess).toHaveBeenCalledWith(mockUser);
46
+ expect(localStorage.getItem('auth_token')).toBe('test-token');
47
+ });
48
+ it('should handle login error', async () => {
49
+ const loginError = new Error('Invalid credentials');
50
+ const loginFn = vi.fn().mockRejectedValue(loginError);
51
+ const onError = vi.fn();
52
+ const { result } = renderHook(() => useAuth({ loginFn, onError, autoLoadUser: false }));
53
+ let thrownError;
54
+ try {
55
+ await act(async () => {
56
+ await result.current.login({ email: 'test@example.com', password: 'wrong' });
57
+ });
58
+ }
59
+ catch (error) {
60
+ thrownError = error;
61
+ }
62
+ expect(thrownError).toEqual(loginError);
63
+ expect(result.current.user).toBeNull();
64
+ expect(result.current.isAuthenticated).toBe(false);
65
+ expect(onError).toHaveBeenCalledWith(loginError, 'login');
66
+ });
67
+ });
68
+ describe('Logout', () => {
69
+ it('should logout successfully', async () => {
70
+ const mockUser = { id: '1', name: 'Test User' };
71
+ const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'test-token' });
72
+ const logoutFn = vi.fn().mockResolvedValue(undefined);
73
+ const onLogoutSuccess = vi.fn();
74
+ const { result } = renderHook(() => useAuth({ loginFn, logoutFn, onLogoutSuccess, autoLoadUser: false }));
75
+ await act(async () => {
76
+ await result.current.login({ email: 'test@example.com', password: 'password' });
77
+ });
78
+ expect(result.current.isAuthenticated).toBe(true);
79
+ await act(async () => {
80
+ await result.current.logout();
81
+ });
82
+ expect(result.current.user).toBeNull();
83
+ expect(result.current.isAuthenticated).toBe(false);
84
+ expect(result.current.token).toBeNull();
85
+ expect(onLogoutSuccess).toHaveBeenCalled();
86
+ expect(localStorage.getItem('auth_token')).toBeNull();
87
+ });
88
+ });
89
+ describe('Register', () => {
90
+ it('should register successfully', async () => {
91
+ const mockUser = { id: '1', name: 'New User' };
92
+ const registerFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'new-token' });
93
+ const onRegisterSuccess = vi.fn();
94
+ const { result } = renderHook(() => useAuth({ registerFn, onRegisterSuccess, autoLoadUser: false }));
95
+ await act(async () => {
96
+ await result.current.register({ email: 'new@example.com', password: 'password', name: 'New User' });
97
+ });
98
+ expect(result.current.user).toEqual(mockUser);
99
+ expect(result.current.isAuthenticated).toBe(true);
100
+ expect(result.current.token).toBe('new-token');
101
+ expect(onRegisterSuccess).toHaveBeenCalledWith(mockUser);
102
+ });
103
+ });
104
+ describe('Token Management', () => {
105
+ it('should manually set token', async () => {
106
+ const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
107
+ await waitFor(() => {
108
+ expect(result.current.isLoading).toBe(false);
109
+ });
110
+ act(() => {
111
+ result.current.setToken('manual-token');
112
+ });
113
+ expect(result.current.token).toBe('manual-token');
114
+ expect(localStorage.getItem('auth_token')).toBe('manual-token');
115
+ });
116
+ it('should manually set user', async () => {
117
+ const mockUser = { id: '1', name: 'Manual User' };
118
+ const { result } = renderHook(() => useAuth({ autoLoadUser: false }));
119
+ await waitFor(() => {
120
+ expect(result.current.isLoading).toBe(false);
121
+ });
122
+ act(() => {
123
+ result.current.setUser(mockUser);
124
+ });
125
+ expect(result.current.user).toEqual(mockUser);
126
+ expect(result.current.isAuthenticated).toBe(true);
127
+ });
128
+ it('should use sessionStorage when useSessionStorage is true', async () => {
129
+ const mockUser = { id: '1', name: 'Test User' };
130
+ const loginFn = vi.fn().mockResolvedValue({ user: mockUser, token: 'session-token' });
131
+ const { result } = renderHook(() => useAuth({ loginFn, useSessionStorage: true, autoLoadUser: false }));
132
+ await act(async () => {
133
+ await result.current.login({ email: 'test@example.com', password: 'password' });
134
+ });
135
+ expect(sessionStorage.getItem('auth_token')).toBe('session-token');
136
+ expect(localStorage.getItem('auth_token')).toBeNull();
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Authentication Hooks
3
+ */
4
+ export * from './useAuth';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/auth/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,WAAW,CAAC"}
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Authentication Hooks
3
+ */
4
+ export * from './useAuth';