npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ph-cms/client-sdk

v0.1.11

Published

Unified PH-CMS Client SDK (React + Core)

Readme

@ph-cms/client-sdk

PH-CMS 클라이언트 SDK — 브라우저 및 React 애플리케이션을 위한 통합 클라이언트.

주요 기능:

  • 타입이 지정된 API 클라이언트
  • Auth Provider 인터페이스 및 구현체 (LocalAuthProvider, FirebaseAuthProvider)
  • React Context 및 Hooks
  • React Query 통합
  • 자동 토큰 갱신 (Proactive + Reactive)
  • Firebase ↔ PH-CMS 인증 동기화
  • 세분화된 인증 상태 관리 (authStatus)

Installation

npm install @ph-cms/client-sdk

@tanstack/react-query는 직접 의존성으로 포함되어 있어 별도 설치가 필요 없습니다. Firebase를 사용하는 경우 firebase를 별도로 설치하세요 (선택적 peer dependency).


Type Imports

SDK에서 사용하는 모든 타입은 @ph-cms/client-sdk에서 직접 import할 수 있습니다. @ph-cms/api-contract를 직접 참조할 필요 없이 SDK 하나로 모든 타입을 가져올 수 있습니다.

// SDK 내부 타입
import type {
  AuthProvider,
  AuthStatus,
  JwtPayload,
  PHCMSClientConfig,
  PHCMSContextType,
  PHCMSProviderProps,
  UseFirebaseAuthSyncOptions,
  UseFirebaseAuthSyncReturn,
  FirebaseAuthSyncProps,
} from '@ph-cms/client-sdk';

// API 도메인 타입 (api-contract에서 re-export)
import type {
  // Auth
  LoginRequest,
  RegisterRequest,
  RefreshTokenRequest,
  FirebaseExchangeRequest,
  AuthResponse,

  // User
  UserDto,

  // Channel
  ChannelDto,
  CreateChannelDto,
  ListChannelQuery,
  CheckHierarchyQuery,
  PagedChannelListResponse,

  // Content
  ContentDto,
  ContentMediaDto,
  CreateContentRequest,
  UpdateContentRequest,
  CreateStampTourRequest,
  UpdateStampTourRequest,
  ListContentQuery,
  PagedContentListResponse,

  // Hierarchy / Policy
  HierarchySetDto,
  PermissionPolicySetDto,

  // Terms
  TermDto,
  ListTermsQuery,
  PagedTermListResponse,

  // Geo
  GeoJSON,
  BoundsQuery,

  // Media
  MediaUploadTicketRequest,
  MediaUploadTicketResponse,
  MediaUploadTicketBatchRequest,
  MediaUploadTicketBatchResponse,

  // Common
  PagedResponse,
} from '@ph-cms/client-sdk';

타입들은 src/types.ts에 도메인별로 정리되어 있으므로, IDE의 자동완성에서 바로 확인할 수 있습니다.


Authentication Architecture

Overview

SDK의 인증 시스템은 세 개의 레이어로 구성됩니다:

| Layer | Component | Role | |---|---|---| | Provider | LocalAuthProvider / FirebaseAuthProvider | 토큰 저장·조회·갱신·삭제를 담당하는 저수준 어댑터 | | Module | AuthModule (client.auth) | 서버 API 호출 (login, loginWithFirebase, me, refresh, logout) | | Hook / Context | PHCMSProvider, useAuth | React 상태 관리 — authStatus, 로그인·로그아웃 액션 |

PHCMSProvideruseQuery(['auth','me'])authProvider.hasToken()true일 때만 실행하여 불필요한 401을 방지하고, context로 { user, authStatus, hasToken, ... }을 제공합니다.

useAuth() — 각 액션은 서버 호출 → provider.setTokens()refreshUser()(= me() 재조회) 순서로 동작합니다:

| 액션 | 서버 호출 | 후속 동작 | |---|---|---| | login() | POST /api/auth/login | setTokens()refreshUser() | | loginWithFirebase() | POST /api/auth/firebase/exchange | setTokens()refreshUser() | | register() | POST /api/auth/register | setTokens()refreshUser() | | logout() | POST /api/auth/logout | provider.logout()refreshUser() (상태 초기화) |

Authentication Lifecycle

1. 초기 마운트 (비인증 상태)

