@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 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
- ## React Usage
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
- `PHCMSProvider` automatically includes a `QueryClientProvider` and manages the global authentication state.
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
- ### Authentication State (`useAuth`)
251
+ `PHCMSProvider`는 내부적으로 `QueryClientProvider`를 포함하므로, 별도로 추가할 필요가 없습니다.
252
+ 외부에서 직접 관리하는 `QueryClient`를 사용하려면 `queryClient` prop으로 전달합니다:
44
253
 
45
- From v0.1.3, the `useAuth` hook provides a unified interface for both authentication state and actions.
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 ProfileComponent() {
51
- const {
52
- user,
53
- isAuthenticated,
54
- isLoading,
55
- login,
56
- logout
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
- if (isLoading) return <div>Checking auth...</div>;
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 (!isAuthenticated) {
62
- return <button onClick={() => login({ email: '...', password: '...' })}>Login</button>;
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
- <div>
67
- <h1>Welcome, {user?.display_name}</h1>
68
- <button onClick={() => logout()}>Logout</button>
69
- </div>
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
- ### Using Data Hooks
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 -> Upload to S3 -> Create/Update Content.
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;