Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/features/retrospectives/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './retrospectiveQueryKeys'
export * from './useCreateSttJob'
export * from './usePublishSummary'
export * from './useSummary'
export * from './useUpdateSummary'
6 changes: 6 additions & 0 deletions src/features/retrospectives/hooks/retrospectiveQueryKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const retrospectiveQueryKeys = {
all: ['retrospectives'] as const,

summaries: () => [...retrospectiveQueryKeys.all, 'summary'] as const,
summary: (meetingId: number) => [...retrospectiveQueryKeys.summaries(), meetingId] as const,
}
50 changes: 50 additions & 0 deletions src/features/retrospectives/hooks/useCreateSttJob.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMutation } from '@tanstack/react-query'
import { useCallback, useEffect, useRef } from 'react'

import type { ApiError } from '@/api'

import { createSttJob } from '../retrospectives.api'
import type { CreateSttJobParams, SttJobResponse } from '../retrospectives.types'

/**
* STT Job 생성 (AI 요약 트리거) mutation 훅
*
* - 동기 API이므로 mutation pending 동안 로딩 오버레이 표시
* - cancel() 호출 시 진행 중인 HTTP 요청 중단
* - 컴포넌트 언마운트 시 자동 취소
*/
export const useCreateSttJob = () => {
const abortControllerRef = useRef<AbortController | null>(null)

useEffect(() => {
return () => {
abortControllerRef.current?.abort()
abortControllerRef.current = null
}
}, [])

const mutation = useMutation<SttJobResponse, ApiError, CreateSttJobParams>({
mutationFn: (params: CreateSttJobParams) => {
abortControllerRef.current?.abort()

const controller = new AbortController()
abortControllerRef.current = controller

return createSttJob(params, controller.signal).finally(() => {
if (abortControllerRef.current === controller) {
abortControllerRef.current = null
}
})
},
})

const { reset } = mutation

const cancel = useCallback(() => {
abortControllerRef.current?.abort()
abortControllerRef.current = null
reset()
}, [reset])

return { ...mutation, cancel }
}
18 changes: 18 additions & 0 deletions src/features/retrospectives/hooks/usePublishSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import type { ApiError } from '@/api'

import { publishSummary } from '../retrospectives.api'
import type { PublishSummaryParams, RetrospectiveSummaryResponse } from '../retrospectives.types'
import { retrospectiveQueryKeys } from './retrospectiveQueryKeys'

export const usePublishSummary = () => {
const queryClient = useQueryClient()

return useMutation<RetrospectiveSummaryResponse, ApiError, PublishSummaryParams>({
mutationFn: (params: PublishSummaryParams) => publishSummary(params),
onSuccess: (data, variables) => {
queryClient.setQueryData(retrospectiveQueryKeys.summary(variables.meetingId), data)
},
})
}
17 changes: 17 additions & 0 deletions src/features/retrospectives/hooks/useSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query'

import type { ApiError } from '@/api'

import { getSummary } from '../retrospectives.api'
import type { RetrospectiveSummaryResponse } from '../retrospectives.types'
import { retrospectiveQueryKeys } from './retrospectiveQueryKeys'

export const useSummary = (meetingId: number) => {
const isValid = !Number.isNaN(meetingId) && meetingId > 0

return useQuery<RetrospectiveSummaryResponse, ApiError>({
queryKey: retrospectiveQueryKeys.summary(meetingId),
queryFn: () => getSummary(meetingId),
enabled: isValid,
})
}
18 changes: 18 additions & 0 deletions src/features/retrospectives/hooks/useUpdateSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'

import type { ApiError } from '@/api'

import { updateSummary } from '../retrospectives.api'
import type { RetrospectiveSummaryResponse, UpdateSummaryParams } from '../retrospectives.types'
import { retrospectiveQueryKeys } from './retrospectiveQueryKeys'

export const useUpdateSummary = () => {
const queryClient = useQueryClient()

return useMutation<RetrospectiveSummaryResponse, ApiError, UpdateSummaryParams>({
mutationFn: (params: UpdateSummaryParams) => updateSummary(params),
onSuccess: (data, variables) => {
queryClient.setQueryData(retrospectiveQueryKeys.summary(variables.meetingId), data)
},
})
}
21 changes: 21 additions & 0 deletions src/features/retrospectives/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,23 @@
// Components
export * from './components'

// Hooks
export * from './hooks'

// API
export { createSttJob, getSummary, publishSummary, updateSummary } from './retrospectives.api'

// Types
export type {
CreateSttJobParams,
KeyPoint,
KeyPointUpdateRequest,
PublishSummaryParams,
RetrospectiveSummaryResponse,
SttJobResponse,
SttJobStatus,
SummaryTopic,
UpdateSummaryParams,
UpdateSummaryRequest,
UpdateSummaryTopicRequest,
} from './retrospectives.types'
82 changes: 82 additions & 0 deletions src/features/retrospectives/retrospectives.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @file retrospectives.api.ts
* @description 약속 회고 AI 요약 API 요청 함수
*/

import { api } from '@/api/client'