App Mount
  → PHCMSProvider mount
    → authProvider.hasToken()  ← false (localStorage에 토큰 없음)
    → useQuery enabled: false  ← me() 호출하지 않음
    → authStatus: 'unauthenticated'

서버에 불필요한 401 요청을 보내지 않습니다.

2. 초기 마운트 (기존 세션 복원)

App Mount
  → PHCMSProvider mount
    → authProvider.hasToken()  ← true (localStorage에 토큰 존재)
    → useQuery enabled: true
    → authStatus: 'loading'    ← 이 시점에 스플래시 화면 표시 가능
    → GET /api/auth/me
    → authStatus: 'authenticated', user: { ... }

3. 로그인 (이메일/비밀번호)

user calls login({ email, password })
  → POST /api/auth/login
  → provider.setTokens(accessToken, refreshToken)
  → refreshUser()
    → GET /api/auth/me
    → authStatus: 'authenticated', user: { ... }

4. 로그아웃

user calls logout()
  → provider.logout()           ← localStorage 토큰 삭제
  → POST /api/auth/logout       ← 서버 세션 무효화 (실패해도 무시)
  → refreshUser()
    → hasToken: false → queryData 초기화
    → authStatus: 'unauthenticated', user: null

Token Refresh (자동 토큰 갱신)

SDK는 두 가지 방식으로 토큰을 자동 갱신합니다:

Proactive Refresh (사전 갱신)

Provider의 getToken() 호출 시 JWT의 exp 클레임을 검사합니다. 만료 임박 시 (expiryBufferMs 이내, 기본 60초) 서버에 갱신 요청을 보냅니다.

Request Interceptor
  → provider.getToken()
    → JWT exp 검사: 만료 임박?
      → Yes: tryRefresh() → POST /api/auth/refresh
              → 새 토큰 저장 → 새 accessToken 반환
      → No:  기존 accessToken 반환
  → Authorization: Bearer {token}

Reactive Refresh (401 대응 갱신)

서버가 401을 반환하면 interceptor가 자동으로 토큰을 갱신하고 원본 요청을 재시도합니다.

API Request → 401 Unauthorized
  → coordinatedRefresh(refreshToken)
    → POST /api/auth/refresh (with _skipAuth flag)
    → 성공: provider.setTokens() → 원본 요청 재시도
    → 실패: ApiError throw

동시 요청 처리: 여러 요청이 동시에 401을 받으면 하나의 refresh 요청만 실행되고, 나머지 요청은 큐에서 대기합니다 (de-duplication).

순환 방지: refresh 요청 자체는 _skipAuth 플래그가 붙어 request interceptor에서 토큰 첨부를 건너뛰므로, getToken()tryRefresh() 재귀 호출이 발생하지 않습니다.


Auth Providers

모든 Provider는 BaseAuthProvider 추상 클래스를 상속합니다. 공통 로직(토큰 저장, 갱신, de-duplication, localStorage 관리)은 BaseAuthProvider에 구현되어 있고, 각 Provider는 getToken()logout() 등 고유 로직만 오버라이드합니다.

LocalAuthProvider

이메일/비밀번호 기반 인증에 사용합니다. 토큰을 localStorage에 저장합니다.

import { LocalAuthProvider, PHCMSClient } from '@ph-cms/client-sdk';

const authProvider = new LocalAuthProvider('my_app_', {
  expiryBufferMs: 60_000, // 선택 — 만료 60초 전부터 갱신 시도 (기본값)
});

const client = new PHCMSClient({
  baseURL: 'https://api.example.com',
  auth: authProvider,
});

| 생성자 인자 | 타입 | 기본값 | 설명 | |---|---|---|---| | storageKeyPrefix | string | 'ph_cms_' | localStorage 키 접두사 ({prefix}access_token, {prefix}refresh_token) | | options.expiryBufferMs | number | 60_000 | 만료 몇 ms 전부터 토큰을 갱신할지 |

FirebaseAuthProvider

Firebase Authentication과 연동합니다. PH-CMS 토큰이 없으면 Firebase ID 토큰으로 fallback합니다.

import { FirebaseAuthProvider, PHCMSClient } from '@ph-cms/client-sdk';
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseApp = initializeApp({ /* Firebase config */ });
const firebaseAuth = getAuth(firebaseApp);

