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
Original file line number Diff line number Diff line change
Expand Up @@ -145,4 +145,23 @@ public ApiResponse<String> endMeeting(

return ApiResponse.onSuccess("회의가 종료되었습니다");
}



@Operation(summary = "회의 STT목록과 AI 추천질문 조회", description =
"# [v1.0 (2025-08-14)](https://www.notion.so/AI-2265da7802c580e8a6cefdcafcd36259)" +
"진행됐던 회의의 STT와 AI추천질문을 연관하여 조회하는 API입니다."
)
@GetMapping("/{meetingId}/transcript")
public ApiResponse<MeetingResponseDTO.TranscriptResponse> getMeetingTranscript(
@PathVariable("meetingId") String meetingId
) {
Long userId = SecurityUtil.getCurrentUserId();

MeetingResponseDTO.TranscriptResponse transcriptResponse = meetingQueryService.getTranscript(userId, Long.parseLong(meetingId));

return ApiResponse.onSuccess(transcriptResponse);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Builder;
import lombok.Getter;
import com.haru.api.infra.api.entity.SpeechSegment;
import lombok.*;
import com.haru.api.infra.api.entity.AIQuestion;

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


public class MeetingResponseDTO {
Expand Down Expand Up @@ -39,4 +43,56 @@ public static class getMeetingProceeding{
private String proceeding;
private LocalDateTime updatedAt;
}

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class TranscriptResponse {
private LocalDateTime meetingStartTime;
private List<Transcript> transcripts;
}

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Transcript {
@JsonSerialize(using = ToStringSerializer.class)
private Long segmentId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JsonSerialize(using = ToStringSerializer.class) 붙여주세요!

@JsonSerialize(using = ToStringSerializer.class)
private String speakerId;
private String text;
private LocalDateTime startTime;
private List<AIQuestionDTO> aiQuestions;

public static Transcript from(SpeechSegment segment) {
return Transcript.builder()
.segmentId(segment.getId())
.speakerId(segment.getSpeakerId())
.text(segment.getText())
.startTime(segment.getStartTime())
.aiQuestions(segment.getAiQuestions().stream()
.map(AIQuestionDTO::from)
.collect(Collectors.toList()))
.build();
}
}

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class AIQuestionDTO {
@JsonSerialize(using = ToStringSerializer.class)
private Long questionId;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JsonSerialize(using = ToStringSerializer.class) 붙여주세요!

private String question;

public static AIQuestionDTO from(AIQuestion aiQuestion) {
return AIQuestionDTO.builder()
.questionId(aiQuestion.getId())
.question(aiQuestion.getQuestion())
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@

public interface MeetingQueryService {

public List<MeetingResponseDTO.getMeetingResponse> getMeetings(Long userId, Long workspaceId);
List<MeetingResponseDTO.getMeetingResponse> getMeetings(Long userId, Long workspaceId);

public MeetingResponseDTO.getMeetingProceeding getMeetingProceeding(Long userId, Long meetingId);
MeetingResponseDTO.getMeetingProceeding getMeetingProceeding(Long userId, Long meetingId);

MeetingResponseDTO.TranscriptResponse getTranscript(Long userId, Long meetingId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import com.haru.api.global.apiPayload.exception.handler.MemberHandler;
import com.haru.api.global.apiPayload.exception.handler.UserWorkspaceHandler;
import com.haru.api.global.apiPayload.exception.handler.WorkspaceHandler;
import com.haru.api.infra.api.entity.SpeechSegment;
import com.haru.api.infra.api.repository.SpeechSegmentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -33,6 +35,7 @@ public class MeetingQueryServiceImpl implements MeetingQueryService{
private final WorkspaceRepository workspaceRepository;
private final UserRepository userRepository;
private final UserWorkspaceRepository userWorkspaceRepository;
private final SpeechSegmentRepository speechSegmentRepository;

@Override
public List<MeetingResponseDTO.getMeetingResponse> getMeetings(Long userId, Long workspaceId) {
Expand Down Expand Up @@ -68,4 +71,32 @@ public MeetingResponseDTO.getMeetingProceeding getMeetingProceeding(Long userId,

return MeetingConverter.toGetMeetingProceedingResponse(foundMeetingCreator, foundMeeting);
}

@Override
public MeetingResponseDTO.TranscriptResponse getTranscript(Long userId, Long meetingId) {
User foundUser = userRepository.findById(userId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));

Meeting foundMeeting = meetingRepository.findById(meetingId)
.orElseThrow(() -> new MeetingHandler(ErrorStatus.MEETING_NOT_FOUND));

Workspace foundWorkspace = meetingRepository.findWorkspaceByMeetingId(meetingId)
.orElseThrow(() -> new WorkspaceHandler(ErrorStatus.WORKSPACE_NOT_FOUND));

UserWorkspace foundUserWorkspace = userWorkspaceRepository.findByUserIdAndWorkspaceId(userId, foundWorkspace.getId())
.orElseThrow(() -> new UserWorkspaceHandler(ErrorStatus.USER_WORKSPACE_NOT_FOUND));

// Repository를 통해 SpeechSegment와 연관된 AIQuestion을 함께 조회 (N+1 문제 해결)
List<SpeechSegment> segments = speechSegmentRepository.findAllByMeetingIdWithAIQuestions(meetingId);

List<MeetingResponseDTO.Transcript> transcriptList = segments.stream()
.map(MeetingResponseDTO.Transcript::from)
.collect(Collectors.toList());

return MeetingResponseDTO.TranscriptResponse.builder()
.meetingStartTime(foundMeeting.getStartTime())
.transcripts(transcriptList)
.build();
}

}
4 changes: 4 additions & 0 deletions src/main/java/com/haru/api/infra/api/entity/AIQuestion.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public class AIQuestion {

private String question;

public void setSpeechSegment(SpeechSegment speechSegment) {
this.speechSegment = speechSegment;
}

}
11 changes: 11 additions & 0 deletions src/main/java/com/haru/api/infra/api/entity/SpeechSegment.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import org.hibernate.annotations.DynamicUpdate;

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

@Entity
@Table(name = "speech_segments")
Expand All @@ -32,6 +34,15 @@ public class SpeechSegment {
@JoinColumn(name = "meeting_id", nullable = false)
private Meeting meeting;

@OneToMany(mappedBy = "speechSegment", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<AIQuestion> aiQuestions = new ArrayList<>();

public void addAIQuestion(AIQuestion aiQuestion) {
this.aiQuestions.add(aiQuestion);
aiQuestion.setSpeechSegment(this);
}

@Override
public String toString() {
return String.format("text_id: %d, text: %s", id, text);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
import com.haru.api.domain.meeting.entity.Meeting;
import com.haru.api.infra.api.entity.SpeechSegment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface SpeechSegmentRepository extends JpaRepository<SpeechSegment, Long> {
// 특정 Meeting에 속한 모든 SpeechSegment를 조회하는 메서드
List<SpeechSegment> findByMeeting(Meeting meeting);

@Query("SELECT DISTINCT s FROM SpeechSegment s LEFT JOIN FETCH s.aiQuestions WHERE s.meeting.id = :meetingId ORDER BY s.startTime ASC")
List<SpeechSegment> findAllByMeetingIdWithAIQuestions(@Param("meetingId") Long meetingId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public AudioProcessingPipeline(
);
this.scoringProcessor = new ScoringProcessor(scoringFunction, audioSessionBuffer);
this.aiQuestionProcessor = new AIQuestionProcessor(
chatGPTClient, aiQuestionRepository, audioSessionBuffer, notificationService, objectMapper
chatGPTClient, aiQuestionRepository, audioSessionBuffer, notificationService, speechSegmentRepository, objectMapper
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import com.haru.api.infra.api.entity.AIQuestion;
import com.haru.api.infra.api.entity.SpeechSegment;
import com.haru.api.infra.api.repository.AIQuestionRepository;
import com.haru.api.infra.api.repository.SpeechSegmentRepository;
import com.haru.api.infra.websocket.AudioSessionBuffer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
Expand All @@ -17,6 +19,7 @@
public class AIQuestionProcessor {
private final ChatGPTClient chatGPTClient;
private final AIQuestionRepository aiQuestionRepository;
private final SpeechSegmentRepository speechSegmentRepository;
private final AudioSessionBuffer audioSessionBuffer;
private final WebSocketNotificationService notificationService;
private final ObjectMapper objectMapper;
Expand All @@ -25,11 +28,13 @@ public AIQuestionProcessor(ChatGPTClient chatGPTClient,
AIQuestionRepository aiQuestionRepository,
AudioSessionBuffer audioSessionBuffer,
WebSocketNotificationService notificationService,
SpeechSegmentRepository speechSegmentRepository,
ObjectMapper objectMapper) {
this.chatGPTClient = chatGPTClient;
this.aiQuestionRepository = aiQuestionRepository;
this.audioSessionBuffer = audioSessionBuffer;
this.notificationService = notificationService;
this.speechSegmentRepository = speechSegmentRepository;
this.objectMapper = objectMapper;
}

Expand Down Expand Up @@ -62,13 +67,14 @@ private Mono<Void> saveAndNotifyAIQuestions(SpeechSegment segment, AIQuestionRes
});
}


private void saveAIQuestions(SpeechSegment segment, List<String> questions) {
questions.forEach(questionText -> {
AIQuestion aiQuestion = AIQuestion.builder()
.speechSegment(segment)
.question(questionText)
.build();
aiQuestionRepository.save(aiQuestion);
segment.addAIQuestion(aiQuestion);
});
speechSegmentRepository.save(segment);
}
}