@blastlabs/utils 1.12.0 → 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 (34) 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 +1 -1
  9. package/dist/components/dev/ApiLogger.js +1 -1
  10. package/dist/components/index.d.ts +1 -0
  11. package/dist/components/index.d.ts.map +1 -1
  12. package/dist/components/index.js +1 -0
  13. package/dist/hooks/auth/__tests__/useAuth.test.d.ts +2 -0
  14. package/dist/hooks/auth/__tests__/useAuth.test.d.ts.map +1 -0
  15. package/dist/hooks/auth/__tests__/useAuth.test.js +139 -0
  16. package/dist/hooks/auth/index.d.ts +5 -0
  17. package/dist/hooks/auth/index.d.ts.map +1 -0
  18. package/dist/hooks/auth/index.js +4 -0
  19. package/dist/hooks/auth/useAuth.d.ts +275 -0
  20. package/dist/hooks/auth/useAuth.d.ts.map +1 -0
  21. package/dist/hooks/auth/useAuth.js +384 -0
  22. package/dist/hooks/form/useCRUDForm.d.ts +7 -28
  23. package/dist/hooks/form/useCRUDForm.d.ts.map +1 -1
  24. package/dist/hooks/index.d.ts +1 -0
  25. package/dist/hooks/index.d.ts.map +1 -1
  26. package/dist/hooks/index.js +2 -0
  27. package/dist/hooks/ui/index.d.ts +1 -0
  28. package/dist/hooks/ui/index.d.ts.map +1 -1
  29. package/dist/hooks/ui/index.js +1 -0
  30. package/dist/hooks/ui/useTabs.d.ts +33 -0
  31. package/dist/hooks/ui/useTabs.d.ts.map +1 -0
  32. package/dist/hooks/ui/useTabs.js +117 -0
  33. package/dist/index.js +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,275 @@