const authProvider = new FirebaseAuthProvider(firebaseAuth, 'my_app_fb_', {
  expiryBufferMs: 60_000,
});

const client = new PHCMSClient({
  baseURL: 'https://api.example.com',
  auth: authProvider,
});

| 생성자 인자 | 타입 | 기본값 | 설명 | |---|---|---|---| | auth | firebase/auth.Auth | (필수) | Firebase Auth 인스턴스 | | storageKeyPrefix | string | 'ph_cms_fb_' | localStorage 키 접두사 | | options.expiryBufferMs | number | 60_000 | 만료 몇 ms 전부터 토큰을 갱신할지 |

getToken() 동작:

  1. PH-CMS access token이 유효하면 → 그대로 반환
  2. 만료 임박이면 → tryRefresh() 시도 → 성공하면 새 토큰 반환
  3. PH-CMS 토큰이 없거나 갱신 실패 → Firebase ID 토큰으로 fallback

AuthProvider Interface

커스텀 인증 Provider를 구현하려면 아래 인터페이스를 따릅니다. BaseAuthProvider를 상속하면 대부분의 메서드가 이미 구현되어 있으므로, typegetToken()만 구현하면 됩니다.

interface AuthProvider {
  type: 'FIREBASE' | 'LOCAL';
  getToken(): Promise<string | null>;
  hasToken(): boolean;
  getRefreshToken(): string | null;
  setRefreshFn(fn: (refreshToken: string) => Promise<{ accessToken: string; refreshToken: string }>): void;
  setTokens(accessToken: string, refreshToken: string): void;
  onTokenExpired(callback: () => Promise<void>): void;
  logout(): Promise<void>;
}

| 메서드 | 설명 | |---|---| | hasToken() | 토큰 존재 여부 (동기). React의 useQuery enabled 조건에 사용되므로 반드시 동기여야 합니다 | | getToken() | 유효한 access token 반환. 만료 시 자동 갱신 시도 후 반환 | | getRefreshToken() | 현재 refresh token 반환 | | setRefreshFn(fn) | PHCMSClient가 생성 시 호출. Provider가 자체적으로 토큰을 갱신할 수 있게 함 | | setTokens(access, refresh) | 새 토큰 쌍 저장 (login/refresh 성공 시 자동 호출) | | onTokenExpired(callback) | 토큰 만료 + 갱신 실패 시 호출할 콜백 등록 | | logout() | 세션 정리 (토큰 삭제) |


React Usage

Basic Setup

import { PHCMSClient, LocalAuthProvider, PHCMSProvider } from '@ph-cms/client-sdk';

const authProvider = new LocalAuthProvider('my_app_');

const client = new PHCMSClient({
  baseURL: 'https://api.example.com',
  auth: authProvider,
});

export function App() {
  return (
    <PHCMSProvider client={client}>
      <YourComponents />
    </PHCMSProvider>
  );
}

PHCMSProvider는 내부적으로 QueryClientProvider를 포함합니다. 외부에서 직접 관리하는 QueryClient를 사용하려면 queryClient prop으로 전달합니다:

import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient();

<PHCMSProvider client={client} queryClient={queryClient}>
  ...
</PHCMSProvider>

Authentication Status (authStatus)

Context는 세분화된 인증 상태를 authStatus로 제공합니다:

| authStatus | hasToken | user | 의미 | |---|---|---|---| | 'unauthenticated' | false | null | 미로그인 (토큰 없음) → 로그인 화면 표시 | | 'loading' | true | null | 세션 복원 중 (me() 호출 중) → 스플래시 화면 표시 | | 'authenticated' | true | UserDto | 인증 완료 → 메인 화면 표시 | | 'unauthenticated' | true | null | 토큰 있으나 me() 실패 → 재로그인 유도 |

import { usePHCMSContext } from '@ph-cms/client-sdk';

function AppRouter() {
  const { authStatus, hasToken } = usePHCMSContext();

  switch (authStatus) {
    case 'loading':
      return <SplashScreen />;
    case 'authenticated':
      return <MainApp />;
    case 'unauthenticated':
      // hasToken이 true이면 토큰은 있었으나 만료/검증 실패
      if (hasToken) return <SessionExpiredScreen />;
      return <LoginScreen />;
  }
}

