@ph-cms/client-sdk 0.1.8 → 0.1.9-beta.1

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
@@ -714,6 +714,90 @@ function ChannelList() {
714
714
 
715
715
  ---
716
716
 
717
+ ## Like (좋아요)
718
+
719
+ 콘텐츠에 좋아요를 남기고, 좋아요 상태를 확인할 수 있습니다.
720
+
721
+ ### 좋아요 토글 (인증 필수)
722
+
723
+ ```ts
724
+ // 모듈 직접 사용
725
+ const result = await client.content.toggleLike('content-uid');
726
+ // => { liked: true, likeCount: 42 }
727
+
728
+ // React Hook 사용 (Optimistic update 포함)
729
+ const { mutate: toggleLike } = useToggleLike();
730
+ toggleLike('content-uid');
731
+ ```
732
+
733
+ ### 좋아요 상태 확인 (인증 선택)
734
+
735
+ ```ts
736
+ // 모듈 직접 사용
737
+ const status = await client.content.getLikeStatus('content-uid');
738
+ // => { liked: true }
739
+
740
+ // React Hook 사용
741
+ const { data: status } = useLikeStatus('content-uid');
742
+ console.log(status?.liked); // true or false
743
+ ```
744
+
745
+ ## Stamp Tour
746
+
747
+ 스탬프 투어 기능을 사용하여 특정 지점(Marker) 방문을 인증하고 진행 현황을 조회할 수 있습니다.
748
+
749
+ ### 스탬프 획득 (인증)
750
+
751
+ 사용자의 현재 GPS 좌표를 전송하여 특정 투어 내의 마커 방문을 인증합니다.
752
+
753
+ ```ts
754
+ // Standalone Usage
755
+ const result = await client.content.stamp(tourUid, markerUid, {
756
+ lat: 37.5511,
757
+ lng: 126.9882
758
+ });
759
+
760
+ console.log(`거리: ${result.distance}m`);
761
+ if (result.isCompletion) {
762
+ alert('축하합니다! 투어를 완주하셨습니다.');
763
+ }
764
+ ```
765
+
766
+ ### 투어 진행 현황 조회
767
+
768
+ 특정 투어에 대해 내가 획득한 스탬프 목록과 완주 여부를 확인합니다.
769
+
770
+ ```ts
771
+ const status = await client.content.getStampStatus(tourUid);
772
+
773
+ console.log(`전체 마커: ${status.total_markers}`);
774
+ console.log(`획득 마커: ${status.collected_markers}`);
775
+ console.log(`완주 여부: ${status.is_completed}`);
776
+
777
+ status.stamps.forEach(s => {
778
+ console.log(`마커 ${s.marker_uid} 획득 일시: ${s.collected_at}`);
779
+ });
780
+ ```
781
+
782
+ ### 투어 전체 통계 (관리자용)
783
+
784
+ 투어의 총 참여자 수, 완주율 등 관리용 데이터를 조회합니다. (적절한 권한 필요)
785
+
786
+ ```ts
787
+ const stats = await client.content.getTourStats(tourUid);
788
+
789
+ console.log(`총 참여자: ${stats.total_participants}`);
790
+ console.log(`총 완주자: ${stats.total_completions}`);
791
+ console.log(`완주율: ${stats.completion_rate * 100}%`);
792
+ console.log(`평균 오차 거리: ${stats.avg_distance_meter}m`);
793
+
794
+ stats.stamps_by_marker.forEach(m => {
795
+ console.log(`${m.marker_title}: ${m.stamp_count}회 방문`);
796
+ });
797
+ ```
798
+
799
+ ---
800
+
717
801
  ## Media & File Upload
718
802
 
719
803
  미디어 업로드는 3단계로 진행됩니다: Ticket 발급 → S3 업로드 → Content 생성/수정.
@@ -27,6 +27,8 @@ export declare const contentKeys: {
27
27
  }];
28
28
  details: () => readonly ["contents", "detail"];
29
29
  detail: (uid: string) => readonly ["contents", "detail", string];
30
+ likes: () => readonly ["contents", "like"];
31
+ like: (uid: string) => readonly ["contents", "like", string];
30
32
  };
