@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.
- package/README.md +20 -13
- 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 +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":"AAEA;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC/D,iBAAiB;IACjB,OAAO,CAAC,EAAE,CAAC,WAAW,EAAE,gBAAgB,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEtF,kBAAkB;IAClB,QAAQ,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/B,kBAAkB;IAClB,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,KAAK,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAE9E,qBAAqB;IACrB,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;IAEjC,eAAe;IACf,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAElD,kBAAkB;IAClB,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAEvC,mBAAmB;IACnB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;IAE7B,mBAAmB;IACnB,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;IAE1C,iBAAiB;IACjB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,KAAK,IAAI,CAAC;IAEtF;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B,+BAA+B;IAC/B,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,uCAAuC;IACvC,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,wCAAwC;IACxC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ;IAC9D,kBAAkB;IAClB,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IAEnB,YAAY;IACZ,eAAe,EAAE,OAAO,CAAC;IAEzB,cAAc;IACd,SAAS,EAAE,OAAO,CAAC;IAEnB,kBAAkB;IAClB,WAAW,EAAE,OAAO,CAAC;IAErB,mBAAmB;IACnB,YAAY,EAAE,OAAO,CAAC;IAEtB,mBAAmB;IACnB,aAAa,EAAE,OAAO,CAAC;IAEvB,SAAS;IACT,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAEpB,aAAa;IACb,KAAK,EAAE,CAAC,WAAW,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5F,cAAc;IACd,MAAM,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B,cAAc;IACd,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE;QAAE,UAAU,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpF,eAAe;IACf,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAElC,kBAAkB;IAClB,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjC,YAAY;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAErB,eAAe;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAEzC,mBAAmB;IACnB,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqJG;AACH,wBAAgB,OAAO,CAAC,KAAK,SAAS,QAAQ,GAAG,QAAQ,EACvD,OAAO,GAAE,cAAc,CAAC,KAAK,CAAM,GAClC,aAAa,CAAC,KAAK,CAAC,CA2QtB"}
|
|
@@ -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
|
+
}
|