@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 +76 -11
- package/bin/cli.js +10 -3
- package/dist/hooks/useAuth.d.ts +16 -20
- package/dist/hooks/useAuth.js +14 -4
- package/dist/hooks/useContent.d.ts +1 -1
- package/dist/hooks/useMedia.d.ts +1 -1
- package/dist/hooks/useUser.d.ts +1 -0
- package/dist/modules/auth.d.ts +8 -1
- package/dist/modules/auth.js +23 -1
- package/dist/modules/content.js +6 -2
- package/package.json +2 -2
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:
|
|
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 (
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
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(
|
|
85
|
+
console.log(`✅ PH-CMS Client SDK Skill has been successfully initialized at ${path.join(baseDir, 'skills/ph-cms-client/SKILL.md')}`);
|
|
79
86
|
}
|
package/dist/hooks/useAuth.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/hooks/useAuth.js
CHANGED
|
@@ -28,10 +28,20 @@ const useAuth = () => {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
const registerMutation = (0, react_query_1.useMutation)({
|
|
31
|
-
mutationFn: (data) =>
|
|
32
|
-
|
|
33
|
-
|
|
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;
|
package/dist/hooks/useMedia.d.ts
CHANGED
|
@@ -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;
|
package/dist/hooks/useUser.d.ts
CHANGED
|
@@ -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;
|
package/dist/modules/auth.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthResponse, FirebaseExchangeRequest, LoginRequest, RegisterRequest, UserDto
|
|
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;
|
package/dist/modules/auth.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/modules/content.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|