1
+ /**
2
+ * 인증 상태
3
+ */
4
+ export interface AuthUser {
5
+ [key: string]: any;
6
+ }
7
+ /**
8
+ * 로그인 credentials
9
+ */
10
+ export interface LoginCredentials {
11
+ [key: string]: any;
12
+ }
13
+ /**
14
+ * 회원가입 데이터
15
+ */
16
+ export interface RegisterData {
17
+ [key: string]: any;
18
+ }
19
+ /**
20
+ * useAuth 옵션
21
+ */
22
+ export interface UseAuthOptions<TUser extends AuthUser = AuthUser> {
23
+ /** 로그인 API 함수 */
24
+ loginFn?: (credentials: LoginCredentials) => Promise<{
25
+ user: TUser;
26
+ token?: string;
27
+ }>;
28
+ /** 로그아웃 API 함수 */
29
+ logoutFn?: () => Promise<void>;
30
+ /** 회원가입 API 함수 */
31
+ registerFn?: (data: RegisterData) => Promise<{
32
+ user: TUser;
33
+ token?: string;
34
+ }>;
35
+ /** 현재 사용자 정보 가져오기 */
36
+ getUserFn?: () => Promise<TUser>;
37
+ /** 토큰 갱신 함수 */
38
+ refreshTokenFn?: () => Promise<{
39
+ token: string;
40
+ }>;
41
+ /** 로그인 성공 시 콜백 */
42
+ onLoginSuccess?: (user: TUser) => void;
43
+ /** 로그아웃 성공 시 콜백 */
44
+ onLogoutSuccess?: () => void;
45
+ /** 회원가입 성공 시 콜백 */
46
+ onRegisterSuccess?: (user: TUser) => void;
47
+ /** 에러 발생 시 콜백 */
48
+ onError?: (error: Error, action: 'login' | 'logout' | 'register' | 'refresh') => void;
49
+ /**
50
+ * localStorage에 저장할 토큰 키 (기본값: 'auth_token')
51
+ *
52
+ * @example
53
+ * ```
54
+ * // localStorage 저장 예시:
55
+ * localStorage.setItem('auth_token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...')
56
+ * ```
57
+ */
58
+ storageKey?: string;
59
+ /**
60
+ * 사용자 정보 저장 키 (기본값: 'auth_user')
61
+ *
62
+ * @example
63
+ * ```
64
+ * // localStorage 저장 예시:
65
+ * localStorage.setItem('auth_user', '{"id":"123","name":"John Doe","email":"john@example.com"}')
66
+ * ```
67
+ */
68
+ userStorageKey?: string;
69
+ /**
70
+ * localStorage 대신 sessionStorage 사용 여부 (기본값: false)
71
+ *
72
+ * - false (기본값): localStorage 사용 → 브라우저 종료 후에도 유지
73
+ * - true: sessionStorage 사용 → 브라우저/탭 종료 시 삭제
74
+ *
75
+ * 참고: login() 함수의 rememberMe 옵션으로 동적 선택도 가능합니다.
76
+ */
77
+ useSessionStorage?: boolean;
78
+ /** 자동 토큰 갱신 여부 (기본값: false) */
79
+ autoRefresh?: boolean;
80
+ /** 토큰 갱신 간격 (밀리초, 기본값: 300000 = 5분) */
81
+ refreshInterval?: number;
82
+ /** 초기화 시 사용자 정보 자동 로드 여부 (기본값: true) */
83
+ autoLoadUser?: boolean;
84
+ }
85
+ /**
86
+ * useAuth 반환 타입
87
+ */
88
+ export interface UseAuthReturn<TUser extends AuthUser = AuthUser> {
89
+ /** 현재 로그인한 사용자 */
90
+ user: TUser | null;
91
+ /** 인증 여부 */
92
+ isAuthenticated: boolean;
93
+ /** 로딩 중 여부 */
94
+ isLoading: boolean;
95
+ /** 로그인 진행 중 여부 */
96
+ isLoggingIn: boolean;
97
+ /** 로그아웃 진행 중 여부 */
98
+ isLoggingOut: boolean;
99
+ /** 회원가입 진행 중 여부 */
100
+ isRegistering: boolean;
101
+ /** 에러 */
102
+ error: Error | null;
103
+ /** 로그인 함수 */
104
+ login: (credentials: LoginCredentials, options?: {
105
+ rememberMe?: boolean;
106
+ }) => Promise<void>;
107
+ /** 로그아웃 함수 */
108
+ logout: () => Promise<void>;
109
+ /** 회원가입 함수 */
110
+ register: (data: RegisterData, options?: {
111
+ rememberMe?: boolean;
112
+ }) => Promise<void>;
113
+ /** 토큰 갱신 함수 */
114
+ refreshToken: () => Promise<void>;
115
+ /** 사용자 정보 새로고침 */
116
+ refetchUser: () => Promise<void>;
117
+ /** 현재 토큰 */
118
+ token: string | null;
119
+ /** 토큰 수동 설정 */
120
+ setToken: (token: string | null) => void;
121
+ /** 사용자 정보 수동 설정 */
122
+ setUser: (user: TUser | null) => void;
123
+ }
124
+ /**
125
+ * 인증 관리 Hook
126
+ *
127
+ * 로그인, 로그아웃, 회원가입, 토큰 관리 등 인증 관련 기능을 제공합니다.
128
+ *
129
+ * @example
130
+ * ```tsx
131
+ * // 기본 사용 (로그인 유지 옵션 포함)
132
+ * function LoginPage() {
133
+ * const [rememberMe, setRememberMe] = useState(false);
134
+ * const { login, isLoggingIn, error, isAuthenticated } = useAuth({
135
+ * loginFn: (credentials) => api.login(credentials),
136
+ * onLoginSuccess: (user) => {
137
+ * console.log('로그인 성공:', user);
138
+ * navigate('/dashboard');
139
+ * },
140
+ * });
141
+ *
142
+ * const handleSubmit = async (e) => {
143
+ * e.preventDefault();
144
+ * // rememberMe가 true면 localStorage, false면 sessionStorage 사용
145
+ * await login({ email, password }, { rememberMe });
146
+ * };
147
+ *
148
+ * if (isAuthenticated) {
149
+ * return <Navigate to="/dashboard" />;
150
+ * }
151
+ *
152
+ * return (
153
+ * <form onSubmit={handleSubmit}>
154
+ * <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
155
+ * <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
156
+ * <label>
157
+ * <input type="checkbox" checked={rememberMe} onChange={(e) => setRememberMe(e.target.checked)} />
158
+ * 로그인 유지
159
+ * </label>
160
+ * <button type="submit" disabled={isLoggingIn}>
161
+ * {isLoggingIn ? '로그인 중...' : '로그인'}
162
+ * </button>
163
+ * {error && <div>{error.message}</div>}
164
+ * </form>
165
+ * );
166
+ * }
167
+ * ```
168
+ *
169
+ * @example
170
+ * ```tsx
171
+ * // 자동 토큰 갱신과 함께 사용
172
+ * function App() {
173
+ * const { user, isAuthenticated, isLoading, logout } = useAuth({
174
+ * getUserFn: () => api.getMe(),
175
+ * logoutFn: () => api.logout(),
176
+ * refreshTokenFn: () => api.refreshToken(),
177
+ * autoRefresh: true,
178
+ * refreshInterval: 300000, // 5분마다 갱신
179
+ * onLogoutSuccess: () => navigate('/login'),
180
+ * });
181
+ *
182
+ * if (isLoading) return <div>로딩중...</div>;
183
+ *
184
+ * return (
185
+ * <div>
186
+ * {isAuthenticated ? (
187
+ * <>
188
+ * <div>환영합니다, {user?.name}님</div>
189
+ * <button onClick={logout}>로그아웃</button>
190
+ * </>
191
+ * ) : (
192
+ * <LoginPage />
193
+ * )}
194
+ * </div>
195
+ * );
196
+ * }
197
+ * ```
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * // 회원가입 포함
202
+ * function RegisterPage() {
203
+ * const { register, isRegistering, error } = useAuth({
204
+ * registerFn: (data) => api.register(data),
205
+ * onRegisterSuccess: (user) => {
206
+ * toast.success('회원가입 성공!');
207
+ * navigate('/');
208
+ * },
209
+ * });
210
+ *
211
+ * const handleSubmit = async (data) => {
212
+ * await register(data);
213
+ * };
214
+ *
215
+ * return (
216
+ * <form onSubmit={handleSubmit}>
217
+ * <input name="email" />
218
+ * <input name="password" type="password" />
219
+ * <input name="name" />
220
+ * <button type="submit" disabled={isRegistering}>
221
+ * 회원가입
222
+ * </button>
223
+ * </form>
224
+ * );
225
+ * }
226
+ * ```
227
+ *
228
+ * @example
229
+ * ```tsx
230
+ * // Storage에 저장되는 값 확인하기
231
+ * function DebugAuth() {
232
+ * const { login, user, token } = useAuth({
233
+ * loginFn: api.login,
234
+ * storageKey: 'my_app_token', // 커스텀 키 사용 가능
235
+ * userStorageKey: 'my_app_user',
236
+ * });
237
+ *
238
+ * const handleLogin = async () => {
239
+ * await login({ email, password }, { rememberMe: true });
240
+ *
241
+ * // 로그인 후 storage 확인
242
+ * console.log('Token:', localStorage.getItem('my_app_token'));
243
+ * // 출력: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
244
+ *
245
+ * console.log('User:', localStorage.getItem('my_app_user'));
246
+ * // 출력: {"id":"123","name":"John Doe","email":"john@example.com"}
247
+ * };
248
+ * }
249
+ * ```
250
+ *
251
+ * @example
252
+ * ```tsx
253
+ * // rememberMe에 따른 storage 동작
254
+ * function LoginWithRememberMe() {
255
+ * const [rememberMe, setRememberMe] = useState(false);
256
+ * const { login } = useAuth({ loginFn: api.login });
257
+ *
258
+ * const handleLogin = async () => {
259
+ * await login({ email, password }, { rememberMe });
260
+ *
261
+ * if (rememberMe) {
262
+ * // localStorage에 저장 (브라우저 종료해도 유지)
263
+ * console.log(localStorage.getItem('auth_token')); // "eyJ..."
264
+ * console.log(sessionStorage.getItem('auth_token')); // null
265
+ * } else {
266
+ * // sessionStorage에 저장 (브라우저 종료 시 삭제)
267
+ * console.log(localStorage.getItem('auth_token')); // null
268
+ * console.log(sessionStorage.getItem('auth_token')); // "eyJ..."
269
+ * }
270
+ * };
271
+ * }
272
+ * ```
273
+ */
274
+ export declare function useAuth<TUser extends AuthUser = AuthUser>(options?: UseAuthOptions<TUser>): UseAuthReturn<TUser>;
275
+ //# sourceMappingURL=useAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useAuth.d.ts","sourceRoot":"","sources":["../../../src/hooks/auth/useAuth.ts"],"names":[],"mappings":"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"}
@@ -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
+ }