@ph-cms/client-sdk 0.1.4 → 0.1.6
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 +617 -22
- package/dist/auth/firebase-provider.d.ts +2 -1
- package/dist/auth/firebase-provider.js +3 -0
- package/dist/auth/interfaces.d.ts +6 -0
- package/dist/auth/local-provider.d.ts +1 -0
- package/dist/auth/local-provider.js +3 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +6 -2
- package/dist/context.d.ts +4 -2
- package/dist/context.js +45 -14
- package/dist/hooks/useAuth.d.ts +25 -1
- package/dist/hooks/useAuth.js +12 -10
- package/dist/hooks/useFirebaseAuthSync.d.ts +108 -0
- package/dist/hooks/useFirebaseAuthSync.js +221 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +7 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,6 +9,8 @@ This package provides:
|
|
|
9
9
|
- React context and hooks for consuming the client in UI code
|
|
10
10
|
- **Integrated React Query support** (from v0.1.1)
|
|
11
11
|
- **Automatic Auth State Management** (from v0.1.3)
|
|
12
|
+
- **Conditional profile fetching & guaranteed post-login profile load** (from v0.1.4)
|
|
13
|
+
- **Firebase Auth State Sync** — automatic Firebase↔PH-CMS session synchronization (from v0.1.6)
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
@@ -18,17 +20,223 @@ npm install @ph-cms/client-sdk
|
|
|
18
20
|
|
|
19
21
|
> **Note:** `@tanstack/react-query` is a direct dependency. You no longer need to install it manually unless you use it in your own application code.
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Authentication Architecture
|
|
26
|
+
|
|
27
|
+
### Overview
|
|
28
|
+
|
|
29
|
+
The SDK's authentication system is composed of three layers:
|
|
30
|
+
|
|
31
|
+
| Layer | Component | Role |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| **Provider** | `LocalAuthProvider` / `FirebaseAuthProvider` | 토큰 저장·조회·삭제를 담당하는 저수준 어댑터 |
|
|
34
|
+
| **Module** | `AuthModule` (`client.auth`) | 서버 API 호출 (`login`, `loginWithFirebase`, `me`, `refresh`, `logout`) |
|
|
35
|
+
| **Hook / Context** | `PHCMSProvider`, `useAuth` | React 상태 관리 — 토큰 유무에 따른 조건부 프로필 조회, 로그인·로그아웃 액션 |
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌─────────────────────────────────────────────────────────┐
|
|
39
|
+
│ React Component Tree │
|
|
40
|
+
│ │
|
|
41
|
+
│ PHCMSProvider │
|
|
42
|
+
│ └─ PHCMSInternalProvider │
|
|
43
|
+
│ ├─ useQuery(['auth','me']) │
|
|
44
|
+
│ │ enabled: authProvider.hasToken() │
|
|
45
|
+
│ │ → 토큰 없으면 호출하지 않음 (401 방지) │
|
|
46
|
+
│ │ → 토큰 있으면 자동으로 me() 호출 │
|
|
47
|
+
│ └─ context value: { user, isAuthenticated, ... } │
|
|
48
|
+
│ │
|
|
49
|
+
│ useAuth() │
|
|
50
|
+
│ ├─ login() ──→ AuthModule.login() │
|
|
51
|
+
│ │ ├─ POST /api/auth/login │
|
|
52
|
+
│ │ └─ provider.setTokens() │
|
|
53
|
+
│ │ ──→ refetchQueries(['auth','me']) │
|
|
54
|
+
│ │ └─ GET /api/auth/me ← 프로필 보장 │
|
|
55
|
+
│ │ │
|
|
56
|
+
│ ├─ loginWithFirebase() ──→ AuthModule.loginWithFirebase() │
|
|
57
|
+
│ │ ├─ POST /api/auth/firebase/exchange │
|
|
58
|
+
│ │ └─ provider.setTokens() │
|
|
59
|
+
│ │ ──→ refetchQueries(['auth','me']) │
|
|
60
|
+
│ │ └─ GET /api/auth/me ← 프로필 보장 │
|
|
61
|
+
│ │ │
|
|
62
|
+
│ ├─ register() ──→ AuthModule.register() │
|
|
63
|
+
│ │ ──→ refetchQueries(['auth','me']) │
|
|
64
|
+
│ │ │
|
|
65
|
+
│ └─ logout() ──→ AuthModule.logout() │
|
|
66
|
+
│ ──→ setQueryData(['auth','me'], null) │
|
|
67
|
+
└─────────────────────────────────────────────────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Authentication Lifecycle
|
|
71
|
+
|
|
72
|
+
#### 1. 초기 마운트 (비인증 상태)
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
App Mount
|
|
76
|
+
→ PHCMSProvider mount
|
|
77
|
+
→ authProvider.hasToken() ← false (localStorage에 토큰 없음)
|
|
78
|
+
→ useQuery enabled: false ← me() 호출하지 않음
|
|
79
|
+
→ user: null, isAuthenticated: false
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
서버에 불필요한 401 요청을 보내지 않습니다.
|
|
83
|
+
|
|
84
|
+
#### 2. 초기 마운트 (기존 세션 복원)
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
App Mount
|
|
88
|
+
→ PHCMSProvider mount
|
|
89
|
+
→ authProvider.hasToken() ← true (localStorage에 토큰 존재)
|
|
90
|
+
→ useQuery enabled: true ← me() 자동 호출
|
|
91
|
+
→ GET /api/auth/me
|
|
92
|
+
→ user: { ... }, isAuthenticated: true
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
페이지 새로고침이나 재방문 시 기존 세션이 자동으로 복원됩니다.
|
|
96
|
+
|
|
97
|
+
#### 3. 로그인 (이메일/비밀번호)
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
user calls login({ email, password })
|
|
101
|
+
→ POST /api/auth/login
|
|
102
|
+
→ 서버 응답: { accessToken, refreshToken }
|
|
103
|
+
→ provider.setTokens(accessToken, refreshToken) ← localStorage 저장
|
|
104
|
+
→ refetchQueries(['auth', 'me'])
|
|
105
|
+
→ GET /api/auth/me
|
|
106
|
+
→ user: { ... }, isAuthenticated: true
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### 4. 로그인 (Firebase)
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
user calls loginWithFirebase({ idToken })
|
|
113
|
+
→ POST /api/auth/firebase/exchange
|
|
114
|
+
→ 서버 응답: { accessToken, refreshToken }
|
|
115
|
+
→ provider.setTokens(accessToken, refreshToken) ← localStorage 저장
|
|
116
|
+
→ refetchQueries(['auth', 'me'])
|
|
117
|
+
→ GET /api/auth/me
|
|
118
|
+
→ user: { ... }, isAuthenticated: true
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Firebase ID 토큰을 PH-CMS 자체 토큰으로 교환한 뒤, 동일한 프로필 조회 흐름을 따릅니다.
|
|
122
|
+
|
|
123
|
+
#### 5. 로그아웃
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
user calls logout()
|
|
127
|
+
→ provider.logout() ← localStorage 토큰 삭제
|
|
128
|
+
→ POST /api/auth/logout ← 서버 세션 무효화 (실패해도 무시)
|
|
129
|
+
→ setQueryData(['auth','me'], null)
|
|
130
|
+
→ user: null, isAuthenticated: false
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### `refetchQueries` vs `invalidateQueries`
|
|
134
|
+
|
|
135
|
+
로그인 성공 후 프로필 조회에는 `invalidateQueries` 대신 `refetchQueries`를 사용합니다.
|
|
136
|
+
|
|
137
|
+
- `invalidateQueries`는 쿼리를 stale로 표시만 하고, `enabled: true`인 쿼리만 자동 refetch합니다.
|
|
138
|
+
- 로그인 직후에는 `provider.setTokens()`로 토큰이 저장되지만, React 리렌더가 아직 발생하지 않아 `hasToken()`의 반환값이 context에 반영되지 않은 상태입니다.
|
|
139
|
+
- 따라서 `enabled`가 여전히 `false`일 수 있고, `invalidateQueries`만으로는 `me()` 호출이 보장되지 않습니다.
|
|
140
|
+
- `refetchQueries`는 `enabled` 상태와 무관하게 즉시 네트워크 요청을 강제하므로, 토큰 저장 직후 프로필 조회를 확실히 보장합니다.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Auth Providers
|
|
145
|
+
|
|
146
|
+
### `LocalAuthProvider`
|
|
147
|
+
|
|
148
|
+
이메일/비밀번호 기반 인증에 사용합니다. 토큰을 `localStorage`에 저장합니다.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { LocalAuthProvider, PHCMSClient } from '@ph-cms/client-sdk';
|
|
152
|
+
|
|
153
|
+
const authProvider = new LocalAuthProvider('my_app_');
|
|
154
|
+
|
|
155
|
+
const client = new PHCMSClient({
|
|
156
|
+
baseURL: 'https://api.example.com',
|
|
157
|
+
auth: authProvider,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
| 생성자 인자 | 타입 | 기본값 | 설명 |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| `storageKeyPrefix` | `string` | `'ph_cms_'` | `localStorage` 키 접두사. `{prefix}access_token`, `{prefix}refresh_token` 형태로 저장됩니다. |
|
|
164
|
+
|
|
165
|
+
#### 주요 메서드
|
|
166
|
+
|
|
167
|
+
| 메서드 | 설명 |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `hasToken()` | `accessToken` 또는 `refreshToken`이 존재하면 `true` 반환 |
|
|
170
|
+
| `getToken()` | 현재 `accessToken` 반환 (`Promise<string \| null>`) |
|
|
171
|
+
| `setTokens(access, refresh)` | 토큰 저장 (login 성공 시 SDK가 자동 호출) |
|
|
172
|
+
| `getRefreshToken()` | 현재 `refreshToken` 반환 |
|
|
173
|
+
| `logout()` | 토큰 삭제 |
|
|
174
|
+
|
|
175
|
+
### `FirebaseAuthProvider`
|
|
176
|
+
|
|
177
|
+
Firebase Authentication과 연동하여 사용합니다. Firebase ID 토큰을 PH-CMS 서버 토큰으로 교환하는 방식입니다.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { FirebaseAuthProvider, PHCMSClient } from '@ph-cms/client-sdk';
|
|
181
|
+
import { initializeApp } from 'firebase/app';
|
|
182
|
+
import { getAuth } from 'firebase/auth';
|
|
183
|
+
|
|
184
|
+
const firebaseApp = initializeApp({ /* Firebase config */ });
|
|
185
|
+
const firebaseAuth = getAuth(firebaseApp);
|
|
186
|
+
|
|
187
|
+
const authProvider = new FirebaseAuthProvider(firebaseAuth, 'my_app_fb_');
|
|
22
188
|
|
|
23
|
-
|
|
189
|
+
const client = new PHCMSClient({
|
|
190
|
+
baseURL: 'https://api.example.com',
|
|
191
|
+
auth: authProvider,
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
| 생성자 인자 | 타입 | 기본값 | 설명 |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| `auth` | `firebase/auth.Auth` | (필수) | Firebase Auth 인스턴스 |
|
|
198
|
+
| `storageKeyPrefix` | `string` | `'ph_cms_fb_'` | `localStorage` 키 접두사 |
|
|
199
|
+
|
|
200
|
+
#### 주요 메서드
|
|
201
|
+
|
|
202
|
+
| 메서드 | 설명 |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `hasToken()` | PH-CMS 교환 토큰이 존재하면 `true` 반환 |
|
|
205
|
+
| `getToken()` | PH-CMS `accessToken` → Firebase ID 토큰 순으로 fallback 반환 |
|
|
206
|
+
| `getIdToken()` | Firebase ID 토큰을 직접 반환 (토큰 교환 시 사용) |
|
|
207
|
+
| `setTokens(access, refresh)` | 교환된 PH-CMS 토큰 저장 (SDK가 자동 호출) |
|
|
208
|
+
| `logout()` | PH-CMS 토큰 삭제 + `firebase.auth.signOut()` 호출 |
|
|
209
|
+
|
|
210
|
+
### `AuthProvider` Interface
|
|
211
|
+
|
|
212
|
+
커스텀 인증 프로바이더를 구현하려면 아래 인터페이스를 구현합니다:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
interface AuthProvider {
|
|
216
|
+
type: 'FIREBASE' | 'LOCAL';
|
|
217
|
+
getToken(): Promise<string | null>;
|
|
218
|
+
hasToken(): boolean;
|
|
219
|
+
onTokenExpired(callback: () => Promise<void>): void;
|
|
220
|
+
logout(): Promise<void>;
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
`hasToken()`은 **동기** 메서드여야 합니다. React 렌더 사이클에서 `useQuery`의 `enabled` 조건으로 사용되므로, 비동기 호출은 허용되지 않습니다.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## React Usage
|
|
24
229
|
|
|
25
230
|
### Basic Setup
|
|
26
231
|
|
|
27
232
|
```tsx
|
|
28
|
-
import { PHCMSClient, PHCMSProvider } from '@ph-cms/client-sdk';
|
|
233
|
+
import { PHCMSClient, LocalAuthProvider, PHCMSProvider } from '@ph-cms/client-sdk';
|
|
234
|
+
|
|
235
|
+
const authProvider = new LocalAuthProvider('my_app_');
|
|
29
236
|
|
|
30
237
|
const client = new PHCMSClient({
|
|
31
238
|
baseURL: 'https://api.example.com',
|
|
239
|
+
auth: authProvider,
|
|
32
240
|
});
|
|
33
241
|
|
|
34
242
|
export function App() {
|
|
@@ -40,38 +248,350 @@ export function App() {
|
|
|
40
248
|
}
|
|
41
249
|
```
|
|
42
250
|
|
|
43
|
-
|
|
251
|
+
`PHCMSProvider`는 내부적으로 `QueryClientProvider`를 포함하므로, 별도로 추가할 필요가 없습니다.
|
|
252
|
+
외부에서 직접 관리하는 `QueryClient`를 사용하려면 `queryClient` prop으로 전달합니다:
|
|
44
253
|
|
|
45
|
-
|
|
254
|
+
```tsx
|
|
255
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
256
|
+
|
|
257
|
+
const queryClient = new QueryClient();
|
|
258
|
+
|
|
259
|
+
<PHCMSProvider client={client} queryClient={queryClient}>
|
|
260
|
+
...
|
|
261
|
+
</PHCMSProvider>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Authentication with `useAuth`
|
|
265
|
+
|
|
266
|
+
`useAuth`는 인증 상태와 액션을 통합 제공하는 메인 훅입니다.
|
|
46
267
|
|
|
47
268
|
```tsx
|
|
48
269
|
import { useAuth } from '@ph-cms/client-sdk';
|
|
49
270
|
|
|
50
|
-
function
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
271
|
+
function AuthComponent() {
|
|
272
|
+
const {
|
|
273
|
+
// 상태
|
|
274
|
+
user, // UserDto | null — 현재 로그인한 사용자 프로필
|
|
275
|
+
isAuthenticated, // boolean
|
|
276
|
+
isLoading, // boolean — 프로필 로딩 중 여부
|
|
277
|
+
|
|
278
|
+
// 액션 (모두 Promise 반환)
|
|
279
|
+
login, // (data: LoginRequest) => Promise<AuthResponse>
|
|
280
|
+
loginWithFirebase, // (data: FirebaseExchangeRequest) => Promise<AuthResponse>
|
|
281
|
+
register, // (data: RegisterRequest) => Promise<AuthResponse>
|
|
282
|
+
logout, // () => Promise<void>
|
|
283
|
+
|
|
284
|
+
// 뮤테이션 상태 (isPending, error 등)
|
|
285
|
+
loginStatus,
|
|
286
|
+
loginWithFirebaseStatus,
|
|
287
|
+
registerStatus,
|
|
288
|
+
logoutStatus,
|
|
57
289
|
} = useAuth();
|
|
58
290
|
|
|
59
|
-
|
|
291
|
+
// ...
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
#### 이메일/비밀번호 로그인
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
function LoginForm() {
|
|
299
|
+
const { login, loginStatus, isAuthenticated, user } = useAuth();
|
|
300
|
+
const [email, setEmail] = useState('');
|
|
301
|
+
const [password, setPassword] = useState('');
|
|
60
302
|
|
|
61
|
-
if (
|
|
62
|
-
return <
|
|
303
|
+
if (isAuthenticated) {
|
|
304
|
+
return <p>Welcome, {user?.displayName}</p>;
|
|
63
305
|
}
|
|
64
306
|
|
|
307
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
try {
|
|
310
|
+
await login({ email, password });
|
|
311
|
+
// 성공 시 자동으로 me() 호출 → user, isAuthenticated 갱신
|
|
312
|
+
} catch (error) {
|
|
313
|
+
console.error('Login failed:', error);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
65
317
|
return (
|
|
66
|
-
<
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
|
|
318
|
+
<form onSubmit={handleSubmit}>
|
|
319
|
+
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
|
|
320
|
+
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
|
|
321
|
+
<button type="submit" disabled={loginStatus.isPending}>
|
|
322
|
+
{loginStatus.isPending ? '로그인 중...' : '로그인'}
|
|
323
|
+
</button>
|
|
324
|
+
{loginStatus.error && <p>{(loginStatus.error as Error).message}</p>}
|
|
325
|
+
</form>
|
|
70
326
|
);
|
|
71
327
|
}
|
|
72
328
|
```
|
|
73
329
|
|
|
74
|
-
|
|
330
|
+
#### Firebase 로그인
|
|
331
|
+
|
|
332
|
+
Firebase 로그인은 두 단계로 이루어집니다:
|
|
333
|
+
1. Firebase SDK로 인증하여 ID 토큰을 획득
|
|
334
|
+
2. `loginWithFirebase()`로 PH-CMS 서버에 토큰 교환 요청
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
import { FirebaseAuthProvider } from '@ph-cms/client-sdk';
|
|
338
|
+
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';
|
|
339
|
+
|
|
340
|
+
// firebaseProvider는 앱 초기화 시 생성
|
|
341
|
+
declare const firebaseProvider: FirebaseAuthProvider;
|
|
342
|
+
|
|
343
|
+
function FirebaseLoginButton() {
|
|
344
|
+
const { loginWithFirebase, loginWithFirebaseStatus } = useAuth();
|
|
345
|
+
|
|
346
|
+
const handleFirebaseLogin = async () => {
|
|
347
|
+
// 1) Firebase 인증 (Google 팝업 예시)
|
|
348
|
+
const auth = getAuth();
|
|
349
|
+
await signInWithPopup(auth, new GoogleAuthProvider());
|
|
350
|
+
|
|
351
|
+
// 2) Firebase ID 토큰 획득
|
|
352
|
+
const idToken = await firebaseProvider.getIdToken();
|
|
353
|
+
if (!idToken) return;
|
|
354
|
+
|
|
355
|
+
// 3) PH-CMS 서버에 토큰 교환 → 자동으로 me() 호출
|
|
356
|
+
await loginWithFirebase({ idToken });
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
return (
|
|
360
|
+
<button onClick={handleFirebaseLogin} disabled={loginWithFirebaseStatus.isPending}>
|
|
361
|
+
{loginWithFirebaseStatus.isPending ? 'Firebase 로그인 중...' : 'Google 로그인'}
|
|
362
|
+
</button>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### 회원가입
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
function RegisterForm() {
|
|
371
|
+
const { register, registerStatus } = useAuth();
|
|
372
|
+
|
|
373
|
+
const handleRegister = async (formData: {
|
|
374
|
+
email: string;
|
|
375
|
+
password: string;
|
|
376
|
+
display_name: string;
|
|
377
|
+
username?: string;
|
|
378
|
+
}) => {
|
|
379
|
+
await register(formData);
|
|
380
|
+
// 성공 시 자동으로 me() 호출 → 즉시 인증 상태로 전환
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// ...
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
#### 로그아웃
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
function LogoutButton() {
|
|
391
|
+
const { logout } = useAuth();
|
|
392
|
+
|
|
393
|
+
return <button onClick={() => logout()}>로그아웃</button>;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Context API (`usePHCMSContext`)
|
|
398
|
+
|
|
399
|
+
`useAuth` 대신 context에 직접 접근할 수도 있습니다.
|
|
400
|
+
|
|
401
|
+
```tsx
|
|
402
|
+
import { usePHCMSContext } from '@ph-cms/client-sdk';
|
|
403
|
+
|
|
404
|
+
function MyComponent() {
|
|
405
|
+
const {
|
|
406
|
+
client, // PHCMSClient 인스턴스
|
|
407
|
+
user, // UserDto | null
|
|
408
|
+
isAuthenticated, // boolean
|
|
409
|
+
isLoading, // boolean
|
|
410
|
+
refreshUser, // () => Promise<void> — 수동으로 프로필 다시 조회
|
|
411
|
+
} = usePHCMSContext();
|
|
412
|
+
|
|
413
|
+
// 프로필 정보가 변경된 후 수동 갱신
|
|
414
|
+
const handleProfileUpdate = async () => {
|
|
415
|
+
await client.auth.me(); // or your update API
|
|
416
|
+
await refreshUser();
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Standalone (Non-React) Usage
|
|
422
|
+
|
|
423
|
+
React 없이 `PHCMSClient`와 `AuthModule`을 직접 사용할 수 있습니다.
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
import { PHCMSClient, LocalAuthProvider } from '@ph-cms/client-sdk';
|
|
427
|
+
|
|
428
|
+
const authProvider = new LocalAuthProvider('my_app_');
|
|
429
|
+
const client = new PHCMSClient({
|
|
430
|
+
baseURL: 'https://api.example.com',
|
|
431
|
+
auth: authProvider,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// 로그인
|
|
435
|
+
const authResponse = await client.auth.login({
|
|
436
|
+
email: 'user@example.com',
|
|
437
|
+
password: 'password',
|
|
438
|
+
});
|
|
439
|
+
// → authProvider에 토큰이 자동 저장됨
|
|
440
|
+
|
|
441
|
+
// 프로필 조회
|
|
442
|
+
const me = await client.auth.me();
|
|
443
|
+
console.log(me.email);
|
|
444
|
+
|
|
445
|
+
// 토큰 갱신
|
|
446
|
+
const refreshToken = authProvider.getRefreshToken();
|
|
447
|
+
if (refreshToken) {
|
|
448
|
+
const newTokens = await client.auth.refresh(refreshToken);
|
|
449
|
+
authProvider.setTokens(newTokens.accessToken, newTokens.refreshToken);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 로그아웃
|
|
453
|
+
await client.auth.logout();
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Legacy Hooks
|
|
457
|
+
|
|
458
|
+
하위 호환성을 위해 개별 훅도 제공됩니다. 새 코드에서는 `useAuth`를 권장합니다.
|
|
459
|
+
|
|
460
|
+
| Hook | 설명 |
|
|
461
|
+
|---|---|
|
|
462
|
+
| `useUser()` | `{ data: UserDto \| null, isLoading, isAuthenticated }` |
|
|
463
|
+
| `useLogin()` | `{ mutateAsync: login }` |
|
|
464
|
+
| `useLogout()` | `{ mutateAsync: logout }` |
|
|
465
|
+
|
|
466
|
+
### Firebase Auth Sync
|
|
467
|
+
|
|
468
|
+
Firebase Authentication을 사용하는 경우, Firebase의 인증 상태가 변경될 때마다 PH-CMS 백엔드와 자동으로 동기화할 수 있습니다.
|
|
469
|
+
|
|
470
|
+
SDK는 두 가지 방식을 제공합니다:
|
|
471
|
+
|
|
472
|
+
| 방식 | 용도 |
|
|
473
|
+
|---|---|
|
|
474
|
+
| `useFirebaseAuthSync` 훅 | 기존 컴포넌트에 동기화 로직을 삽입할 때 |
|
|
475
|
+
| `<FirebaseAuthSync>` 컴포넌트 | 컴포넌트 트리를 감싸서 선언적으로 사용할 때 |
|
|
476
|
+
|
|
477
|
+
#### 동기화 동작
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
Firebase onAuthStateChanged
|
|
481
|
+
│
|
|
482
|
+
├─ fbUser 존재 + PH-CMS 비인증 상태
|
|
483
|
+
│ → fbUser.getIdToken()
|
|
484
|
+
│ → client.auth.loginWithFirebase({ idToken })
|
|
485
|
+
│ → provider.setTokens(...)
|
|
486
|
+
│ → refreshUser() ← me() 호출하여 프로필 로드
|
|
487
|
+
│
|
|
488
|
+
└─ fbUser null (로그아웃) + PH-CMS 인증 상태
|
|
489
|
+
→ client.auth.logout()
|
|
490
|
+
→ refreshUser() ← 상태 초기화
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
#### `<FirebaseAuthSync>` 컴포넌트 사용
|
|
494
|
+
|
|
495
|
+
`<PHCMSProvider>` 안에서 사용합니다:
|
|
496
|
+
|
|
497
|
+
```tsx
|
|
498
|
+
import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
499
|
+
import { getAuth } from 'firebase/auth';
|
|
500
|
+
|
|
501
|
+
const firebaseAuth = getAuth(firebaseApp);
|
|
502
|
+
|
|
503
|
+
function App() {
|
|
504
|
+
return (
|
|
505
|
+
<PHCMSProvider client={client}>
|
|
506
|
+
<FirebaseAuthSync firebaseAuth={firebaseAuth}>
|
|
507
|
+
<MainContent />
|
|
508
|
+
</FirebaseAuthSync>
|
|
509
|
+
</PHCMSProvider>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Props:
|
|
515
|
+
|
|
516
|
+
| Prop | 타입 | 기본값 | 설명 |
|
|
517
|
+
|---|---|---|---|
|
|
518
|
+
| `firebaseAuth` | `firebase/auth.Auth` | (필수) | Firebase Auth 인스턴스 |
|
|
519
|
+
| `logoutOnFirebaseSignOut` | `boolean` | `true` | Firebase 로그아웃 시 PH-CMS도 자동 로그아웃할지 여부 |
|
|
520
|
+
| `onSyncSuccess` | `() => void` | — | 토큰 교환 성공 시 콜백 |
|
|
521
|
+
| `onSyncError` | `(error: unknown) => void` | — | 토큰 교환 실패 시 콜백 |
|
|
522
|
+
|
|
523
|
+
#### `useFirebaseAuthSync` 훅 사용
|
|
524
|
+
|
|
525
|
+
```tsx
|
|
526
|
+
import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
527
|
+
import { getAuth } from 'firebase/auth';
|
|
528
|
+
|
|
529
|
+
const firebaseAuth = getAuth(firebaseApp);
|
|
530
|
+
|
|
531
|
+
function AppContent() {
|
|
532
|
+
const { isSyncing } = useFirebaseAuthSync({
|
|
533
|
+
firebaseAuth,
|
|
534
|
+
onSyncSuccess: () => console.log('Firebase↔PH-CMS 동기화 완료'),
|
|
535
|
+
onSyncError: (err) => console.error('동기화 실패:', err),
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
if (isSyncing) return <div>인증 동기화 중...</div>;
|
|
539
|
+
|
|
540
|
+
return <MainContent />;
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
반환값:
|
|
545
|
+
|
|
546
|
+
| 필드 | 타입 | 설명 |
|
|
547
|
+
|---|---|---|
|
|
548
|
+
| `isSyncing` | `boolean` | 토큰 교환 요청이 진행 중인지 여부 |
|
|
549
|
+
|
|
550
|
+
#### 전체 구성 예시 (Firebase + LocalAuthProvider)
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
import { PHCMSClient, LocalAuthProvider, FirebaseAuthProvider, PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
|
|
554
|
+
import { initializeApp } from 'firebase/app';
|
|
555
|
+
import { getAuth } from 'firebase/auth';
|
|
556
|
+
|
|
557
|
+
// 1. Firebase 초기화
|
|
558
|
+
const firebaseApp = initializeApp({ /* config */ });
|
|
559
|
+
const firebaseAuth = getAuth(firebaseApp);
|
|
560
|
+
|
|
561
|
+
// 2. PH-CMS 프로바이더 (Firebase 타입)
|
|
562
|
+
const authProvider = new FirebaseAuthProvider(firebaseAuth, 'my_app_');
|
|
563
|
+
|
|
564
|
+
// 3. PH-CMS 클라이언트
|
|
565
|
+
const client = new PHCMSClient({
|
|
566
|
+
baseURL: 'https://api.example.com',
|
|
567
|
+
auth: authProvider,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// 4. 앱 구성
|
|
571
|
+
function App() {
|
|
572
|
+
return (
|
|
573
|
+
<PHCMSProvider client={client}>
|
|
574
|
+
<FirebaseAuthSync
|
|
575
|
+
firebaseAuth={firebaseAuth}
|
|
576
|
+
onSyncError={(err) => alert('인증 동기화에 실패했습니다.')}
|
|
577
|
+
>
|
|
578
|
+
<Router />
|
|
579
|
+
</FirebaseAuthSync>
|
|
580
|
+
</PHCMSProvider>
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
이 구성에서는:
|
|
586
|
+
- 사용자가 Firebase (예: Google 로그인)로 인증하면, `FirebaseAuthSync`가 자동으로 PH-CMS 토큰 교환 + 프로필 조회를 수행합니다.
|
|
587
|
+
- 사용자가 Firebase에서 로그아웃하면, PH-CMS 세션도 자동으로 정리됩니다.
|
|
588
|
+
- 앱 재방문 시 localStorage에 토큰이 남아있으면 `PHCMSProvider`가 자동으로 `me()`를 호출하여 세션을 복원합니다.
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
## Using Data Hooks
|
|
593
|
+
|
|
594
|
+
### Content
|
|
75
595
|
|
|
76
596
|
```tsx
|
|
77
597
|
import { useContentList, useContentDetail } from '@ph-cms/client-sdk';
|
|
@@ -91,9 +611,22 @@ function ContentList() {
|
|
|
91
611
|
}
|
|
92
612
|
```
|
|
93
613
|
|
|
614
|
+
### Channel
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
import { useChannelList } from '@ph-cms/client-sdk';
|
|
618
|
+
|
|
619
|
+
function ChannelList() {
|
|
620
|
+
const { data, isLoading } = useChannelList({ limit: 20 });
|
|
621
|
+
// ...
|
|
622
|
+
}
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
94
627
|
## Media & File Upload
|
|
95
628
|
|
|
96
|
-
Uploading media files follows a 3-step process: Request a Ticket
|
|
629
|
+
Uploading media files follows a 3-step process: Request a Ticket → Upload to S3 → Create/Update Content.
|
|
97
630
|
|
|
98
631
|
### Simple Workflow Example
|
|
99
632
|
|
|
@@ -133,6 +666,68 @@ function MediaUploader() {
|
|
|
133
666
|
}
|
|
134
667
|
```
|
|
135
668
|
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
## Error Handling
|
|
672
|
+
|
|
673
|
+
SDK는 세 종류의 에러를 throw합니다:
|
|
674
|
+
|
|
675
|
+
| Error Class | 상황 | 주요 필드 |
|
|
676
|
+
|---|---|---|
|
|
677
|
+
| `ValidationError` | Zod 스키마 검증 실패 (클라이언트 측) | `errors: ZodIssue[]` |
|
|
678
|
+
| `ApiError` | 서버가 2xx 이외의 응답을 반환 | `statusCode: number`, `originalError: any` |
|
|
679
|
+
| `PHCMSError` | 네트워크 오류 등 기타 에러 | `message: string` |
|
|
680
|
+
|
|
681
|
+
```tsx
|
|
682
|
+
import { ApiError } from '@ph-cms/client-sdk';
|
|
683
|
+
|
|
684
|
+
try {
|
|
685
|
+
await login({ email, password });
|
|
686
|
+
} catch (error) {
|
|
687
|
+
if (error instanceof ApiError) {
|
|
688
|
+
if (error.statusCode === 401) {
|
|
689
|
+
console.error('잘못된 인증 정보입니다.');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## API Reference
|
|
698
|
+
|
|
699
|
+
### `PHCMSClient`
|
|
700
|
+
|
|
701
|
+
```ts
|
|
702
|
+
const client = new PHCMSClient({
|
|
703
|
+
baseURL: string; // 서버 URL (필수)
|
|
704
|
+
apiPrefix?: string; // API 경로 접두사 (기본값: '/api')
|
|
705
|
+
auth?: AuthProvider; // 인증 프로바이더
|
|
706
|
+
timeout?: number; // 요청 타임아웃 ms (기본값: 10000)
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
client.authProvider // AuthProvider | undefined — 인증 프로바이더 접근
|
|
710
|
+
client.axiosInstance // AxiosInstance — 내부 axios 인스턴스 직접 접근
|
|
711
|
+
client.auth // AuthModule
|
|
712
|
+
client.content // ContentModule
|
|
713
|
+
client.channel // ChannelModule
|
|
714
|
+
client.terms // TermsModule
|
|
715
|
+
client.media // MediaModule
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
### `AuthModule` (`client.auth`)
|
|
719
|
+
|
|
720
|
+
| 메서드 | 설명 |
|
|
721
|
+
|---|---|
|
|
722
|
+
| `login(data: LoginRequest)` | 이메일/비밀번호 로그인 → `AuthResponse` |
|
|
723
|
+
| `loginWithFirebase(data: FirebaseExchangeRequest)` | Firebase ID 토큰 교환 → `AuthResponse` |
|
|
724
|
+
| `register(data: RegisterRequest)` | 회원가입 → `AuthResponse` |
|
|
725
|
+
| `me()` | 현재 사용자 프로필 조회 → `UserDto` |
|
|
726
|
+
| `refresh(refreshToken: string)` | 토큰 갱신 → `{ accessToken, refreshToken }` |
|
|
727
|
+
| `logout()` | 로그아웃 (프로바이더 토큰 삭제 + 서버 세션 무효화) |
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
136
731
|
## License
|
|
137
732
|
|
|
138
|
-
MIT
|
|
733
|
+
MIT
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AuthProvider } from "./interfaces";
|
|
2
1
|
import type { Auth } from "firebase/auth";
|
|
2
|
+
import { AuthProvider } from "./interfaces";
|
|
3
3
|
export declare class FirebaseAuthProvider implements AuthProvider {
|
|
4
4
|
private readonly auth;
|
|
5
5
|
private readonly storageKeyPrefix;
|
|
@@ -8,6 +8,7 @@ export declare class FirebaseAuthProvider implements AuthProvider {
|
|
|
8
8
|
private refreshToken;
|
|
9
9
|
private onExpiredCallback;
|
|
10
10
|
constructor(auth: Auth, storageKeyPrefix?: string);
|
|
11
|
+
hasToken(): boolean;
|
|
11
12
|
setTokens(accessToken: string, refreshToken: string): void;
|
|
12
13
|
getToken(): Promise<string | null>;
|
|
13
14
|
onTokenExpired(callback: () => Promise<void>): void;
|