isAuthenticatedisLoading은 하위 호환을 위해 유지되지만, 내부적으로 authStatus에서 파생됩니다 (isAuthenticated === authStatus === 'authenticated').

Authentication with useAuth

useAuth는 인증 상태와 액션을 통합 제공하는 메인 훅입니다.

import { useAuth } from '@ph-cms/client-sdk';

function AuthComponent() {
  const {
    // 상태
    user,              // UserDto | null
    isAuthenticated,   // boolean
    isLoading,         // boolean

    // 액션 (모두 Promise 반환)
    login,             // (data: LoginRequest) => Promise<AuthResponse>
    loginWithFirebase, // (data: FirebaseExchangeRequest) => Promise<AuthResponse>
    register,          // (data: RegisterRequest) => Promise<AuthResponse>
    logout,            // () => Promise<void>

    // 뮤테이션 상태 (isPending, error 등)
    loginStatus,
    loginWithFirebaseStatus,
    registerStatus,
    logoutStatus,
  } = useAuth();
}

이메일/비밀번호 로그인

function LoginForm() {
  const { login, loginStatus, isAuthenticated, user } = useAuth();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  if (isAuthenticated) {
    return <p>Welcome, {user?.display_name}</p>;
  }

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await login({ email, password });
      // 성공 → 자동으로 me() 호출 → user, isAuthenticated 갱신
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <button type="submit" disabled={loginStatus.isPending}>
        {loginStatus.isPending ? '로그인 중...' : '로그인'}
      </button>
      {loginStatus.error && <p>{(loginStatus.error as Error).message}</p>}
    </form>
  );
}

Firebase 로그인

Firebase 로그인은 두 단계로 이루어집니다:

  1. Firebase SDK로 인증하여 ID 토큰 획득
  2. loginWithFirebase()로 PH-CMS 서버에 토큰 교환