import { RETROSPECTIVES_ENDPOINTS } from './retrospectives.endpoints'
import type {
CreateSttJobParams,
PublishSummaryParams,
RetrospectiveSummaryResponse,
SttJobResponse,
UpdateSummaryParams,
} from './retrospectives.types'

/** STT 요청 타임아웃: 5분 (동기 API이므로 충분히 길게) */
const STT_TIMEOUT = 5 * 60 * 1000

/**
* STT Job 생성 (AI 요약 트리거)
*
* @description
* 녹음 파일을 업로드하거나 사전 의견만으로 STT + AI 요약을 실행합니다.
* 동기 API이므로 완료될 때까지 블로킹됩니다.
*
* @param params - 생성 파라미터
* @param signal - AbortSignal (취소 지원)
*/
export const createSttJob = async (
params: CreateSttJobParams,
signal?: AbortSignal
): Promise<SttJobResponse> => {
const { gatheringId, meetingId, file } = params

const formData = new FormData()
if (file) {
formData.append('file', file)
}

return api.post<SttJobResponse>(
RETROSPECTIVES_ENDPOINTS.STT_JOBS(gatheringId, meetingId),
formData,
{
headers: { 'Content-Type': undefined },
timeout: STT_TIMEOUT,
signal,
}
)
}

/**
* 회고 요약 조회
*
* @param meetingId - 약속 식별자
*/
export const getSummary = async (meetingId: number): Promise<RetrospectiveSummaryResponse> => {
return api.get<RetrospectiveSummaryResponse>(RETROSPECTIVES_ENDPOINTS.SUMMARY(meetingId))
}

/**
* 회고 요약 수정
*
* @param params - 수정 파라미터 (meetingId + topics 데이터)
*/
export const updateSummary = async (
params: UpdateSummaryParams
): Promise<RetrospectiveSummaryResponse> => {
const { meetingId, data } = params
return api.patch<RetrospectiveSummaryResponse>(RETROSPECTIVES_ENDPOINTS.SUMMARY(meetingId), data)
}

/**
* 회고 요약 발행 (약속 회고 생성)
*
* @param params - 발행 파라미터 (meetingId)
*/
export const publishSummary = async (
params: PublishSummaryParams
): Promise<RetrospectiveSummaryResponse> => {
return api.post<RetrospectiveSummaryResponse>(RETROSPECTIVES_ENDPOINTS.PUBLISH(params.meetingId))
}
14 changes: 14 additions & 0 deletions src/features/retrospectives/retrospectives.endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { API_PATHS } from '@/api'

export const RETROSPECTIVES_ENDPOINTS = {
/** STT Job 생성 (POST) */
STT_JOBS: (gatheringId: number, meetingId: number) =>
`${API_PATHS.GATHERINGS}/${gatheringId}/meetings/${meetingId}/stt/jobs`,

/** 회고 요약 조회/수정 (GET/PATCH) */
SUMMARY: (meetingId: number) => `${API_PATHS.MEETINGS}/${meetingId}/retrospectives/summary`,

/** 회고 요약 발행 (POST) */
PUBLISH: (meetingId: number) =>
`${API_PATHS.MEETINGS}/${meetingId}/retrospectives/summary/publish`,
} as const
84 changes: 84 additions & 0 deletions src/features/retrospectives/retrospectives.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* @file retrospectives.types.ts
* @description 약속 회고 AI 요약 관련 타입 정의
*/

// ─── STT Job ───

/** STT Job 상태 */
export type SttJobStatus = 'PENDING' | 'PROCESSING' | 'DONE' | 'FAILED'

/** STT Job 생성 요청 파라미터 */
export type CreateSttJobParams = {
gatheringId: number
meetingId: number
file?: File
}

/** STT Job 응답 */
export interface SttJobResponse {
jobId: number
meetingId: number
userId: number
status: SttJobStatus
summary: string | null
highlights: string[] | null
errorMessage: string | null
createdAt: string
}

// ─── Retrospective Summary ───

/** 주요 포인트 항목 */
export interface KeyPoint {
title: string
details: string[]
}

/** 토픽별 요약 */
export interface SummaryTopic {
topicId: number
confirmOrder: number
topicTitle: string
topicDescription: string
summary: string
keyPoints: KeyPoint[]
}

/** 회고 요약 응답 (GET, PATCH, POST publish 공통) */
export interface RetrospectiveSummaryResponse {
meetingId: number
isPublished: boolean
publishedAt: string | null
topics: SummaryTopic[]
}

// ─── Request Types ───

/** 주요 포인트 수정 요청 (KeyPoint와 동일 형태) */
export type KeyPointUpdateRequest = KeyPoint

/** 토픽 요약 수정 요청 */
export type UpdateSummaryTopicRequest = {
topicId: number
summary: string
keyPoints: KeyPointUpdateRequest[]
}

/** 회고 요약 수정 요청 바디 */
export type UpdateSummaryRequest = {
topics: UpdateSummaryTopicRequest[]
}

// ─── Hook Params ───

/** 요약 수정 훅 파라미터 */
export type UpdateSummaryParams = {
meetingId: number
data: UpdateSummaryRequest
}

/** 요약 발행 훅 파라미터 */
export type PublishSummaryParams = {
meetingId: number
}
Loading