31
33
  export declare const useContentList: (params: ListContentQuery, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<{
32
34
  number: number;
@@ -64,6 +66,19 @@ export declare const useCreateContent: () => import("@tanstack/react-query").Use
64
66
  sortOrder?: number | undefined;
65
67
  mediaAttachments?: string[] | undefined;
66
68
  }, unknown>;
69
+ export declare const useLikeStatus: (uid: string, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<{
70
+ liked: boolean;
71
+ }, Error>;
72
+ export declare const useToggleLike: () => import("@tanstack/react-query").UseMutationResult<{
73
+ liked: boolean;
74
+ likeCount: number;
75
+ }, Error, string, {
76
+ previousLikeStatus: {
77
+ liked: boolean;
78
+ } | undefined;
79
+ previousDetail: ContentDto | undefined;
80
+ uid: string;
81
+ }>;
67
82
  export declare const useUpdateContent: () => import("@tanstack/react-query").UseMutationResult<ContentDto, Error, {
68
83
  uid: string;
69
84
  data: UpdateContentRequest;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useDeleteContent = exports.useUpdateContent = exports.useCreateContent = exports.useContentDetail = exports.useContentGeoList = exports.useContentList = exports.contentKeys = void 0;
3
+ exports.useDeleteContent = exports.useUpdateContent = exports.useToggleLike = exports.useLikeStatus = exports.useCreateContent = exports.useContentDetail = exports.useContentGeoList = exports.useContentList = exports.contentKeys = void 0;
4
4
  const react_query_1 = require("@tanstack/react-query");
5
5
  const context_1 = require("../context");
6
6
  exports.contentKeys = {
@@ -11,6 +11,8 @@ exports.contentKeys = {
11
11
  geoList: (bounds) => [...exports.contentKeys.geoLists(), bounds],
12
12
  details: () => [...exports.contentKeys.all, 'detail'],
13
13
  detail: (uid) => [...exports.contentKeys.details(), uid],
14
+ likes: () => [...exports.contentKeys.all, 'like'],
15
+ like: (uid) => [...exports.contentKeys.likes(), uid],
14
16
  };
15
17
  const useContentList = (params, enabled = true) => {
16
18
  const client = (0, context_1.usePHCMS)();
@@ -51,6 +53,79 @@ const useCreateContent = () => {
51
53
  });
52
54
  };
53
55
  exports.useCreateContent = useCreateContent;
56
+ const useLikeStatus = (uid, enabled = true) => {
57
+ const client = (0, context_1.usePHCMS)();
58
+ return (0, react_query_1.useQuery)({
59
+ queryKey: exports.contentKeys.like(uid),
60
+ queryFn: () => client.content.getLikeStatus(uid),
61
+ enabled: !!uid && enabled,
62
+ });
63
+ };
64
+ exports.useLikeStatus = useLikeStatus;
65
+ const useToggleLike = () => {
66
+ const client = (0, context_1.usePHCMS)();
67
+ const queryClient = (0, react_query_1.useQueryClient)();
68
+ return (0, react_query_1.useMutation)({
69
+ mutationFn: (uid) => client.content.toggleLike(uid),
70
+ onMutate: async (uid) => {
71
+ // Cancel any outgoing refetches so they don't overwrite our optimistic update
72
+ await queryClient.cancelQueries({ queryKey: exports.contentKeys.like(uid) });
73
+ await queryClient.cancelQueries({ queryKey: exports.contentKeys.detail(uid) });
74
+ // Snapshot the previous values
75
+ const previousLikeStatus = queryClient.getQueryData(exports.contentKeys.like(uid));
76
+ const previousDetail = queryClient.getQueryData(exports.contentKeys.detail(uid));
77
+ // Optimistically update to the new value
78
+ const isCurrentlyLiked = previousLikeStatus?.liked ?? previousDetail?.liked ?? false;
79
+ const newLikedState = !isCurrentlyLiked;
80
+ // Update like status
81
+ queryClient.setQueryData(exports.contentKeys.like(uid), {
82
+ liked: newLikedState,
83
+ });
84
+ // Update detail if it exists
85
+ if (previousDetail) {
86
+ queryClient.setQueryData(exports.contentKeys.detail(uid), {
87
+ ...previousDetail,
88
+ liked: newLikedState,
89
+ stat: {
90
+ ...previousDetail.stat,
91
+ like_count: previousDetail.stat.like_count + (newLikedState ? 1 : -1),
92
+ },
93
+ });
94
+ }
95
+ // We could also optimistically update items in lists, but for simplicity we'll just invalidate on success/settled
96
+ // Return a context object with the snapshotted values
97
+ return { previousLikeStatus, previousDetail, uid };
98
+ },
99
+ onError: (err, uid, context) => {
100
+ // If the mutation fails, use the context returned from onMutate to roll back
101
+ if (context?.previousLikeStatus !== undefined) {
102
+ queryClient.setQueryData(exports.contentKeys.like(uid), context.previousLikeStatus);
103
+ }
104
+ if (context?.previousDetail !== undefined) {
105
+ queryClient.setQueryData(exports.contentKeys.detail(uid), context.previousDetail);
106
+ }
107
+ },
108
+ onSuccess: (data, uid) => {
109
+ // In case the optimistic update got the count wrong or anything, set exact values from server
110
+ queryClient.setQueryData(exports.contentKeys.like(uid), { liked: data.liked });
111
+ queryClient.setQueryData(exports.contentKeys.detail(uid), (old) => {
112
+ if (!old)
113
+ return old;
114
+ return {
115
+ ...old,
116
+ liked: data.liked,
117
+ stat: { ...old.stat, like_count: data.likeCount },
118
+ };
119
+ });
120
+ },
121
+ onSettled: (data, error, uid) => {
122
+ // Invalidate the lists so they fetch the latest liked status and like counts
123
+ queryClient.invalidateQueries({ queryKey: exports.contentKeys.lists() });
124
+ queryClient.invalidateQueries({ queryKey: exports.contentKeys.geoLists() });
125
+ },
126
+ });
127
+ };
128
+ exports.useToggleLike = useToggleLike;
54
129
  const useUpdateContent = () => {
55
130
  const client = (0, context_1.usePHCMS)();
56
131
  const queryClient = (0, react_query_1.useQueryClient)();
@@ -1,5 +1,5 @@
1
1
  import { AxiosInstance } from "axios";
2
- import { CreateContentRequest, UpdateContentRequest, ListContentQuery, ContentDto, PagedContentListResponse, BoundsQuery } from "@ph-cms/api-contract";
2
+ import { CreateContentRequest, UpdateContentRequest, ListContentQuery, ContentDto, PagedContentListResponse, BoundsQuery, CollectStampRequest, StampStatusDto, TourStatsDto, ToggleLikeResponse, LikeStatusResponse } from "@ph-cms/api-contract";
3
3
  export declare class ContentModule {
4
4
  private client;
5
5
  constructor(client: AxiosInstance);
@@ -9,4 +9,12 @@ export declare class ContentModule {
9
9
  create(data: CreateContentRequest): Promise<ContentDto>;
10
10
  update(uid: string, data: UpdateContentRequest): Promise<ContentDto>;
11
11
  delete(uid: string): Promise<void>;
12
+ stamp(tourUid: string, markerUid: string, data: CollectStampRequest): Promise<{
13
+ isCompletion: boolean;
14
+ distance: number;
15
+ }>;
16
+ getStampStatus(tourUid: string): Promise<StampStatusDto>;
17
+ getTourStats(tourUid: string): Promise<TourStatsDto>;
18
+ toggleLike(uid: string): Promise<ToggleLikeResponse>;
19
+ getLikeStatus(uid: string): Promise<LikeStatusResponse>;
12
20
  }
@@ -47,5 +47,34 @@ class ContentModule {
47
47
  throw new errors_1.ValidationError("UID is required", []);
48
48
  return this.client.delete(`/api/contents/${uid}`);
49
49
  }
50
+ async stamp(tourUid, markerUid, data) {
51
+ if (!tourUid || !markerUid)
52
+ throw new errors_1.ValidationError("Both tourUid and markerUid are required", []);
53
+ const validation = api_contract_1.CollectStampSchema.safeParse(data);
54
+ if (!validation.success) {
55
+ throw new errors_1.ValidationError("Invalid stamp data", validation.error.errors);
56
+ }
57
+ return this.client.post(`/api/contents/${tourUid}/stamp/${markerUid}`, data);
58
+ }
59
+ async getStampStatus(tourUid) {
60
+ if (!tourUid)
61
+ throw new errors_1.ValidationError("tourUid is required", []);
62
+ return this.client.get(`/api/contents/${tourUid}/stamp-status`);
63
+ }
64
+ async getTourStats(tourUid) {
65
+ if (!tourUid)
66
+ throw new errors_1.ValidationError("tourUid is required", []);
67
+ return this.client.get(`/api/contents/${tourUid}/stamp-stats`);
68
+ }
69
+ async toggleLike(uid) {
70
+ if (!uid)
71
+ throw new errors_1.ValidationError("UID is required", []);
72
+ return this.client.post(`/api/contents/${uid}/like`);
73
+ }
74
+ async getLikeStatus(uid) {
75
+ if (!uid)
76
+ throw new errors_1.ValidationError("UID is required", []);
77
+ return this.client.get(`/api/contents/${uid}/like`);
78
+ }
50
79
  }
51
80
  exports.ContentModule = ContentModule;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ph-cms/client-sdk",
3
- "version": "0.1.8",
3
+ "version": "0.1.9-beta.1",
4
4
  "description": "Unified PH-CMS Client SDK (React + Core)",
5
5
  "keywords": [],
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "dependencies": {
24
- "@ph-cms/api-contract": "^0.1.1",
24
+ "@ph-cms/api-contract": "0.1.2-beta.1",
25
25
  "@tanstack/react-query": "^5.0.0",
26
26
  "axios": "^1.6.0",
27
27
  "zod": "^3.22.4"