import { getAuth, signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

function FirebaseLoginButton() {
  const { loginWithFirebase, loginWithFirebaseStatus } = useAuth();

  const handleFirebaseLogin = async () => {
    const auth = getAuth();
    const result = await signInWithPopup(auth, new GoogleAuthProvider());
    const idToken = await result.user.getIdToken();

    await loginWithFirebase({ idToken });
    // 성공 → 자동으로 me() 호출 → 인증 상태 갱신
  };

  return (
    <button onClick={handleFirebaseLogin} disabled={loginWithFirebaseStatus.isPending}>
      {loginWithFirebaseStatus.isPending ? 'Firebase 로그인 중...' : 'Google 로그인'}
    </button>
  );
}

회원가입

function RegisterForm() {
  const { register, registerStatus } = useAuth();

  const handleRegister = async (formData: {
    email: string;
    password: string;
    display_name: string;
    username?: string;
  }) => {
    await register(formData);
    // 성공 → 자동으로 me() 호출 → 즉시 인증 상태로 전환
  };

  // ...
}

로그아웃

function LogoutButton() {
  const { logout } = useAuth();

  return <button onClick={() => logout()}>로그아웃</button>;
}

Context API (usePHCMSContext)

useAuth 대신 context에 직접 접근할 수도 있습니다.

import { usePHCMSContext } from '@ph-cms/client-sdk';

function MyComponent() {
  const {
    client,          // PHCMSClient 인스턴스
    user,            // UserDto | null
    isAuthenticated, // boolean
    isLoading,       // boolean
    authStatus,      // 'loading' | 'authenticated' | 'unauthenticated'
    hasToken,        // boolean — Provider에 토큰이 존재하는지
    refreshUser,     // () => Promise<void> — 수동으로 프로필 다시 조회
  } = usePHCMSContext();

  const handleProfileUpdate = async () => {
    // 프로필 수정 API 호출 후
    await refreshUser(); // context 갱신
  };
}

Profile Update (useUpdateProfile)

사용자가 자신의 프로필을 수정할 수 있는 훅입니다. 업데이트가 성공하면 내부적으로 refreshUser()가 호출되어 컨텍스트와 UI가 즉각적으로 갱신됩니다.

import { useUpdateProfile } from '@ph-cms/client-sdk';

function ProfileEditor({ userUid }) {
  const { mutateAsync: updateProfile, isPending } = useUpdateProfile();

  const handleSave = async () => {
    await updateProfile({
      uid: userUid,
      data: {
        display_name: 'New Name',
        avatar_url: 'https://example.com/new-avatar.png',
        profile_data: { bio: 'Hello World' },
      }
    });
    alert('프로필이 업데이트되었습니다.');
  };

  return <button onClick={handleSave} disabled={isPending}>저장</button>;
}

Standalone (Non-React) Usage

React 없이 PHCMSClient를 직접 사용할 수 있습니다.

import { PHCMSClient, LocalAuthProvider } from '@ph-cms/client-sdk';

const authProvider = new LocalAuthProvider('my_app_');
const client = new PHCMSClient({
  baseURL: 'https://api.example.com',
  auth: authProvider,
});

// 로그인 — provider에 토큰이 자동 저장됨
const authResponse = await client.auth.login({
  email: '[email protected]',
  password: 'password',
});

// 프로필 조회
const me = await client.auth.me();
console.log(me.email);

// 토큰 갱신 (수동)
// 일반적으로 SDK가 자동으로 처리하지만, 필요 시 직접 호출 가능
const refreshToken = authProvider.getRefreshToken();
if (refreshToken) {
  const newTokens = await client.auth.refresh(refreshToken);
  authProvider.setTokens(newTokens.accessToken, newTokens.refreshToken);
}

// 로그아웃
await client.auth.logout();

Legacy Hooks

하위 호환성을 위해 개별 훅도 제공됩니다. 새 코드에서는 useAuth를 권장합니다.

| Hook | 설명 | |---|---| | useUser() | { data: UserDto \| null, isLoading, isAuthenticated } | | useLogin() | { mutateAsync: login } | | useLogout() | { mutateAsync: logout } |

Firebase Auth Sync

Firebase 인증 상태가 변경될 때 PH-CMS 백엔드와 자동으로 동기화할 수 있습니다.

| 방식 | 용도 | |---|---| | useFirebaseAuthSync 훅 | 기존 컴포넌트에 동기화 로직을 삽입할 때 | | <FirebaseAuthSync> 컴포넌트 | 컴포넌트 트리를 감싸서 선언적으로 사용할 때 |

동기화 동작

Firebase onAuthStateChanged
  │
  ├─ fbUser 존재 + PH-CMS 비인증 상태
  │   → fbUser.getIdToken()
  │   → client.auth.loginWithFirebase({ idToken })
  │   → provider.setTokens(...)
  │   → refreshUser()  ← me() 호출하여 프로필 로드
  │
  └─ fbUser null (로그아웃) + PH-CMS 인증 상태
      → client.auth.logout()
      → refreshUser()  ← 상태 초기화

<FirebaseAuthSync> 컴포넌트

<PHCMSProvider> 안에서 사용합니다:

import { PHCMSProvider, FirebaseAuthSync } from '@ph-cms/client-sdk';
import { getAuth } from 'firebase/auth';

const firebaseAuth = getAuth(firebaseApp);

function App() {
  return (
    <PHCMSProvider client={client}>
      <FirebaseAuthSync firebaseAuth={firebaseAuth}>
        <MainContent />
      </FirebaseAuthSync>
    </PHCMSProvider>
  );
}

| Prop | 타입 | 기본값 | 설명 | |---|---|---|---| | firebaseAuth | firebase/auth.Auth | (필수) | Firebase Auth 인스턴스 | | logoutOnFirebaseSignOut | boolean | true | Firebase 로그아웃 시 PH-CMS도 자동 로그아웃할지 | | onSyncSuccess | () => void | — | 토큰 교환 성공 시 콜백 | | onSyncError | (error: unknown) => void | — | 토큰 교환 실패 시 콜백 |

useFirebaseAuthSync

import { useFirebaseAuthSync } from '@ph-cms/client-sdk';
import { getAuth } from 'firebase/auth';

const firebaseAuth = getAuth(firebaseApp);

function AppContent() {
  const { isSyncing } = useFirebaseAuthSync({
    firebaseAuth,
    onSyncSuccess: () => console.log('Firebase↔PH-CMS 동기화 완료'),
    onSyncError: (err) => console.error('동기화 실패:', err),
  });

  if (isSyncing) return <div>인증 동기화 중...</div>;

  return <MainContent />;
}

전체 구성 예시 (Firebase)

import {
  PHCMSClient,
  FirebaseAuthProvider,
  PHCMSProvider,
  FirebaseAuthSync,
} from '@ph-cms/client-sdk';
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';

const firebaseApp = initializeApp({ /* config */ });
const firebaseAuth = getAuth(firebaseApp);

const authProvider = new FirebaseAuthProvider(firebaseAuth, 'my_app_fb_');

const client = new PHCMSClient({
  baseURL: 'https://api.example.com',
  auth: authProvider,
});

function App() {
  return (
    <PHCMSProvider client={client}>
      <FirebaseAuthSync
        firebaseAuth={firebaseAuth}
        onSyncError={(err) => alert('인증 동기화에 실패했습니다.')}
      >
        <Router />
      </FirebaseAuthSync>
    </PHCMSProvider>
  );
}

이 구성에서는:

  • Firebase 로그인 시 FirebaseAuthSync가 자동으로 PH-CMS 토큰 교환 + 프로필 조회를 수행
  • Firebase 로그아웃 시 PH-CMS 세션도 자동 정리
  • 앱 재방문 시 localStorage 토큰으로 세션 자동 복원
  • 토큰 만료 시 자동 갱신 (proactive + reactive)

Using Data Hooks

Content

import { useContentList, useContentDetail } from '@ph-cms/client-sdk';

function ContentList() {
  const { data, isLoading } = useContentList({ limit: 10 });

  if (isLoading) return <div>Loading...</div>;

  return (
    <ul>
      {data?.content.map(item => (
        <li key={item.uid}>{item.title}</li>
      ))}
    </ul>
  );
}

Channel

import { useChannelList } from '@ph-cms/client-sdk';

function ChannelList() {
  const { data, isLoading } = useChannelList({ limit: 20 });
  // ...
}

Like (좋아요)

콘텐츠에 좋아요를 남기고, 좋아요 상태를 확인할 수 있습니다.

좋아요 토글 (인증 필수)

// 모듈 직접 사용
const result = await client.content.toggleLike('content-uid');
// => { liked: true, likeCount: 42 }

// React Hook 사용 (Optimistic update 포함)
const { mutate: toggleLike } = useToggleLike();
toggleLike('content-uid');

좋아요 상태 확인 (인증 선택)

// 모듈 직접 사용
const status = await client.content.getLikeStatus('content-uid');
// => { liked: true }

// React Hook 사용
const { data: status } = useLikeStatus('content-uid');
console.log(status?.liked); // true or false

Stamp Tour

스탬프 투어 기능을 사용하여 특정 지점(Marker) 방문을 인증하고 진행 현황을 조회하거나, 새로운 투어를 생성할 수 있습니다.

스탬프 투어 생성 (인증 필수)

여러 개의 마커(Marker)를 묶어 새로운 스탬프 투어를 생성합니다.

import { useCreateStampTour } from '@ph-cms/client-sdk';

function CreateTourForm() {
  const { mutateAsync: createTour, isPending } = useCreateStampTour();

  const handleCreate = async () => {
    await createTour({
      channelSlug: 'my-channel',
      parentUid: 'folder-uid',
      title: '서울 명소 투어',
      summary: '서울의 주요 명소를 방문하세요.',
      markerUids: ['marker-1-uid', 'marker-2-uid'],
      isActive: true,
      tags: ['서울', '여행'],
      startsAt: '2026-04-01T00:00:00Z',
      endsAt: '2026-06-30T23:59:59Z'
    });
    alert('투어가 생성되었습니다.');
  };

  return <button onClick={handleCreate} disabled={isPending}>투어 만들기</button>;
}

스탬프 투어 수정 (인증 필수)

기존 스탬프 투어의 정보나 마커 목록을 수정합니다. (참여자가 있는 경우 마커 목록 수정은 제한될 수 있습니다.)

import { useUpdateStampTour } from '@ph-cms/client-sdk';

function UpdateTourForm({ tourUid }) {
  const { mutateAsync: updateTour, isPending } = useUpdateStampTour();

  const handleUpdate = async () => {
    await updateTour({
      uid: tourUid,
      data: {
        title: '수정된 투어 제목',
        isActive: false,
        markerUids: ['marker-1-uid', 'marker-3-uid']
      }
    });
    alert('투어가 수정되었습니다.');
  };

  return <button onClick={handleUpdate} disabled={isPending}>투어 수정</button>;
}

스탬프 획득 (인증)

사용자의 현재 GPS 좌표를 전송하여 특정 투어 내의 마커 방문을 인증합니다.

import { useStamp } from '@ph-cms/client-sdk';

function StampButton({ tourUid, markerUid }) {
  const { mutateAsync: collectStamp, isPending } = useStamp();

  const handleStamp = async () => {
    try {
      const result = await collectStamp({
        tourUid,
        markerUid,
        data: { lat: 37.5511, lng: 126.9882 }
      });
      
      console.log(`거리: ${result.distance}m`);
      if (result.isCompletion) {
        alert('축하합니다! 투어를 완주하셨습니다.');
      }
    } catch (error) {
      alert('스탬프를 획득할 수 없습니다. (너무 멀거나 이미 획득함)');
    }
  };

  return <button onClick={handleStamp} disabled={isPending}>스탬프 찍기</button>;
}

클라이언트 위치 검증 (Geo)

스탬프를 획득하기 전 클라이언트 창에서 사용자의 현재 GPS 좌표와 마커 위치를 비교하여, 반경 내에 들어왔는지 선제적으로 검증할 수 있습니다. checkStampAvailability 함수와 지리 정보 훅 useGeolocation을 조합하여 다양한 검증 상태(available, distance_far, not_stamp_location, checking_geo 등)를 처리합니다.

import { useGeolocation, checkStampAvailability } from '@ph-cms/client-sdk';

function StampLocationGuard({ marker, stampStatus, isAuthenticated }) {
  // 실시간 GPS 좌표 및 권한 상태 감지
  const { latitude, longitude, geoError } = useGeolocation();

  // 클라이언트 단에서 마커 접근 가능성 확인
  const availability = checkStampAvailability({
    markerUid: marker.uid,
    markerLocation: marker.location, // { latitude, longitude }
    stampStatus,                     // 내가 획득한 스탬프 정보 (StampStatusDto)
    isLoggedIn: isAuthenticated,     // 권한 상태 확인
    latitude,
    longitude,
    geoError,
    distanceThreshold: 40,           // 스탬프 획득 가능 반경 (단위: 미터)
  });

  return (
    <div>
      <p>상태: {availability.stateMessage}</p>
      <p>힌트: {availability.hintMessage}</p>
      <p>거리: {availability.distance ? `${availability.distance.toFixed(1)}m` : '알 수 없음'}</p>
      
      {availability.state === 'available' && (
        <button onClick={handleCollect}>스탬프 획득하기</button>
      )}
    </div>
  );
}

투어 진행 현황 조회

특정 투어에 대해 내가 획득한 스탬프 목록과 완주 여부를 확인합니다.

import { useStampStatus } from '@ph-cms/client-sdk';

function TourProgress({ tourUid }) {
  const { data: status, isLoading } = useStampStatus(tourUid);

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <p>진행도: {status?.collected_markers} / {status?.total_markers}</p>
      {status?.is_completed && <p>🏆 완주 완료!</p>}
    </div>
  );
}

