@sayren/store-sdk 0.30.19 → 0.31.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/src/schemas/ai.ts CHANGED
@@ -113,9 +113,16 @@ export type AiToolChange = z.infer<typeof aiToolChangeSchema>;
113
113
 
114
114
  /**
115
115
  * 에이전트 작업 단계(2026-10-13, AI 상점 도우미 작업 공간) — 에이전트가 패널 도구로 알린다.
116
- * 상점·상품·배포 준비 → 쇼핑몰 구조 설계 → 개발 환경 준비(라이브러리·SDK) → 코드 수정 루프 → 완료
116
+ * 상점·상품·배포 준비 → 쇼핑몰 구조 설계 → 디자인(아트보드, 2026-10-14 사용자 결정) → 개발 환경 준비(라이브러리·SDK) → 코드 수정 루프 → 완료
117
117
  */
118
- export const AI_AGENT_PHASES = ["STORE", "STRUCTURE", "ENVIRONMENT", "BUILD", "DONE"] as const;
118
+ export const AI_AGENT_PHASES = [
119
+ "STORE",
120
+ "STRUCTURE",
121
+ "DESIGN",
122
+ "ENVIRONMENT",
123
+ "BUILD",
124
+ "DONE",
125
+ ] as const;
119
126
  export const aiAgentPhaseSchema = z.enum(AI_AGENT_PHASES);
120
127
  export type AiAgentPhase = z.infer<typeof aiAgentPhaseSchema>;
121
128
 
@@ -135,6 +142,8 @@ export const aiTodoSchema = z.object({
135
142
  id: z.string().max(10).optional(),
136
143
  content: z.string().min(1).max(200),
137
144
  status: z.enum(AI_TODO_STATUSES),
145
+ /** 이 할 일이 속한 작업 단계 — 진행중이 되면 실행기가 단계를 알린다(2026-10-14, 모델이 단계 알림을 빠뜨렸다) */
146
+ phase: aiAgentPhaseSchema.optional(),
138
147
  });
139
148
  export type AiTodo = z.infer<typeof aiTodoSchema>;
140
149
  export const AI_TODOS_MAX = 30;
@@ -143,11 +152,106 @@ export function isAiAgentNotesPath(path: string): boolean {
143
152
  return path === AI_AGENT_NOTES_DIR || path.startsWith(`${AI_AGENT_NOTES_DIR}/`);
144
153
  }
145
154
 
