Forráskód Böngészése

feat: add customer simulation gallery api

ChenYu 4 hónapja
szülő
commit
47763dd046

+ 81 - 0
frontend/src/services/api/__tests__/simulations.test.ts

@@ -0,0 +1,81 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import {
+  createSimulation,
+  getSimulationById,
+  listCustomerSimulationGallery,
+  listSimulationTemplates,
+} from '../simulations'
+import { http } from '../../http/client'
+
+vi.mock('../../http/client', () => ({
+  http: {
+    get: vi.fn(),
+    post: vi.fn(),
+    patch: vi.fn(),
+  },
+  ApiError: class ApiError extends Error {
+    status = 500
+  },
+}))
+
+describe('simulations api', () => {
+  beforeEach(() => {
+    vi.mocked(http.get).mockReset()
+    vi.mocked(http.post).mockReset()
+  })
+
+  it('builds simulation detail requests with path params', async () => {
+    vi.mocked(http.get).mockResolvedValue({ id: '91' })
+
+    await getSimulationById('91')
+
+    expect(http.get).toHaveBeenCalledWith('/simulations/91')
+  })
+
+  it('serializes simulation template query params', async () => {
+    vi.mocked(http.get).mockResolvedValue({
+      items: [],
+      total: 0,
+      page: 1,
+      pageSize: 12,
+    })
+
+    await listSimulationTemplates({
+      source: 'SYSTEM',
+      category: 'SHORT',
+      q: 'bob',
+      page: 1,
+      pageSize: 12,
+    })
+
+    expect(http.get).toHaveBeenCalledWith(
+      '/simulations/template-options?source=SYSTEM&category=SHORT&q=bob&page=1&pageSize=12',
+    )
+  })
+
+  it('posts the new simulation payload shape', async () => {
+    vi.mocked(http.post).mockResolvedValue({ simulationId: '91' })
+
+    await createSimulation({
+      customerId: '5',
+      originImage: 'https://cdn.example.com/origin.jpg',
+      templateIds: ['11'],
+      simulateRemark: 'airy',
+    })
+
+    expect(http.post).toHaveBeenCalledWith('/simulations', {
+      customerId: '5',
+      originImage: 'https://cdn.example.com/origin.jpg',
+      templateIds: ['11'],
+      simulateRemark: 'airy',
+    })
+  })
+
+  it('builds customer simulation gallery requests with customerId', async () => {
+    vi.mocked(http.get).mockResolvedValue({ items: [] })
+
+    await listCustomerSimulationGallery('5')
+
+    expect(http.get).toHaveBeenCalledWith('/simulations/customer/5/gallery')
+  })
+})

+ 51 - 0
frontend/src/services/api/contracts.ts

@@ -77,6 +77,57 @@ export type SimulationTemplateEntity = {
   image: string
   category: string
 }
+export type SimulationTemplateOption = {
+  id: string
+  templateName: string
+  coverUrl: string
+  category: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
+  tags: string[]
+  description: string | null
+  templateUrl: string
+  isSystemTemplate: boolean
+}
+export type AiHairSimulationTaskDetail = {
+  id: string
+  templateId: string
+  templateName: string
+  templateCoverUrl: string
+  templateCategory: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
+  templateSource: 'STYLIST' | 'SYSTEM'
+  taskId: string
+  taskStatus: 'PROCESSING' | 'SUCCESSES' | 'FAILED'
+  resultImages: string[]
+  errorMsg: string | null
+  taskStartTime: string
+  taskFinishTime: string | null
+}
+export type AiHairSimulationDetail = {
+  id: string
+  createdAt: string
+  customer: {
+    id: string
+    name: string
+    avatar: string
+    phone: string
+    expectContact: 'LINE' | 'PHONE' | 'EMAIL' | 'NONE'
+  } | null
+  originImage: string
+  simulateRemark: string | null
+  totalTaskCount: number
+  processingTaskCount: number
+  successTaskCount: number
+  failedTaskCount: number
+  overallStatus: 'PROCESSING' | 'PARTIAL_SUCCESS' | 'SUCCESSES' | 'FAILED'
+  tasks: AiHairSimulationTaskDetail[]
+}
+export type CustomerSimulationGalleryItem = {
+  simulationId: string
+  taskId: string
+  imageUrl: string
+  templateName: string
+  createdAt: string
+  taskStatus: 'SUCCESSES'
+}
 export type StylistProfileTemplateEntity = MockStylistProfileTemplate
 export type StylistReviewCardEntity = MockStylistReviewCard
 export type CustomerHistoryEntity = MockCustomerHistory

+ 51 - 3
frontend/src/services/api/simulations.ts

@@ -1,6 +1,11 @@
 import { http, type ListEnvelope } from '../http/client'
 import { mockDb } from '../../mocks/db'
-import type { SimulationResultEntity } from './contracts'
+import type {
+  AiHairSimulationDetail,
+  CustomerSimulationGalleryItem,
+  SimulationResultEntity,
+  SimulationTemplateOption,
+} from './contracts'
 
 export { uploadMedia } from './media'
 
@@ -27,6 +32,13 @@ export type SimulationJob = {
 }
 
 export type CreateSimulationInput = {
+  customerId?: string
+  originImage: string
+  templateIds: string[]
+  simulateRemark?: string
+}
+
+export type LegacyCreateSimulationInput = {
   customerMode: 'selected' | 'guest'
   customerId?: string
   sourcePhoto: string
@@ -43,8 +55,44 @@ export type AdjustmentInput = {
   replacementPhoto?: string
 }
 
-export async function createSimulation(input: CreateSimulationInput): Promise<SimulationJob> {
-  return http.post<SimulationJob>('/simulations', input)
+export type ListSimulationTemplatesParams = {
+  source: 'STYLIST' | 'SYSTEM'
+  category?: 'LONG' | 'MEDIUM' | 'SHORT' | 'DYE'
+  q?: string
+  page?: number
+  pageSize?: number
+}
+
+export async function listSimulationTemplates(
+  params: ListSimulationTemplatesParams,
+): Promise<ListEnvelope<SimulationTemplateOption> & { page: number; pageSize: number }> {
+  const query = new URLSearchParams()
+
+  query.set('source', params.source)
+  if (params.category) query.set('category', params.category)
+  if (params.q) query.set('q', params.q)
+  if (params.page) query.set('page', String(params.page))
+  if (params.pageSize) query.set('pageSize', String(params.pageSize))
+
+  return http.get(`/simulations/template-options?${query.toString()}`)
+}
+
+export function createSimulation(input: CreateSimulationInput): Promise<{ simulationId: string }>
+export function createSimulation(input: LegacyCreateSimulationInput): Promise<SimulationJob>
+export async function createSimulation(
+  input: CreateSimulationInput | LegacyCreateSimulationInput,
+): Promise<{ simulationId: string } | SimulationJob> {
+  return http.post('/simulations', input)
+}
+
+export async function getSimulationById(simulationId: string): Promise<AiHairSimulationDetail> {
+  return http.get(`/simulations/${simulationId}`)
+}
+
+export async function listCustomerSimulationGallery(
+  customerId: string,
+): Promise<{ items: CustomerSimulationGalleryItem[] }> {
+  return http.get(`/simulations/customer/${customerId}/gallery`)
 }
 
 export async function getSimulationJob(jobId: number): Promise<SimulationJob> {