@ph-cms/client-sdk 0.1.9 → 0.1.10
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 +112 -1
- package/dist/hooks/useContent.d.ts +2 -0
- package/dist/hooks/useStampTour.d.ts +74 -2
- package/dist/hooks/useStampTour.js +164 -2
- package/dist/hooks/useTerms.d.ts +2 -2
- package/dist/modules/content.d.ts +3 -1
- package/dist/modules/content.js +16 -0
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -66,6 +66,8 @@ import type {
|
|
|
66
66
|
ContentMediaDto,
|
|
67
67
|
CreateContentRequest,
|
|
68
68
|
UpdateContentRequest,
|
|
69
|
+
CreateStampTourRequest,
|
|
70
|
+
UpdateStampTourRequest,
|
|
69
71
|
ListContentQuery,
|
|
70
72
|
PagedContentListResponse,
|
|
71
73
|
|
|
@@ -770,7 +772,62 @@ console.log(status?.liked); // true or false
|
|
|
770
772
|
|
|
771
773
|
## Stamp Tour
|
|
772
774
|
|
|
773
|
-
스탬프 투어 기능을 사용하여 특정 지점(Marker) 방문을 인증하고 진행 현황을
|
|
775
|
+
스탬프 투어 기능을 사용하여 특정 지점(Marker) 방문을 인증하고 진행 현황을 조회하거나, 새로운 투어를 생성할 수 있습니다.
|
|
776
|
+
|
|
777
|
+
#### 스탬프 투어 생성 (인증 필수)
|
|
778
|
+
|
|
779
|
+
여러 개의 마커(Marker)를 묶어 새로운 스탬프 투어를 생성합니다.
|
|
780
|
+
|
|
781
|
+
```tsx
|
|
782
|
+
import { useCreateStampTour } from '@ph-cms/client-sdk';
|
|
783
|
+
|
|
784
|
+
function CreateTourForm() {
|
|
785
|
+
const { mutateAsync: createTour, isPending } = useCreateStampTour();
|
|
786
|
+
|
|
787
|
+
const handleCreate = async () => {
|
|
788
|
+
await createTour({
|
|
789
|
+
channelSlug: 'my-channel',
|
|
790
|
+
parentUid: 'folder-uid',
|
|
791
|
+
title: '서울 명소 투어',
|
|
792
|
+
summary: '서울의 주요 명소를 방문하세요.',
|
|
793
|
+
markerUids: ['marker-1-uid', 'marker-2-uid'],
|
|
794
|
+
isActive: true,
|
|
795
|
+
tags: ['서울', '여행'],
|
|
796
|
+
startsAt: '2026-04-01T00:00:00Z',
|
|
797
|
+
endsAt: '2026-06-30T23:59:59Z'
|
|
798
|
+
});
|
|
799
|
+
alert('투어가 생성되었습니다.');
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
return <button onClick={handleCreate} disabled={isPending}>투어 만들기</button>;
|
|
803
|
+
}
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
#### 스탬프 투어 수정 (인증 필수)
|
|
807
|
+
|
|
808
|
+
기존 스탬프 투어의 정보나 마커 목록을 수정합니다. (참여자가 있는 경우 마커 목록 수정은 제한될 수 있습니다.)
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
import { useUpdateStampTour } from '@ph-cms/client-sdk';
|
|
812
|
+
|
|
813
|
+
function UpdateTourForm({ tourUid }) {
|
|
814
|
+
const { mutateAsync: updateTour, isPending } = useUpdateStampTour();
|
|
815
|
+
|
|
816
|
+
const handleUpdate = async () => {
|
|
817
|
+
await updateTour({
|
|
818
|
+
uid: tourUid,
|
|
819
|
+
data: {
|
|
820
|
+
title: '수정된 투어 제목',
|
|
821
|
+
isActive: false,
|
|
822
|
+
markerUids: ['marker-1-uid', 'marker-3-uid']
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
alert('투어가 수정되었습니다.');
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
return <button onClick={handleUpdate} disabled={isPending}>투어 수정</button>;
|
|
829
|
+
}
|
|
830
|
+
```
|
|
774
831
|
|
|
775
832
|
#### 스탬프 획득 (인증)
|
|
776
833
|
|
|
@@ -803,6 +860,43 @@ function StampButton({ tourUid, markerUid }) {
|
|
|
803
860
|
}
|
|
804
861
|
```
|
|
805
862
|
|
|
863
|
+
#### 클라이언트 위치 검증 (Geo)
|
|
864
|
+
|
|
865
|
+
스탬프를 획득하기 전 클라이언트 창에서 사용자의 현재 GPS 좌표와 마커 위치를 비교하여, 반경 내에 들어왔는지 선제적으로 검증할 수 있습니다. `checkStampAvailability` 함수와 지리 정보 훅 `useGeolocation`을 조합하여 다양한 검증 상태(`available`, `distance_far`, `not_stamp_location`, `checking_geo` 등)를 처리합니다.
|
|
866
|
+
|
|
867
|
+
```tsx
|
|
868
|
+
import { useGeolocation, checkStampAvailability } from '@ph-cms/client-sdk';
|
|
869
|
+
|
|
870
|
+
function StampLocationGuard({ marker, stampStatus, isAuthenticated }) {
|
|
871
|
+
// 실시간 GPS 좌표 및 권한 상태 감지
|
|
872
|
+
const { latitude, longitude, geoError } = useGeolocation();
|
|
873
|
+
|
|
874
|
+
// 클라이언트 단에서 마커 접근 가능성 확인
|
|
875
|
+
const availability = checkStampAvailability({
|
|
876
|
+
markerUid: marker.uid,
|
|
877
|
+
markerLocation: marker.location, // { latitude, longitude }
|
|
878
|
+
stampStatus, // 내가 획득한 스탬프 정보 (StampStatusDto)
|
|
879
|
+
isLoggedIn: isAuthenticated, // 권한 상태 확인
|
|
880
|
+
latitude,
|
|
881
|
+
longitude,
|
|
882
|
+
geoError,
|
|
883
|
+
distanceThreshold: 40, // 스탬프 획득 가능 반경 (단위: 미터)
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
return (
|
|
887
|
+
<div>
|
|
888
|
+
<p>상태: {availability.stateMessage}</p>
|
|
889
|
+
<p>힌트: {availability.hintMessage}</p>
|
|
890
|
+
<p>거리: {availability.distance ? `${availability.distance.toFixed(1)}m` : '알 수 없음'}</p>
|
|
891
|
+
|
|
892
|
+
{availability.state === 'available' && (
|
|
893
|
+
<button onClick={handleCollect}>스탬프 획득하기</button>
|
|
894
|
+
)}
|
|
895
|
+
</div>
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
806
900
|
#### 투어 진행 현황 조회
|
|
807
901
|
|
|
808
902
|
특정 투어에 대해 내가 획득한 스탬프 목록과 완주 여부를 확인합니다.
|
|
@@ -946,6 +1040,23 @@ client.media // MediaModule
|
|
|
946
1040
|
| `refresh(refreshToken: string)` | 토큰 갱신 → `{ accessToken, refreshToken }` |
|
|
947
1041
|
| `logout()` | 로그아웃 (프로바이더 토큰 삭제 + 서버 세션 무효화) |
|
|
948
1042
|
|
|
1043
|
+
### `ContentModule` (`client.content`)
|
|
1044
|
+
|
|
1045
|
+
| 메서드 | 설명 |
|
|
1046
|
+
|---|---|
|
|
1047
|
+
| `list(query: ListContentQuery)` | 콘텐츠 목록 조회 → `PagedContentListResponse` |
|
|
1048
|
+
| `get(uid: string)` | 단일 콘텐츠 상세 조회 → `ContentDto` |
|
|
1049
|
+
| `create(data: CreateContentRequest)` | 일반 콘텐츠 생성 → `ContentDto` |
|
|
1050
|
+
| `update(uid: string, data: UpdateContentRequest)` | 콘텐츠 수정 → `ContentDto` |
|
|
1051
|
+
| `delete(uid: string)` | 콘텐츠 삭제 |
|
|
1052
|
+
| `createStampTour(data: CreateStampTourRequest)` | **스탬프 투어 생성** → `ContentDto` |
|
|
1053
|
+
| `updateStampTour(uid: string, data: UpdateStampTourRequest)` | **스탬프 투어 수정** → `ContentDto` |
|
|
1054
|
+
| `stamp(tourUid: string, markerUid: string, data: CollectStampRequest)` | 스탬프 획득 인증 → `{ isCompletion, distance }` |
|
|
1055
|
+
| `getStampStatus(tourUid: string)` | 내 스탬프 획득 현황 조회 → `StampStatusDto` |
|
|
1056
|
+
| `getTourStats(tourUid: string)` | 투어 전체 통계 조회 → `TourStatsDto` |
|
|
1057
|
+
| `toggleLike(uid: string)` | 좋아요 토글 → `ToggleLikeResponse` |
|
|
1058
|
+
| `getLikeStatus(uid: string)` | 내 좋아요 여부 확인 → `LikeStatusResponse` |
|
|
1059
|
+
|
|
949
1060
|
### JWT Utilities
|
|
950
1061
|
|
|
951
1062
|
클라이언트에서 토큰 상태를 확인할 수 있는 유틸리티입니다 (서명 검증은 하지 않음).
|
|
@@ -9,8 +9,10 @@ export declare const contentKeys: {
|
|
|
9
9
|
type?: string | undefined;
|
|
10
10
|
tags?: string[] | undefined;
|
|
11
11
|
channelUid?: string | undefined;
|
|
12
|
+
channelSlug?: string | undefined;
|
|
12
13
|
parentUid?: string | undefined;
|
|
13
14
|
authorUid?: string | undefined;
|
|
15
|
+
uids?: string[] | undefined;
|
|
14
16
|
rootUid?: string | undefined;
|
|
15
17
|
keyword?: string | undefined;
|
|
16
18
|
orderBy?: "created_at" | "updated_at" | "published_at" | "title" | "view_count" | "like_count" | undefined;
|
|
@@ -1,16 +1,42 @@
|
|
|
1
|
-
import { CollectStampRequest } from '@ph-cms/api-contract';
|
|
1
|
+
import { CollectStampRequest, StampStatusDto, UpdateStampTourRequest, ContentDto } from '@ph-cms/api-contract';
|
|
2
2
|
export declare const stampTourKeys: {
|
|
3
3
|
all: readonly ["stamp-tours"];
|
|
4
4
|
status: (tourUid: string) => readonly ["stamp-tours", "status", string];
|
|
5
5
|
stats: (tourUid: string) => readonly ["stamp-tours", "stats", string];
|
|
6
6
|
};
|
|
7
|
+
export declare const useCreateStampTour: () => import("@tanstack/react-query").UseMutationResult<ContentDto, Error, {
|
|
8
|
+
title: string;
|
|
9
|
+
parentUid: string;
|
|
10
|
+
markerUids: string[];
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
status?: string | undefined;
|
|
13
|
+
geometry?: {
|
|
14
|
+
type: "Point" | "LineString" | "Polygon" | "MultiPoint" | "MultiLineString" | "MultiPolygon" | "GeometryCollection" | "Feature" | "FeatureCollection";
|
|
15
|
+
coordinates?: any[] | undefined;
|
|
16
|
+
geometries?: any[] | undefined;
|
|
17
|
+
features?: any[] | undefined;
|
|
18
|
+
properties?: Record<string, any> | undefined;
|
|
19
|
+
geometry?: any;
|
|
20
|
+
} | undefined;
|
|
21
|
+
image?: string | null | undefined;
|
|
22
|
+
summary?: string | null | undefined;
|
|
23
|
+
tags?: string[] | undefined;
|
|
24
|
+
channelUid?: string | undefined;
|
|
25
|
+
channelSlug?: string | undefined;
|
|
26
|
+
startsAt?: string | null | undefined;
|
|
27
|
+
endsAt?: string | null | undefined;
|
|
28
|
+
}, unknown>;
|
|
29
|
+
export declare const useUpdateStampTour: () => import("@tanstack/react-query").UseMutationResult<ContentDto, Error, {
|
|
30
|
+
uid: string;
|
|
31
|
+
data: UpdateStampTourRequest;
|
|
32
|
+
}, unknown>;
|
|
7
33
|
export declare const useStamp: () => import("@tanstack/react-query").UseMutationResult<{
|
|
8
34
|
isCompletion: boolean;
|
|
9
35
|
distance: number;
|
|
10
36
|
}, Error, {
|
|
11
37
|
tourUid: string;
|
|
12
38
|
markerUid: string;
|
|
13
|
-
data
|
|
39
|
+
data?: CollectStampRequest;
|
|
14
40
|
}, unknown>;
|
|
15
41
|
export declare const useStampStatus: (tourUid: string, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<{
|
|
16
42
|
total_markers: number;
|
|
@@ -35,3 +61,49 @@ export declare const useTourStats: (tourUid: string, enabled?: boolean) => impor
|
|
|
35
61
|
stamp_count: number;
|
|
36
62
|
}[];
|
|
37
63
|
}, Error>;
|
|
64
|
+
export interface StampAvailability {
|
|
65
|
+
currentLocation?: {
|
|
66
|
+
latitude: number;
|
|
67
|
+
longitude: number;
|
|
68
|
+
};
|
|
69
|
+
stampLocation?: {
|
|
70
|
+
latitude: number;
|
|
71
|
+
longitude: number;
|
|
72
|
+
};
|
|
73
|
+
state: "available" | "not_logged_in" | "invalid_location" | "distance_far" | "checking_geo" | "geo_error" | "already_acquired" | "not_stamp_location";
|
|
74
|
+
stateMessage: string;
|
|
75
|
+
hintMessage: string;
|
|
76
|
+
distance: number | null;
|
|
77
|
+
}
|
|
78
|
+
export declare const calculateDistance: (lat1: number, lon1: number, lat2: number, lon2: number) => number;
|
|
79
|
+
export declare const getStampAvailability: ({ state, currentLocation, stampLocation, distance, }: {
|
|
80
|
+
state: StampAvailability["state"];
|
|
81
|
+
currentLocation?: {
|
|
82
|
+
latitude: number;
|
|
83
|
+
longitude: number;
|
|
84
|
+
};
|
|
85
|
+
stampLocation?: {
|
|
86
|
+
latitude: number;
|
|
87
|
+
longitude: number;
|
|
88
|
+
};
|
|
89
|
+
distance?: number;
|
|
90
|
+
}) => StampAvailability;
|
|
91
|
+
export interface CheckStampAvailabilityParams {
|
|
92
|
+
markerUid: string;
|
|
93
|
+
markerLocation?: {
|
|
94
|
+
latitude?: number | null;
|
|
95
|
+
longitude?: number | null;
|
|
96
|
+
} | null;
|
|
97
|
+
stampStatus?: StampStatusDto | null;
|
|
98
|
+
isLoggedIn: boolean;
|
|
99
|
+
latitude: number | null;
|
|
100
|
+
longitude: number | null;
|
|
101
|
+
geoError: any;
|
|
102
|
+
distanceThreshold?: number;
|
|
103
|
+
}
|
|
104
|
+
export declare const checkStampAvailability: ({ markerUid, markerLocation, stampStatus, isLoggedIn, latitude, longitude, geoError, distanceThreshold, }: CheckStampAvailabilityParams) => StampAvailability;
|
|
105
|
+
export declare const useGeolocation: (enabled?: boolean) => {
|
|
106
|
+
latitude: number | null;
|
|
107
|
+
longitude: number | null;
|
|
108
|
+
geoError: string | null;
|
|
109
|
+
};
|
|
@@ -1,18 +1,65 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useTourStats = exports.useStampStatus = exports.useStamp = exports.stampTourKeys = void 0;
|
|
3
|
+
exports.useGeolocation = exports.checkStampAvailability = exports.getStampAvailability = exports.calculateDistance = exports.useTourStats = exports.useStampStatus = exports.useStamp = exports.useUpdateStampTour = exports.useCreateStampTour = exports.stampTourKeys = void 0;
|
|
4
4
|
const react_query_1 = require("@tanstack/react-query");
|
|
5
5
|
const context_1 = require("../context");
|
|
6
|
+
const useContent_1 = require("./useContent");
|
|
7
|
+
const react_1 = require("react");
|
|
6
8
|
exports.stampTourKeys = {
|
|
7
9
|
all: ['stamp-tours'],
|
|
8
10
|
status: (tourUid) => [...exports.stampTourKeys.all, 'status', tourUid],
|
|
9
11
|
stats: (tourUid) => [...exports.stampTourKeys.all, 'stats', tourUid],
|
|
10
12
|
};
|
|
13
|
+
const useCreateStampTour = () => {
|
|
14
|
+
const client = (0, context_1.usePHCMS)();
|
|
15
|
+
const queryClient = (0, react_query_1.useQueryClient)();
|
|
16
|
+
return (0, react_query_1.useMutation)({
|
|
17
|
+
mutationFn: (data) => client.content.createStampTour(data),
|
|
18
|
+
onSuccess: () => {
|
|
19
|
+
// Invalidate content lists since a new tour is a content item
|
|
20
|
+
queryClient.invalidateQueries({ queryKey: useContent_1.contentKeys.lists() });
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
exports.useCreateStampTour = useCreateStampTour;
|
|
25
|
+
const useUpdateStampTour = () => {
|
|
26
|
+
const client = (0, context_1.usePHCMS)();
|
|
27
|
+
const queryClient = (0, react_query_1.useQueryClient)();
|
|
28
|
+
return (0, react_query_1.useMutation)({
|
|
29
|
+
mutationFn: ({ uid, data }) => client.content.updateStampTour(uid, data),
|
|
30
|
+
onSuccess: (data, variables) => {
|
|
31
|
+
queryClient.invalidateQueries({ queryKey: useContent_1.contentKeys.detail(variables.uid) });
|
|
32
|
+
queryClient.invalidateQueries({ queryKey: useContent_1.contentKeys.lists() });
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
exports.useUpdateStampTour = useUpdateStampTour;
|
|
11
37
|
const useStamp = () => {
|
|
12
38
|
const client = (0, context_1.usePHCMS)();
|
|
13
39
|
const queryClient = (0, react_query_1.useQueryClient)();
|
|
14
40
|
return (0, react_query_1.useMutation)({
|
|
15
|
-
mutationFn: ({ tourUid, markerUid, data }) =>
|
|
41
|
+
mutationFn: async ({ tourUid, markerUid, data }) => {
|
|
42
|
+
let requestData = data;
|
|
43
|
+
if (!requestData) {
|
|
44
|
+
try {
|
|
45
|
+
const position = await new Promise((resolve, reject) => {
|
|
46
|
+
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
47
|
+
enableHighAccuracy: true,
|
|
48
|
+
timeout: 5000,
|
|
49
|
+
maximumAge: 0
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
requestData = {
|
|
53
|
+
lat: position.coords.latitude,
|
|
54
|
+
lng: position.coords.longitude,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error('위치 정보를 가져올 수 없습니다. 위치 권한을 확인해주세요.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return client.content.stamp(tourUid, markerUid, requestData);
|
|
62
|
+
},
|
|
16
63
|
onSuccess: (data, variables) => {
|
|
17
64
|
queryClient.invalidateQueries({ queryKey: exports.stampTourKeys.status(variables.tourUid) });
|
|
18
65
|
},
|
|
@@ -37,3 +84,118 @@ const useTourStats = (tourUid, enabled = true) => {
|
|
|
37
84
|
});
|
|
38
85
|
};
|
|
39
86
|
exports.useTourStats = useTourStats;
|
|
87
|
+
const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
88
|
+
const R = 6371000;
|
|
89
|
+
const dLat = ((lat2 - lat1) * Math.PI) / 180;
|
|
90
|
+
const dLon = ((lon2 - lon1) * Math.PI) / 180;
|
|
91
|
+
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
92
|
+
Math.cos((lat1 * Math.PI) / 180) *
|
|
93
|
+
Math.cos((lat2 * Math.PI) / 180) *
|
|
94
|
+
Math.sin(dLon / 2) *
|
|
95
|
+
Math.sin(dLon / 2);
|
|
96
|
+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
97
|
+
return R * c;
|
|
98
|
+
};
|
|
99
|
+
exports.calculateDistance = calculateDistance;
|
|
100
|
+
const getStampAvailability = ({ state, currentLocation, stampLocation, distance, }) => {
|
|
101
|
+
const stateMessages = {
|
|
102
|
+
available: "스탬프 획득 가능",
|
|
103
|
+
not_logged_in: "스탬프 획득 불가 (로그인 필요)",
|
|
104
|
+
invalid_location: "장소 정보를 찾을 수 없습니다.",
|
|
105
|
+
distance_far: `스탬프 획득 불가 (${distance?.toFixed(1) || 0}m)`,
|
|
106
|
+
checking_geo: "현재 위치를 확인하고 있습니다.",
|
|
107
|
+
geo_error: "스탬프 획득 불가 (위치 권한 필요)",
|
|
108
|
+
already_acquired: "스탬프 획득 완료",
|
|
109
|
+
not_stamp_location: "스탬프 획득 불가 (스탬프 위치 아님)",
|
|
110
|
+
};
|
|
111
|
+
const hintMessages = {
|
|
112
|
+
available: "이 버튼을 눌러 스탬프를 획득해주세요.",
|
|
113
|
+
not_logged_in: "로그인 후 스탬프를 획득할 수 있습니다.",
|
|
114
|
+
invalid_location: "유효한 장소에서 시도해주세요.",
|
|
115
|
+
distance_far: "이 버튼을 눌러 위치를 업데이트 하세요.",
|
|
116
|
+
checking_geo: "잠시만 기다려주세요.",
|
|
117
|
+
geo_error: "위치 권한을 허용해주세요.",
|
|
118
|
+
already_acquired: "획득한 스탬프 목록을 확인해보세요.",
|
|
119
|
+
not_stamp_location: "스탬프 위치에서 시도해주세요.",
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
currentLocation,
|
|
123
|
+
stampLocation,
|
|
124
|
+
state,
|
|
125
|
+
stateMessage: stateMessages[state],
|
|
126
|
+
hintMessage: hintMessages[state],
|
|
127
|
+
distance: distance ?? null,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
exports.getStampAvailability = getStampAvailability;
|
|
131
|
+
const checkStampAvailability = ({ markerUid, markerLocation, stampStatus, isLoggedIn, latitude, longitude, geoError, distanceThreshold = 40, }) => {
|
|
132
|
+
if (!markerLocation) {
|
|
133
|
+
return (0, exports.getStampAvailability)({ state: "not_stamp_location" });
|
|
134
|
+
}
|
|
135
|
+
if (!markerLocation.latitude || !markerLocation.longitude) {
|
|
136
|
+
return (0, exports.getStampAvailability)({ state: "invalid_location" });
|
|
137
|
+
}
|
|
138
|
+
if (geoError) {
|
|
139
|
+
return (0, exports.getStampAvailability)({ state: "geo_error" });
|
|
140
|
+
}
|
|
141
|
+
if (latitude === null || longitude === null) {
|
|
142
|
+
return (0, exports.getStampAvailability)({ state: "checking_geo" });
|
|
143
|
+
}
|
|
144
|
+
if (!isLoggedIn) {
|
|
145
|
+
return (0, exports.getStampAvailability)({ state: "not_logged_in" });
|
|
146
|
+
}
|
|
147
|
+
const acquiredUids = stampStatus?.stamps?.map((s) => s.marker_uid) || [];
|
|
148
|
+
if (acquiredUids.includes(markerUid)) {
|
|
149
|
+
return (0, exports.getStampAvailability)({ state: "already_acquired" });
|
|
150
|
+
}
|
|
151
|
+
const currentLocation = { latitude, longitude };
|
|
152
|
+
const stampLocationCoords = {
|
|
153
|
+
latitude: markerLocation.latitude,
|
|
154
|
+
longitude: markerLocation.longitude,
|
|
155
|
+
};
|
|
156
|
+
const distance = (0, exports.calculateDistance)(latitude, longitude, markerLocation.latitude, markerLocation.longitude);
|
|
157
|
+
if (distance > distanceThreshold) {
|
|
158
|
+
return (0, exports.getStampAvailability)({
|
|
159
|
+
state: "distance_far",
|
|
160
|
+
currentLocation,
|
|
161
|
+
stampLocation: stampLocationCoords,
|
|
162
|
+
distance,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return (0, exports.getStampAvailability)({
|
|
166
|
+
state: "available",
|
|
167
|
+
currentLocation,
|
|
168
|
+
stampLocation: stampLocationCoords,
|
|
169
|
+
distance,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
exports.checkStampAvailability = checkStampAvailability;
|
|
173
|
+
const useGeolocation = (enabled = true) => {
|
|
174
|
+
const [latitude, setLatitude] = (0, react_1.useState)(null);
|
|
175
|
+
const [longitude, setLongitude] = (0, react_1.useState)(null);
|
|
176
|
+
const [geoError, setGeoError] = (0, react_1.useState)(null);
|
|
177
|
+
(0, react_1.useEffect)(() => {
|
|
178
|
+
if (!enabled || typeof navigator === 'undefined' || !navigator.geolocation) {
|
|
179
|
+
if (enabled && typeof navigator !== 'undefined') {
|
|
180
|
+
setGeoError('Geolocation is not supported by your browser');
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const watchId = navigator.geolocation.watchPosition((position) => {
|
|
185
|
+
setLatitude(position.coords.latitude);
|
|
186
|
+
setLongitude(position.coords.longitude);
|
|
187
|
+
setGeoError(null);
|
|
188
|
+
}, (error) => {
|
|
189
|
+
setGeoError(error.message);
|
|
190
|
+
}, {
|
|
191
|
+
enableHighAccuracy: true,
|
|
192
|
+
maximumAge: 10000,
|
|
193
|
+
timeout: 5000,
|
|
194
|
+
});
|
|
195
|
+
return () => {
|
|
196
|
+
navigator.geolocation.clearWatch(watchId);
|
|
197
|
+
};
|
|
198
|
+
}, [enabled]);
|
|
199
|
+
return { latitude, longitude, geoError };
|
|
200
|
+
};
|
|
201
|
+
exports.useGeolocation = useGeolocation;
|
package/dist/hooks/useTerms.d.ts
CHANGED
|
@@ -5,8 +5,8 @@ export declare const termsKeys: {
|
|
|
5
5
|
list: (params?: ListTermsQuery) => readonly ["terms", "list", {
|
|
6
6
|
code?: string | undefined;
|
|
7
7
|
limit?: number | undefined;
|
|
8
|
-
offset?: number | undefined;
|
|
9
8
|
isActive?: boolean | undefined;
|
|
9
|
+
offset?: number | undefined;
|
|
10
10
|
} | undefined];
|
|
11
11
|
};
|
|
12
12
|
export declare const useTermsList: (params?: ListTermsQuery) => import("@tanstack/react-query").UseQueryResult<{
|
|
@@ -14,9 +14,9 @@ export declare const useTermsList: (params?: ListTermsQuery) => import("@tanstac
|
|
|
14
14
|
code: string;
|
|
15
15
|
title: string;
|
|
16
16
|
content: string;
|
|
17
|
+
isActive: boolean;
|
|
17
18
|
id: number;
|
|
18
19
|
version: string;
|
|
19
|
-
isActive: boolean;
|
|
20
20
|
createdAt: string;
|
|
21
21
|
}[];
|
|
22
22
|
total: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { BoundsQuery, CollectStampRequest, ContentDto, CreateContentRequest, CreateStampTourRequest, LikeStatusResponse, ListContentQuery, PagedContentListResponse, StampStatusDto, ToggleLikeResponse, TourStatsDto, UpdateContentRequest, UpdateStampTourRequest } from "@ph-cms/api-contract";
|
|
1
2
|
import { AxiosInstance } from "axios";
|
|
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,6 +9,8 @@ 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
|
+
createStampTour(data: CreateStampTourRequest): Promise<ContentDto>;
|
|
13
|
+
updateStampTour(uid: string, data: UpdateStampTourRequest): Promise<ContentDto>;
|
|
12
14
|
stamp(tourUid: string, markerUid: string, data: CollectStampRequest): Promise<{
|
|
13
15
|
isCompletion: boolean;
|
|
14
16
|
distance: number;
|
package/dist/modules/content.js
CHANGED
|
@@ -47,6 +47,22 @@ 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 createStampTour(data) {
|
|
51
|
+
const validation = api_contract_1.CreateStampTourSchema.safeParse(data);
|
|
52
|
+
if (!validation.success) {
|
|
53
|
+
throw new errors_1.ValidationError("Invalid create stamp tour data", validation.error.errors);
|
|
54
|
+
}
|
|
55
|
+
return this.client.post('/api/contents/stamp-tours', data);
|
|
56
|
+
}
|
|
57
|
+
async updateStampTour(uid, data) {
|
|
58
|
+
if (!uid)
|
|
59
|
+
throw new errors_1.ValidationError("UID is required", []);
|
|
60
|
+
const validation = api_contract_1.UpdateStampTourSchema.safeParse(data);
|
|
61
|
+
if (!validation.success) {
|
|
62
|
+
throw new errors_1.ValidationError("Invalid update stamp tour data", validation.error.errors);
|
|
63
|
+
}
|
|
64
|
+
return this.client.put(`/api/contents/stamp-tours/${uid}`, data);
|
|
65
|
+
}
|
|
50
66
|
async stamp(tourUid, markerUid, data) {
|
|
51
67
|
if (!tourUid || !markerUid)
|
|
52
68
|
throw new errors_1.ValidationError("Both tourUid and markerUid are required", []);
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { JwtPayload } from './auth/jwt-utils';
|
|
|
3
3
|
export type { PHCMSClientConfig } from './client';
|
|
4
4
|
export type { AuthStatus, PHCMSContextType, PHCMSProviderProps } from './context';
|
|
5
5
|
export type { FirebaseAuthSyncProps, UseFirebaseAuthSyncOptions, UseFirebaseAuthSyncReturn } from './hooks/useFirebaseAuthSync';
|
|
6
|
+
export type { StampAvailability, CheckStampAvailabilityParams } from './hooks/useStampTour';
|
|
6
7
|
export type { AuthResponse, FirebaseExchangeRequest, LoginRequest, RefreshTokenRequest, RegisterRequest } from '@ph-cms/api-contract';
|
|
7
8
|
export type { UserDto } from '@ph-cms/api-contract';
|
|
8
9
|
export type { ChannelDto, CheckHierarchyQuery, CreateChannelDto, ListChannelQuery, PagedChannelListResponse } from '@ph-cms/api-contract';
|
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.10",
|
|
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": "
|
|
24
|
+
"@ph-cms/api-contract": "file:../api-contract",
|
|
25
25
|
"@tanstack/react-query": "^5.0.0",
|
|
26
26
|
"axios": "^1.6.0",
|
|
27
27
|
"zod": "^3.22.4"
|