155
+ /**
156
+ * 디자인 아트보드 폴더(2026-10-14 사용자 결정, Claude Code Design 캔버스의 파일 계약을 옮겼다) — DESIGN 단계에 에이전트가 쓰는 자기완결 HTML 아트보드와
157
+ * 인덱스 `canvas.json`이다. 앱 코드가 import하지 않아 발행 빌드에 들어가지 않고, 상점 플랫폼 작업 공간의 디자인 탭이 iframe(srcdoc)으로 그린다.
158
+ * 코드 탭 트리에서는 작업 메모처럼 뺀다
159
+ */
160
+ export const AI_DESIGN_DIR = "src/site/design";
161
+ export const AI_DESIGN_CANVAS_PATH = `${AI_DESIGN_DIR}/canvas.json`;
162
+ export function isAiDesignPath(path: string): boolean {
163
+ return path === AI_DESIGN_DIR || path.startsWith(`${AI_DESIGN_DIR}/`);
164
+ }
165
+ /** 아트보드 파일 이름 — `src/site/design/` 바로 아래의 `.html` 하나(폴더 없음) */
166
+ export const AI_DESIGN_BOARD_FILE_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}\.html$/;
167
+ export const AI_DESIGN_BOARD_KINDS = ["direction", "page", "scene"] as const;
168
+ export const AI_DESIGN_VIEWPORTS = ["desktop", "mobile"] as const;
169
+ /** 아트보드 상한 — 사이트맵의 화면마다 아트보드를 그린다(2026-10-15 사용자 결정, 화면 로드맵 29개 + 방향 후보 + 모바일). 24에서 올렸다 */
170
+ export const AI_DESIGN_BOARDS_MAX = 64;
171
+ /**
172
+ * 아트보드 하나 — `file`은 `src/site/design/` 아래 파일 이름, `kind`는 방향(저충실 후보)·페이지(고충실 화면)·장면(모션 시안, 셀러가 스크롤하며 본다),
173
+ * `viewport`는 데스크톱(1280)·모바일(390) 폭, `page`는 그 아트보드가 그리는 사이트 경로(구현 대조에 쓴다), `status`는 후보·채택·보관
174
+ */
175
+ export const aiDesignBoardSchema = z.object({
176
+ file: z.string().regex(AI_DESIGN_BOARD_FILE_PATTERN),
177
+ title: z.string().min(1).max(60),
178
+ kind: z.enum(AI_DESIGN_BOARD_KINDS),
179
+ viewport: z.enum(AI_DESIGN_VIEWPORTS).default("desktop"),
180
+ page: z.string().max(200).optional(),
181
+ status: z.enum(["candidate", "chosen", "archived"]).default("chosen"),
182
+ });
183
+ export type AiDesignBoard = z.infer<typeof aiDesignBoardSchema>;
184
+ export const aiDesignCanvasSchema = z.object({
185
+ version: z.literal(1),
186
+ /** 정한 방향 이름 — 방향 선택지에서 셀러가 고른 뒤 적는다(이름을 바꾸지 않는다) */
187
+ direction: z.string().max(80).nullable().default(null),
188
+ boards: z.array(aiDesignBoardSchema).max(AI_DESIGN_BOARDS_MAX),
189
+ /** 캔버스에 붙이는 메모(장 전환 규칙 등) */
190
+ notes: z.array(z.string().max(300)).max(20).default([]),
191
+ });
192
+ export type AiDesignCanvas = z.infer<typeof aiDesignCanvasSchema>;
193
+
194
+ /** 셀러가 디자인 탭에서 고른 아트보드(2026-10-14) — 메시지와 함께 보내면 모델은 그 아트보드를 기준으로 고친다 */
195
+ export const aiDesignSelectionSchema = z.object({
196
+ file: z.string().regex(AI_DESIGN_BOARD_FILE_PATTERN),
197
+ title: z.string().max(60),
198
+ });
199
+ export type AiDesignSelection = z.infer<typeof aiDesignSelectionSchema>;
200
+
201
+ /** 쇼핑몰 형태(#124, 2026-10-13) — 템플릿은 basic 하나이고 형태는 에이전트에게 주는 화면 구성 지시다 */
202
+ export const STORE_SHOP_TYPES = [
203
+ "BASIC",
204
+ "INTERACTIVE",
205
+ "STORYTELLING",
206
+ "MOBILE",
207
+ "CATALOG",
208
+ "LOOKBOOK",
209
+ ] as const;
210
+ export const storeShopTypeSchema = z.enum(STORE_SHOP_TYPES);
211
+ export type StoreShopType = z.infer<typeof storeShopTypeSchema>;
212
+
213
+ /**
214
+ * 선택지 하나 — `templateId`는 스토어프론트 템플릿을, `shopType`은 쇼핑몰 형태를 고르는 선택지일 때의 값이다(2026-10-14). 상점 플랫폼이 썸네일을 붙인다
215
+ */
216
+ export const aiChoiceOptionSchema = z.object({
217
+ label: z.string(),
218
+ value: z.string(),
219
+ templateId: z.string().max(64).optional(),
220
+ /** 쇼핑몰 형태를 고르는 선택지면 그 형태 — 상점 플랫폼이 형태 와이어프레임을 붙인다 */
221
+ shopType: storeShopTypeSchema.optional(),
222
+ /** 디자인 방향을 고르는 선택지면 그 아트보드 파일(`src/site/design/` 아래) — 상점 플랫폼이 아트보드를 썸네일로 붙인다(2026-10-14) */
223
+ boardFile: z.string().regex(AI_DESIGN_BOARD_FILE_PATTERN).optional(),
224
+ });
225
+ export type AiChoiceOption = z.infer<typeof aiChoiceOptionSchema>;
226
+
227
+ /**
228
+ * 쇼핑몰 만들기 확인(2026-10-14) — 에이전트가 대화로 모은 값(라벨·값)과 고른 템플릿을 보이고 「이렇게 쇼핑몰을 만들까요?」를 묻는다.
229
+ * 셀러가 승인(`AI_SETUP_CONFIRM_APPROVE` 선택지)하면 만들기를 시작한다
230
+ */
231
+ export const aiSetupConfirmItemSchema = z.object({ label: z.string(), value: z.string() });
232
+ export type AiSetupConfirmItem = z.infer<typeof aiSetupConfirmItemSchema>;
233
+ /** 확인 카드의 승인 선택지 값 */
234
+ export const AI_SETUP_CONFIRM_APPROVE = "setup:approve";
235
+
236
+ /**
237
+ * 디자인 확인(2026-10-14 사용자 결정) — DESIGN 단계 끝에 에이전트가 채택한 아트보드를 카드로 보이고 「이 디자인으로 만들까요?」를 묻는다.
238
+ * 셀러가 승인(`AI_DESIGN_CONFIRM_APPROVE` 선택지)해야 구현(ENVIRONMENT·BUILD)으로 넘어간다
239
+ */
240
+ export const aiDesignConfirmBoardSchema = z.object({
241
+ file: z.string().regex(AI_DESIGN_BOARD_FILE_PATTERN),
242
+ title: z.string().max(60),
243
+ });
244
+ export type AiDesignConfirmBoard = z.infer<typeof aiDesignConfirmBoardSchema>;
245
+ export const AI_DESIGN_CONFIRM_APPROVE = "design:approve";
246
+
146
247
  /**
147
248
  * 화면에 그리는 메시지 조각. 모델 원문(tool_use 블록 등)은 api가 보관하고 화면에는 이 형태만 준다.
148
249
  * - `text`: 글
149
250
  * - `tool`: 도구 호출 한 번(이름은 사람이 읽는 라벨, 결과 요약·링크)
150
251
  * - `choices`: 선택지 버튼(가장 최근 것만 누를 수 있다)
252
+ * - `setup_confirm`: 쇼핑몰 만들기 확인 카드(모은 값·템플릿, 승인은 뒤따르는 `choices`)
253
+ * - `design_confirm`: 디자인 확인 카드(채택한 아트보드, 승인은 뒤따르는 `choices`)
254
+ * - `design_selection`: 사용자가 디자인 탭에서 고른 아트보드
151
255
  * - `business_form`: 사업자 정보 입력 폼(가장 최근 것만 제출할 수 있다, 건너뛸 수 있다)
152
256
  * - `attachment`: 사용자가 올린 첨부
153
257
  * - `approval`: 승인 대기로 남은 쓰기
@@ -174,13 +278,23 @@ export const aiMessagePartSchema = z.discriminatedUnion("type", [
174
278
  z.object({
175
279
  type: z.literal("choices"),
176
280
  question: z.string(),
177
- options: z
178
- .array(z.object({ label: z.string(), value: z.string() }))
179
- .min(1)
180
- .max(6),
281
+ options: z.array(aiChoiceOptionSchema).min(1).max(6),
181
282
  }),
182
283
  z.object({ type: z.literal("business_form"), question: z.string() }),
284
+ /** 쇼핑몰 만들기 확인 카드 — 아래 선택지 조각이 승인을 묻는다 */
285
+ z.object({
286
+ type: z.literal("setup_confirm"),
287
+ items: z.array(aiSetupConfirmItemSchema).max(12),
288
+ templateId: z.string().max(64).optional(),
289
+ }),
290
+ /** 디자인 확인 카드 — 채택한 아트보드, 아래 선택지 조각이 승인을 묻는다 */
291
+ z.object({
292
+ type: z.literal("design_confirm"),
293
+ boards: z.array(aiDesignConfirmBoardSchema).min(1).max(AI_DESIGN_BOARDS_MAX),
294
+ }),
183
295
  z.object({ type: z.literal("attachment"), attachment: aiAttachmentSchema }),