투어 전체 통계 (관리자용)

투어의 총 참여자 수, 완주율 등 관리용 데이터를 조회합니다. (적절한 권한 필요)

import { useTourStats } from '@ph-cms/client-sdk';

function AdminStats({ tourUid }) {
  const { data: stats, isLoading } = useTourStats(tourUid);
  // ...
}

Media & File Upload

미디어 업로드는 3단계로 진행됩니다: Ticket 발급 → S3 업로드 → Content 생성/수정.

import { useMediaUploadTickets, useUploadToS3, useCreateContent } from '@ph-cms/client-sdk';

function MediaUploader() {
  const { mutateAsync: getTickets } = useMediaUploadTickets();
  const { mutateAsync: uploadToS3 } = useUploadToS3();
  const { mutateAsync: createContent } = useCreateContent();

  const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    // Step 1: Upload Ticket 발급 (Presigned URL)
    const tickets = await getTickets([{
      filename: file.name,
      contentType: file.type,
      fileSize: file.size,
    }]);

    const { mediaUid, uploadUrl } = tickets[0];

    // Step 2: S3에 파일 직접 업로드
    await uploadToS3({ url: uploadUrl, file });

    // Step 3: mediaUid로 Content 생성
    await createContent({
      type: 'post',
      title: 'Post with image',
      mediaAttachments: [mediaUid],
    });
  };

  return <input type="file" onChange={handleFileChange} />;
}

