@blastlabs/utils 1.12.0 → 1.13.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 +20 -13
- package/bin/entity-generator-guide.md +126 -0
- package/bin/generate-entity.cjs +282 -0
- package/dist/components/auth/AuthGuard.d.ts +95 -0
- package/dist/components/auth/AuthGuard.d.ts.map +1 -0
- package/dist/components/auth/AuthGuard.js +103 -0
- package/dist/components/auth/index.d.ts +5 -0
- package/dist/components/auth/index.d.ts.map +1 -0
- package/dist/components/auth/index.js +4 -0
- package/dist/components/dev/ApiLogger.d.ts +1 -1
- package/dist/components/dev/ApiLogger.js +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/hooks/auth/__tests__/useAuth.test.d.ts +2 -0
- package/dist/hooks/auth/__tests__/useAuth.test.d.ts.map +1 -0
- package/dist/hooks/auth/__tests__/useAuth.test.js +139 -0
- package/dist/hooks/auth/index.d.ts +5 -0
- package/dist/hooks/auth/index.d.ts.map +1 -0
- package/dist/hooks/auth/index.js +4 -0
- package/dist/hooks/auth/useAuth.d.ts +275 -0
- package/dist/hooks/auth/useAuth.d.ts.map +1 -0
- package/dist/hooks/auth/useAuth.js +384 -0
- package/dist/hooks/form/useCRUDForm.d.ts +7 -28
- package/dist/hooks/form/useCRUDForm.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/index.js +1 -0
- package/dist/hooks/ui/useTabs.d.ts +33 -0
- package/dist/hooks/ui/useTabs.d.ts.map +1 -0
- package/dist/hooks/ui/useTabs.js +117 -0
- package/dist/index.js +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 인증 관리 Hook
|
|
4
|
+
*
|
|
5
|
+
* 로그인, 로그아웃, 회원가입, 토큰 관리 등 인증 관련 기능을 제공합니다.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // 기본 사용 (로그인 유지 옵션 포함)
|
|
10
|
+
* function LoginPage() {
|
|
11
|
+
* const [rememberMe, setRememberMe] = useState(false);
|
|
12
|
+
* const { login, isLoggingIn, error, isAuthenticated } = useAuth({
|
|
13
|
+
* loginFn: (credentials) => api.login(credentials),
|
|
14
|
+
* onLoginSuccess: (user) => {
|
|
15
|
+
* console.log('로그인 성공:', user);
|
|
16
|
+
* navigate('/dashboard');
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* const handleSubmit = async (e) => {
|
|
21
|
+
* e.preventDefault();
|
|
22
|
+
* // rememberMe가 true면 localStorage, false면 sessionStorage 사용
|
|
23
|
+
* await login({ email, password }, { rememberMe });
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* if (isAuthenticated) {
|
|
27
|
+
* return <Navigate to="/dashboard" />;
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* return (
|
|
31
|
+
* <form onSubmit={handleSubmit}>
|
|
32
|
+
* <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
|
33
|
+
* <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
|
|
34
|
+
* <label>
|
|
35
|
+
* <input type="checkbox" checked={rememberMe} onChange={(e) => setRememberMe(e.target.checked)} />
|
|
36
|
+
* 로그인 유지
|
|
37
|
+
* </label>
|
|
38
|
+
* <button type="submit" disabled={isLoggingIn}>
|
|
39
|
+
* {isLoggingIn ? '로그인 중...' : '로그인'}
|
|
40
|
+
* </button>
|
|
41
|
+
* {error && <div>{error.message}</div>}
|
|
42
|
+
* </form>
|
|
43
|
+
* );
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* // 자동 토큰 갱신과 함께 사용
|
|
50
|
+
* function App() {
|
|
51
|
+
* const { user, isAuthenticated, isLoading, logout } = useAuth({
|
|
52
|
+
* getUserFn: () => api.getMe(),
|
|
53
|
+
* logoutFn: () => api.logout(),
|
|
54
|
+
* refreshTokenFn: () => api.refreshToken(),
|
|
55
|
+
* autoRefresh: true,
|
|
56
|
+
* refreshInterval: 300000, // 5분마다 갱신
|
|
57
|
+
* onLogoutSuccess: () => navigate('/login'),
|
|
58
|
+
* });
|
|
59
|
+
*
|
|
60
|
+
* if (isLoading) return <div>로딩중...</div>;
|
|
61
|
+
*
|
|
62
|
+
* return (
|
|
63
|
+
* <div>
|
|
64
|
+
* {isAuthenticated ? (
|
|
65
|
+
* <>
|
|
66
|
+
* <div>환영합니다, {user?.name}님</div>
|
|
67
|
+
* <button onClick={logout}>로그아웃</button>
|
|
68
|
+
* </>
|
|
69
|
+
* ) : (
|
|
70
|
+
* <LoginPage />
|
|
71
|
+
* )}
|
|
72
|
+
* </div>
|
|
73
|
+
* );
|
|
74
|
+
* }
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```tsx
|
|
79
|
+
* // 회원가입 포함
|
|
80
|
+
* function RegisterPage() {
|
|
81
|
+
* const { register, isRegistering, error } = useAuth({
|
|
82
|
+
* registerFn: (data) => api.register(data),
|
|
83
|
+
* onRegisterSuccess: (user) => {
|
|
84
|
+
* toast.success('회원가입 성공!');
|
|
85
|
+
* navigate('/');
|
|
86
|
+
* },
|
|
87
|
+
* });
|
|
88
|
+
*
|
|
89
|
+
* const handleSubmit = async (data) => {
|
|
90
|
+
* await register(data);
|
|
91
|
+
* };
|
|
92
|
+
*
|
|
93
|
+
* return (
|
|
94
|
+
* <form onSubmit={handleSubmit}>
|
|
95
|
+
* <input name="email" />
|
|
96
|
+
* <input name="password" type="password" />
|
|
97
|
+
* <input name="name" />
|
|
98
|
+
* <button type="submit" disabled={isRegistering}>
|
|
99
|
+
* 회원가입
|
|
100
|
+
* </button>
|
|
101
|
+
* </form>
|
|
102
|
+
* );
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* // Storage에 저장되는 값 확인하기
|
|
109
|
+
* function DebugAuth() {
|
|
110
|
+
* const { login, user, token } = useAuth({
|
|
111
|
+
* loginFn: api.login,
|
|
112
|
+
* storageKey: 'my_app_token', // 커스텀 키 사용 가능
|
|
113
|
+
* userStorageKey: 'my_app_user',
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* const handleLogin = async () => {
|
|
117
|
+
* await login({ email, password }, { rememberMe: true });
|
|
118
|
+
*
|
|
119
|
+
* // 로그인 후 storage 확인
|
|
120
|
+
* console.log('Token:', localStorage.getItem('my_app_token'));
|
|
121
|
+
* // 출력: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
122
|
+
*
|
|
123
|
+
* console.log('User:', localStorage.getItem('my_app_user'));
|
|
124
|
+
* // 출력: {"id":"123","name":"John Doe","email":"john@example.com"}
|
|
125
|
+
* };
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```tsx
|
|
131
|
+
* // rememberMe에 따른 storage 동작
|
|
132
|
+
* function LoginWithRememberMe() {
|
|
133
|
+
* const [rememberMe, setRememberMe] = useState(false);
|
|
134
|
+
* const { login } = useAuth({ loginFn: api.login });
|
|
135
|
+
*
|
|
136
|
+
* const handleLogin = async () => {
|
|
137
|
+
* await login({ email, password }, { rememberMe });
|
|
138
|
+
*
|
|
139
|
+
* if (rememberMe) {
|
|
140
|
+
* // localStorage에 저장 (브라우저 종료해도 유지)
|
|
141
|
+
* console.log(localStorage.getItem('auth_token')); // "eyJ..."
|
|
142
|
+
* console.log(sessionStorage.getItem('auth_token')); // null
|
|
143
|
+
* } else {
|
|
144
|
+
* // sessionStorage에 저장 (브라우저 종료 시 삭제)
|
|
145
|
+
* console.log(localStorage.getItem('auth_token')); // null
|
|
146
|
+
* console.log(sessionStorage.getItem('auth_token')); // "eyJ..."
|
|
147
|
+
* }
|
|
148
|
+
* };
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*/
|
|
152
|
+
export function useAuth(options = {}) {
|
|
153
|
+
const { loginFn, logoutFn, registerFn, getUserFn, refreshTokenFn, onLoginSuccess, onLogoutSuccess, onRegisterSuccess, onError, storageKey = 'auth_token', userStorageKey = 'auth_user', useSessionStorage = false, autoRefresh = false, refreshInterval = 300000, // 5분
|
|
154
|
+
autoLoadUser = true, } = options;
|
|
155
|
+
const [user, setUser] = useState(null);
|
|
156
|
+
const [token, setTokenState] = useState(() => {
|
|
157
|
+
// SSR 환경에서는 storage 접근 불가
|
|
158
|
+
if (typeof window === 'undefined')
|
|
159
|
+
return null;
|
|
160
|
+
// 초기 로드 시 localStorage와 sessionStorage 모두 확인
|
|
161
|
+
return localStorage.getItem(storageKey) || sessionStorage.getItem(storageKey);
|
|
162
|
+
});
|
|
163
|
+
const [isLoading, setIsLoading] = useState(autoLoadUser);
|
|
164
|
+
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
|
165
|
+
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
|
166
|
+
const [isRegistering, setIsRegistering] = useState(false);
|
|
167
|
+
const [error, setError] = useState(null);
|
|
168
|
+
const refreshIntervalRef = useRef(null);
|
|
169
|
+
// 토큰 설정 함수 (storage 타입 동적 선택 가능)
|
|
170
|
+
const setToken = useCallback((newToken, rememberMe = !useSessionStorage) => {
|
|
171
|
+
setTokenState(newToken);
|
|
172
|
+
// SSR 환경에서는 storage 접근 불가
|
|
173
|
+
if (typeof window === 'undefined')
|
|
174
|
+
return;
|
|
175
|
+
const storage = rememberMe ? localStorage : sessionStorage;
|
|
176
|
+
const otherStorage = rememberMe ? sessionStorage : localStorage;
|
|
177
|
+
if (newToken) {
|
|
178
|
+
storage.setItem(storageKey, newToken);
|
|
179
|
+
// 다른 storage에서는 제거 (중복 방지)
|
|
180
|
+
otherStorage.removeItem(storageKey);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// 로그아웃 시 양쪽 모두 제거
|
|
184
|
+
localStorage.removeItem(storageKey);
|
|
185
|
+
sessionStorage.removeItem(storageKey);
|
|
186
|
+
}
|
|
187
|
+
}, [storageKey, useSessionStorage]);
|
|
188
|
+
// 사용자 정보 저장 (rememberMe 옵션 추가)
|
|
189
|
+
const saveUser = useCallback((userData, rememberMe = !useSessionStorage) => {
|
|
190
|
+
setUser(userData);
|
|
191
|
+
// SSR 환경에서는 storage 접근 불가
|
|
192
|
+
if (typeof window === 'undefined')
|
|
193
|
+
return;
|
|
194
|
+
const storage = rememberMe ? localStorage : sessionStorage;
|
|
195
|
+
const otherStorage = rememberMe ? sessionStorage : localStorage;
|
|
196
|
+
if (userData) {
|
|
197
|
+
storage.setItem(userStorageKey, JSON.stringify(userData));
|
|
198
|
+
// 다른 storage에서는 제거
|
|
199
|
+
otherStorage.removeItem(userStorageKey);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// 로그아웃 시 양쪽 모두 제거
|
|
203
|
+
localStorage.removeItem(userStorageKey);
|
|
204
|
+
sessionStorage.removeItem(userStorageKey);
|
|
205
|
+
}
|
|
206
|
+
}, [userStorageKey, useSessionStorage]);
|
|
207
|
+
// 사용자 정보 로드
|
|
208
|
+
const loadUser = useCallback(async () => {
|
|
209
|
+
if (!getUserFn || !token) {
|
|
210
|
+
setIsLoading(false);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const userData = await getUserFn();
|
|
215
|
+
saveUser(userData);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const error = err instanceof Error ? err : new Error('Failed to load user');
|
|
219
|
+
setError(error);
|
|
220
|
+
setToken(null);
|
|
221
|
+
saveUser(null);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
setIsLoading(false);
|
|
225
|
+
}
|
|
226
|
+
}, [getUserFn, token, saveUser, setToken]);
|
|
227
|
+
// 초기 로드
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
// SSR 환경에서는 storage 접근 불가
|
|
230
|
+
if (typeof window === 'undefined') {
|
|
231
|
+
setIsLoading(false);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (autoLoadUser && token) {
|
|
235
|
+
// localStorage와 sessionStorage 모두 확인
|
|
236
|
+
const savedUser = localStorage.getItem(userStorageKey) || sessionStorage.getItem(userStorageKey);
|
|
237
|
+
if (savedUser) {
|
|
238
|
+
try {
|
|
239
|
+
setUser(JSON.parse(savedUser));
|
|
240
|
+
setIsLoading(false);
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// JSON 파싱 실패 시 API에서 다시 가져오기
|
|
244
|
+
loadUser();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
loadUser();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
setIsLoading(false);
|
|
253
|
+
}
|
|
254
|
+
}, []);
|
|
255
|
+
// 로그인
|
|
256
|
+
const login = useCallback(async (credentials, loginOptions) => {
|
|
257
|
+
if (!loginFn) {
|
|
258
|
+
throw new Error('loginFn is required for login');
|
|
259
|
+
}
|
|
260
|
+
const rememberMe = loginOptions?.rememberMe ?? !useSessionStorage;
|
|
261
|
+
setIsLoggingIn(true);
|
|
262
|
+
setError(null);
|
|
263
|
+
try {
|
|
264
|
+
const result = await loginFn(credentials);
|
|
265
|
+
if (result.token) {
|
|
266
|
+
setToken(result.token, rememberMe);
|
|
267
|
+
}
|
|
268
|
+
saveUser(result.user, rememberMe);
|
|
269
|
+
onLoginSuccess?.(result.user);
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
const error = err instanceof Error ? err : new Error('Login failed');
|
|
273
|
+
setError(error);
|
|
274
|
+
onError?.(error, 'login');
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
setIsLoggingIn(false);
|
|
279
|
+
}
|
|
280
|
+
}, [loginFn, setToken, saveUser, onLoginSuccess, onError, useSessionStorage]);
|
|
281
|
+
// 로그아웃
|
|
282
|
+
const logout = useCallback(async () => {
|
|
283
|
+
setIsLoggingOut(true);
|
|
284
|
+
setError(null);
|
|
285
|
+
try {
|
|
286
|
+
if (logoutFn) {
|
|
287
|
+
await logoutFn();
|
|
288
|
+
}
|
|
289
|
+
setToken(null);
|
|
290
|
+
saveUser(null);
|
|
291
|
+
onLogoutSuccess?.();
|
|
292
|
+
}
|
|
293
|
+
catch (err) {
|
|
294
|
+
const error = err instanceof Error ? err : new Error('Logout failed');
|
|
295
|
+
setError(error);
|
|
296
|
+
onError?.(error, 'logout');
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
setIsLoggingOut(false);
|
|
301
|
+
}
|
|
302
|
+
}, [logoutFn, setToken, saveUser, onLogoutSuccess, onError]);
|
|
303
|
+
// 회원가입
|
|
304
|
+
const register = useCallback(async (data, registerOptions) => {
|
|
305
|
+
if (!registerFn) {
|
|
306
|
+
throw new Error('registerFn is required for register');
|
|
307
|
+
}
|
|
308
|
+
const rememberMe = registerOptions?.rememberMe ?? !useSessionStorage;
|
|
309
|
+
setIsRegistering(true);
|
|
310
|
+
setError(null);
|
|
311
|
+
try {
|
|
312
|
+
const result = await registerFn(data);
|
|
313
|
+
if (result.token) {
|
|
314
|
+
setToken(result.token, rememberMe);
|
|
315
|
+
}
|
|
316
|
+
saveUser(result.user, rememberMe);
|
|
317
|
+
onRegisterSuccess?.(result.user);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
const error = err instanceof Error ? err : new Error('Registration failed');
|
|
321
|
+
setError(error);
|
|
322
|
+
onError?.(error, 'register');
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
setIsRegistering(false);
|
|
327
|
+
}
|
|
328
|
+
}, [registerFn, setToken, saveUser, onRegisterSuccess, onError, useSessionStorage]);
|
|
329
|
+
// 토큰 갱신
|
|
330
|
+
const refreshToken = useCallback(async () => {
|
|
331
|
+
if (!refreshTokenFn) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const result = await refreshTokenFn();
|
|
336
|
+
setToken(result.token);
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
const error = err instanceof Error ? err : new Error('Token refresh failed');
|
|
340
|
+
setError(error);
|
|
341
|
+
onError?.(error, 'refresh');
|
|
342
|
+
// 토큰 갱신 실패 시 로그아웃
|
|
343
|
+
await logout();
|
|
344
|
+
}
|
|
345
|
+
}, [refreshTokenFn, setToken, onError, logout]);
|
|
346
|
+
// 자동 토큰 갱신
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
if (autoRefresh && token && refreshTokenFn) {
|
|
349
|
+
refreshIntervalRef.current = setInterval(() => {
|
|
350
|
+
refreshToken();
|
|
351
|
+
}, refreshInterval);
|
|
352
|
+
return () => {
|
|
353
|
+
if (refreshIntervalRef.current) {
|
|
354
|
+
clearInterval(refreshIntervalRef.current);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}, [autoRefresh, token, refreshTokenFn, refreshToken, refreshInterval]);
|
|
359
|
+
// 사용자 정보 새로고침
|
|
360
|
+
const refetchUser = useCallback(async () => {
|
|
361
|
+
if (!getUserFn) {
|
|
362
|
+
throw new Error('getUserFn is required for refetchUser');
|
|
363
|
+
}
|
|
364
|
+
setIsLoading(true);
|
|
365
|
+
await loadUser();
|
|
366
|
+
}, [getUserFn, loadUser]);
|
|
367
|
+
return {
|
|
368
|
+
user,
|
|
369
|
+
isAuthenticated: !!user,
|
|
370
|
+
isLoading,
|
|
371
|
+
isLoggingIn,
|
|
372
|
+
isLoggingOut,
|
|
373
|
+
isRegistering,
|
|
374
|
+
error,
|
|
375
|
+
login,
|
|
376
|
+
logout,
|
|
377
|
+
register,
|
|
378
|
+
refreshToken,
|
|
379
|
+
refetchUser,
|
|
380
|
+
token,
|
|
381
|
+
setToken,
|
|
382
|
+
setUser: saveUser,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
@@ -1,37 +1,15 @@
|
|
|
1
|
-
import type { FieldValues } from 'react-hook-form';
|
|
1
|
+
import type { FieldValues, UseFormReturn } from 'react-hook-form';
|
|
2
2
|
/**
|
|
3
3
|
* CRUD Form Mode
|
|
4
4
|
*/
|
|
5
5
|
export type FormMode = 'create' | 'edit';
|
|
6
|
-
|
|
7
|
-
watch: (name?: any) => any;
|
|
8
|
-
getValues: (name?: any) => any;
|
|
9
|
-
setValue: (name: any, value: any, options?: {
|
|
10
|
-
shouldValidate?: boolean;
|
|
11
|
-
shouldDirty?: boolean;
|
|
12
|
-
shouldTouch?: boolean;
|
|
13
|
-
}) => void;
|
|
14
|
-
trigger?: (name?: any) => Promise<boolean>;
|
|
15
|
-
formState: {
|
|
16
|
-
errors?: Record<string, any>;
|
|
17
|
-
dirtyFields?: Record<string, any>;
|
|
18
|
-
touchedFields?: Record<string, any>;
|
|
19
|
-
isValid?: boolean;
|
|
20
|
-
isSubmitting?: boolean;
|
|
21
|
-
submitCount?: number;
|
|
22
|
-
defaultValues?: TFieldValues;
|
|
23
|
-
};
|
|
24
|
-
handleSubmit?: (...args: any[]) => any;
|
|
25
|
-
register?: (...args: any[]) => any;
|
|
26
|
-
reset?: (...args: any[]) => any;
|
|
27
|
-
[key: string]: any;
|
|
28
|
-
};
|
|
6
|
+
type TId = string | number | null;
|
|
29
7
|
/**
|
|
30
8
|
* useCRUDForm 옵션
|
|
31
9
|
*/
|
|
32
|
-
export interface UseCRUDFormOptions<TData extends FieldValues = FieldValues
|
|
10
|
+
export interface UseCRUDFormOptions<TData extends FieldValues = FieldValues> {
|
|
33
11
|
/** 편집 모드일 때의 ID (있으면 edit mode, 없으면 create mode) */
|
|
34
|
-
id?: TId
|
|
12
|
+
id?: TId;
|
|
35
13
|
/** react-hook-form instance */
|
|
36
14
|
form: UseFormReturn<TData>;
|
|
37
15
|
/** 편집 모드일 때 데이터를 fetch하는 함수 */
|
|
@@ -39,7 +17,7 @@ export interface UseCRUDFormOptions<TData extends FieldValues = FieldValues, TId
|
|
|
39
17
|
/** 생성 API 함수 */
|
|
40
18
|
createFn?: (data: TData) => Promise<any>;
|
|
41
19
|
/** 수정 API 함수 */
|
|
42
|
-
updateFn?: (id: TId, data: TData) => Promise<any>;
|
|
20
|
+
updateFn?: (id: Exclude<TId, null | undefined>, data: TData) => Promise<any>;
|
|
43
21
|
/** 삭제 API 함수 (옵션) */
|
|
44
22
|
deleteFn?: (id: TId) => Promise<any>;
|
|
45
23
|
/** 성공 시 콜백 */
|
|
@@ -228,5 +206,6 @@ export interface UseCRUDFormReturn<TData = any, TId = string | number> {
|
|
|
228
206
|
* }
|
|
229
207
|
* ```
|
|
230
208
|
*/
|
|
231
|
-
export declare function useCRUDForm<TData extends FieldValues = FieldValues
|
|
209
|
+
export declare function useCRUDForm<TData extends FieldValues = FieldValues>(options: UseCRUDFormOptions<TData>): UseCRUDFormReturn<TData>;
|
|
210
|
+
export {};
|
|
232
211
|
//# sourceMappingURL=useCRUDForm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useCRUDForm.d.ts","sourceRoot":"","sources":["../../../src/hooks/form/useCRUDForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"useCRUDForm.d.ts","sourceRoot":"","sources":["../../../src/hooks/form/useCRUDForm.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAiCzC,KAAK,GAAG,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAClC;;GAEG;AACH,MAAM,WAAW,kBAAkB,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW;IACzE,oDAAoD;IACpD,EAAE,CAAC,EAAE,GAAG,CAAC;IAET,+BAA+B;IAC/B,IAAI,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;IAE3B,+BAA+B;IAC/B,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,KAAK,CAAC,CAAC;IAEtC,gBAAgB;IAChB,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAEzC,gBAAgB;IAChB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,GAAG,SAAS,CAAC,EAAE,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAE7E,qBAAqB;IACrB,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IAErC,cAAc;IACd,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE,cAAc;IACd,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,KAAK,IAAI,CAAC;IAEjD,qCAAqC;IACrC,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAEvC,oCAAoC;IACpC,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,KAAK,GAAG,CAAC;IAErD,sCAAsC;IACtC,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB,CAAC,KAAK,GAAG,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,MAAM;IACnE,cAAc;IACd,IAAI,EAAE,QAAQ,CAAC;IAEf,eAAe;IACf,UAAU,EAAE,OAAO,CAAC;IAEpB,eAAe;IACf,YAAY,EAAE,OAAO,CAAC;IAEtB,uBAAuB;IACvB,SAAS,EAAE,OAAO,CAAC;IAEnB,+BAA+B;IAC/B,YAAY,EAAE,OAAO,CAAC;IAEtB,WAAW;IACX,UAAU,EAAE,OAAO,CAAC;IAEpB,SAAS;IACT,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,4CAA4C;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,aAAa;IACb,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC,iBAAiB;IACjB,YAAY,EAAE,KAAK,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqJG;AACH,wBAAgB,WAAW,CAAC,KAAK,SAAS,WAAW,GAAG,WAAW,EACjE,OAAO,EAAE,kBAAkB,CAAC,KAAK,CAAC,GACjC,iBAAiB,CAAC,KAAK,CAAC,CAiI1B"}
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,eAAe,CAAC;AAG9B,cAAc,SAAS,CAAC;AAGxB,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,MAAM,CAAC;AAGrB,cAAc,WAAW,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,SAAS,CAAC;AAGxB,cAAc,eAAe,CAAC;AAG9B,cAAc,SAAS,CAAC;AAGxB,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,MAAM,CAAC;AAGrB,cAAc,WAAW,CAAC;AAG1B,cAAc,QAAQ,CAAC"}
|
package/dist/hooks/index.js
CHANGED
package/dist/hooks/ui/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC"}
|
package/dist/hooks/ui/index.js
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
type UseTabsOptions = {
|
|
2
|
+
items: Array<{
|
|
3
|
+
id: string;
|
|
4
|
+
}>;
|
|
5
|
+
defaultTab?: string;
|
|
6
|
+
syncWithUrl?: boolean;
|
|
7
|
+
urlParamName?: string;
|
|
8
|
+
};
|
|
9
|
+
type UseTabsReturn = {
|
|
10
|
+
selectedTab: string;
|
|
11
|
+
selectedIndex: number;
|
|
12
|
+
setSelectedTab: (tabId: string) => void;
|
|
13
|
+
setSelectedIndex: (index: number) => void;
|
|
14
|
+
getTabId: (index: number) => string | undefined;
|
|
15
|
+
getTabIndex: (tabId: string) => number;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 탭 상태 관리 훅
|
|
19
|
+
* URL 동기화 및 상태 관리 로직을 제공
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* const { selectedTab, setSelectedTab, selectedIndex } = useTabs({
|
|
24
|
+
* items: tabs,
|
|
25
|
+
* defaultTab: 'tab1',
|
|
26
|
+
* syncWithUrl: true,
|
|
27
|
+
* urlParamName: 'tab'
|
|
28
|
+
* });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function useTabs({ items, defaultTab, syncWithUrl, urlParamName, }: UseTabsOptions): UseTabsReturn;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=useTabs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTabs.d.ts","sourceRoot":"","sources":["../../../src/hooks/ui/useTabs.ts"],"names":[],"mappings":"AAIA,KAAK,cAAc,GAAG;IACpB,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,gBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAChD,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CACxC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,EACtB,KAAK,EACL,UAAU,EACV,WAAmB,EACnB,YAAoB,GACrB,EAAE,cAAc,GAAG,aAAa,CA4GhC"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect, useMemo, useCallback } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* 탭 상태 관리 훅
|
|
5
|
+
* URL 동기화 및 상태 관리 로직을 제공
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const { selectedTab, setSelectedTab, selectedIndex } = useTabs({
|
|
10
|
+
* items: tabs,
|
|
11
|
+
* defaultTab: 'tab1',
|
|
12
|
+
* syncWithUrl: true,
|
|
13
|
+
* urlParamName: 'tab'
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function useTabs({ items, defaultTab, syncWithUrl = false, urlParamName = 'tab', }) {
|
|
18
|
+
// URL에서 탭 ID 가져오기
|
|
19
|
+
const getTabFromUrl = useCallback(() => {
|
|
20
|
+
if (!syncWithUrl || typeof window === 'undefined') {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const params = new URLSearchParams(window.location.search);
|
|
24
|
+
return params.get(urlParamName) || undefined;
|
|
25
|
+
}, [syncWithUrl, urlParamName]);
|
|
26
|
+
// 초기 선택된 탭 결정 (우선순위: URL > defaultTab > 첫 번째)
|
|
27
|
+
const initialSelectedTab = useMemo(() => {
|
|
28
|
+
if (syncWithUrl) {
|
|
29
|
+
const urlTab = getTabFromUrl();
|
|
30
|
+
if (urlTab)
|
|
31
|
+
return urlTab;
|
|
32
|
+
}
|
|
33
|
+
return defaultTab || items[0]?.id || '';
|
|
34
|
+
}, [syncWithUrl, defaultTab, items, getTabFromUrl]);
|
|
35
|
+
const [selectedTab, setSelectedTabState] = useState(initialSelectedTab);
|
|
36
|
+
// 선택된 탭의 인덱스
|
|
37
|
+
const selectedIndex = useMemo(() => {
|
|
38
|
+
const index = items.findIndex((item) => item.id === selectedTab);
|
|
39
|
+
return index >= 0 ? index : 0;
|
|
40
|
+
}, [items, selectedTab]);
|
|
41
|
+
// URL 업데이트 함수
|
|
42
|
+
const updateUrl = (tabId) => {
|
|
43
|
+
if (!syncWithUrl || typeof window === 'undefined')
|
|
44
|
+
return;
|
|
45
|
+
const url = new URL(window.location.href);
|
|
46
|
+
if (tabId === defaultTab) {
|
|
47
|
+
// 기본 탭이면 URL에서 제거 (깔끔한 URL 유지)
|
|
48
|
+
url.searchParams.delete(urlParamName);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
url.searchParams.set(urlParamName, tabId);
|
|
52
|
+
}
|
|
53
|
+
window.history.pushState({}, '', url.toString());
|
|
54
|
+
};
|
|
55
|
+
// 탭 ID로 선택
|
|
56
|
+
const setSelectedTab = (tabId) => {
|
|
57
|
+
setSelectedTabState(tabId);
|
|
58
|
+
if (syncWithUrl) {
|
|
59
|
+
updateUrl(tabId);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
// 인덱스로 선택
|
|
63
|
+
const setSelectedIndex = (index) => {
|
|
64
|
+
const tabId = items[index]?.id;
|
|
65
|
+
if (tabId) {
|
|
66
|
+
setSelectedTab(tabId);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
// 인덱스로 탭 ID 가져오기
|
|
70
|
+
const getTabId = (index) => {
|
|
71
|
+
return items[index]?.id;
|
|
72
|
+
};
|
|
73
|
+
// 탭 ID로 인덱스 가져오기
|
|
74
|
+
const getTabIndex = (tabId) => {
|
|
75
|
+
return items.findIndex((item) => item.id === tabId);
|
|
76
|
+
};
|
|
77
|
+
// 브라우저 뒤로가기/앞으로가기 지원
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!syncWithUrl || typeof window === 'undefined')
|
|
80
|
+
return;
|
|
81
|
+
const handlePopState = () => {
|
|
82
|
+
const urlTab = getTabFromUrl();
|
|
83
|
+
if (urlTab) {
|
|
84
|
+
setSelectedTabState(urlTab);
|
|
85
|
+
}
|
|
86
|
+
else if (defaultTab) {
|
|
87
|
+
setSelectedTabState(defaultTab);
|
|
88
|
+
}
|
|
89
|
+
else if (items[0]?.id) {
|
|
90
|
+
setSelectedTabState(items[0].id);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
window.addEventListener('popstate', handlePopState);
|
|
94
|
+
return () => {
|
|
95
|
+
window.removeEventListener('popstate', handlePopState);
|
|
96
|
+
};
|
|
97
|
+
}, [syncWithUrl, defaultTab, items, getTabFromUrl]);
|
|
98
|
+
// URL 초기 동기화
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!syncWithUrl || typeof window === 'undefined')
|
|
101
|
+
return;
|
|
102
|
+
const urlTab = getTabFromUrl();
|
|
103
|
+
if (urlTab && urlTab !== selectedTab) {
|
|
104
|
+
// URL과 상태가 다를 때만 업데이트 (초기 로드 시)
|
|
105
|
+
setSelectedTabState(urlTab);
|
|
106
|
+
}
|
|
107
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
108
|
+
}, [syncWithUrl, urlParamName]); // 초기 마운트 시에만 실행
|
|
109
|
+
return {
|
|
110
|
+
selectedTab,
|
|
111
|
+
selectedIndex,
|
|
112
|
+
setSelectedTab,
|
|
113
|
+
setSelectedIndex,
|
|
114
|
+
getTabId,
|
|
115
|
+
getTabIndex,
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export * from './string';
|
|
|
8
8
|
export * from './number';
|
|
9
9
|
// Mock data utilities
|
|
10
10
|
export * from './mock';
|
|
11
|
-
// React Hooks (import separately: '
|
|
11
|
+
// React Hooks (import separately: '@blastlabs/utils/hooks')
|
|
12
12
|
// Note: Hooks are not exported from main entry to avoid React dependency for non-React users
|
|
13
13
|
// export * from './hooks';
|
|
14
14
|
// Array utilities (placeholder for future)
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blastlabs/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"blastlabs-generate-entity": "./bin/generate-entity.cjs"
|
|
9
|
+
},
|
|
7
10
|
"files": [
|
|
8
|
-
"dist"
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
9
13
|
],
|
|
10
14
|
"scripts": {
|
|
11
15
|
"prepare": "npm run build:tsc",
|