@sayren/store-sdk 0.1.1 → 0.3.0
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/dist/index.d.mts +403 -3
- package/dist/index.mjs +175 -2
- package/package.json +1 -1
- package/src/client.ts +44 -0
- package/src/index.ts +1 -0
- package/src/schemas/analytics.ts +200 -0
- package/src/schemas/scope.ts +4 -1
- package/src/schemas/store.ts +53 -0
package/dist/index.d.mts
CHANGED
|
@@ -16,6 +16,270 @@ type TokenSource = string | null | undefined | (() => string | null | undefined)
|
|
|
16
16
|
type Query = Record<string, string | number | boolean | undefined>;
|
|
17
17
|
export declare function buildQuery(query?: Query): string;
|
|
18
18
|
//#endregion
|
|
19
|
+
//#region src/schemas/analytics.d.ts
|
|
20
|
+
export declare const analyticsRangeQuerySchema: z.ZodObject<{
|
|
21
|
+
from: z.ZodString;
|
|
22
|
+
to: z.ZodString;
|
|
23
|
+
includeTest: z.ZodOptional<z.ZodEnum<{
|
|
24
|
+
true: "true";
|
|
25
|
+
false: "false";
|
|
26
|
+
}>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type AnalyticsRangeParams = z.infer<typeof analyticsRangeQuerySchema>;
|
|
29
|
+
export declare const analyticsTotalsSchema: z.ZodObject<{
|
|
30
|
+
visitors: z.ZodNumber;
|
|
31
|
+
sessions: z.ZodNumber;
|
|
32
|
+
engagedSessions: z.ZodNumber;
|
|
33
|
+
pageViews: z.ZodNumber;
|
|
34
|
+
engagementSeconds: z.ZodNumber;
|
|
35
|
+
productViews: z.ZodNumber;
|
|
36
|
+
productViewSessions: z.ZodNumber;
|
|
37
|
+
addToCartSessions: z.ZodNumber;
|
|
38
|
+
beginCheckoutSessions: z.ZodNumber;
|
|
39
|
+
purchaseSessions: z.ZodNumber;
|
|
40
|
+
purchases: z.ZodNumber;
|
|
41
|
+
revenue: z.ZodNumber;
|
|
42
|
+
engagementRate: z.ZodNullable<z.ZodNumber>;
|
|
43
|
+
conversionRate: z.ZodNullable<z.ZodNumber>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
export type AnalyticsTotals = z.infer<typeof analyticsTotalsSchema>;
|
|
46
|
+
export declare const analyticsFunnelStepSchema: z.ZodObject<{
|
|
47
|
+
step: z.ZodEnum<{
|
|
48
|
+
SESSION: "SESSION";
|
|
49
|
+
PRODUCT_VIEW: "PRODUCT_VIEW";
|
|
50
|
+
ADD_TO_CART: "ADD_TO_CART";
|
|
51
|
+
BEGIN_CHECKOUT: "BEGIN_CHECKOUT";
|
|
52
|
+
PURCHASE: "PURCHASE";
|
|
53
|
+
}>;
|
|
54
|
+
sessions: z.ZodNumber;
|
|
55
|
+
rateFromPrevious: z.ZodNullable<z.ZodNumber>;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export type AnalyticsFunnelStep = z.infer<typeof analyticsFunnelStepSchema>;
|
|
58
|
+
export declare const analyticsOverviewSchema: z.ZodObject<{
|
|
59
|
+
summary: z.ZodObject<{
|
|
60
|
+
visitors: z.ZodNumber;
|
|
61
|
+
sessions: z.ZodNumber;
|
|
62
|
+
engagedSessions: z.ZodNumber;
|
|
63
|
+
pageViews: z.ZodNumber;
|
|
64
|
+
engagementSeconds: z.ZodNumber;
|
|
65
|
+
productViews: z.ZodNumber;
|
|
66
|
+
productViewSessions: z.ZodNumber;
|
|
67
|
+
addToCartSessions: z.ZodNumber;
|
|
68
|
+
beginCheckoutSessions: z.ZodNumber;
|
|
69
|
+
purchaseSessions: z.ZodNumber;
|
|
70
|
+
purchases: z.ZodNumber;
|
|
71
|
+
revenue: z.ZodNumber;
|
|
72
|
+
engagementRate: z.ZodNullable<z.ZodNumber>;
|
|
73
|
+
conversionRate: z.ZodNullable<z.ZodNumber>;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
funnel: z.ZodArray<z.ZodObject<{
|
|
76
|
+
step: z.ZodEnum<{
|
|
77
|
+
SESSION: "SESSION";
|
|
78
|
+
PRODUCT_VIEW: "PRODUCT_VIEW";
|
|
79
|
+
ADD_TO_CART: "ADD_TO_CART";
|
|
80
|
+
BEGIN_CHECKOUT: "BEGIN_CHECKOUT";
|
|
81
|
+
PURCHASE: "PURCHASE";
|
|
82
|
+
}>;
|
|
83
|
+
sessions: z.ZodNumber;
|
|
84
|
+
rateFromPrevious: z.ZodNullable<z.ZodNumber>;
|
|
85
|
+
}, z.core.$strip>>;
|
|
86
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
87
|
+
visitors: z.ZodNumber;
|
|
88
|
+
sessions: z.ZodNumber;
|
|
89
|
+
engagedSessions: z.ZodNumber;
|
|
90
|
+
pageViews: z.ZodNumber;
|
|
91
|
+
engagementSeconds: z.ZodNumber;
|
|
92
|
+
productViews: z.ZodNumber;
|
|
93
|
+
productViewSessions: z.ZodNumber;
|
|
94
|
+
addToCartSessions: z.ZodNumber;
|
|
95
|
+
beginCheckoutSessions: z.ZodNumber;
|
|
96
|
+
purchaseSessions: z.ZodNumber;
|
|
97
|
+
purchases: z.ZodNumber;
|
|
98
|
+
revenue: z.ZodNumber;
|
|
99
|
+
engagementRate: z.ZodNullable<z.ZodNumber>;
|
|
100
|
+
conversionRate: z.ZodNullable<z.ZodNumber>;
|
|
101
|
+
date: z.ZodString;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
from: z.ZodString;
|
|
104
|
+
to: z.ZodString;
|
|
105
|
+
timezone: z.ZodLiteral<"Asia/Seoul">;
|
|
106
|
+
currency: z.ZodLiteral<"KRW">;
|
|
107
|
+
includeTest: z.ZodBoolean;
|
|
108
|
+
lastComputedAt: z.ZodNullable<z.ZodString>;
|
|
109
|
+
}, z.core.$strip>;
|
|
110
|
+
export type AnalyticsOverview = z.infer<typeof analyticsOverviewSchema>;
|
|
111
|
+
export declare const analyticsProductSortSchema: z.ZodEnum<{
|
|
112
|
+
VIEWS: "VIEWS";
|
|
113
|
+
ADD_TO_CARTS: "ADD_TO_CARTS";
|
|
114
|
+
PURCHASES: "PURCHASES";
|
|
115
|
+
REVENUE: "REVENUE";
|
|
116
|
+
IMPRESSIONS: "IMPRESSIONS";
|
|
117
|
+
}>;
|
|
118
|
+
export type AnalyticsProductSort = z.infer<typeof analyticsProductSortSchema>;
|
|
119
|
+
export declare const analyticsProductsQuerySchema: z.ZodObject<{
|
|
120
|
+
from: z.ZodString;
|
|
121
|
+
to: z.ZodString;
|
|
122
|
+
includeTest: z.ZodOptional<z.ZodEnum<{
|
|
123
|
+
true: "true";
|
|
124
|
+
false: "false";
|
|
125
|
+
}>>;
|
|
126
|
+
sort: z.ZodOptional<z.ZodEnum<{
|
|
127
|
+
VIEWS: "VIEWS";
|
|
128
|
+
ADD_TO_CARTS: "ADD_TO_CARTS";
|
|
129
|
+
PURCHASES: "PURCHASES";
|
|
130
|
+
REVENUE: "REVENUE";
|
|
131
|
+
IMPRESSIONS: "IMPRESSIONS";
|
|
132
|
+
}>>;
|
|
133
|
+
limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
|
|
134
|
+
}, z.core.$strip>;
|
|
135
|
+
export type AnalyticsProductsParams = z.infer<typeof analyticsProductsQuerySchema>;
|
|
136
|
+
export declare const analyticsProductRowSchema: z.ZodObject<{
|
|
137
|
+
productId: z.ZodString;
|
|
138
|
+
productName: z.ZodNullable<z.ZodString>;
|
|
139
|
+
impressions: z.ZodNumber;
|
|
140
|
+
views: z.ZodNumber;
|
|
141
|
+
viewSessions: z.ZodNumber;
|
|
142
|
+
addToCarts: z.ZodNumber;
|
|
143
|
+
purchases: z.ZodNumber;
|
|
144
|
+
quantity: z.ZodNumber;
|
|
145
|
+
revenue: z.ZodNumber;
|
|
146
|
+
purchaseRate: z.ZodNullable<z.ZodNumber>;
|
|
147
|
+
}, z.core.$strip>;
|
|
148
|
+
export type AnalyticsProductRow = z.infer<typeof analyticsProductRowSchema>;
|
|
149
|
+
export declare const analyticsProductsSchema: z.ZodObject<{
|
|
150
|
+
sort: z.ZodEnum<{
|
|
151
|
+
VIEWS: "VIEWS";
|
|
152
|
+
ADD_TO_CARTS: "ADD_TO_CARTS";
|
|
153
|
+
PURCHASES: "PURCHASES";
|
|
154
|
+
REVENUE: "REVENUE";
|
|
155
|
+
IMPRESSIONS: "IMPRESSIONS";
|
|
156
|
+
}>;
|
|
157
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
158
|
+
productId: z.ZodString;
|
|
159
|
+
productName: z.ZodNullable<z.ZodString>;
|
|
160
|
+
impressions: z.ZodNumber;
|
|
161
|
+
views: z.ZodNumber;
|
|
162
|
+
viewSessions: z.ZodNumber;
|
|
163
|
+
addToCarts: z.ZodNumber;
|
|
164
|
+
purchases: z.ZodNumber;
|
|
165
|
+
quantity: z.ZodNumber;
|
|
166
|
+
revenue: z.ZodNumber;
|
|
167
|
+
purchaseRate: z.ZodNullable<z.ZodNumber>;
|
|
168
|
+
}, z.core.$strip>>;
|
|
169
|
+
from: z.ZodString;
|
|
170
|
+
to: z.ZodString;
|
|
171
|
+
timezone: z.ZodLiteral<"Asia/Seoul">;
|
|
172
|
+
currency: z.ZodLiteral<"KRW">;
|
|
173
|
+
includeTest: z.ZodBoolean;
|
|
174
|
+
lastComputedAt: z.ZodNullable<z.ZodString>;
|
|
175
|
+
}, z.core.$strip>;
|
|
176
|
+
export type AnalyticsProducts = z.infer<typeof analyticsProductsSchema>;
|
|
177
|
+
export declare const analyticsChannelSchema: z.ZodEnum<{
|
|
178
|
+
PAID: "PAID";
|
|
179
|
+
DIRECT: "DIRECT";
|
|
180
|
+
CAMPAIGN: "CAMPAIGN";
|
|
181
|
+
ORGANIC_SEARCH: "ORGANIC_SEARCH";
|
|
182
|
+
SOCIAL: "SOCIAL";
|
|
183
|
+
EMAIL: "EMAIL";
|
|
184
|
+
REFERRAL: "REFERRAL";
|
|
185
|
+
UNKNOWN: "UNKNOWN";
|
|
186
|
+
}>;
|
|
187
|
+
export type AnalyticsChannel = z.infer<typeof analyticsChannelSchema>;
|
|
188
|
+
export declare const analyticsSourceGroupBySchema: z.ZodEnum<{
|
|
189
|
+
CHANNEL: "CHANNEL";
|
|
190
|
+
SOURCE: "SOURCE";
|
|
191
|
+
CAMPAIGN: "CAMPAIGN";
|
|
192
|
+
DEVICE: "DEVICE";
|
|
193
|
+
}>;
|
|
194
|
+
export type AnalyticsSourceGroupBy = z.infer<typeof analyticsSourceGroupBySchema>;
|
|
195
|
+
export declare const analyticsSourcesQuerySchema: z.ZodObject<{
|
|
196
|
+
from: z.ZodString;
|
|
197
|
+
to: z.ZodString;
|
|
198
|
+
includeTest: z.ZodOptional<z.ZodEnum<{
|
|
199
|
+
true: "true";
|
|
200
|
+
false: "false";
|
|
201
|
+
}>>;
|
|
202
|
+
groupBy: z.ZodOptional<z.ZodEnum<{
|
|
203
|
+
CHANNEL: "CHANNEL";
|
|
204
|
+
SOURCE: "SOURCE";
|
|
205
|
+
CAMPAIGN: "CAMPAIGN";
|
|
206
|
+
DEVICE: "DEVICE";
|
|
207
|
+
}>>;
|
|
208
|
+
}, z.core.$strip>;
|
|
209
|
+
export type AnalyticsSourcesParams = z.infer<typeof analyticsSourcesQuerySchema>;
|
|
210
|
+
export declare const analyticsSourceRowSchema: z.ZodObject<{
|
|
211
|
+
channel: z.ZodNullable<z.ZodEnum<{
|
|
212
|
+
PAID: "PAID";
|
|
213
|
+
DIRECT: "DIRECT";
|
|
214
|
+
CAMPAIGN: "CAMPAIGN";
|
|
215
|
+
ORGANIC_SEARCH: "ORGANIC_SEARCH";
|
|
216
|
+
SOCIAL: "SOCIAL";
|
|
217
|
+
EMAIL: "EMAIL";
|
|
218
|
+
REFERRAL: "REFERRAL";
|
|
219
|
+
UNKNOWN: "UNKNOWN";
|
|
220
|
+
}>>;
|
|
221
|
+
source: z.ZodNullable<z.ZodString>;
|
|
222
|
+
medium: z.ZodNullable<z.ZodString>;
|
|
223
|
+
campaign: z.ZodNullable<z.ZodString>;
|
|
224
|
+
device: z.ZodNullable<z.ZodEnum<{
|
|
225
|
+
unknown: "unknown";
|
|
226
|
+
desktop: "desktop";
|
|
227
|
+
mobile: "mobile";
|
|
228
|
+
tablet: "tablet";
|
|
229
|
+
}>>;
|
|
230
|
+
sessions: z.ZodNumber;
|
|
231
|
+
visitors: z.ZodNumber;
|
|
232
|
+
engagedSessions: z.ZodNumber;
|
|
233
|
+
purchaseSessions: z.ZodNumber;
|
|
234
|
+
purchases: z.ZodNumber;
|
|
235
|
+
revenue: z.ZodNumber;
|
|
236
|
+
conversionRate: z.ZodNullable<z.ZodNumber>;
|
|
237
|
+
}, z.core.$strip>;
|
|
238
|
+
export type AnalyticsSourceRow = z.infer<typeof analyticsSourceRowSchema>;
|
|
239
|
+
export declare const analyticsSourcesSchema: z.ZodObject<{
|
|
240
|
+
groupBy: z.ZodEnum<{
|
|
241
|
+
CHANNEL: "CHANNEL";
|
|
242
|
+
SOURCE: "SOURCE";
|
|
243
|
+
CAMPAIGN: "CAMPAIGN";
|
|
244
|
+
DEVICE: "DEVICE";
|
|
245
|
+
}>;
|
|
246
|
+
rows: z.ZodArray<z.ZodObject<{
|
|
247
|
+
channel: z.ZodNullable<z.ZodEnum<{
|
|
248
|
+
PAID: "PAID";
|
|
249
|
+
DIRECT: "DIRECT";
|
|
250
|
+
CAMPAIGN: "CAMPAIGN";
|
|
251
|
+
ORGANIC_SEARCH: "ORGANIC_SEARCH";
|
|
252
|
+
SOCIAL: "SOCIAL";
|
|
253
|
+
EMAIL: "EMAIL";
|
|
254
|
+
REFERRAL: "REFERRAL";
|
|
255
|
+
UNKNOWN: "UNKNOWN";
|
|
256
|
+
}>>;
|
|
257
|
+
source: z.ZodNullable<z.ZodString>;
|
|
258
|
+
medium: z.ZodNullable<z.ZodString>;
|
|
259
|
+
campaign: z.ZodNullable<z.ZodString>;
|
|
260
|
+
device: z.ZodNullable<z.ZodEnum<{
|
|
261
|
+
unknown: "unknown";
|
|
262
|
+
desktop: "desktop";
|
|
263
|
+
mobile: "mobile";
|
|
264
|
+
tablet: "tablet";
|
|
265
|
+
}>>;
|
|
266
|
+
sessions: z.ZodNumber;
|
|
267
|
+
visitors: z.ZodNumber;
|
|
268
|
+
engagedSessions: z.ZodNumber;
|
|
269
|
+
purchaseSessions: z.ZodNumber;
|
|
270
|
+
purchases: z.ZodNumber;
|
|
271
|
+
revenue: z.ZodNumber;
|
|
272
|
+
conversionRate: z.ZodNullable<z.ZodNumber>;
|
|
273
|
+
}, z.core.$strip>>;
|
|
274
|
+
from: z.ZodString;
|
|
275
|
+
to: z.ZodString;
|
|
276
|
+
timezone: z.ZodLiteral<"Asia/Seoul">;
|
|
277
|
+
currency: z.ZodLiteral<"KRW">;
|
|
278
|
+
includeTest: z.ZodBoolean;
|
|
279
|
+
lastComputedAt: z.ZodNullable<z.ZodString>;
|
|
280
|
+
}, z.core.$strip>;
|
|
281
|
+
export type AnalyticsSources = z.infer<typeof analyticsSourcesSchema>;
|
|
282
|
+
//#endregion
|
|
19
283
|
//#region src/schemas/api-client.d.ts
|
|
20
284
|
/**
|
|
21
285
|
* 앱(OAuth 클라이언트) 관리 — `/v1/store/api-clients` (auth-spec 부록 B-13).
|
|
@@ -3056,6 +3320,21 @@ export declare const STORE_CODE_PATTERN: RegExp;
|
|
|
3056
3320
|
export declare const RESERVED_STORE_CODES: readonly string[];
|
|
3057
3321
|
export declare function isReservedStoreCode(code: string): boolean;
|
|
3058
3322
|
export declare const storeCodeSchema: z.ZodString;
|
|
3323
|
+
/** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
|
|
3324
|
+
export declare function isWebOrigin(value: string): boolean;
|
|
3325
|
+
/** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
|
|
3326
|
+
export declare const analyticsOriginSchema: z.ZodString;
|
|
3327
|
+
/** 허용 오리진 상한 */
|
|
3328
|
+
export declare const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
3329
|
+
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
3330
|
+
export declare const updateStoreRequestSchema: z.ZodObject<{
|
|
3331
|
+
name: z.ZodOptional<z.ZodString>;
|
|
3332
|
+
logoUrl: z.ZodOptional<z.ZodURL>;
|
|
3333
|
+
customerCenterPhone: z.ZodOptional<z.ZodString>;
|
|
3334
|
+
businessHours: z.ZodOptional<z.ZodString>;
|
|
3335
|
+
analyticsAllowedOrigins: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
3336
|
+
}, z.core.$strip>;
|
|
3337
|
+
export type UpdateStoreRequest = z.infer<typeof updateStoreRequestSchema>;
|
|
3059
3338
|
export declare const storeSchema: z.ZodObject<{
|
|
3060
3339
|
storeId: z.ZodString;
|
|
3061
3340
|
name: z.ZodString;
|
|
@@ -3069,6 +3348,7 @@ export declare const storeSchema: z.ZodObject<{
|
|
|
3069
3348
|
logoUrl: z.ZodNullable<z.ZodURL>;
|
|
3070
3349
|
businessNumber: z.ZodNullable<z.ZodString>;
|
|
3071
3350
|
customerCenterPhone: z.ZodNullable<z.ZodString>;
|
|
3351
|
+
analyticsAllowedOrigins: z.ZodArray<z.ZodString>;
|
|
3072
3352
|
memberCount: z.ZodNumber;
|
|
3073
3353
|
createdAt: z.ZodString;
|
|
3074
3354
|
}, z.core.$strip>;
|
|
@@ -4318,6 +4598,106 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
4318
4598
|
}[];
|
|
4319
4599
|
}>;
|
|
4320
4600
|
};
|
|
4601
|
+
/**
|
|
4602
|
+
* 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
|
|
4603
|
+
* 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
|
|
4604
|
+
* 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
|
|
4605
|
+
*/
|
|
4606
|
+
analytics: {
|
|
4607
|
+
/** `GET /v1/analytics/overview` — 합계·세션 퍼널·날짜별 값 */
|
|
4608
|
+
overview: (params: AnalyticsRangeParams) => Promise<{
|
|
4609
|
+
summary: {
|
|
4610
|
+
visitors: number;
|
|
4611
|
+
sessions: number;
|
|
4612
|
+
engagedSessions: number;
|
|
4613
|
+
pageViews: number;
|
|
4614
|
+
engagementSeconds: number;
|
|
4615
|
+
productViews: number;
|
|
4616
|
+
productViewSessions: number;
|
|
4617
|
+
addToCartSessions: number;
|
|
4618
|
+
beginCheckoutSessions: number;
|
|
4619
|
+
purchaseSessions: number;
|
|
4620
|
+
purchases: number;
|
|
4621
|
+
revenue: number;
|
|
4622
|
+
engagementRate: number | null;
|
|
4623
|
+
conversionRate: number | null;
|
|
4624
|
+
};
|
|
4625
|
+
funnel: {
|
|
4626
|
+
step: "SESSION" | "PRODUCT_VIEW" | "ADD_TO_CART" | "BEGIN_CHECKOUT" | "PURCHASE";
|
|
4627
|
+
sessions: number;
|
|
4628
|
+
rateFromPrevious: number | null;
|
|
4629
|
+
}[];
|
|
4630
|
+
rows: {
|
|
4631
|
+
visitors: number;
|
|
4632
|
+
sessions: number;
|
|
4633
|
+
engagedSessions: number;
|
|
4634
|
+
pageViews: number;
|
|
4635
|
+
engagementSeconds: number;
|
|
4636
|
+
productViews: number;
|
|
4637
|
+
productViewSessions: number;
|
|
4638
|
+
addToCartSessions: number;
|
|
4639
|
+
beginCheckoutSessions: number;
|
|
4640
|
+
purchaseSessions: number;
|
|
4641
|
+
purchases: number;
|
|
4642
|
+
revenue: number;
|
|
4643
|
+
engagementRate: number | null;
|
|
4644
|
+
conversionRate: number | null;
|
|
4645
|
+
date: string;
|
|
4646
|
+
}[];
|
|
4647
|
+
from: string;
|
|
4648
|
+
to: string;
|
|
4649
|
+
timezone: "Asia/Seoul";
|
|
4650
|
+
currency: "KRW";
|
|
4651
|
+
includeTest: boolean;
|
|
4652
|
+
lastComputedAt: string | null;
|
|
4653
|
+
}>;
|
|
4654
|
+
/** `GET /v1/analytics/products` — 상품별 노출·조회·장바구니·구매 */
|
|
4655
|
+
products: (params: AnalyticsProductsParams) => Promise<{
|
|
4656
|
+
sort: "VIEWS" | "ADD_TO_CARTS" | "PURCHASES" | "REVENUE" | "IMPRESSIONS";
|
|
4657
|
+
rows: {
|
|
4658
|
+
productId: string;
|
|
4659
|
+
productName: string | null;
|
|
4660
|
+
impressions: number;
|
|
4661
|
+
views: number;
|
|
4662
|
+
viewSessions: number;
|
|
4663
|
+
addToCarts: number;
|
|
4664
|
+
purchases: number;
|
|
4665
|
+
quantity: number;
|
|
4666
|
+
revenue: number;
|
|
4667
|
+
purchaseRate: number | null;
|
|
4668
|
+
}[];
|
|
4669
|
+
from: string;
|
|
4670
|
+
to: string;
|
|
4671
|
+
timezone: "Asia/Seoul";
|
|
4672
|
+
currency: "KRW";
|
|
4673
|
+
includeTest: boolean;
|
|
4674
|
+
lastComputedAt: string | null;
|
|
4675
|
+
}>;
|
|
4676
|
+
/** `GET /v1/analytics/sources` — 채널·소스·캠페인·기기별 세션과 구매 */
|
|
4677
|
+
sources: (params: AnalyticsSourcesParams) => Promise<{
|
|
4678
|
+
groupBy: "CHANNEL" | "SOURCE" | "CAMPAIGN" | "DEVICE";
|
|
4679
|
+
rows: {
|
|
4680
|
+
channel: "PAID" | "DIRECT" | "CAMPAIGN" | "ORGANIC_SEARCH" | "SOCIAL" | "EMAIL" | "REFERRAL" | "UNKNOWN" | null;
|
|
4681
|
+
source: string | null;
|
|
4682
|
+
medium: string | null;
|
|
4683
|
+
campaign: string | null;
|
|
4684
|
+
device: "unknown" | "desktop" | "mobile" | "tablet" | null;
|
|
4685
|
+
sessions: number;
|
|
4686
|
+
visitors: number;
|
|
4687
|
+
engagedSessions: number;
|
|
4688
|
+
purchaseSessions: number;
|
|
4689
|
+
purchases: number;
|
|
4690
|
+
revenue: number;
|
|
4691
|
+
conversionRate: number | null;
|
|
4692
|
+
}[];
|
|
4693
|
+
from: string;
|
|
4694
|
+
to: string;
|
|
4695
|
+
timezone: "Asia/Seoul";
|
|
4696
|
+
currency: "KRW";
|
|
4697
|
+
includeTest: boolean;
|
|
4698
|
+
lastComputedAt: string | null;
|
|
4699
|
+
}>;
|
|
4700
|
+
};
|
|
4321
4701
|
/**
|
|
4322
4702
|
* 결제 내역 — 스코프 `settlement:r`. 결제 건(세션) 단위이고 주문 목록과 다르다. 주문이 만들어지지 않은
|
|
4323
4703
|
* 결제(카드 거절·결제창 이탈·승인 결과 확인 중)도 그대로 나온다.
|
|
@@ -5342,6 +5722,24 @@ export declare function createStoreClient(options: StoreClientOptions): {
|
|
|
5342
5722
|
logoUrl: string | null;
|
|
5343
5723
|
businessNumber: string | null;
|
|
5344
5724
|
customerCenterPhone: string | null;
|
|
5725
|
+
analyticsAllowedOrigins: string[];
|
|
5726
|
+
memberCount: number;
|
|
5727
|
+
createdAt: string;
|
|
5728
|
+
}>;
|
|
5729
|
+
/**
|
|
5730
|
+
* `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
|
|
5731
|
+
* 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
|
|
5732
|
+
*/
|
|
5733
|
+
update: (body: UpdateStoreRequest) => Promise<{
|
|
5734
|
+
storeId: string;
|
|
5735
|
+
name: string;
|
|
5736
|
+
storeCode: string;
|
|
5737
|
+
storefrontUrl: string;
|
|
5738
|
+
status: "ACTIVE" | "SUSPENDED" | "CLOSED";
|
|
5739
|
+
logoUrl: string | null;
|
|
5740
|
+
businessNumber: string | null;
|
|
5741
|
+
customerCenterPhone: string | null;
|
|
5742
|
+
analyticsAllowedOrigins: string[];
|
|
5345
5743
|
memberCount: number;
|
|
5346
5744
|
createdAt: string;
|
|
5347
5745
|
}>;
|
|
@@ -5438,7 +5836,7 @@ export declare const scopeAccessSchema: z.ZodEnum<{
|
|
|
5438
5836
|
rw: "rw";
|
|
5439
5837
|
}>;
|
|
5440
5838
|
export type ScopeAccess = z.infer<typeof scopeAccessSchema>;
|
|
5441
|
-
/** 리소스별로 부여 가능한 접근 수준. settlement는 쓰기 API가 없어 `r`만 존재한다 */
|
|
5839
|
+
/** 리소스별로 부여 가능한 접근 수준. settlement·analytics는 쓰기 API가 없어 `r`만 존재한다 */
|
|
5442
5840
|
export declare const STORE_SCOPE_RESOURCES: {
|
|
5443
5841
|
readonly product: readonly ["r", "rw"];
|
|
5444
5842
|
readonly category: readonly ["r", "rw"];
|
|
@@ -5447,6 +5845,7 @@ export declare const STORE_SCOPE_RESOURCES: {
|
|
|
5447
5845
|
readonly inquiry: readonly ["r", "rw"];
|
|
5448
5846
|
readonly review: readonly ["r", "rw"];
|
|
5449
5847
|
readonly settlement: readonly ["r"];
|
|
5848
|
+
readonly analytics: readonly ["r"];
|
|
5450
5849
|
readonly webhook: readonly ["r", "rw"];
|
|
5451
5850
|
readonly store: readonly ["r", "rw"];
|
|
5452
5851
|
readonly member: readonly ["r", "rw"];
|
|
@@ -5460,6 +5859,7 @@ export declare const storeScopeResourceSchema: z.ZodEnum<{
|
|
|
5460
5859
|
inquiry: "inquiry";
|
|
5461
5860
|
review: "review";
|
|
5462
5861
|
settlement: "settlement";
|
|
5862
|
+
analytics: "analytics";
|
|
5463
5863
|
webhook: "webhook";
|
|
5464
5864
|
store: "store";
|
|
5465
5865
|
member: "member";
|
|
@@ -5472,8 +5872,8 @@ export declare function isStoreScope(scope: string): boolean;
|
|
|
5472
5872
|
* 발급 시 지정한 스코프만 갖는다(발급자 보유 스코프 이내).
|
|
5473
5873
|
*/
|
|
5474
5874
|
export declare const STORE_ROLE_SCOPES: {
|
|
5475
|
-
readonly OWNER: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "settlement:r", "webhook:rw", "store:rw", "member:rw"];
|
|
5476
|
-
readonly ADMIN: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "webhook:rw", "store:rw", "member:rw"];
|
|
5875
|
+
readonly OWNER: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "settlement:r", "analytics:r", "webhook:rw", "store:rw", "member:rw"];
|
|
5876
|
+
readonly ADMIN: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw", "analytics:r", "webhook:rw", "store:rw", "member:rw"];
|
|
5477
5877
|
readonly STAFF: readonly ["product:rw", "category:rw", "order:rw", "claim:rw", "inquiry:rw", "review:rw"];
|
|
5478
5878
|
};
|
|
5479
5879
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -146,6 +146,136 @@ function createHttp(options) {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
//#endregion
|
|
149
|
+
//#region src/schemas/analytics.ts
|
|
150
|
+
/**
|
|
151
|
+
* 방문 분석 조회 (관리 API `/v1/analytics/*`, 스코프 `analytics:r`).
|
|
152
|
+
*
|
|
153
|
+
* 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
|
|
154
|
+
* 갱신되므로 오늘 값은 `lastComputedAt` 시점까지 반영돼 있다. 봇·자동화 브라우저의 세션은 모든 집계에서 빠진다.
|
|
155
|
+
*/
|
|
156
|
+
const reportDate$1 = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "날짜 형식은 YYYY-MM-DD 입니다");
|
|
157
|
+
const includeTest = z.enum(["true", "false"]).optional().describe("테스트 결제(샌드박스 결제)로 만든 구매 포함 여부. 기본 `false`");
|
|
158
|
+
const analyticsRangeQuerySchema = z.object({
|
|
159
|
+
from: reportDate$1.describe("시작일 (YYYY-MM-DD, Asia/Seoul 기준, 포함)"),
|
|
160
|
+
to: reportDate$1.describe("종료일 (YYYY-MM-DD, 포함). from부터 최대 92일"),
|
|
161
|
+
includeTest
|
|
162
|
+
});
|
|
163
|
+
const rangeMeta = {
|
|
164
|
+
from: z.string(),
|
|
165
|
+
to: z.string(),
|
|
166
|
+
timezone: z.literal("Asia/Seoul"),
|
|
167
|
+
currency: z.literal("KRW"),
|
|
168
|
+
includeTest: z.boolean().describe("테스트 결제 구매를 포함했는가"),
|
|
169
|
+
lastComputedAt: z.string().nullable().describe("기간 안에서 가장 최근에 집계한 시각(ISO 8601). 집계된 날이 없으면 null")
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* 비율(0 이상). 단계가 세션 안에서 꼭 이어지지는 않아 1을 넘을 수 있다 — 예: 조회 없이 장바구니에서 바로 산 구매,
|
|
173
|
+
* 자정을 넘겨 다음 날 확정된 구매.
|
|
174
|
+
*/
|
|
175
|
+
const rate = (description) => z.number().min(0).nullable().describe(description);
|
|
176
|
+
const analyticsTotalsSchema = z.object({
|
|
177
|
+
visitors: z.number().int().describe("방문자 수 — 날짜별 순방문자의 합. 여러 날 방문한 사람은 날마다 센다"),
|
|
178
|
+
sessions: z.number().int().describe("세션 수. 30분 동안 활동이 없거나 다른 캠페인으로 들어오면 새 세션"),
|
|
179
|
+
engagedSessions: z.number().int().describe("참여 세션 수 — 페이지뷰 2개 이상, 체류 10초 이상, 장바구니 담기·결제 시작·구매 중 하나"),
|
|
180
|
+
pageViews: z.number().int(),
|
|
181
|
+
engagementSeconds: z.number().int().describe("페이지가 화면에 보인 시간의 합(초). 페이지뷰 하나는 최대 30분, 세션은 세션 길이 + 30분까지 센다"),
|
|
182
|
+
productViews: z.number().int().describe("상품 조회 수"),
|
|
183
|
+
productViewSessions: z.number().int().describe("상품을 조회한 세션 수"),
|
|
184
|
+
addToCartSessions: z.number().int().describe("장바구니에 담은 세션 수"),
|
|
185
|
+
beginCheckoutSessions: z.number().int().describe("주문서를 연 세션 수"),
|
|
186
|
+
purchaseSessions: z.number().int().describe("구매한 세션 수"),
|
|
187
|
+
purchases: z.number().int().describe("구매(주문) 수"),
|
|
188
|
+
revenue: z.number().int().describe("구매 금액 합계 — 주문 결제 금액(배송비 포함). 이후 취소·환불은 빼지 않으므로 매출 리포트의 순매출과 다르다"),
|
|
189
|
+
engagementRate: rate("참여율 = 참여 세션 / 세션. 세션이 없으면 null"),
|
|
190
|
+
conversionRate: rate("전환율 = 구매 세션 / 세션. 세션이 없으면 null")
|
|
191
|
+
});
|
|
192
|
+
const analyticsFunnelStepSchema = z.object({
|
|
193
|
+
step: z.enum([
|
|
194
|
+
"SESSION",
|
|
195
|
+
"PRODUCT_VIEW",
|
|
196
|
+
"ADD_TO_CART",
|
|
197
|
+
"BEGIN_CHECKOUT",
|
|
198
|
+
"PURCHASE"
|
|
199
|
+
]),
|
|
200
|
+
sessions: z.number().int(),
|
|
201
|
+
rateFromPrevious: rate("직전 단계 대비 비율. 첫 단계이거나 직전 단계가 0이면 null")
|
|
202
|
+
});
|
|
203
|
+
const analyticsOverviewSchema = z.object({
|
|
204
|
+
...rangeMeta,
|
|
205
|
+
summary: analyticsTotalsSchema.describe("기간 합계"),
|
|
206
|
+
funnel: z.array(analyticsFunnelStepSchema).describe("세션 기준 구매 퍼널. 각 단계는 그 행동을 한 세션 수이고, 앞 단계를 거쳤는지는 따지지 않는다"),
|
|
207
|
+
rows: z.array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") })).describe("날짜별 값. 세션이 없는 날은 생략된다")
|
|
208
|
+
});
|
|
209
|
+
const analyticsProductSortSchema = z.enum([
|
|
210
|
+
"VIEWS",
|
|
211
|
+
"ADD_TO_CARTS",
|
|
212
|
+
"PURCHASES",
|
|
213
|
+
"REVENUE",
|
|
214
|
+
"IMPRESSIONS"
|
|
215
|
+
]).describe("정렬 기준(내림차순). 기본 `VIEWS`");
|
|
216
|
+
const analyticsProductsQuerySchema = analyticsRangeQuerySchema.extend({
|
|
217
|
+
sort: analyticsProductSortSchema.optional(),
|
|
218
|
+
limit: z.coerce.number().int().min(1).max(100).optional().describe("최대 행 수. 기본 20, 최대 100")
|
|
219
|
+
});
|
|
220
|
+
const analyticsProductRowSchema = z.object({
|
|
221
|
+
productId: z.string(),
|
|
222
|
+
productName: z.string().nullable().describe("현재 상품명. 삭제된 상품이면 null"),
|
|
223
|
+
impressions: z.number().int().describe("목록 노출 수"),
|
|
224
|
+
views: z.number().int().describe("상세 조회 수"),
|
|
225
|
+
viewSessions: z.number().int().describe("상세를 조회한 세션 수"),
|
|
226
|
+
addToCarts: z.number().int().describe("장바구니 담기 수"),
|
|
227
|
+
purchases: z.number().int().describe("이 상품이 포함된 구매(주문) 수"),
|
|
228
|
+
quantity: z.number().int().describe("구매 수량"),
|
|
229
|
+
revenue: z.number().int().describe("이 상품의 구매 금액 — 주문 상품 금액(할인 반영, 배송비 제외) 합계. 이후 취소·환불은 빼지 않는다"),
|
|
230
|
+
purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null")
|
|
231
|
+
});
|
|
232
|
+
const analyticsProductsSchema = z.object({
|
|
233
|
+
...rangeMeta,
|
|
234
|
+
sort: analyticsProductSortSchema,
|
|
235
|
+
rows: z.array(analyticsProductRowSchema)
|
|
236
|
+
});
|
|
237
|
+
const analyticsChannelSchema = z.enum([
|
|
238
|
+
"DIRECT",
|
|
239
|
+
"ORGANIC_SEARCH",
|
|
240
|
+
"SOCIAL",
|
|
241
|
+
"PAID",
|
|
242
|
+
"EMAIL",
|
|
243
|
+
"CAMPAIGN",
|
|
244
|
+
"REFERRAL",
|
|
245
|
+
"UNKNOWN"
|
|
246
|
+
]).describe("유입 채널 — 세션 첫 페이지뷰의 utm·리퍼러로 정한다. `PAID`(utm_medium이 cpc·ppc·paid·display 등), `EMAIL`(utm_medium email), `SOCIAL`(SNS 리퍼러·utm_source), `ORGANIC_SEARCH`(검색 엔진 리퍼러), `CAMPAIGN`(그 밖의 utm), `REFERRAL`(그 밖의 리퍼러), `DIRECT`(없음), `UNKNOWN`(페이지뷰 없이 서버 이벤트만 있는 세션)");
|
|
247
|
+
const analyticsSourceGroupBySchema = z.enum([
|
|
248
|
+
"CHANNEL",
|
|
249
|
+
"SOURCE",
|
|
250
|
+
"CAMPAIGN",
|
|
251
|
+
"DEVICE"
|
|
252
|
+
]).describe("행 집계 기준. 기본 `CHANNEL`");
|
|
253
|
+
const analyticsSourcesQuerySchema = analyticsRangeQuerySchema.extend({ groupBy: analyticsSourceGroupBySchema.optional() });
|
|
254
|
+
const analyticsSourceRowSchema = z.object({
|
|
255
|
+
channel: analyticsChannelSchema.nullable().describe("groupBy=CHANNEL일 때만 값"),
|
|
256
|
+
source: z.string().nullable().describe("groupBy=SOURCE·CAMPAIGN일 때 값. utm_source, 없으면 리퍼러 호스트, 둘 다 없으면 `(direct)`"),
|
|
257
|
+
medium: z.string().nullable().describe("groupBy=SOURCE·CAMPAIGN일 때 값. utm_medium, 없으면 `referral`·`(none)`"),
|
|
258
|
+
campaign: z.string().nullable().describe("groupBy=CAMPAIGN일 때 값. utm_campaign, 없으면 `(not set)`"),
|
|
259
|
+
device: z.enum([
|
|
260
|
+
"desktop",
|
|
261
|
+
"mobile",
|
|
262
|
+
"tablet",
|
|
263
|
+
"unknown"
|
|
264
|
+
]).nullable().describe("groupBy=DEVICE일 때만 값"),
|
|
265
|
+
sessions: z.number().int(),
|
|
266
|
+
visitors: z.number().int().describe("날짜별 순방문자의 합"),
|
|
267
|
+
engagedSessions: z.number().int(),
|
|
268
|
+
purchaseSessions: z.number().int(),
|
|
269
|
+
purchases: z.number().int(),
|
|
270
|
+
revenue: z.number().int(),
|
|
271
|
+
conversionRate: rate("전환율 = 구매 세션 / 세션")
|
|
272
|
+
});
|
|
273
|
+
const analyticsSourcesSchema = z.object({
|
|
274
|
+
...rangeMeta,
|
|
275
|
+
groupBy: analyticsSourceGroupBySchema,
|
|
276
|
+
rows: z.array(analyticsSourceRowSchema).describe("세션 수 내림차순")
|
|
277
|
+
});
|
|
278
|
+
//#endregion
|
|
149
279
|
//#region src/schemas/api-client.ts
|
|
150
280
|
/**
|
|
151
281
|
* 앱(OAuth 클라이언트) 관리 — `/v1/store/api-clients` (auth-spec 부록 B-13).
|
|
@@ -1580,6 +1710,27 @@ function isReservedStoreCode(code) {
|
|
|
1580
1710
|
return RESERVED_STORE_CODES.includes(code.toLowerCase());
|
|
1581
1711
|
}
|
|
1582
1712
|
const storeCodeSchema = z.string().regex(STORE_CODE_PATTERN, "소문자·숫자·하이픈 3~30자여야 하고 하이픈으로 시작하거나 끝날 수 없어요").refine((code) => !isReservedStoreCode(code), "플랫폼이 쓰는 이름이라 스토어 코드로 쓸 수 없어요");
|
|
1713
|
+
/** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
|
|
1714
|
+
function isWebOrigin(value) {
|
|
1715
|
+
try {
|
|
1716
|
+
const url = new URL(value);
|
|
1717
|
+
return (url.protocol === "https:" || url.protocol === "http:") && !url.username && !url.password && url.pathname === "/" && !url.search && !url.hash && !value.includes("*");
|
|
1718
|
+
} catch {
|
|
1719
|
+
return false;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
/** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
|
|
1723
|
+
const analyticsOriginSchema = z.string().trim().max(255).refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요").describe("스토어프런트 오리진. 예: `https://shop.example.com`");
|
|
1724
|
+
/** 허용 오리진 상한 */
|
|
1725
|
+
const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
1726
|
+
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
1727
|
+
const updateStoreRequestSchema = z.object({
|
|
1728
|
+
name: z.string().min(2).max(50).optional(),
|
|
1729
|
+
logoUrl: z.url().optional(),
|
|
1730
|
+
customerCenterPhone: z.string().optional(),
|
|
1731
|
+
businessHours: z.string().max(100).optional(),
|
|
1732
|
+
analyticsAllowedOrigins: z.array(analyticsOriginSchema).max(20).optional().describe("방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. 넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다")
|
|
1733
|
+
}).refine((value) => Object.values(value).some((v) => v !== void 0), { message: "최소 1개 필드가 필요합니다" });
|
|
1583
1734
|
const storeSchema = z.object({
|
|
1584
1735
|
storeId: z.string(),
|
|
1585
1736
|
name: z.string(),
|
|
@@ -1593,6 +1744,7 @@ const storeSchema = z.object({
|
|
|
1593
1744
|
logoUrl: z.url().nullable(),
|
|
1594
1745
|
businessNumber: z.string().nullable(),
|
|
1595
1746
|
customerCenterPhone: z.string().nullable(),
|
|
1747
|
+
analyticsAllowedOrigins: z.array(z.string()).describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
|
|
1596
1748
|
memberCount: z.number().int(),
|
|
1597
1749
|
createdAt: z.string()
|
|
1598
1750
|
});
|
|
@@ -1927,6 +2079,19 @@ function createStoreClient(options) {
|
|
|
1927
2079
|
* 400 `INVALID_PARAMETER`(없는 날짜·to<from), 400 `RANGE_TOO_WIDE`(기간 초과)
|
|
1928
2080
|
*/
|
|
1929
2081
|
get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, { query: salesReportQuerySchema.parse(params) }) },
|
|
2082
|
+
/**
|
|
2083
|
+
* 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
|
|
2084
|
+
* 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
|
|
2085
|
+
* 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
|
|
2086
|
+
*/
|
|
2087
|
+
analytics: {
|
|
2088
|
+
/** `GET /v1/analytics/overview` — 합계·세션 퍼널·날짜별 값 */
|
|
2089
|
+
overview: (params) => storeHttp.request("GET", "/analytics/overview", analyticsOverviewSchema, { query: analyticsRangeQuerySchema.parse(params) }),
|
|
2090
|
+
/** `GET /v1/analytics/products` — 상품별 노출·조회·장바구니·구매 */
|
|
2091
|
+
products: (params) => storeHttp.request("GET", "/analytics/products", analyticsProductsSchema, { query: analyticsProductsQuerySchema.parse(params) }),
|
|
2092
|
+
/** `GET /v1/analytics/sources` — 채널·소스·캠페인·기기별 세션과 구매 */
|
|
2093
|
+
sources: (params) => storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, { query: analyticsSourcesQuerySchema.parse(params) })
|
|
2094
|
+
},
|
|
1930
2095
|
/**
|
|
1931
2096
|
* 결제 내역 — 스코프 `settlement:r`. 결제 건(세션) 단위이고 주문 목록과 다르다. 주문이 만들어지지 않은
|
|
1932
2097
|
* 결제(카드 거절·결제창 이탈·승인 결과 확인 중)도 그대로 나온다.
|
|
@@ -2069,6 +2234,11 @@ get: (params) => storeHttp.request("GET", "/sales-report", salesReportSchema, {
|
|
|
2069
2234
|
},
|
|
2070
2235
|
store: {
|
|
2071
2236
|
get: () => storeHttp.request("GET", "/store", storeSchema),
|
|
2237
|
+
/**
|
|
2238
|
+
* `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
|
|
2239
|
+
* 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
|
|
2240
|
+
*/
|
|
2241
|
+
update: (body) => storeHttp.request("PATCH", "/store", storeSchema, { body: updateStoreRequestSchema.parse(body) }),
|
|
2072
2242
|
listMembers: () => storeHttp.request("GET", "/store/members", z.array(storeMemberSchema)),
|
|
2073
2243
|
listInvitations: () => storeHttp.request("GET", "/store/invitations", z.array(invitationSchema))
|
|
2074
2244
|
}
|
|
@@ -2162,7 +2332,7 @@ async function exchangeStoreTokenViaOAuth(options) {
|
|
|
2162
2332
|
* 서버 인가가 갈라지지 않는다.
|
|
2163
2333
|
*/
|
|
2164
2334
|
const scopeAccessSchema = z.enum(["r", "rw"]).describe("접근 수준. `r`=읽기, `rw`=읽기+쓰기(r을 함의)");
|
|
2165
|
-
/** 리소스별로 부여 가능한 접근 수준. settlement는 쓰기 API가 없어 `r`만 존재한다 */
|
|
2335
|
+
/** 리소스별로 부여 가능한 접근 수준. settlement·analytics는 쓰기 API가 없어 `r`만 존재한다 */
|
|
2166
2336
|
const STORE_SCOPE_RESOURCES = {
|
|
2167
2337
|
product: ["r", "rw"],
|
|
2168
2338
|
category: ["r", "rw"],
|
|
@@ -2171,6 +2341,7 @@ const STORE_SCOPE_RESOURCES = {
|
|
|
2171
2341
|
inquiry: ["r", "rw"],
|
|
2172
2342
|
review: ["r", "rw"],
|
|
2173
2343
|
settlement: ["r"],
|
|
2344
|
+
analytics: ["r"],
|
|
2174
2345
|
webhook: ["r", "rw"],
|
|
2175
2346
|
store: ["r", "rw"],
|
|
2176
2347
|
member: ["r", "rw"]
|
|
@@ -2196,6 +2367,7 @@ const STORE_ROLE_SCOPES = {
|
|
|
2196
2367
|
"inquiry:rw",
|
|
2197
2368
|
"review:rw",
|
|
2198
2369
|
"settlement:r",
|
|
2370
|
+
"analytics:r",
|
|
2199
2371
|
"webhook:rw",
|
|
2200
2372
|
"store:rw",
|
|
2201
2373
|
"member:rw"
|
|
@@ -2207,6 +2379,7 @@ const STORE_ROLE_SCOPES = {
|
|
|
2207
2379
|
"claim:rw",
|
|
2208
2380
|
"inquiry:rw",
|
|
2209
2381
|
"review:rw",
|
|
2382
|
+
"analytics:r",
|
|
2210
2383
|
"webhook:rw",
|
|
2211
2384
|
"store:rw",
|
|
2212
2385
|
"member:rw"
|
|
@@ -2448,4 +2621,4 @@ function createStoreSession(options) {
|
|
|
2448
2621
|
};
|
|
2449
2622
|
}
|
|
2450
2623
|
//#endregion
|
|
2451
|
-
export { ApiError, MAX_OPTION_GROUPS, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerInquiryCategorySchema, customerInquiryListQuerySchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema };
|
|
2624
|
+
export { ApiError, MAX_ANALYTICS_ALLOWED_ORIGINS, MAX_OPTION_GROUPS, MAX_VALUES_PER_GROUP, MAX_VARIANTS, PAYMENT_ROUTING_EXCLUSION_CODES, PORTONE_CHANNEL_NAME_MAX_LENGTH, PORTONE_MAX_CHANNELS, PORTONE_MIN_CHANNELS, RESERVED_STORE_CODES, STORE_CODE_PATTERN, STORE_ROLE_SCOPES, STORE_SCOPE_RESOURCES, STORE_SESSION_KEYS, TOSS_WIDGET_CLIENT_KEY_PATTERN, TOSS_WIDGET_KEY_MESSAGE, TOSS_WIDGET_SECRET_KEY_PATTERN, VARIANT_STOCK_EXCLUSIVE_MESSAGE, WEBHOOK_DEFAULT_TOLERANCE_SEC, WEBHOOK_DELIVERY_ID_HEADER, WEBHOOK_EVENT_ID_HEADER, WEBHOOK_EVENT_TYPE_HEADER, WEBHOOK_HEADERS, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_USER_AGENT, addressChangeRequestListSchema, addressTypeSchema, analyticsChannelSchema, analyticsFunnelStepSchema, analyticsOriginSchema, analyticsOverviewSchema, analyticsProductRowSchema, analyticsProductSortSchema, analyticsProductsQuerySchema, analyticsProductsSchema, analyticsRangeQuerySchema, analyticsSourceGroupBySchema, analyticsSourceRowSchema, analyticsSourcesQuerySchema, analyticsSourcesSchema, analyticsTotalsSchema, apiClientListQuerySchema, apiClientSchema, apiClientSecretRotatedSchema, apiClientStatusSchema, apiClientTypeSchema, apiLogErrorSchema, apiLogListQuerySchema, apiLogPageSchema, apiLogPrincipalSchema, apiLogPrincipalTypeSchema, apiLogSchema, apiLogStatusFilterSchema, apiLogSummarySchema, apiTokenCreateRequestSchema, apiTokenCreatedSchema, apiTokenSchema, approveClaimResultSchema, buildAuthorizeUrl, buildQuery, bulkResultSchema, cancelBySellerRequestSchema, cancelBySellerResultSchema, carrierSchema, categoryAttributeSchema, categoryCreateRequestSchema, categoryDeleteQuerySchema, categoryDeleteStrategySchema, categoryListQuerySchema, categoryPatchRequestSchema, categoryProductSortSchema, categorySchema, categorySearchResultSchema, categoryTreeNodeSchema, categoryWithPathSchema, changeTypeSchema, claimDetailSchema, claimHoldResultSchema, claimListQuerySchema, claimReasonSchema, claimSchema, claimStatusSchema, claimTypeSchema, claimWebhookEventDataSchema, claimWebhookEventSchema, clientCredentialsTokenResponseSchema, createPkcePair, createStoreClient, createStoreSession, customerInquiryCategorySchema, customerInquiryListQuerySchema, deliveryInfoSchema, deliveryMethodSchema, deliveryStatusSchema, deliveryTypeSchema, discountRequestSchema, discountSchema, dispatchRequestSchema, effectivePaymentRoutingModeSchema, eventTypeSchema, exchangeAuthorizationCode, exchangeStoreTokenViaOAuth, holdOrderItemRequestSchema, inquiryListQuerySchema, inquirySchema, inquiryWebhookEventDataSchema, inquiryWebhookEventSchema, invitationListQuerySchema, invitationSchema, isReservedStoreCode, isStoreScope, isWebOrigin, liveGateConditionCodeSchema, liveGateConditionSchema, liveGateErrorDetailsSchema, liveGateSchema, maskedSecretSchema, myAccountSchema, myStoreSchema, oauthClientRegistrationRequestSchema, oauthClientRegistrationResponseSchema, oauthTokenResponseSchema, optionGroupRequestSchema, optionGroupSchema, optionModelRequestSchema, optionModelSchema, optionValueRequestSchema, optionValueSchema, orderItemListQuerySchema, orderItemMemoResultSchema, orderItemPageSchema, orderItemSchema, orderItemStatusSchema, orderListQuerySchema, orderSchema, orderSummaryPageSchema, orderSummarySchema, orderWebhookEventDataSchema, orderWebhookEventSchema, pageInfoSchema, pageQueryShape, pageSchema, paymentAttemptRecordSchema, paymentAttemptStatusSchema, paymentCredentialSetSchema, paymentListQuerySchema, paymentProviderSchema, paymentProviderVerificationSchema, paymentRecordDetailSchema, paymentRecordPageSchema, paymentRecordSchema, paymentRecordStatusSchema, paymentRefundRecordSchema, paymentRoutingModeSchema, paymentSettingsSchema, pgEnvironmentSchema, pgPaymentMethodSchema, pgProviderSchema, pgVerificationResultSchema, portOneChannelSchema, portOneCredentialsInputSchema, productCreateRequestSchema, productImageUploadResultSchema, productListQuerySchema, productOutOfStockWebhookEventDataSchema, productOutOfStockWebhookEventSchema, productPatchRequestSchema, productSchema, productSortSchema, productStatusSchema, productSummarySchema, productVariantRequestSchema, productVariantSchema, purchaseDecisionExtendResultSchema, purchaseDecisionRequestResultSchema, refreshUserToken, rejectClaimRequestSchema, reviewListQuerySchema, reviewReportResultSchema, reviewSchema, reviewSortSchema, reviewWebhookEventDataSchema, reviewWebhookEventSchema, salesReportGroupBySchema, salesReportQuerySchema, salesReportRowSchema, salesReportSchema, salesTotalsSchema, scopeAccessSchema, sellerAddressListQuerySchema, sellerAddressRequestSchema, sellerAddressSchema, sellerCustomerInquirySchema, settlementDetailQuerySchema, settlementDetailSchema, settlementListQuerySchema, settlementStatusSchema, settlementSummarySchema, settlementWithDetailsSchema, shippingAddressSchema, storeCodeSchema, storeMemberRoleUpdatedSchema, storeMemberSchema, storeRoleSchema, storeSchema, storeScopeResourceSchema, storeTokenResponseSchema, tossPaymentsCredentialsInputSchema, trackingUpdateResultSchema, updatePaymentProviderRequestSchema, updatePaymentSettingsRequestSchema, updateStoreRequestSchema, uploadRequestSchema, uploadTicketSchema, upsertPortOneProviderRequestSchema, upsertTossPaymentsProviderRequestSchema, userTokenResponseSchema, variantStockRequestSchema, variantStockSchema, verifyPaymentProviderRequestSchema, webhookCreatedSchema, webhookDeliveryErrorCodeSchema, webhookDeliveryListQuerySchema, webhookDeliverySchema, webhookDeliveryStatusSchema, webhookEventSchema, webhookRedeliverFailedResultSchema, webhookRequestSchema, webhookSchema, webhookSecretRotatedSchema, webhookStatusSchema, webhookTestRequestSchema, webhookTestResultSchema };
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { base64Utf8 } from "./base64";
|
|
3
3
|
import { createHttp, path, type Query, resolveToken, type TokenSource, unwrapData } from "./http";
|
|
4
|
+
import {
|
|
5
|
+
type AnalyticsProductsParams,
|
|
6
|
+
type AnalyticsRangeParams,
|
|
7
|
+
type AnalyticsSourcesParams,
|
|
8
|
+
analyticsOverviewSchema,
|
|
9
|
+
analyticsProductsQuerySchema,
|
|
10
|
+
analyticsProductsSchema,
|
|
11
|
+
analyticsRangeQuerySchema,
|
|
12
|
+
analyticsSourcesQuerySchema,
|
|
13
|
+
analyticsSourcesSchema,
|
|
14
|
+
} from "./schemas/analytics";
|
|
4
15
|
import {
|
|
5
16
|
type ApiClientListParams,
|
|
6
17
|
apiClientSchema,
|
|
@@ -133,6 +144,8 @@ import {
|
|
|
133
144
|
sellerAddressSchema,
|
|
134
145
|
storeMemberSchema,
|
|
135
146
|
storeSchema,
|
|
147
|
+
type UpdateStoreRequest,
|
|
148
|
+
updateStoreRequestSchema,
|
|
136
149
|
} from "./schemas/store";
|
|
137
150
|
import { type UploadRequest, uploadRequestSchema, uploadTicketSchema } from "./schemas/upload";
|
|
138
151
|
import type { AuthorizeOptions, StoreSession } from "./session";
|
|
@@ -606,6 +619,29 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
606
619
|
}),
|
|
607
620
|
},
|
|
608
621
|
|
|
622
|
+
/**
|
|
623
|
+
* 방문 분석 — 스코프 `analytics:r`. 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 Asia/Seoul 날짜별로
|
|
624
|
+
* 집계한 값이다. 집계는 주기적으로 갱신되고 응답 `lastComputedAt`이 마지막 집계 시각이다. 봇 세션은 빠진다.
|
|
625
|
+
* 기간은 양끝 포함 최대 92일이고, 테스트 결제 구매는 `includeTest: "true"`일 때만 포함한다.
|
|
626
|
+
*/
|
|
627
|
+
analytics: {
|
|
628
|
+
/** `GET /v1/analytics/overview` — 합계·세션 퍼널·날짜별 값 */
|
|
629
|
+
overview: (params: AnalyticsRangeParams) =>
|
|
630
|
+
storeHttp.request("GET", "/analytics/overview", analyticsOverviewSchema, {
|
|
631
|
+
query: analyticsRangeQuerySchema.parse(params),
|
|
632
|
+
}),
|
|
633
|
+
/** `GET /v1/analytics/products` — 상품별 노출·조회·장바구니·구매 */
|
|
634
|
+
products: (params: AnalyticsProductsParams) =>
|
|
635
|
+
storeHttp.request("GET", "/analytics/products", analyticsProductsSchema, {
|
|
636
|
+
query: analyticsProductsQuerySchema.parse(params) as unknown as Query,
|
|
637
|
+
}),
|
|
638
|
+
/** `GET /v1/analytics/sources` — 채널·소스·캠페인·기기별 세션과 구매 */
|
|
639
|
+
sources: (params: AnalyticsSourcesParams) =>
|
|
640
|
+
storeHttp.request("GET", "/analytics/sources", analyticsSourcesSchema, {
|
|
641
|
+
query: analyticsSourcesQuerySchema.parse(params),
|
|
642
|
+
}),
|
|
643
|
+
},
|
|
644
|
+
|
|
609
645
|
/**
|
|
610
646
|
* 결제 내역 — 스코프 `settlement:r`. 결제 건(세션) 단위이고 주문 목록과 다르다. 주문이 만들어지지 않은
|
|
611
647
|
* 결제(카드 거절·결제창 이탈·승인 결과 확인 중)도 그대로 나온다.
|
|
@@ -835,6 +871,14 @@ export function createStoreClient(options: StoreClientOptions) {
|
|
|
835
871
|
|
|
836
872
|
store: {
|
|
837
873
|
get: () => storeHttp.request("GET", "/store", storeSchema),
|
|
874
|
+
/**
|
|
875
|
+
* `PATCH /v1/store` — 스코프 `store:rw`. 바꿀 필드만 보낸다. `analyticsAllowedOrigins`는 방문 분석 수집을 받을
|
|
876
|
+
* 스토어프런트 오리진 목록이다(빈 배열이면 모든 오리진 허용).
|
|
877
|
+
*/
|
|
878
|
+
update: (body: UpdateStoreRequest) =>
|
|
879
|
+
storeHttp.request("PATCH", "/store", storeSchema, {
|
|
880
|
+
body: updateStoreRequestSchema.parse(body),
|
|
881
|
+
}),
|
|
838
882
|
listMembers: () => storeHttp.request("GET", "/store/members", z.array(storeMemberSchema)),
|
|
839
883
|
listInvitations: () =>
|
|
840
884
|
storeHttp.request("GET", "/store/invitations", z.array(invitationSchema)),
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 방문 분석 조회 (관리 API `/v1/analytics/*`, 스코프 `analytics:r`).
|
|
5
|
+
*
|
|
6
|
+
* 스토어프런트가 `@sayren/storefront-sdk`로 수집한 이벤트를 서버가 날짜(Asia/Seoul)별로 집계한 값이다. 집계는 주기적으로
|
|
7
|
+
* 갱신되므로 오늘 값은 `lastComputedAt` 시점까지 반영돼 있다. 봇·자동화 브라우저의 세션은 모든 집계에서 빠진다.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const reportDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "날짜 형식은 YYYY-MM-DD 입니다");
|
|
11
|
+
|
|
12
|
+
const includeTest = z
|
|
13
|
+
.enum(["true", "false"])
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("테스트 결제(샌드박스 결제)로 만든 구매 포함 여부. 기본 `false`");
|
|
16
|
+
|
|
17
|
+
export const analyticsRangeQuerySchema = z.object({
|
|
18
|
+
from: reportDate.describe("시작일 (YYYY-MM-DD, Asia/Seoul 기준, 포함)"),
|
|
19
|
+
to: reportDate.describe("종료일 (YYYY-MM-DD, 포함). from부터 최대 92일"),
|
|
20
|
+
includeTest,
|
|
21
|
+
});
|
|
22
|
+
export type AnalyticsRangeParams = z.infer<typeof analyticsRangeQuerySchema>;
|
|
23
|
+
|
|
24
|
+
const rangeMeta = {
|
|
25
|
+
from: z.string(),
|
|
26
|
+
to: z.string(),
|
|
27
|
+
timezone: z.literal("Asia/Seoul"),
|
|
28
|
+
currency: z.literal("KRW"),
|
|
29
|
+
includeTest: z.boolean().describe("테스트 결제 구매를 포함했는가"),
|
|
30
|
+
lastComputedAt: z
|
|
31
|
+
.string()
|
|
32
|
+
.nullable()
|
|
33
|
+
.describe("기간 안에서 가장 최근에 집계한 시각(ISO 8601). 집계된 날이 없으면 null"),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 비율(0 이상). 단계가 세션 안에서 꼭 이어지지는 않아 1을 넘을 수 있다 — 예: 조회 없이 장바구니에서 바로 산 구매,
|
|
38
|
+
* 자정을 넘겨 다음 날 확정된 구매.
|
|
39
|
+
*/
|
|
40
|
+
const rate = (description: string) => z.number().min(0).nullable().describe(description);
|
|
41
|
+
|
|
42
|
+
export const analyticsTotalsSchema = z.object({
|
|
43
|
+
visitors: z
|
|
44
|
+
.number()
|
|
45
|
+
.int()
|
|
46
|
+
.describe("방문자 수 — 날짜별 순방문자의 합. 여러 날 방문한 사람은 날마다 센다"),
|
|
47
|
+
sessions: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.describe("세션 수. 30분 동안 활동이 없거나 다른 캠페인으로 들어오면 새 세션"),
|
|
51
|
+
engagedSessions: z
|
|
52
|
+
.number()
|
|
53
|
+
.int()
|
|
54
|
+
.describe(
|
|
55
|
+
"참여 세션 수 — 페이지뷰 2개 이상, 체류 10초 이상, 장바구니 담기·결제 시작·구매 중 하나",
|
|
56
|
+
),
|
|
57
|
+
pageViews: z.number().int(),
|
|
58
|
+
engagementSeconds: z
|
|
59
|
+
.number()
|
|
60
|
+
.int()
|
|
61
|
+
.describe(
|
|
62
|
+
"페이지가 화면에 보인 시간의 합(초). 페이지뷰 하나는 최대 30분, 세션은 세션 길이 + 30분까지 센다",
|
|
63
|
+
),
|
|
64
|
+
productViews: z.number().int().describe("상품 조회 수"),
|
|
65
|
+
productViewSessions: z.number().int().describe("상품을 조회한 세션 수"),
|
|
66
|
+
addToCartSessions: z.number().int().describe("장바구니에 담은 세션 수"),
|
|
67
|
+
beginCheckoutSessions: z.number().int().describe("주문서를 연 세션 수"),
|
|
68
|
+
purchaseSessions: z.number().int().describe("구매한 세션 수"),
|
|
69
|
+
purchases: z.number().int().describe("구매(주문) 수"),
|
|
70
|
+
revenue: z
|
|
71
|
+
.number()
|
|
72
|
+
.int()
|
|
73
|
+
.describe(
|
|
74
|
+
"구매 금액 합계 — 주문 결제 금액(배송비 포함). 이후 취소·환불은 빼지 않으므로 매출 리포트의 순매출과 다르다",
|
|
75
|
+
),
|
|
76
|
+
engagementRate: rate("참여율 = 참여 세션 / 세션. 세션이 없으면 null"),
|
|
77
|
+
conversionRate: rate("전환율 = 구매 세션 / 세션. 세션이 없으면 null"),
|
|
78
|
+
});
|
|
79
|
+
export type AnalyticsTotals = z.infer<typeof analyticsTotalsSchema>;
|
|
80
|
+
|
|
81
|
+
export const analyticsFunnelStepSchema = z.object({
|
|
82
|
+
step: z.enum(["SESSION", "PRODUCT_VIEW", "ADD_TO_CART", "BEGIN_CHECKOUT", "PURCHASE"]),
|
|
83
|
+
sessions: z.number().int(),
|
|
84
|
+
rateFromPrevious: rate("직전 단계 대비 비율. 첫 단계이거나 직전 단계가 0이면 null"),
|
|
85
|
+
});
|
|
86
|
+
export type AnalyticsFunnelStep = z.infer<typeof analyticsFunnelStepSchema>;
|
|
87
|
+
|
|
88
|
+
export const analyticsOverviewSchema = z.object({
|
|
89
|
+
...rangeMeta,
|
|
90
|
+
summary: analyticsTotalsSchema.describe("기간 합계"),
|
|
91
|
+
funnel: z
|
|
92
|
+
.array(analyticsFunnelStepSchema)
|
|
93
|
+
.describe(
|
|
94
|
+
"세션 기준 구매 퍼널. 각 단계는 그 행동을 한 세션 수이고, 앞 단계를 거쳤는지는 따지지 않는다",
|
|
95
|
+
),
|
|
96
|
+
rows: z
|
|
97
|
+
.array(analyticsTotalsSchema.extend({ date: z.string().describe("일자 (YYYY-MM-DD)") }))
|
|
98
|
+
.describe("날짜별 값. 세션이 없는 날은 생략된다"),
|
|
99
|
+
});
|
|
100
|
+
export type AnalyticsOverview = z.infer<typeof analyticsOverviewSchema>;
|
|
101
|
+
|
|
102
|
+
export const analyticsProductSortSchema = z
|
|
103
|
+
.enum(["VIEWS", "ADD_TO_CARTS", "PURCHASES", "REVENUE", "IMPRESSIONS"])
|
|
104
|
+
.describe("정렬 기준(내림차순). 기본 `VIEWS`");
|
|
105
|
+
export type AnalyticsProductSort = z.infer<typeof analyticsProductSortSchema>;
|
|
106
|
+
|
|
107
|
+
export const analyticsProductsQuerySchema = analyticsRangeQuerySchema.extend({
|
|
108
|
+
sort: analyticsProductSortSchema.optional(),
|
|
109
|
+
limit: z.coerce
|
|
110
|
+
.number()
|
|
111
|
+
.int()
|
|
112
|
+
.min(1)
|
|
113
|
+
.max(100)
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("최대 행 수. 기본 20, 최대 100"),
|
|
116
|
+
});
|
|
117
|
+
export type AnalyticsProductsParams = z.infer<typeof analyticsProductsQuerySchema>;
|
|
118
|
+
|
|
119
|
+
export const analyticsProductRowSchema = z.object({
|
|
120
|
+
productId: z.string(),
|
|
121
|
+
productName: z.string().nullable().describe("현재 상품명. 삭제된 상품이면 null"),
|
|
122
|
+
impressions: z.number().int().describe("목록 노출 수"),
|
|
123
|
+
views: z.number().int().describe("상세 조회 수"),
|
|
124
|
+
viewSessions: z.number().int().describe("상세를 조회한 세션 수"),
|
|
125
|
+
addToCarts: z.number().int().describe("장바구니 담기 수"),
|
|
126
|
+
purchases: z.number().int().describe("이 상품이 포함된 구매(주문) 수"),
|
|
127
|
+
quantity: z.number().int().describe("구매 수량"),
|
|
128
|
+
revenue: z
|
|
129
|
+
.number()
|
|
130
|
+
.int()
|
|
131
|
+
.describe(
|
|
132
|
+
"이 상품의 구매 금액 — 주문 상품 금액(할인 반영, 배송비 제외) 합계. 이후 취소·환불은 빼지 않는다",
|
|
133
|
+
),
|
|
134
|
+
purchaseRate: rate("구매율 = 구매 / 조회 세션. 조회 세션이 없으면 null"),
|
|
135
|
+
});
|
|
136
|
+
export type AnalyticsProductRow = z.infer<typeof analyticsProductRowSchema>;
|
|
137
|
+
|
|
138
|
+
export const analyticsProductsSchema = z.object({
|
|
139
|
+
...rangeMeta,
|
|
140
|
+
sort: analyticsProductSortSchema,
|
|
141
|
+
rows: z.array(analyticsProductRowSchema),
|
|
142
|
+
});
|
|
143
|
+
export type AnalyticsProducts = z.infer<typeof analyticsProductsSchema>;
|
|
144
|
+
|
|
145
|
+
export const analyticsChannelSchema = z
|
|
146
|
+
.enum(["DIRECT", "ORGANIC_SEARCH", "SOCIAL", "PAID", "EMAIL", "CAMPAIGN", "REFERRAL", "UNKNOWN"])
|
|
147
|
+
.describe(
|
|
148
|
+
"유입 채널 — 세션 첫 페이지뷰의 utm·리퍼러로 정한다. " +
|
|
149
|
+
"`PAID`(utm_medium이 cpc·ppc·paid·display 등), `EMAIL`(utm_medium email), `SOCIAL`(SNS 리퍼러·utm_source), " +
|
|
150
|
+
"`ORGANIC_SEARCH`(검색 엔진 리퍼러), `CAMPAIGN`(그 밖의 utm), `REFERRAL`(그 밖의 리퍼러), `DIRECT`(없음), " +
|
|
151
|
+
"`UNKNOWN`(페이지뷰 없이 서버 이벤트만 있는 세션)",
|
|
152
|
+
);
|
|
153
|
+
export type AnalyticsChannel = z.infer<typeof analyticsChannelSchema>;
|
|
154
|
+
|
|
155
|
+
export const analyticsSourceGroupBySchema = z
|
|
156
|
+
.enum(["CHANNEL", "SOURCE", "CAMPAIGN", "DEVICE"])
|
|
157
|
+
.describe("행 집계 기준. 기본 `CHANNEL`");
|
|
158
|
+
export type AnalyticsSourceGroupBy = z.infer<typeof analyticsSourceGroupBySchema>;
|
|
159
|
+
|
|
160
|
+
export const analyticsSourcesQuerySchema = analyticsRangeQuerySchema.extend({
|
|
161
|
+
groupBy: analyticsSourceGroupBySchema.optional(),
|
|
162
|
+
});
|
|
163
|
+
export type AnalyticsSourcesParams = z.infer<typeof analyticsSourcesQuerySchema>;
|
|
164
|
+
|
|
165
|
+
export const analyticsSourceRowSchema = z.object({
|
|
166
|
+
channel: analyticsChannelSchema.nullable().describe("groupBy=CHANNEL일 때만 값"),
|
|
167
|
+
source: z
|
|
168
|
+
.string()
|
|
169
|
+
.nullable()
|
|
170
|
+
.describe(
|
|
171
|
+
"groupBy=SOURCE·CAMPAIGN일 때 값. utm_source, 없으면 리퍼러 호스트, 둘 다 없으면 `(direct)`",
|
|
172
|
+
),
|
|
173
|
+
medium: z
|
|
174
|
+
.string()
|
|
175
|
+
.nullable()
|
|
176
|
+
.describe("groupBy=SOURCE·CAMPAIGN일 때 값. utm_medium, 없으면 `referral`·`(none)`"),
|
|
177
|
+
campaign: z
|
|
178
|
+
.string()
|
|
179
|
+
.nullable()
|
|
180
|
+
.describe("groupBy=CAMPAIGN일 때 값. utm_campaign, 없으면 `(not set)`"),
|
|
181
|
+
device: z
|
|
182
|
+
.enum(["desktop", "mobile", "tablet", "unknown"])
|
|
183
|
+
.nullable()
|
|
184
|
+
.describe("groupBy=DEVICE일 때만 값"),
|
|
185
|
+
sessions: z.number().int(),
|
|
186
|
+
visitors: z.number().int().describe("날짜별 순방문자의 합"),
|
|
187
|
+
engagedSessions: z.number().int(),
|
|
188
|
+
purchaseSessions: z.number().int(),
|
|
189
|
+
purchases: z.number().int(),
|
|
190
|
+
revenue: z.number().int(),
|
|
191
|
+
conversionRate: rate("전환율 = 구매 세션 / 세션"),
|
|
192
|
+
});
|
|
193
|
+
export type AnalyticsSourceRow = z.infer<typeof analyticsSourceRowSchema>;
|
|
194
|
+
|
|
195
|
+
export const analyticsSourcesSchema = z.object({
|
|
196
|
+
...rangeMeta,
|
|
197
|
+
groupBy: analyticsSourceGroupBySchema,
|
|
198
|
+
rows: z.array(analyticsSourceRowSchema).describe("세션 수 내림차순"),
|
|
199
|
+
});
|
|
200
|
+
export type AnalyticsSources = z.infer<typeof analyticsSourcesSchema>;
|
package/src/schemas/scope.ts
CHANGED
|
@@ -14,7 +14,7 @@ export const scopeAccessSchema = z
|
|
|
14
14
|
.describe("접근 수준. `r`=읽기, `rw`=읽기+쓰기(r을 함의)");
|
|
15
15
|
export type ScopeAccess = z.infer<typeof scopeAccessSchema>;
|
|
16
16
|
|
|
17
|
-
/** 리소스별로 부여 가능한 접근 수준. settlement는 쓰기 API가 없어 `r`만 존재한다 */
|
|
17
|
+
/** 리소스별로 부여 가능한 접근 수준. settlement·analytics는 쓰기 API가 없어 `r`만 존재한다 */
|
|
18
18
|
export const STORE_SCOPE_RESOURCES = {
|
|
19
19
|
product: ["r", "rw"],
|
|
20
20
|
category: ["r", "rw"],
|
|
@@ -23,6 +23,7 @@ export const STORE_SCOPE_RESOURCES = {
|
|
|
23
23
|
inquiry: ["r", "rw"],
|
|
24
24
|
review: ["r", "rw"],
|
|
25
25
|
settlement: ["r"],
|
|
26
|
+
analytics: ["r"],
|
|
26
27
|
webhook: ["r", "rw"],
|
|
27
28
|
store: ["r", "rw"],
|
|
28
29
|
member: ["r", "rw"],
|
|
@@ -56,6 +57,7 @@ export const STORE_ROLE_SCOPES = {
|
|
|
56
57
|
"inquiry:rw",
|
|
57
58
|
"review:rw",
|
|
58
59
|
"settlement:r",
|
|
60
|
+
"analytics:r",
|
|
59
61
|
"webhook:rw",
|
|
60
62
|
"store:rw",
|
|
61
63
|
"member:rw",
|
|
@@ -67,6 +69,7 @@ export const STORE_ROLE_SCOPES = {
|
|
|
67
69
|
"claim:rw",
|
|
68
70
|
"inquiry:rw",
|
|
69
71
|
"review:rw",
|
|
72
|
+
"analytics:r",
|
|
70
73
|
"webhook:rw",
|
|
71
74
|
"store:rw",
|
|
72
75
|
"member:rw",
|
package/src/schemas/store.ts
CHANGED
|
@@ -73,6 +73,56 @@ export const storeCodeSchema = z
|
|
|
73
73
|
"플랫폼이 쓰는 이름이라 스토어 코드로 쓸 수 없어요",
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
+
/** `https://shop.example.com`처럼 경로·쿼리 없는 http(s) 오리진인가 */
|
|
77
|
+
export function isWebOrigin(value: string): boolean {
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(value);
|
|
80
|
+
return (
|
|
81
|
+
(url.protocol === "https:" || url.protocol === "http:") &&
|
|
82
|
+
!url.username &&
|
|
83
|
+
!url.password &&
|
|
84
|
+
url.pathname === "/" &&
|
|
85
|
+
!url.search &&
|
|
86
|
+
!url.hash &&
|
|
87
|
+
!value.includes("*")
|
|
88
|
+
);
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 방문 분석을 보낼 수 있는 스토어프런트 오리진 — 스킴·호스트(·포트)만. 서버가 소문자·끝 슬래시 제거로 정규화한다 */
|
|
95
|
+
export const analyticsOriginSchema = z
|
|
96
|
+
.string()
|
|
97
|
+
.trim()
|
|
98
|
+
.max(255)
|
|
99
|
+
.refine(isWebOrigin, "https://shop.example.com 형식(경로 없이)으로 입력해 주세요")
|
|
100
|
+
.describe("스토어프런트 오리진. 예: `https://shop.example.com`");
|
|
101
|
+
|
|
102
|
+
/** 허용 오리진 상한 */
|
|
103
|
+
export const MAX_ANALYTICS_ALLOWED_ORIGINS = 20;
|
|
104
|
+
|
|
105
|
+
/** `PATCH /v1/store` 본문 — 바꿀 필드만 보낸다 */
|
|
106
|
+
export const updateStoreRequestSchema = z
|
|
107
|
+
.object({
|
|
108
|
+
name: z.string().min(2).max(50).optional(),
|
|
109
|
+
logoUrl: z.url().optional(),
|
|
110
|
+
customerCenterPhone: z.string().optional(),
|
|
111
|
+
businessHours: z.string().max(100).optional(),
|
|
112
|
+
analyticsAllowedOrigins: z
|
|
113
|
+
.array(analyticsOriginSchema)
|
|
114
|
+
.max(MAX_ANALYTICS_ALLOWED_ORIGINS)
|
|
115
|
+
.optional()
|
|
116
|
+
.describe(
|
|
117
|
+
"방문 분석 수집을 받을 스토어프런트 오리진(최대 20개). 비우면 모든 오리진에서 받는다. " +
|
|
118
|
+
"넣으면 목록에 없는 오리진의 수집 요청은 403 `ORIGIN_NOT_ALLOWED`이고, 페이지 URL이 목록 밖인 이벤트는 버린다",
|
|
119
|
+
),
|
|
120
|
+
})
|
|
121
|
+
.refine((value) => Object.values(value).some((v) => v !== undefined), {
|
|
122
|
+
message: "최소 1개 필드가 필요합니다",
|
|
123
|
+
});
|
|
124
|
+
export type UpdateStoreRequest = z.infer<typeof updateStoreRequestSchema>;
|
|
125
|
+
|
|
76
126
|
export const storeSchema = z.object({
|
|
77
127
|
storeId: z.string(),
|
|
78
128
|
name: z.string(),
|
|
@@ -82,6 +132,9 @@ export const storeSchema = z.object({
|
|
|
82
132
|
logoUrl: z.url().nullable(),
|
|
83
133
|
businessNumber: z.string().nullable(),
|
|
84
134
|
customerCenterPhone: z.string().nullable(),
|
|
135
|
+
analyticsAllowedOrigins: z
|
|
136
|
+
.array(z.string())
|
|
137
|
+
.describe("방문 분석 수집을 받는 스토어프런트 오리진. 비어 있으면 모든 오리진에서 받는다"),
|
|
85
138
|
memberCount: z.number().int(),
|
|
86
139
|
createdAt: z.string(),
|
|
87
140
|
});
|