Error Handling

SDK는 세 종류의 에러를 throw합니다:

| Error Class | 상황 | 주요 필드 | |---|---|---| | ValidationError | Zod 스키마 검증 실패 (클라이언트 측) | errors: ZodIssue[] | | ApiError | 서버가 2xx 이외의 응답을 반환 | statusCode: number, originalError: any | | PHCMSError | 네트워크 오류 등 기타 에러 | message: string |

import { ApiError } from '@ph-cms/client-sdk';

try {
  await login({ email, password });
} catch (error) {
  if (error instanceof ApiError) {
    if (error.statusCode === 401) {
      console.error('잘못된 인증 정보입니다.');
    }
  }
}

API Reference

PHCMSClient

const client = new PHCMSClient({
  baseURL: string;       // 서버 URL (필수)
  apiPrefix?: string;    // API 경로 접두사 (기본값: '/api')
  auth?: AuthProvider;   // 인증 프로바이더
  timeout?: number;      // 요청 타임아웃 ms (기본값: 10000)
});

client.authProvider   // AuthProvider | undefined
client.axiosInstance  // AxiosInstance — 내부 axios 인스턴스 직접 접근
client.auth           // AuthModule
client.user           // UserModule
client.content        // ContentModule
client.channel        // ChannelModule
client.terms          // TermsModule
client.media          // MediaModule

