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
29 changes: 29 additions & 0 deletions src/main/java/com/dokdok/ai/client/AiSttClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dokdok.ai.client;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.retrospective.dto.response.RetrospectiveSummaryResponse;
import com.dokdok.ai.dto.SttRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

@Component
@RequiredArgsConstructor
public class AiSttClient {

private final WebClient aiWebClient;

@Value("${ai.api.stt-path}")
private String sttPath;

public ApiResponse<RetrospectiveSummaryResponse> requestStt(SttRequest request) {
return aiWebClient.post()
.uri(sttPath)
.bodyValue(request)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<ApiResponse<RetrospectiveSummaryResponse>>() {})
.block();
}
}
26 changes: 0 additions & 26 deletions src/main/java/com/dokdok/ai/client/AiSummaryClient.java

This file was deleted.

19 changes: 19 additions & 0 deletions src/main/java/com/dokdok/ai/dto/SttRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dokdok.ai.dto;

import java.util.List;

public record SttRequest(
Long jobId,
Long meetingId,
String filePath,
String language,
List<PreAnswer> preAnswers
) {
public record PreAnswer(
Long topicId,
String topicTitle,
Long userId,
String content
) {
}
}
15 changes: 0 additions & 15 deletions src/main/java/com/dokdok/ai/dto/TopicSummaryRequest.java

This file was deleted.

111 changes: 111 additions & 0 deletions src/main/java/com/dokdok/stt/api/SttApi.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.dokdok.stt.api;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.stt.dto.SttJobResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@Tag(name = "STT", description = "STT 작업 관련 API")
@RequestMapping("/api/gatherings/{gatheringId}/meetings/{meetingId}/stt/jobs")
public interface SttApi {

@Operation(
summary = "STT 작업 생성",
description = "오디오 파일 업로드 또는 사전의견만으로 STT 요약 작업을 생성합니다.",
parameters = {
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "201",
description = "STT 작업 생성 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = SttJobResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"E000","message":"잘못된 요청입니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "약속 멤버가 아님",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"G002","message":"모임의 멤버가 아닙니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "약속을 찾을 수 없음",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"M001","message":"약속을 찾을 수 없습니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"E000","message":"서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.","data":null}
""")))
})
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<SttJobResponse>> createJob(
@PathVariable Long gatheringId,
@PathVariable Long meetingId,
@RequestPart(value = "file", required = false) MultipartFile file
);

@Operation(
summary = "STT 작업 조회",
description = "STT 작업 상태/결과를 조회합니다.",
parameters = {
@Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true),
@Parameter(name = "jobId", description = "STT 작업 식별자", in = ParameterIn.PATH, required = true)
}
)
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(
responseCode = "200",
description = "STT 작업 조회 성공",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = @Schema(implementation = SttJobResponse.class))
),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"E000","message":"잘못된 요청입니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "약속 멤버가 아님",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"G002","message":"모임의 멤버가 아닙니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "작업을 찾을 수 없음",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"E101","message":"작업을 찾을 수 없습니다.","data":null}
"""))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류",
content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE,
examples = @io.swagger.v3.oas.annotations.media.ExampleObject(value = """
{"code":"E000","message":"서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.","data":null}
""")))
})
@GetMapping(value = "/{jobId}", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<ApiResponse<SttJobResponse>> getJob(
@PathVariable Long gatheringId,
@PathVariable Long meetingId,
@PathVariable Long jobId
);
}
33 changes: 33 additions & 0 deletions src/main/java/com/dokdok/stt/controller/SttController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.dokdok.stt.controller;

import com.dokdok.global.response.ApiResponse;
import com.dokdok.stt.api.SttApi;
import com.dokdok.stt.dto.SttJobResponse;
import com.dokdok.stt.service.SttJobService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequiredArgsConstructor
public class SttController implements SttApi {

private final SttJobService sttJobService;

@Override
public ResponseEntity<ApiResponse<SttJobResponse>> createJob(
Long gatheringId,
Long meetingId,
MultipartFile file
) {
SttJobResponse response = sttJobService.createJob(gatheringId, meetingId, file);
return ApiResponse.created(response, "STT 작업이 생성되었습니다.");
}

@Override
public ResponseEntity<ApiResponse<SttJobResponse>> getJob(Long gatheringId, Long meetingId, Long jobId) {
SttJobResponse response = sttJobService.getJob(gatheringId, meetingId, jobId);
return ApiResponse.success(response, "STT 작업 조회를 완료했습니다.");
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/dokdok/stt/dto/SttJobResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.dokdok.stt.dto;

import com.dokdok.stt.entity.SttJob;
import com.dokdok.stt.entity.SttJobStatus;
import com.dokdok.stt.entity.SttSummary;

import java.time.LocalDateTime;
import java.util.List;

public record SttJobResponse(
Long jobId,
Long meetingId,
Long userId,
SttJobStatus status,
String summary,
List<String> highlights,
String errorMessage,
LocalDateTime createdAt
) {
public static SttJobResponse from(SttJob job, SttSummary summary) {
return new SttJobResponse(
job.getId(),
job.getMeeting().getId(),
job.getUser().getId(),
job.getStatus(),
summary != null ? summary.getSummary() : null,
summary != null ? summary.getHighlights() : null,
job.getErrorMessage(),
job.getCreatedAt()
);
}
}
68 changes: 68 additions & 0 deletions src/main/java/com/dokdok/stt/entity/SttJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.dokdok.stt.entity;

import com.dokdok.global.BaseTimeEntity;
import com.dokdok.meeting.entity.Meeting;
import com.dokdok.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
import lombok.experimental.SuperBuilder;

@Entity
@Table(name = "stt_job")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@SuperBuilder
public class SttJob extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "stt_job_id")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "meeting_id", nullable = false)
private Meeting meeting;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;

@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false, length = 20)
@Builder.Default
private SttJobStatus status = SttJobStatus.PENDING;

@Column(name = "error_message", columnDefinition = "TEXT")
private String errorMessage;

@Column(name = "original_filename", length = 255)
private String originalFilename;

@Column(name = "content_type", length = 100)
private String contentType;

@Column(name = "file_size")
private Long fileSize;

@Column(name = "temp_file_path", length = 500)
private String tempFilePath;

@Column(name = "language", length = 20)
@Builder.Default
private String language = "ko-KR";

public void markProcessing() {
this.status = SttJobStatus.PROCESSING;
}

public void markDone() {
this.status = SttJobStatus.DONE;
this.errorMessage = null;
}

public void markFailed(String errorMessage) {
this.status = SttJobStatus.FAILED;
this.errorMessage = errorMessage;
}
}
8 changes: 8 additions & 0 deletions src/main/java/com/dokdok/stt/entity/SttJobStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dokdok.stt.entity;

public enum SttJobStatus {
PENDING,
PROCESSING,
DONE,
FAILED
}
46 changes: 46 additions & 0 deletions src/main/java/com/dokdok/stt/entity/SttSummary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.dokdok.stt.entity;

import com.dokdok.global.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

import java.util.List;

@Entity
@Table(name = "stt_summary")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@SuperBuilder
public class SttSummary extends BaseTimeEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "stt_summary_id")
private Long id;

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "stt_job_id", nullable = false)
private SttJob sttJob;

@Column(name = "summary", columnDefinition = "TEXT")
private String summary;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "highlights", columnDefinition = "jsonb")
private List<String> highlights;
}
Loading