296
+ /** 사용자가 디자인 탭에서 고른 아트보드 */
297
+ z.object({ type: z.literal("design_selection"), selection: aiDesignSelectionSchema }),
184
298
  /** 공개 뒤 승인이 필요한 쓰기를 승인 요청으로 남겼다(셀러가 승인함에서 결정한다) */
185
299
  z.object({ type: z.literal("approval"), approvalId: z.string(), title: z.string() }),
186
300
  /** 사용자가 미리보기에서 고른 요소(#65) */
@@ -218,18 +332,6 @@ export const aiConversationSchema = aiConversationSummarySchema.extend({
218
332
  });
219
333
  export type AiConversation = z.infer<typeof aiConversationSchema>;
220
334
 
221
- /** 쇼핑몰 형태(#124, 2026-10-13) — 템플릿은 basic 하나이고 형태는 에이전트에게 주는 화면 구성 지시다 */
222
- export const STORE_SHOP_TYPES = [
223
- "BASIC",
224
- "INTERACTIVE",
225
- "STORYTELLING",
226
- "MOBILE",
227
- "CATALOG",
228
- "LOOKBOOK",
229
- ] as const;
230
- export const storeShopTypeSchema = z.enum(STORE_SHOP_TYPES);
231
- export type StoreShopType = z.infer<typeof storeShopTypeSchema>;
232
-
233
335
  /**
234
336
  * AI 상점 만들기 질문 화면의 답(#124, 2026-10-13) — 화면이 고정 질문으로 모은다. 초안과 함께 첫 메시지가 된다.
235
337
  * 배송비는 묻지 않는다(설정하지 않으면 무료배송). `slug`는 바라는 주소이고 쓸 수 있는지는 에이전트가 사이트를 만들 때 확인한다
@@ -268,6 +370,8 @@ export const sendAiMessageRequestSchema = z
268
370
  choice: z.string().max(200).optional(),
269
371
  /** 편집 미리보기에서 고른 요소(#65) — 모델은 이 요소를 기준으로 고친다 */