UserModule (client.user)

| 메서드 | 설명 | |---|---| | updateProfile(uid: string, data: UpdateUserProfileRequest) | 유저의 프로필 정보 업데이트 (일반 유저는 UpdateUserProfileRequest 필드만 허용) → UserDto |

AuthModule (client.auth)

| 메서드 | 설명 | |---|---| | login(data: LoginRequest) | 이메일/비밀번호 로그인 → AuthResponse | | loginWithFirebase(data: FirebaseExchangeRequest) | Firebase ID 토큰 교환 → AuthResponse | | register(data: RegisterRequest) | 회원가입 → AuthResponse | | me() | 현재 사용자 프로필 조회 → UserDto | | refresh(refreshToken: string) | 토큰 갱신 → { accessToken, refreshToken } | | logout() | 로그아웃 (프로바이더 토큰 삭제 + 서버 세션 무효화) |

ContentModule (client.content)

| 메서드 | 설명 | |---|---| | list(query: ListContentQuery) | 콘텐츠 목록 조회 → PagedContentListResponse | | get(uid: string) | 단일 콘텐츠 상세 조회 → ContentDto | | create(data: CreateContentRequest) | 일반 콘텐츠 생성 → ContentDto | | update(uid: string, data: UpdateContentRequest) | 콘텐츠 수정 → ContentDto | | delete(uid: string) | 콘텐츠 삭제 | | createStampTour(data: CreateStampTourRequest) | 스탬프 투어 생성ContentDto | | updateStampTour(uid: string, data: UpdateStampTourRequest) | 스탬프 투어 수정ContentDto | | stamp(tourUid: string, markerUid: string, data: CollectStampRequest) | 스탬프 획득 인증 → { isCompletion, distance } | | getStampStatus(tourUid: string) | 내 스탬프 획득 현황 조회 → StampStatusDto | | getTourStats(tourUid: string) | 투어 전체 통계 조회 → TourStatsDto | | toggleLike(uid: string) | 좋아요 토글 → ToggleLikeResponse | | getLikeStatus(uid: string) | 내 좋아요 여부 확인 → LikeStatusResponse |

JWT Utilities

클라이언트에서 토큰 상태를 확인할 수 있는 유틸리티입니다 (서명 검증은 하지 않음).

import {
  decodeJwtPayload,
  getTokenExpirationMs,
  isTokenExpired,
  getTokenTTL,
} from '@ph-cms/client-sdk';

const payload = decodeJwtPayload(token);   // JwtPayload | null
const expiresAt = getTokenExpirationMs(token); // number (ms) | null
const expired = isTokenExpired(token, 60_000); // boolean | null
const ttl = getTokenTTL(token);            // number (ms) | null

License

MIT