From 3eac06666c429c4e8d5379277699e5531085ba7d Mon Sep 17 00:00:00 2001 From: junyong Date: Wed, 6 May 2026 20:40:18 +0900 Subject: [PATCH 1/2] =?UTF-8?q?Fix:=20=EB=AF=B8=ED=8C=85=20=EC=83=81?= =?UTF-8?q?=ED=83=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/repository/MeetingRepository.java | 2 +- .../domain/meeting/service/MeetingUseCase.java | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/meeting/repository/MeetingRepository.java b/src/main/java/com/whylog/server/domain/meeting/repository/MeetingRepository.java index d6ee164..7982b49 100644 --- a/src/main/java/com/whylog/server/domain/meeting/repository/MeetingRepository.java +++ b/src/main/java/com/whylog/server/domain/meeting/repository/MeetingRepository.java @@ -16,7 +16,7 @@ public interface MeetingRepository extends JpaRepository { SELECT m FROM Meeting m LEFT JOIN FETCH m.meetingAnalysis - WHERE m.team.id = :teamId AND m.isNormallyEnded IS TRUE + WHERE m.team.id = :teamId ORDER BY m.startDateTime DESC """) List findWithAnalysis(@Param("teamId") Long teamId); diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingUseCase.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingUseCase.java index 5a972fa..90f90a7 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingUseCase.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingUseCase.java @@ -3,7 +3,6 @@ import com.whylog.server.domain.meeting.entity.Meeting; import com.whylog.server.domain.meeting.entity.MeetingMember; import com.whylog.server.domain.meeting.exception.MeetingNotFoundException; -import com.whylog.server.domain.meeting.repository.MeetingAnalysisRepository; import com.whylog.server.domain.meeting.repository.MeetingRepository; import com.whylog.server.domain.user.entity.Member; import lombok.RequiredArgsConstructor; @@ -16,29 +15,28 @@ public class MeetingUseCase { private final MeetingRepository meetingRepository; - private final MeetingAnalysisRepository meetingAnalysisRepository; - public Meeting findMeetingById(Long id){ + public Meeting findMeetingById(Long id) { return meetingRepository.findById(id) .orElseThrow(MeetingNotFoundException::new); } - public Meeting findMeetingWithMembersById(Long id){ + public Meeting findMeetingWithMembersById(Long id) { return meetingRepository.findWithMembers(id) .orElseThrow(MeetingNotFoundException::new); } - public List findMeetingByTeamId(Long teamId){ + public List findMeetingByTeamId(Long teamId) { return meetingRepository.findWithAnalysis(teamId); } // 회의 참여자 수 - public int getMeetingMemberCount(Meeting meeting){ + public int getMeetingMemberCount(Meeting meeting) { return meeting.getMeetingMembers().size(); } // 회의의 참여자 정보 - public List getParticipantsInfo(Meeting meeting){ + public List getParticipantsInfo(Meeting meeting) { return meeting.getMeetingMembers().stream() .map(MeetingMember::getMember) .toList(); @@ -48,5 +46,4 @@ public Meeting findWithAnalysisByMeetingId(Long meetingId) { return meetingRepository.findByMeetingId(meetingId) .orElseThrow(MeetingNotFoundException::new); } - } From 75923da9ec6d90a6e92febd8df4ef8672b038403 Mon Sep 17 00:00:00 2001 From: junyong Date: Wed, 6 May 2026 20:41:23 +0900 Subject: [PATCH 2/2] =?UTF-8?q?Refactor:=20=ED=9A=8C=EC=9D=98=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=EC=B6=9C=EB=A0=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + .../service/MeetingCommandService.java | 19 +-- .../meeting/socket/MeetingSocketHandler.java | 128 ++++++++---------- .../socket/MeetingSocketRoomService.java | 43 +----- 4 files changed, 76 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index 06ab520..58ce205 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ out/ ### VS Code ### .vscode/ /.env + +## Agents +AGENTS.md diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingCommandService.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingCommandService.java index 9fea6e7..b0d9910 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingCommandService.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingCommandService.java @@ -128,15 +128,12 @@ public void autoEndMeetingIfEmpty(Long meetingId) { @Transactional public MeetingResponse.MeetingDeleteResponseDTO deleteMeeting(Long memberId, Long meetingId) { - - meetingRepository.findById(meetingId) + Meeting meeting = meetingRepository.findById(meetingId) .orElseThrow(MeetingNotFoundException::new); meetingMemberRepository.findOwnerMeetingMember(memberId, meetingId, MeetingRole.OWNER) .orElseThrow(() -> new ErrorHandler(MeetingErrorCode.MEETING_NOT_OWNER)); - Meeting meeting = meetingRepository.findById(meetingId) - .orElseThrow(MeetingNotFoundException::new); stopRecording(meeting); meetingCleanupService.deleteByMeetingId(meetingId); scheduleAfterCommit(() -> meetingSocketRoomService.closeRoom(meetingId)); @@ -182,11 +179,7 @@ private MeetingResponse.MeetingEndResponseDTO finishMeeting(Meeting meeting, boo stopRecording(meeting); meetingSocketRoomService.closeRoom(meeting.getId()); - scheduleAfterCommit(() -> CompletableFuture.runAsync(() -> meetingAnalysisService.analyzeMeetingAudio(meeting.getId())) - .exceptionally(ex -> { - log.error("회의 오디오 분석 실패: meetingId={}", meeting.getId(), ex); - return null; - })); + scheduleAfterCommit(() -> analyzeMeetingAudioAsync(meeting.getId())); return MeetingResponse.MeetingEndResponseDTO.builder() .meetingId(meeting.getId()) @@ -194,4 +187,12 @@ private MeetingResponse.MeetingEndResponseDTO finishMeeting(Meeting meeting, boo .build(); } + private void analyzeMeetingAudioAsync(Long meetingId) { + CompletableFuture.runAsync(() -> meetingAnalysisService.analyzeMeetingAudio(meetingId)) + .exceptionally(ex -> { + log.error("회의 오디오 분석 실패: meetingId={}", meetingId, ex); + return null; + }); + } + } diff --git a/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketHandler.java b/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketHandler.java index 3ccb5cf..b1c37b2 100644 --- a/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketHandler.java +++ b/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketHandler.java @@ -42,21 +42,22 @@ public void afterConnectionEstablished(@NonNull WebSocketSession session) throws } if (meetingSocketRoomService.existsParticipant(participant.meetingId(), participant.memberId())) { - sendError(session, MeetingMessageType.PARTICIPANT_ALREADY_JOINED, "이미 실시간으로 참여 중인 회의입니다."); - session.close(CloseStatus.NORMAL); - return; + sendError(session, MeetingMessageType.PARTICIPANT_ALREADY_JOINED, "이미 실시간으로 참여 중인 회의입니다."); + session.close(CloseStatus.NORMAL); + return; } meetingSocketRoomService.join(participant); + List currentParticipants = participantSummaries(participant.meetingId()); ConnectedMessage connectedMessage = ConnectedMessage.create( - participant, participantSummaries(participant.meetingId()) + participant, currentParticipants ); session.sendMessage(new TextMessage(JsonConverter.toJson(connectedMessage))); - broadcastRoster(participant.meetingId()); + broadcastRoster(participant.meetingId(), currentParticipants); meetingSocketRoomService.broadcastText( participant.meetingId(), - JsonConverter.toJson(ParticipantJoinedMessage.create(participant)) + new TextMessage(JsonConverter.toJson(ParticipantJoinedMessage.create(participant))) ); } @@ -68,7 +69,8 @@ protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull Tex try { incoming = JsonConverter.readValue(message, MeetingSocketMessage.class); } catch (JsonProcessingException exception) { - throw new IllegalArgumentException("Invalid websocket message payload", exception); + sendError(session, "Invalid websocket message payload"); + return; } MeetingMessageType type = incoming.type(); @@ -78,63 +80,8 @@ protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull Tex } switch (type) { - case CHAT -> { - logIncomingText(participant, type, incoming); - meetingSocketRoomService.broadcastChatText( - participant.meetingId(), - JsonConverter.toJson(MeetingTextMessage.createTextMessage( - participant, - type, - null, - Optional.ofNullable(incoming.text()).orElse(""), - incoming.payload() - )) - ); - } - case SPEECH -> { - logIncomingText(participant, type, incoming); - meetingSocketRoomService.broadcastSpeechText( - participant.meetingId(), - JsonConverter.toJson(MeetingTextMessage.createTextMessage( - participant, - type, - null, - Optional.ofNullable(incoming.text()).orElse(""), - incoming.payload() - )) - ); - } - case AUDIO_TEXT -> { - logIncomingText(participant, type, incoming); - meetingSocketRoomService.broadcastAudioText( - participant.meetingId(), - JsonConverter.toJson(MeetingTextMessage.createTextMessage( - participant, - type, - null, - Optional.ofNullable(incoming.text()).orElse(""), - incoming.payload() - )) - ); - } - case OFFER, ANSWER, ICE -> { - if (incoming.targetMemberId() == null) { - sendError(session, "targetMemberId is required for " + type.value()); - return; - } - - meetingSocketRoomService.sendToMember( - participant.meetingId(), - incoming.targetMemberId(), - JsonConverter.toJson(MeetingTextMessage.createTextMessage( - participant, - type, - incoming.targetMemberId(), - null, - incoming.payload() - )) - ); - } + case CHAT, SPEECH, AUDIO_TEXT -> broadcastTextMessage(participant, type, incoming); + case OFFER, ANSWER, ICE -> forwardSignal(session, participant, type, incoming); default -> sendError(session, "Unsupported message type: " + type.value()); } } @@ -172,25 +119,26 @@ private void removeParticipant(WebSocketSession session) { return; } - meetingSocketRoomService.broadcastText(meetingId, JsonConverter.toJson(new ParticipantLeftMessage( + List currentParticipants = participantSummaries(meetingId); + meetingSocketRoomService.broadcastText(meetingId, new TextMessage(JsonConverter.toJson(new ParticipantLeftMessage( MeetingMessageType.PARTICIPANT_LEFT, meetingId, removed.memberId(), removed.name(), now() - ))); - broadcastRoster(meetingId); + )))); + broadcastRoster(meetingId, currentParticipants); - if (meetingSocketRoomService.listParticipants(meetingId).isEmpty()) { + if (currentParticipants.isEmpty()) { meetingCommandService.autoEndMeetingIfEmpty(meetingId); } } // 현재 회의 참가자 목록을 모든 클라이언트에 전파합니다. - private void broadcastRoster(Long meetingId) { + private void broadcastRoster(Long meetingId, List participantSummaries) { meetingSocketRoomService.broadcastText( meetingId, - JsonConverter.toJson(RosterMessage.create(meetingId, participantSummaries(meetingId))) + new TextMessage(JsonConverter.toJson(RosterMessage.create(meetingId, participantSummaries))) ); } @@ -242,8 +190,46 @@ private List participantSummaries(Long meetingId) { return meetingSocketRoomService.listParticipants(meetingId); } + private void broadcastTextMessage(MeetingParticipant participant, MeetingMessageType type, MeetingSocketMessage incoming) { + logIncomingText(participant, type, incoming); + meetingSocketRoomService.broadcastText( + participant.meetingId(), + JsonConverter.toJson(MeetingTextMessage.createTextMessage( + participant, + type, + null, + Optional.ofNullable(incoming.text()).orElse(""), + incoming.payload() + )) + ); + } + + private void forwardSignal( + WebSocketSession session, + MeetingParticipant participant, + MeetingMessageType type, + MeetingSocketMessage incoming + ) { + if (incoming.targetMemberId() == null) { + sendError(session, "targetMemberId is required for " + type.value()); + return; + } + + meetingSocketRoomService.sendToMember( + participant.meetingId(), + incoming.targetMemberId(), + JsonConverter.toJson(MeetingTextMessage.createTextMessage( + participant, + type, + incoming.targetMemberId(), + null, + incoming.payload() + )) + ); + } + // 웹소켓 메시지에 사용할 현재 시각 문자열을 생성합니다. - private String now() { + private String now() { return Instant.now().toString(); } diff --git a/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketRoomService.java b/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketRoomService.java index ad8cb45..d9e82da 100644 --- a/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketRoomService.java +++ b/src/main/java/com/whylog/server/domain/meeting/socket/MeetingSocketRoomService.java @@ -18,7 +18,6 @@ import java.util.Comparator; import java.util.List; import java.util.concurrent.Executor; -import java.util.function.Function; // 회의별 참가자 세션 저장소 역할을 하며 텍스트/오디오 메시지 전달을 담당합니다. @Service @@ -99,34 +98,12 @@ public List listParticipants(Long meetingId) { // 텍스트 메시지를 회의방의 모든 참가자에게 브로드캐스트합니다. public void broadcastText(Long meetingId, String payload) { - dispatch(() -> broadcast( - meetingId, - participant -> new TextMessage(payload), - participant -> false)); - } - - // 채팅 메시지를 별도 경로로 브로드캐스트합니다. - public void broadcastChatText(Long meetingId, String payload) { - dispatch(() -> broadcast( - meetingId, - participant -> new TextMessage(payload), - participant -> false)); + broadcastText(meetingId, new TextMessage(payload)); } - // 실시간 자막/STT 메시지를 채팅과 분리해 브로드캐스트합니다. - public void broadcastSpeechText(Long meetingId, String payload) { - dispatch(() -> broadcast( - meetingId, - participant -> new TextMessage(payload), - participant -> false)); - } - - // 오디오 텍스트 변환 결과를 별도 경로로 브로드캐스트합니다. - public void broadcastAudioText(Long meetingId, String payload) { - dispatch(() -> broadcast( - meetingId, - participant -> new TextMessage(payload), - participant -> false)); + // 이미 직렬화된 텍스트 메시지를 회의방의 모든 참가자에게 브로드캐스트합니다. + public void broadcastText(Long meetingId, TextMessage message) { + dispatch(() -> broadcast(meetingId, message)); } // 특정 대상 참가자 한 명에게만 시그널링 메시지를 전달합니다. @@ -178,11 +155,7 @@ private void sendToMemberInternal(Long meetingId, Long targetMemberId, String pa } // 회의방 참가자 전체를 순회하면서 메시지를 보내고 끊어진 세션은 정리합니다. - private void broadcast( - Long meetingId, - Function> messageFactory, - Function skipCondition - ) { + private void broadcast(Long meetingId, WebSocketMessage message) { MeetingRoomRepository room = getRoom(meetingId); if (room == null) { @@ -191,11 +164,7 @@ private void broadcast( List disconnectedParticipants = new ArrayList<>(); for (MeetingParticipant participant : new ArrayList<>(room.participants())) { - if (skipCondition.apply(participant)) { - continue; - } - - if (!sendMessage(participant, messageFactory.apply(participant))) { + if (!sendMessage(participant, message)) { disconnectedParticipants.add(participant); } }