270
372
  selection: aiPreviewSelectionSchema.optional(),
373
+ /** 디자인 탭에서 고른 아트보드(2026-10-14) — 모델은 그 아트보드를 기준으로 고친다 */
374
+ designSelection: aiDesignSelectionSchema.optional(),
271
375
  })
272
376
  .refine((body) => body.text.length > 0 || body.attachments.length > 0, {
273
377
  message: "내용이나 첨부가 있어야 합니다",
@@ -428,10 +532,23 @@ export const aiStreamEventSchema = z.discriminatedUnion("type", [
428
532
  type: z.literal("choices"),
429
533
  messageId: z.string(),
430
534
  question: z.string(),
431
- options: z.array(z.object({ label: z.string(), value: z.string() })),
535
+ options: z.array(aiChoiceOptionSchema),
432
536
  }),
433
537
  /** 사업자 정보 입력 폼을 보였다 — 답은 다음 메시지로 온다 */
434
538
  z.object({ type: z.literal("business_form"), messageId: z.string(), question: z.string() }),
539
+ /** 쇼핑몰 만들기 확인 카드를 보였다 — 이어서 승인 선택지(`choices`)가 온다 */
540
+ z.object({
541
+ type: z.literal("setup_confirm"),
542
+ messageId: z.string(),
543
+ items: z.array(aiSetupConfirmItemSchema),
544
+ templateId: z.string().max(64).optional(),
545
+ }),
546
+ /** 디자인 확인 카드를 보였다 — 이어서 승인 선택지(`choices`)가 온다 */
547
+ z.object({
548
+ type: z.literal("design_confirm"),
549
+ messageId: z.string(),
550
+ boards: z.array(aiDesignConfirmBoardSchema),
551
+ }),
435
552
  /** 공개 뒤 승인이 필요한 쓰기를 승인 요청으로 남겼다 */
436
553
  z.object({
437
554
  type: z.literal("approval"),
@@ -10,10 +10,21 @@ import { type PageParams, pageSchema } from "./common";
10
10
  */
11
11
 
12
12
  export const pgProviderSchema = z
13
- .enum(["tosspayments", "portone"])
14
- .describe("PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2)");
13
+ .enum(["tosspayments", "portone", "kakaopay", "naverpay"])
14
+ .describe(
15
+ "PG 식별자. `tosspayments`=토스페이먼츠, `portone`=포트원(V2), `kakaopay`=카카오페이(직접 연동), `naverpay`=네이버페이 결제형(직접 연동)",
16
+ );
15
17
  export type PgProvider = z.infer<typeof pgProviderSchema>;
16
18
 
19
+ /**
20
+ * 간편결제 전용 PG — 결제수단이 간편결제 하나이고 간편결제사가 PG 자신이다(`kakaopay` → `KAKAOPAY`).
21
+ * 이 PG는 `easyPayProviders` 설정 없이 결제 옵션이 하나(`EASY_PAY` + 그 간편결제사) 나온다
22
+ */
23
+ export const DEDICATED_EASY_PAY_PROVIDER = {
24
+ kakaopay: "KAKAOPAY",
25
+ naverpay: "NAVERPAY",
26
+ } as const satisfies Partial<Record<PgProvider, string>>;
27
+
17
28
  export const pgPaymentMethodSchema = z
18
29
  .enum(["CARD", "BANK_TRANSFER", "VIRTUAL_ACCOUNT", "MOBILE", "EASY_PAY"])
19
30
  .describe("결제수단 (스토어프론트 결제 요청의 `paymentMethod`와 같은 값)");
@@ -138,6 +149,45 @@ export const portOneCredentialsInputSchema = z.object({
138
149
  });
139
150
  export type PortOneCredentialsInput = z.infer<typeof portOneCredentialsInputSchema>;
140
151
 
152
+ /** 카카오페이 테스트 가맹점 코드 — 카카오페이 개발자센터의 단건 결제 테스트 CID */
153
+ export const KAKAOPAY_TEST_CID = "TC0ONETIME";
154
+
155
+ export const kakaoPayCredentialsInputSchema = z.object({
156
+ cid: z
157
+ .string()
158
+ .trim()
159
+ .min(4)
160
+ .max(20)
161
+ .describe(
162
+ "가맹점 코드(CID). 카카오페이와 계약하면 받는다. 공개 값. 테스트 세트는 `TC0ONETIME`(단건 결제 테스트 CID)을 쓸 수 있다",
163
+ ),
164
+ secretKey: secretInput()
165
+ .optional()
166
+ .describe(
167
+ "카카오페이 개발자센터 앱의 Secret key(테스트 세트는 dev, 실결제 세트는 운영 키). 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다",
168
+ ),
169
+ });
170
+ export type KakaoPayCredentialsInput = z.infer<typeof kakaoPayCredentialsInputSchema>;
171
+
172
+ export const naverPayCredentialsInputSchema = z.object({
173
+ clientId: z
174
+ .string()
175
+ .trim()
176
+ .min(4)
177
+ .max(100)
178
+ .describe("네이버페이 결제형 가입 후 받은 Client ID. 공개 값 — 결제창에서 쓰인다"),
179
+ chainId: z
180
+ .string()
181
+ .trim()
182
+ .min(4)
183
+ .max(100)
184
+ .describe("네이버페이 Chain ID. 공개 값 — 결제창과 API 요청에 쓰인다"),
185
+ clientSecret: secretInput()
186
+ .optional()
187
+ .describe("Client Secret. 비밀 값. 최초 연결 시 필수, 수정 시 생략하면 기존 값을 유지한다"),
188
+ });
189
+ export type NaverPayCredentialsInput = z.infer<typeof naverPayCredentialsInputSchema>;
190
+
141
191
  const upsertEnvironment = () =>
142
192
  pgEnvironmentSchema.describe(
143
193
  "저장할 자격 증명 세트. PG마다 `TEST`(샌드박스)·`LIVE`(실결제) 세트를 따로 둔다. " +
@@ -158,6 +208,18 @@ export const upsertPortOneProviderRequestSchema = z.object({
158
208
  });
159
209
  export type UpsertPortOneProviderRequest = z.infer<typeof upsertPortOneProviderRequestSchema>;
160
210
 
211
+ export const upsertKakaoPayProviderRequestSchema = z.object({
212
+ environment: upsertEnvironment(),
213
+ credentials: kakaoPayCredentialsInputSchema,
214
+ });
215
+ export type UpsertKakaoPayProviderRequest = z.infer<typeof upsertKakaoPayProviderRequestSchema>;
216
+
217
+ export const upsertNaverPayProviderRequestSchema = z.object({
218
+ environment: upsertEnvironment(),
219
+ credentials: naverPayCredentialsInputSchema,
220
+ });
221
+ export type UpsertNaverPayProviderRequest = z.infer<typeof upsertNaverPayProviderRequestSchema>;
222
+
161
223
  export const updatePaymentProviderRequestSchema = z
162
224
  .object({
163
225
  enabled: z
@@ -240,6 +302,8 @@ export type MaskedSecret = z.infer<typeof maskedSecretSchema>;
240
302
  const publicConfigSchema = z.union([
241
303
  z.object({ clientKey: z.string() }),
242
304
  z.object({ storeId: z.string(), channels: z.array(portOneChannelSchema) }),
305
+ z.object({ cid: z.string() }),
306
+ z.object({ clientId: z.string(), chainId: z.string() }),
243
307
  ]);
244
308
 
245
309
  const verificationSchema = z.object({
@@ -251,7 +315,7 @@ const verificationSchema = z.object({
251
315
  export const paymentCredentialSetSchema = z.object({
252
316
  environment: pgEnvironmentSchema,
253
317
  publicConfig: publicConfigSchema.describe(
254
- "공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`",
318
+ "공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, 네이버페이는 `{clientId, chainId}`",
255
319
  ),
256
320
  secrets: z
257
321
  .array(maskedSecretSchema)
@@ -324,7 +388,8 @@ export const paymentProviderSchema = z.object({
324
388
  publicConfig: publicConfigSchema
325
389
  .nullable()
326
390
  .describe(
327
- "활성 환경 세트의 공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`. 활성 세트가 없으면 null",
391
+ "활성 환경 세트의 공개 값. 토스페이먼츠는 `{clientKey}`, 포트원은 `{storeId, channels}`, 카카오페이는 `{cid}`, " +
392
+ "네이버페이는 `{clientId, chainId}`. 활성 세트가 없으면 null",
328
393
  ),
329
394
  secrets: z
330
395
  .array(maskedSecretSchema)
@@ -345,6 +410,18 @@ export const paymentProviderSchema = z.object({
345
410
  })
346
411
  .describe("환경별 자격 증명 세트. 등록되지 않은 세트는 null"),
347
412
  liveGate: liveGateSchema.describe("라이브 개방 조건. 조건을 모두 충족해야 샌드박스를 끌 수 있다"),
413
+ platformTestKeys: z
414
+ .boolean()
415
+ .optional()
416
+ .default(false)
417
+ .describe(
418
+ "테스트모드 키 활성화. `true`면 TEST 세트가 sayren 지정 테스트 키로 고정된다(TEST 세트를 직접 저장·제거하면 409 `PLATFORM_TEST_KEYS_ENABLED`)",
419
+ ),
420
+ platformTestKeysAvailable: z
421
+ .boolean()
422
+ .optional()
423
+ .default(false)
424
+ .describe("이 PG에 sayren 지정 테스트 키가 있어 테스트모드 키를 켤 수 있는가"),
348
425
  easyPayProviders: z
349
426
  .array(easyPayProviderResponseSchema)
350
427
  .describe(
@@ -421,7 +498,17 @@ export const paymentSettingsSchema = z.object({
421
498
  .describe("결제 도메인 — 결제를 마친 구매자가 돌아올 스토어프론트 origin 목록"),
422
499
  providers: z
423
500
  .array(paymentProviderSchema)
424
- .describe("지원하는 모든 PG. 연결된 것은 우선순위순, 연결되지 않은 것은 뒤에 온다"),
501
+ .describe(
502
+ "연결된 PG(우선순위순)와 연결되지 않은 토스페이먼츠·포트원. 카카오페이·네이버페이는 연결했을 때만 여기에 온다" +
503
+ "(이 PG를 모르는 이전 SDK가 응답을 읽을 수 있게). 연결할 수 있는 PG 전체는 `connectableProviders`다",
504
+ ),
505
+ connectableProviders: z
506
+ .array(paymentProviderSchema)
507
+ .optional()
508
+ .default(() => [])
509
+ .describe(
510
+ "연결되지 않은 PG 전체(카카오페이·네이버페이 포함). 서버는 항상 싣는다. 이 필드가 없는 이전 서버 응답은 기본값(빈 배열)으로 읽는다",
511
+ ),
425
512
  environment: pgEnvironmentSchema
426
513
  .nullable()
427
514
  .optional()
@@ -776,3 +863,38 @@ export interface PaymentListParams extends PageParams {
776
863
  /** 환불 대기가 있는 결제만(#115) */
777
864
  refundHeld?: boolean;
778
865
  }
866
+
867
+ /** 테스트모드 키 활성화 요청(2026-10-14) — `PUT /v1/store/payment-providers/{provider}/platform-test-keys` */
868
+ export const setPlatformTestKeysRequestSchema = z.object({
869
+ enabled: z
870
+ .boolean()
871
+ .describe(
872
+ "`true`면 sayren 지정 테스트 키를 TEST 세트로 저장하고 PG를 켠다. `false`면 고정만 푼다(TEST 세트는 그대로)",
873
+ ),
874
+ });
875
+ export type SetPlatformTestKeysRequest = z.infer<typeof setPlatformTestKeysRequestSchema>;
876
+
877
+ /** 결제 상태 PG 한 줄 — 비밀 값·공개 키 없이 연결·사용·환경·테스트모드 키만 */
878
+ export const paymentStatusProviderSchema = z.object({
879
+ provider: pgProviderSchema,
880
+ displayName: z.string(),
881
+ connected: z.boolean().describe("자격 증명이 저장돼 있는가"),
882
+ enabled: z.boolean().describe("결제에 사용 중인가"),
883
+ environment: pgEnvironmentSchema
884
+ .nullable()
885
+ .describe("새 결제에 쓰는 환경. 연결되지 않았으면 null"),
886
+ platformTestKeys: z.boolean().describe("테스트모드 키 활성화 여부"),
887
+ platformTestKeysAvailable: z.boolean().describe("sayren 지정 테스트 키로 바로 켤 수 있는가"),
888
+ });
889
+ export type PaymentStatusProvider = z.infer<typeof paymentStatusProviderSchema>;
890
+
891
+ /** 결제 상태(2026-10-14) — `GET /v1/store/payment-status`. 지원 PG 전체와 상점의 연결 상태 */
892
+ export const paymentStatusSchema = z.object({
893
+ environment: pgEnvironmentSchema
894
+ .nullable()
895
+ .describe("새 결제 세션의 환경(사용 중인 PG 기준). 쓸 수 있는 PG가 없으면 null"),
896
+ providers: z
897
+ .array(paymentStatusProviderSchema)
898
+ .describe("sayren이 지원하는 PG 전체(연결 안 된 PG 포함)"),
899
+ });
900
+ export type PaymentStatus = z.infer<typeof paymentStatusSchema>;
@@ -952,6 +952,20 @@ export type StorefrontEditSession = z.infer<typeof storefrontEditSessionSchema>;
952
952
  export const storefrontDevLogsSchema = z.object({
953
953
  stdout: z.string(),
954
954
  stderr: z.string(),
955
+ browser: z
956
+ .array(
957
+ z.object({
958
+ seq: z.number().int().describe("세션 안에서 오르는 순번"),
959
+ at: z.string().describe("받은 시각"),
960
+ level: z.enum(["error", "warn"]),
961
+ message: z.string(),
962
+ url: z.string().describe("로그를 남긴 페이지 경로"),
963
+ }),
964
+ )
965
+ .default([])
966
+ .describe(
967
+ "편집 미리보기를 연 브라우저의 오류·경고(window 오류, 처리되지 않은 Promise 거부, console.error·warn) 최근 100건. 미리보기를 열어 둔 동안만 모인다",
968
+ ),
955
969
  at: z.string(),
956
970
  });
957
971
  export type StorefrontDevLogs = z.infer<typeof storefrontDevLogsSchema>;