@ph-cms/client-sdk 0.1.22 → 0.1.24

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
+ );
610
+ }
611
+ ```
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
+ );
589
644
  }
590
645
  ```
591
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
@@ -1334,7 +1398,8 @@ const result = await content.list({ channelUid: 'my-channel' });
1334
1398
  |---|---|
1335
1399
  | `login(data: LoginRequest)` | 이메일/비밀번호 로그인 → `AuthResponse` |
1336
1400
  | `loginWithFirebase(data: FirebaseExchangeRequest)` | Firebase ID 토큰 교환 → `AuthResponse` |
1337
- | `register(data: RegisterRequest)` | 회원가입 → `AuthResponse` |
1401
+ | `register(data: RegisterRequest)` | 이메일 회원가입 → `AuthResponse` (`channelUid` \| `channelSlug` 필수) |
1402
+ | `registerWithFirebase(data: FirebaseRegisterRequest)` | 서버 사이드 Firebase 회원가입 → `AuthResponse` (`channelUid` \| `channelSlug` 필수) |
1338
1403
  | `me(params?: { channelUid?: string })` | 현재 사용자 프로필 조회 → `UserDto` |
1339
1404
  | `refresh(refreshToken: string)` | 토큰 갱신 → `{ accessToken, refreshToken }` |
1340
1405
  | `logout()` | 로그아웃 (프로바이더 토큰 삭제 + 서버 세션 무효화) |
package/bin/cli.js CHANGED
@@ -8,12 +8,19 @@ const command = process.argv[2];
8
8
  if (command === 'init-skill') {
9
9
  initSkill();
10
10
  } else {
11
- console.log('Usage: npx @ph-cms/client-sdk init-skill');
11
+ console.log('Usage: npx @ph-cms/client-sdk init-skill [--dir <path>]');
12
+ console.log(' --dir <path> 스킬 파일을 생성할 루트 디렉토리 (기본값: .agents)');
12
13
  process.exit(1);
13
14
  }
14
15
 
15
16
  function initSkill() {
16
- const skillDir = path.join(process.cwd(), '.gemini/skills/ph-cms-client');
17
+ // --dir 옵션 파싱
18
+ const dirFlagIndex = process.argv.indexOf('--dir');
19
+ const baseDir = dirFlagIndex !== -1 && process.argv[dirFlagIndex + 1]
20
+ ? process.argv[dirFlagIndex + 1]
21
+ : '.agents';
22
+
23
+ const skillDir = path.join(process.cwd(), baseDir, 'skills/ph-cms-client');
17
24
  const skillMdPath = path.join(skillDir, 'SKILL.md');
18
25
 
19
26
  const skillContent = `# PH-CMS Client SDK Skill
@@ -75,5 +82,5 @@ If you need specific details about a module (e.g., \`ContentModule\`, \`AuthModu
75
82
  }
76
83
 
77
84
  fs.writeFileSync(skillMdPath, skillContent);
78
- console.log('✅ PH-CMS Client SDK Skill has been successfully initialized at .gemini/skills/ph-cms-client/SKILL.md');
85
+ console.log(`✅ PH-CMS Client SDK Skill has been successfully initialized at ${path.join(baseDir, 'skills/ph-cms-client/SKILL.md')}`);
79
86
  }
@@ -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
  }
@@ -74,7 +94,9 @@ class AuthModule {
74
94
  }
75
95
  // Call the server endpoint to invalidate the session if applicable.
76
96
  // JWT-based auth is stateless, but the server may implement a blacklist.
77
- await this.client.post(`${this.prefix}/auth/logout`).catch(() => { }); // Ignore error on logout
97
+ await this.client.post(`${this.prefix}/auth/logout`, null, {
98
+ headers: { 'Content-Type': 'application/json' },
99
+ }).catch(() => { }); // Ignore error on logout
78
100
  }
79
101
  }
80
102
  exports.AuthModule = AuthModule;
@@ -23,7 +23,9 @@ class ContentModule {
23
23
  async incrementView(uid) {
24
24
  if (!uid)
25
25
  throw new errors_1.ValidationError("UID is required", []);
26
- await this.client.patch(`${this.prefix}/contents/${uid}/view`);
26
+ await this.client.patch(`${this.prefix}/contents/${uid}/view`, null, {
27
+ headers: { 'Content-Type': 'application/json' },
28
+ });
27
29
  }
28
30
  async create(data) {
29
31
  const validation = api_contract_1.CreateContentSchema.safeParse(data);
@@ -84,7 +86,9 @@ class ContentModule {
84
86
  async toggleLike(uid) {
85
87
  if (!uid)
86
88
  throw new errors_1.ValidationError("UID is required", []);
87
- return this.client.post(`${this.prefix}/contents/${uid}/like`, {});
89
+ return this.client.post(`${this.prefix}/contents/${uid}/like`, null, {
90
+ headers: { 'Content-Type': 'application/json' },
91
+ });
88
92
  }
89
93
  async getLikeStatus(uid) {
90
94
  if (!uid)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ph-cms/client-sdk",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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"