@ph-cms/client-sdk 0.1.23 → 0.1.25

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
@@ -47,6 +47,8 @@ import type {
47
47
  // Auth
48
48
  LoginRequest,
49
49
  RegisterRequest,
50
+ RegisterInput, // register() 훅에 전달하는 유니온 타입 (method: 'email' | 'firebase')
51
+ FirebaseRegisterRequest, // method: 'firebase' 방식의 페이로드 타입
50
52
  RefreshTokenRequest,
51
53
  FirebaseExchangeRequest,
52
54
  AuthResponse,
@@ -113,7 +115,8 @@ SDK의 인증 시스템은 세 개의 레이어로 구성됩니다:
113
115
  |---|---|---|
114
116
  | `login()` | `POST /api/auth/login` | `setTokens()` → `refreshUser()` |
115
117
  | `loginWithFirebase()` | `POST /api/auth/firebase/exchange` | `setTokens()` → `refreshUser()` |
116
- | `register()` | `POST /api/auth/register` | `setTokens()` → `refreshUser()` |
118
+ | `register()` (method: 'email') | `POST /api/auth/register` | `setTokens()` → `refreshUser()` |
119
+ | `register()` (method: 'firebase') | `POST /api/auth/register` (provider: firebase:password) | `setTokens()` → `refreshUser()` |
117
120
  | `logout()` | `POST /api/auth/logout` | `provider.logout()` → `refreshUser()` (상태 초기화) |
118
121
 
119
122
  ### Authentication Lifecycle
@@ -381,7 +384,7 @@ function AuthComponent() {
381
384
  // 액션 (모두 Promise 반환)
382
385
  login, // (data: LoginRequest) => Promise<AuthResponse>
383
386
  loginWithFirebase, // (data: FirebaseExchangeRequest) => Promise<AuthResponse>
384
- register, // (data: RegisterRequest) => Promise<AuthResponse>
387
+ register, // (data: RegisterInput) => Promise<AuthResponse> ← method: 'email' | 'firebase'
385
388
  loginAnonymous, // (data?: AnonymousLoginRequest) => Promise<AuthResponse>
386
389
  upgradeAnonymous, // (data: { email, password, display_name?, username? }) => Promise<UserDto>
387
390
  logout, // () => Promise<void>
@@ -571,24 +574,85 @@ function UpgradeForm() {
571
574
 
572
575
  #### 회원가입
573
576
 
577
+ `register()`는 `method` 필드로 이메일 방식과 Firebase 방식을 구분합니다.
578
+ `channelUid` 또는 `channelSlug` 중 하나는 필수입니다 (`PHCMSProvider`에 설정된 값이 자동으로 사용됩니다).
579
+
580
+ ```ts
581
+ import type { RegisterInput } from '@ph-cms/client-sdk';
582
+ ```
583
+
584
+ ---
585
+
586
+ **방식 1 — 이메일/비밀번호 (`method: 'email'` 또는 생략)**
587
+
574
588
  ```tsx
575
589
  function RegisterForm() {
576
590
  const { register, registerStatus } = useAuth();
577
591
 
578
- const handleRegister = async (formData: {
579
- email: string;
580
- password: string;
581
- display_name: string;
582
- username?: string;
583
- }) => {
584
- await register(formData);
592
+ const handleRegister = async () => {
593
+ await register({
594
+ // method: 'email', // 생략 가능 (기본값)
595
+ email: 'user@example.com',
596
+ password: 'password123',
597
+ display_name: '홍길동',
598
+ termCodes: ['SERVICE_TERM', 'PRIVACY_TERM'], // 동의할 약관 코드 목록
599
+ // channelUid는 PHCMSProvider에 설정된 값이 자동 사용됨
600
+ // 명시적으로 지정하려면: channelUid: 'my-channel'
601
+ });
585
602
  // 성공 → 자동으로 me() 호출 → 즉시 인증 상태로 전환
586
603
  };
587
604
 
588
- // ...
605
+ return (
606
+ <button onClick={handleRegister} disabled={registerStatus.isPending}>
607
+ {registerStatus.isPending ? '가입 중...' : '회원가입'}
608
+ </button>
609
+ );
589
610
  }
590
611
  ```
591
612
 
613
+ ---
614
+
615
+ **방식 2 — Firebase 회원가입 (`method: 'firebase'`)**
616
+
617
+ 서버가 Firebase 계정 생성과 PH-CMS 계정 생성을 하나의 트랜잭션으로 처리합니다.
618
+ DB 트랜잭션 실패 시 서버가 Firebase 계정을 즉시 롤백하므로 고아 계정이 발생하지 않습니다.
619
+
620
+ 클라이언트는 Firebase ID 토큰 없이 이메일/비밀번호만 전송합니다.
621
+
622
+ ```tsx
623
+ function FirebaseRegisterForm() {
624
+ const { register, registerStatus } = useAuth();
625
+
626
+ const handleRegister = async () => {
627
+ await register({
628
+ method: 'firebase',
629
+ email: 'user@example.com',
630
+ password: 'password123',
631
+ display_name: '홍길동',
632
+ termCodes: ['SERVICE_TERM', 'PRIVACY_TERM'],
633
+ // channelUid는 PHCMSProvider에 설정된 값이 자동 사용됨
634
+ });
635
+ // 성공 → Firebase 계정 + PH-CMS 계정 동시 생성
636
+ // → 자동으로 me() 호출 → 즉시 인증 상태로 전환
637
+ };
638
+
639
+ return (
640
+ <button onClick={handleRegister} disabled={registerStatus.isPending}>
641
+ {registerStatus.isPending ? '가입 중...' : 'Firebase로 회원가입'}
642
+ </button>
643
+ );
644
+ }
645
+ ```
646
+
647
+ > **Firebase 방식과 `loginWithFirebase()`의 차이**
648
+ >
649
+ > | | `register({ method: 'firebase' })` | `loginWithFirebase({ idToken })` |
650
+ > |---|---|---|
651
+ > | **목적** | 신규 가입 | 기존 Firebase 계정으로 로그인 / 신규 가입 |
652
+ > | **클라이언트 역할** | 이메일·비밀번호만 전송 | Firebase SDK로 직접 인증 후 ID 토큰 전달 |
653
+ > | **Firebase 계정 생성** | 서버가 처리 (롤백 보장) | 클라이언트가 처리 |
654
+ > | **용도** | 서버 사이드 Firebase 가입 플로우 | 소셜 로그인(Google 등) 또는 자체 Firebase 로그인 |
655
+
592
656
  #### 로그아웃
593
657
 
594
658
  ```tsx
@@ -1238,6 +1302,18 @@ client.content // ContentModule
1238
1302
  client.channel // ChannelModule
1239
1303
  client.terms // TermsModule
1240
1304
  client.media // MediaModule
1305
+
1306
+ await client.getToken() // 현재 유효한 PH-CMS access token 반환 (만료 시 자동 갱신, 없으면 null)
1307
+ ```
1308
+
1309
+ `getToken()`은 PH-CMS SDK가 아닌 외부 서비스(예: 알림 서비스)를 클라이언트에서 직접 호출할 때 Authorization 헤더에 넣을 토큰을 가져오는 데 사용합니다.
1310
+
1311
+ ```ts
1312
+ const token = await client.getToken();
1313
+
1314
+ const res = await fetch(`${NOTIFICATION_SERVICE_URL}/notifications`, {
1315
+ headers: { Authorization: `Bearer ${token}` },
1316
+ });
1241
1317
  ```
1242
1318
 
1243
1319
  `apiPrefix`는 생성 시 각 모듈에 직접 주입됩니다. 예를 들어 `apiPrefix: '/v2/api'`로 설정하면 모든 모듈의 요청 URL이 `/v2/api/contents/...`, `/v2/api/auth/...` 형태로 전송됩니다.
@@ -1334,7 +1410,8 @@ const result = await content.list({ channelUid: 'my-channel' });
1334
1410
  |---|---|
1335
1411
  | `login(data: LoginRequest)` | 이메일/비밀번호 로그인 → `AuthResponse` |
1336
1412
  | `loginWithFirebase(data: FirebaseExchangeRequest)` | Firebase ID 토큰 교환 → `AuthResponse` |
1337
- | `register(data: RegisterRequest)` | 회원가입 → `AuthResponse` |
1413
+ | `register(data: RegisterRequest)` | 이메일 회원가입 → `AuthResponse` (`channelUid` \| `channelSlug` 필수) |
1414
+ | `registerWithFirebase(data: FirebaseRegisterRequest)` | 서버 사이드 Firebase 회원가입 → `AuthResponse` (`channelUid` \| `channelSlug` 필수) |
1338
1415
  | `me(params?: { channelUid?: string })` | 현재 사용자 프로필 조회 → `UserDto` |
1339
1416
  | `refresh(refreshToken: string)` | 토큰 갱신 → `{ accessToken, refreshToken }` |
1340
1417
  | `logout()` | 로그아웃 (프로바이더 토큰 삭제 + 서버 세션 무효화) |
package/bin/cli.js CHANGED
@@ -13,7 +13,7 @@ if (command === 'init-skill') {
13
13
  process.exit(1);
14
14
  }
15
15
 
16
- function initSkill() {g
16
+ function initSkill() {
17
17
  // --dir 옵션 파싱
18
18
  const dirFlagIndex = process.argv.indexOf('--dir');
19
19
  const baseDir = dirFlagIndex !== -1 && process.argv[dirFlagIndex + 1]
@@ -30,15 +30,13 @@ export declare class FirebaseAuthProvider extends BaseAuthProvider {
30
30
  expiryBufferMs?: number;
31
31
  });
32
32
  /**
33
- * Returns a valid access token for PH-CMS API calls.
33
+ * Returns the current valid PH-CMS access token.
34
34
  *
35
35
  * 1. If a PH-CMS access token exists and is still valid, return it.
36
36
  * 2. If the PH-CMS access token is expired (or will expire within
37
37
  * `expiryBufferMs`), attempt an automatic refresh using the stored
38
38
  * refresh token.
39
- * 3. If no PH-CMS token exists at all (or refresh failed), fall back to
40
- * the Firebase ID token (useful for the initial exchange call or if the
41
- * server supports Firebase tokens directly).
39
+ * 3. If no PH-CMS token exists or refresh fails, return `null`.
42
40
  */
43
41
  getToken(): Promise<string | null>;
44
42
  /**
@@ -28,32 +28,22 @@ class FirebaseAuthProvider extends base_provider_1.BaseAuthProvider {
28
28
  this.type = 'FIREBASE';
29
29
  }
30
30
  /**
31
- * Returns a valid access token for PH-CMS API calls.
31
+ * Returns the current valid PH-CMS access token.
32
32
  *
33
33
  * 1. If a PH-CMS access token exists and is still valid, return it.
34
34
  * 2. If the PH-CMS access token is expired (or will expire within
35
35
  * `expiryBufferMs`), attempt an automatic refresh using the stored
36
36
  * refresh token.
37
- * 3. If no PH-CMS token exists at all (or refresh failed), fall back to
38
- * the Firebase ID token (useful for the initial exchange call or if the
39
- * server supports Firebase tokens directly).
37
+ * 3. If no PH-CMS token exists or refresh fails, return `null`.
40
38
  */
41
39
  async getToken() {
42
- if (this.accessToken) {
43
- const expired = this.isCurrentTokenExpired();
44
- if (expired === true) {
45
- const refreshed = await this.tryRefresh();
46
- if (refreshed)
47
- return refreshed;
48
- // Refresh failed — fall through to Firebase ID token fallback.
49
- }
50
- else {
51
- // Token is valid (or unparseable — let server decide).
52
- return this.accessToken;
53
- }
40
+ if (!this.accessToken)
41
+ return null;
42
+ const expired = this.isCurrentTokenExpired();
43
+ if (expired === true) {
44
+ return this.tryRefresh();
54
45
  }
55
- // Fallback to Firebase ID token if no valid PH-CMS token is available.
56
- return this.getIdToken();
46
+ return this.accessToken;
57
47
  }
58
48
  /**
59
49
  * Clears PH-CMS tokens **and** signs the user out of Firebase.
package/dist/client.d.ts CHANGED
@@ -35,6 +35,15 @@ export declare class PHCMSClient {
35
35
  private refreshQueue;
36
36
  /** Exposes the auth provider so UI layers can check token state synchronously. */
37
37
  get authProvider(): AuthProvider | undefined;
38
+ /**
39
+ * Returns the current valid PH-CMS access token.
40
+ *
41
+ * Delegates to the configured {@link AuthProvider.getToken}, which handles
42
+ * expiry checks and automatic refresh internally.
43
+ *
44
+ * Returns `null` when no provider is configured or no valid token exists.
45
+ */
46
+ getToken(): Promise<string | null>;
38
47
  private readonly normalizedApiPrefix;
39
48
  constructor(config: PHCMSClientConfig);
40
49
  /**
package/dist/client.js CHANGED
@@ -17,6 +17,17 @@ class PHCMSClient {
17
17
  get authProvider() {
18
18
  return this.config.auth;
19
19
  }
20
+ /**
21
+ * Returns the current valid PH-CMS access token.
22
+ *
23
+ * Delegates to the configured {@link AuthProvider.getToken}, which handles
24
+ * expiry checks and automatic refresh internally.
25
+ *
26
+ * Returns `null` when no provider is configured or no valid token exists.
27
+ */
28
+ async getToken() {
29
+ return this.config.auth?.getToken() ?? null;
30
+ }
20
31
  constructor(config) {
21
32
  this.config = config;
22
33
  /**
@@ -1,3 +1,16 @@
1
+ import { FirebaseRegisterRequest, RegisterRequest } from '@ph-cms/api-contract';
2
+ type FirebaseRegisterInput = {
3
+ method: 'firebase';
4
+ } & FirebaseRegisterRequest;
5
+ type EmailRegisterInput = {
6
+ method?: 'email';
7
+ } & RegisterRequest;
8
+ /**
9
+ * register()에 전달할 수 있는 입력 타입.
10
+ * - method: 'firebase' → Firebase idToken 기반 가입 (FirebaseRegisterRequest)
11
+ * - method: 'email' (기본값) → 이메일/비밀번호 기반 가입 (RegisterRequest)
12
+ */
13
+ export type RegisterInput = FirebaseRegisterInput | EmailRegisterInput;
1
14
  /**
2
15
  * Unified Auth Hook
3
16
  * Returns both the current auth status and the actions.
@@ -150,16 +163,7 @@ export declare const useAuth: () => {
150
163
  }[] | undefined;
151
164
  };
152
165
  accessToken: string;
153
- }, Error, {
154
- email: string;
155
- display_name: string;
156
- username?: string | null | undefined;
157
- password?: string | undefined;
158
- provider?: string | undefined;
159
- provider_subject?: string | undefined;
160
- termCodes?: string[] | undefined;
161
- channelUid?: string | undefined;
162
- }, unknown>;
166
+ }, Error, RegisterInput, unknown>;
163
167
  loginAnonymous: import("@tanstack/react-query").UseMutateAsyncFunction<{
164
168
  refreshToken: string;
165
169
  user: {
@@ -349,16 +353,7 @@ export declare const useAuth: () => {
349
353
  }[] | undefined;
350
354
  };
351
355
  accessToken: string;
352
- }, Error, {
353
- email: string;
354
- display_name: string;
355
- username?: string | null | undefined;
356
- password?: string | undefined;
357
- provider?: string | undefined;
358
- provider_subject?: string | undefined;
359
- termCodes?: string[] | undefined;
360
- channelUid?: string | undefined;
361
- }, unknown>;
356
+ }, Error, RegisterInput, unknown>;
362
357
  loginAnonymousStatus: import("@tanstack/react-query").UseMutationResult<{
363
358
  refreshToken: string;
364
359
  user: {
@@ -551,3 +546,4 @@ export declare const useUpgradeAnonymous: () => import("@tanstack/react-query").
551
546
  display_name?: string;
552
547
  username?: string;
553
548
  }, unknown>;
549
+ export {};
@@ -28,10 +28,20 @@ const useAuth = () => {
28
28
  },
29
29
  });
30
30
  const registerMutation = (0, react_query_1.useMutation)({
31
- mutationFn: (data) => client.auth.register({
32
- channelUid: data.channelUid || channelUid,
33
- ...data
34
- }),
31
+ mutationFn: (data) => {
32
+ if (data.method === 'firebase') {
33
+ const { method, channelUid: explicitChannelUid, ...rest } = data;
34
+ return client.auth.registerWithFirebase({
35
+ ...rest,
36
+ channelUid: explicitChannelUid || channelUid,
37
+ });
38
+ }
39
+ const { method, channelUid: explicitChannelUid, ...rest } = data;
40
+ return client.auth.register({
41
+ ...rest,
42
+ channelUid: explicitChannelUid || channelUid,
43
+ });
44
+ },
35
45
  onSuccess: async () => {
36
46
  await refreshUser();
37
47
  },
@@ -44,6 +44,7 @@ export declare const useCreateContent: () => import("@tanstack/react-query").Use
44
44
  type: string;
45
45
  title: string;
46
46
  status?: string | undefined;
47
+ text?: string | null | undefined;
47
48
  channelUid?: string | undefined;
48
49
  channelSlug?: string | undefined;
49
50
  geometry?: {
@@ -55,7 +56,6 @@ export declare const useCreateContent: () => import("@tanstack/react-query").Use
55
56
  geometry?: any;
56
57
  } | undefined;
57
58
  image?: string | null | undefined;
58
- text?: string | null | undefined;
59
59
  attributes?: Record<string, any> | undefined;
60
60
  summary?: string | null | undefined;
61
61
  slug?: string | null | undefined;
@@ -20,8 +20,8 @@ export declare const useUploadToS3: () => import("@tanstack/react-query").UseMut
20
20
  }, unknown>;
21
21
  export declare const useMediaDetail: (uid: string) => import("@tanstack/react-query").UseQueryResult<{
22
22
  type: "image" | "video" | "audio" | "document" | "file";
23
- uid: string;
24
23
  url: string;
24
+ uid: string;
25
25
  name: string;
26
26
  mimeType: string;
27
27
  size: number;
@@ -87,6 +87,7 @@ export declare const useUpdateProfile: () => import("@tanstack/react-query").Use
87
87
  */
88
88
  export declare const useChannelTerms: () => import("@tanstack/react-query").UseQueryResult<{
89
89
  code: string;
90
+ type: "text" | "url";
90
91
  id: number;
91
92
  version: string;
92
93
  title: string;
@@ -1,4 +1,4 @@
1
- import { AuthResponse, FirebaseExchangeRequest, LoginRequest, RegisterRequest, UserDto, AnonymousLoginRequest } from "@ph-cms/api-contract";
1
+ import { AnonymousLoginRequest, AuthResponse, FirebaseExchangeRequest, FirebaseRegisterRequest, LoginRequest, RegisterRequest, UserDto } from "@ph-cms/api-contract";
2
2
  import { AxiosInstance } from "axios";
3
3
  import { AuthProvider } from "../auth/interfaces";
4
4
  export declare class AuthModule {
@@ -15,6 +15,13 @@ export declare class AuthModule {
15
15
  */
16
16
  loginWithFirebase(data: FirebaseExchangeRequest): Promise<AuthResponse>;
17
17
  register(data: RegisterRequest): Promise<AuthResponse>;
18
+ /**
19
+ * 서버 사이드 Firebase 회원가입.
20
+ * 클라이언트는 idToken 없이 이메일/비밀번호를 전송하고,
21
+ * 서버가 Firebase 계정 생성과 백엔드 유저 생성을 원자적으로 처리한다.
22
+ * DB 실패 시 서버가 Firebase 계정을 롤백하므로 고아 계정이 발생하지 않는다.
23
+ */
24
+ registerWithFirebase(data: FirebaseRegisterRequest): Promise<AuthResponse>;
18
25
  me(params?: {
19
26
  channelUid?: string;
20
27
  channelSlug?: string;
@@ -48,6 +48,26 @@ class AuthModule {
48
48
  }
49
49
  return response;
50
50
  }
51
+ /**
52
+ * 서버 사이드 Firebase 회원가입.
53
+ * 클라이언트는 idToken 없이 이메일/비밀번호를 전송하고,
54
+ * 서버가 Firebase 계정 생성과 백엔드 유저 생성을 원자적으로 처리한다.
55
+ * DB 실패 시 서버가 Firebase 계정을 롤백하므로 고아 계정이 발생하지 않는다.
56
+ */
57
+ async registerWithFirebase(data) {
58
+ const validation = api_contract_1.FirebaseRegisterSchema.safeParse(data);
59
+ if (!validation.success) {
60
+ throw new errors_1.ValidationError("Invalid Firebase register data", validation.error.errors);
61
+ }
62
+ const response = await this.client.post(`${this.prefix}/auth/register`, {
63
+ ...data,
64
+ provider: 'firebase:password',
65
+ });
66
+ if (this.provider) {
67
+ this.provider.setTokens(response.accessToken, response.refreshToken);
68
+ }
69
+ return response;
70
+ }
51
71
  async me(params) {
52
72
  return this.client.get(`${this.prefix}/auth/me`, { params });
53
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ph-cms/client-sdk",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Unified PH-CMS Client SDK (React + Core)",
5
5
  "keywords": [],
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "dependencies": {
33
- "@ph-cms/api-contract": "^0.1.8",
33
+ "@ph-cms/api-contract": "^0.1.9",
34
34
  "@tanstack/react-query": "^5.0.0",
35
35
  "axios": "^1.6.0",
36
36
  "zod": "^3.22.4"