From d1559c979420acea7a833765d0f7a032e0a34b5d Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Mon, 4 May 2026 14:37:06 +0900 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD/=EC=A0=81=EC=9A=A9=EC=82=AC=ED=95=AD=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/decision/dto/DecisionResponse.java | 5 ++- .../domain/decision/entity/Application.java | 10 +++++ .../domain/decision/entity/Decision.java | 7 ++++ .../repository/DecisionRepository.java | 3 ++ .../service/MeetingAnalysisService.java | 41 +++++++++++++++++-- .../domain/team/service/TeamQueryService.java | 11 ++++- 6 files changed, 71 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java index bf2f046..386b671 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java @@ -1,6 +1,7 @@ package com.whylog.server.domain.decision.dto; import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; @@ -21,8 +22,8 @@ public static class DecisionListDTO { @Schema(description = "회의 명", example = "백엔드 비상대책회의") private String name; - @Schema(description = "적용사항 개수", example = "3") - private Integer applicationCount; + @Schema(description = "적용사항 목록") + private List applications; } @Getter diff --git a/src/main/java/com/whylog/server/domain/decision/entity/Application.java b/src/main/java/com/whylog/server/domain/decision/entity/Application.java index 6e3ac0d..dde8694 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/Application.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/Application.java @@ -33,6 +33,9 @@ public class Application extends BaseEntity { @JoinColumn(name = "decision_id", nullable = false) private Decision decision; + @Column(name = "name") + private String name; + // @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) // private final List applicationTimelines = new ArrayList<>(); // @@ -41,4 +44,11 @@ public class Application extends BaseEntity { // // @OneToMany(mappedBy = "application", cascade = CascadeType.ALL, orphanRemoval = true) // private final List applicationBases = new ArrayList<>(); + + public static Application create(Decision decision, String name) { + Application application = new Application(); + application.decision = decision; + application.name = name; + return application; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/Decision.java b/src/main/java/com/whylog/server/domain/decision/entity/Decision.java index 114aafc..6e7f507 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/Decision.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/Decision.java @@ -54,4 +54,11 @@ public class Decision extends BaseEntity { // // @OneToMany(mappedBy = "decision", cascade = CascadeType.ALL, orphanRemoval = true) // private final List effectRatios = new ArrayList<>(); + + public static Decision create(Meeting meeting, boolean isCreated) { + Decision decision = new Decision(); + decision.meeting = meeting; + decision.isCreated = isCreated; + return decision; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java index 9e81ab3..e943acb 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java @@ -1,6 +1,7 @@ package com.whylog.server.domain.decision.repository; import com.whylog.server.domain.decision.entity.Decision; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -8,6 +9,8 @@ public interface DecisionRepository extends JpaRepository { + Optional findByMeetingId(Long meetingId); + @Modifying @Query("DELETE FROM Application a WHERE a.decision.meeting.team.id = :teamId") void deleteApplicationsByTeamId(@Param("teamId") Long teamId); diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java index d93be18..104d8c4 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java @@ -2,6 +2,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.whylog.server.domain.decision.entity.Application; +import com.whylog.server.domain.decision.entity.Decision; +import com.whylog.server.domain.decision.repository.ApplicationRepository; +import com.whylog.server.domain.decision.repository.DecisionRepository; import com.whylog.server.domain.meeting.dto.MeetingResponse; import com.whylog.server.domain.meeting.entity.Dialogue; import com.whylog.server.domain.meeting.entity.Meeting; @@ -51,6 +55,8 @@ public class MeetingAnalysisService { private final MeetingAudioReplayService meetingAudioReplayService; private final MeetingAudioFileService meetingAudioFileService; private final FastApiTranscribeClient fastApiTranscribeClient; + private final ApplicationRepository applicationRepository; + private final DecisionRepository decisionRepository; private final MeetingAnalysisRepository meetingAnalysisRepository; private final DialogueRepository dialogueRepository; private final TransactionTemplate transactionTemplate; @@ -179,12 +185,12 @@ private void persistMeetingAnalysis(Meeting meeting, TranscribeApplicationRunRes TranscribeApplicationRunResponse.AnalysisResultResponse analysisResult = runResult.analysisResult(); TranscribeApplicationRunResponse.OverallAnalysisResponse overallAnalysis = analysisResult != null ? analysisResult.overallAnalysis() : null; + List applications = + analysisResult != null && analysisResult.applications() != null ? analysisResult.applications() : List.of(); MeetingAnalysis.MeetingAnalysisPayload payload = buildMeetingAnalysisPayload(overallAnalysis); transactionTemplate.executeWithoutResult(status -> { Meeting managedMeeting = meetingUseCase.findMeetingWithMembersById(meeting.getId()); - meetingAnalysisRepository.deleteByMeetingId(managedMeeting.getId()); - dialogueRepository.deleteByMeetingId(managedMeeting.getId()); MeetingAnalysis meetingAnalysis = MeetingAnalysis.create(managedMeeting, payload); meetingAnalysisRepository.save(meetingAnalysis); @@ -195,13 +201,42 @@ private void persistMeetingAnalysis(Meeting meeting, TranscribeApplicationRunRes dialogueRepository.saveAll(dialogues); dialogues.forEach(managedMeeting::addDialogue); } + + Decision decision = createDecisionIfAbsent(managedMeeting); + replaceApplications(managedMeeting.getId(), decision, applications); }); log.info("회의 오디오 분석 저장 완료: meetingId={}, transcriptSegmentCount={}", meeting.getId(), transcriptSegments.size()); - // TODO: FastAPI의 applications 결과는 아직 저장하지 않는다. // TODO: applications 저장 후 applicationId를 발급해 /api/meeting-analysis/embeddings로 전달한다. } + // Decision이 없을 때 새로 생성한다. + private Decision createDecisionIfAbsent(Meeting meeting) { + return decisionRepository.findByMeetingId(meeting.getId()) + .orElseGet(() -> { + Decision decision = decisionRepository.save(Decision.create(meeting, true)); + log.info("결정사항 저장 완료: meetingId={}, decisionId={}", meeting.getId(), decision.getId()); + return decision; + }); + } + + // 분석 결과의 적용사항 제목 목록을 저장한다. + private void replaceApplications(Long meetingId, + Decision decision, + List applications) { + List newApplications = applications.stream() + .map(TranscribeApplicationRunResponse.ApplicationResponse::applicationTitle) + .filter(title -> title != null && !title.isBlank()) + .map(title -> Application.create(decision, title.trim())) + .toList(); + + if (!newApplications.isEmpty()) { + applicationRepository.saveAll(newApplications); + log.info("적용사항 저장 완료: meetingId={}, decisionId={}, applicationCount={}", + meetingId, decision.getId(), newApplications.size()); + } + } + // OverallAnalysis를 MeetingAnalysis 저장용 payload로 변환한다. private MeetingAnalysis.MeetingAnalysisPayload buildMeetingAnalysisPayload( diff --git a/src/main/java/com/whylog/server/domain/team/service/TeamQueryService.java b/src/main/java/com/whylog/server/domain/team/service/TeamQueryService.java index 364483a..54dce8b 100644 --- a/src/main/java/com/whylog/server/domain/team/service/TeamQueryService.java +++ b/src/main/java/com/whylog/server/domain/team/service/TeamQueryService.java @@ -1,5 +1,6 @@ package com.whylog.server.domain.team.service; +import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionResponse; import com.whylog.server.domain.decision.entity.Decision; import com.whylog.server.domain.team.repository.TeamRepository; @@ -24,7 +25,15 @@ public List decisions( Long teamId ){ .map(d -> DecisionResponse.DecisionListDTO.builder() .decisionId(d.getId()) .name(d.getMeeting().getName()) - .applicationCount( d.getApplications().size() ) + .applications( + d.getApplications().stream() + .map(application -> ApplicationResponse.ApplicationDTO.builder() + .decisionId(d.getId()) + .applicationId(application.getId()) + .name(application.getName()) + .build()) + .toList() + ) .build() ).toList(); } From 2a1e554bd8e47b5db04737d42d41b47fa908695d Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Mon, 4 May 2026 15:10:07 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=97=90=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=EC=82=AC=ED=95=AD=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/decision/controller/DecisionController.java | 8 +------- .../server/domain/team/controller/TeamController.java | 9 ++++++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java index 2815333..cb64fef 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; + import java.util.List; @RestController @@ -18,13 +19,6 @@ @Tag(name = "Decision", description = "결정사항 관련 API") public class DecisionController { - @GetMapping("/{decisionId}/applications") - @Operation(summary = "적용사항 목록 조회 API", description = "특정 결정사항의 적용사항 목록을 조회하는 API입니다.") - public ApiResponse> getApplications( - @PathVariable Long decisionId) { - return ApiResponse.onSuccess(null); - } - @GetMapping("/{decisionId}/reliability") @Operation(summary = "신뢰도 조회 API", description = "특정 결정사항의 신뢰도 정보를 조회하는 API입니다.") public ApiResponse getReliability( diff --git a/src/main/java/com/whylog/server/domain/team/controller/TeamController.java b/src/main/java/com/whylog/server/domain/team/controller/TeamController.java index eb942b7..28d7bfd 100644 --- a/src/main/java/com/whylog/server/domain/team/controller/TeamController.java +++ b/src/main/java/com/whylog/server/domain/team/controller/TeamController.java @@ -37,7 +37,14 @@ public class TeamController { private final TeamQueryService teamQueryService; @GetMapping("/{teamId}/decisions") - @Operation(summary = "결정사항 목록 조회 API", description = "특정 팀의 결정사항 목록을 조회하는 API입니다.") + @Operation( + summary = "결정사항 목록 조회 API", + description = """ + 특정 팀의 결정사항 목록을 조회하는 API입니다. + 각 결정사항에는 해당 결정사항에 연결된 적용사항 목록도 함께 포함됩니다. + 페이징 없습니다. + """ + ) @ApiErrorCodeExamples({ @ApiErrorCodeExample(value = ErrorStatus.class, name = "_UNAUTHORIZED"), @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST") From 5eb56d9b8201648f917bfd544d2985132f79bdfb Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 11:11:38 +0900 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20=EB=B6=84=EC=84=9D=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/decision/entity/DecisionBase.java | 12 ++++++- .../decision/entity/DecisionTimeline.java | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java b/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java index 1c5f4f0..7e21d74 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/DecisionBase.java @@ -14,7 +14,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; -// 적용현황 +// 결정근거 @Entity @Getter @Table(name = "Decision_Base") @@ -29,4 +29,14 @@ public class DecisionBase extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "decision_id", nullable = false) private Decision decision; + + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + public static DecisionBase create(Decision decision, String content) { + DecisionBase decisionBase = new DecisionBase(); + decisionBase.decision = decision; + decisionBase.content = content; + return decisionBase; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java b/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java index d71a5f5..aa42c8b 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/DecisionTimeline.java @@ -28,4 +28,39 @@ public class DecisionTimeline extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "decision_id", nullable = false) private Decision decision; + + @Column(name = "timestamp", length = 30) + private String timestamp; + + // 타임라인 단계 + @Column(name = "step", length = 10) + private String step; + + // 타임라인 내용 요약 + @Column(name = "content", columnDefinition = "TEXT") + private String content; + + // 발화자 + @Column(name = "member_id") + private Long memberId; + + // 발화 원문 + @Column(name = "utterance", columnDefinition = "TEXT") + private String utterance; + + public static DecisionTimeline create(Decision decision, + String timestamp, + String step, + String content, + Long memberId, + String utterance) { + DecisionTimeline decisionTimeline = new DecisionTimeline(); + decisionTimeline.decision = decision; + decisionTimeline.timestamp = timestamp; + decisionTimeline.step = step; + decisionTimeline.content = content; + decisionTimeline.memberId = memberId; + decisionTimeline.utterance = utterance; + return decisionTimeline; + } } From 9c51d64e584df3d8dca77580702bb48886e32125 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 11:15:42 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=A0=81=EC=9A=A9=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A0=95=EB=B3=B4=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/decision/entity/ApplicationBase.java | 8 ++++++++ .../server/domain/decision/entity/ApplicationBaseId.java | 2 ++ .../domain/decision/entity/ApplicationTimeline.java | 8 ++++++++ .../domain/decision/entity/ApplicationTimelineId.java | 2 ++ .../decision/repository/ApplicationBaseRepository.java | 8 ++++++++ .../repository/ApplicationTimelineRepository.java | 8 ++++++++ .../decision/repository/DecisionBaseRepository.java | 7 +++++++ .../decision/repository/DecisionTimelineRepository.java | 7 +++++++ .../server/domain/meeting/service/MeetingUseCase.java | 4 ++++ 9 files changed, 54 insertions(+) create mode 100644 src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java create mode 100644 src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java create mode 100644 src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java create mode 100644 src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java diff --git a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java index 4ac69b4..9f63395 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBase.java @@ -24,4 +24,12 @@ public class ApplicationBase extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "decision_base_pk", nullable = false) private DecisionBase decisionBase; + + public static ApplicationBase create(Application application, DecisionBase decisionBase) { + ApplicationBase applicationBase = new ApplicationBase(); + applicationBase.id = new ApplicationBaseId(application.getId(), decisionBase.getId()); + applicationBase.application = application; + applicationBase.decisionBase = decisionBase; + return applicationBase; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBaseId.java b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBaseId.java index c6c6cc2..7ebd42e 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBaseId.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationBaseId.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.io.Serializable; +import lombok.AllArgsConstructor; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -11,6 +12,7 @@ @Getter @Embeddable @EqualsAndHashCode +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApplicationBaseId implements Serializable { diff --git a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java index 67b1abb..c315695 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimeline.java @@ -30,4 +30,12 @@ public class ApplicationTimeline extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "decision_timeline_pk", nullable = false) private DecisionTimeline decisionTimeline; + + public static ApplicationTimeline create(Application application, DecisionTimeline decisionTimeline) { + ApplicationTimeline applicationTimeline = new ApplicationTimeline(); + applicationTimeline.id = new ApplicationTimelineId(application.getId(), decisionTimeline.getId()); + applicationTimeline.application = application; + applicationTimeline.decisionTimeline = decisionTimeline; + return applicationTimeline; + } } diff --git a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimelineId.java b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimelineId.java index de00599..01dc439 100644 --- a/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimelineId.java +++ b/src/main/java/com/whylog/server/domain/decision/entity/ApplicationTimelineId.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.io.Serializable; +import lombok.AllArgsConstructor; import lombok.AccessLevel; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -11,6 +12,7 @@ @Getter @Embeddable @EqualsAndHashCode +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApplicationTimelineId implements Serializable { diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java new file mode 100644 index 0000000..b67c0ed --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java @@ -0,0 +1,8 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.ApplicationBase; +import com.whylog.server.domain.decision.entity.ApplicationBaseId; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ApplicationBaseRepository extends JpaRepository { +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java new file mode 100644 index 0000000..30ee47f --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java @@ -0,0 +1,8 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.ApplicationTimeline; +import com.whylog.server.domain.decision.entity.ApplicationTimelineId; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ApplicationTimelineRepository extends JpaRepository { +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java new file mode 100644 index 0000000..4b5c554 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionBaseRepository.java @@ -0,0 +1,7 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.DecisionBase; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DecisionBaseRepository extends JpaRepository { +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java new file mode 100644 index 0000000..488ed0d --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionTimelineRepository.java @@ -0,0 +1,7 @@ +package com.whylog.server.domain.decision.repository; + +import com.whylog.server.domain.decision.entity.DecisionTimeline; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DecisionTimelineRepository extends JpaRepository { +} 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..f5916f9 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 @@ -49,4 +49,8 @@ public Meeting findWithAnalysisByMeetingId(Long meetingId) { .orElseThrow(MeetingNotFoundException::new); } + public Long resolveMemberIdBySpeakerId(Long meetingId, String speakerId) { + // TODO: 대화 내역을 기준으로 speakerId와 실제 memberId를 매칭한다. + return 1L; + } } From 2827cbffb23e66e6a27f60799158a5d7eef14df5 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 11:23:29 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9D=98=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EA=B2=B0=EA=B3=BC=20=EC=A0=80=EC=9E=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../meeting/controller/MeetingController.java | 16 +++ .../domain/meeting/dto/MeetingRequest.java | 21 +++ .../service/MeetingAnalysisService.java | 120 +++++++++++++++++- 3 files changed, 152 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/whylog/server/domain/meeting/controller/MeetingController.java b/src/main/java/com/whylog/server/domain/meeting/controller/MeetingController.java index 421a469..45c1504 100644 --- a/src/main/java/com/whylog/server/domain/meeting/controller/MeetingController.java +++ b/src/main/java/com/whylog/server/domain/meeting/controller/MeetingController.java @@ -5,6 +5,7 @@ import com.whylog.server.domain.meeting.enums.MeetingStatus; import com.whylog.server.domain.meeting.exception.MeetingErrorCode; import com.whylog.server.domain.meeting.service.MeetingCommandService; +import com.whylog.server.domain.meeting.service.MeetingAnalysisService; import com.whylog.server.domain.meeting.service.MeetingQueryService; import com.whylog.server.domain.meeting.service.MeetingRtcService; import com.whylog.server.domain.team.exception.TeamErrorCode; @@ -39,6 +40,7 @@ public class MeetingController { private final MeetingCommandService meetingCommandService; private final MeetingQueryService meetingQueryService; private final MeetingRtcService meetingRtcService; + private final MeetingAnalysisService meetingAnalysisService; @GetMapping("/teams/{teamId}/meetings") @Operation(summary = "회의 목록 조회 API", description = """ @@ -134,6 +136,20 @@ public ApiResponse endMeeting( return ApiResponse.onSuccess(meetingCommandService.endMeeting(memberId, meetingId)); } + @PostMapping("/meetings/{meetingId}/analysis-test") + @Operation(summary = "회의 분석 저장 테스트 API", description = """ + FastAPI 상태 조회 응답 JSON을 그대로 전달해 결정사항/적용사항 저장 로직만 테스트하는 API입니다. + 외부 분석 호출 없이 저장 흐름만 검증할 때 사용합니다. + """) + public ApiResponse analyzeTestResponse( + @Parameter(hidden = true) @CurrentMember Long memberId, + @PathVariable Long meetingId, + @RequestBody MeetingRequest.MeetingAnalysisTestDTO request + ) { + meetingAnalysisService.persistTestMeetingAnalysis(memberId, meetingId, request); + return ApiResponse.onSuccess(null); + } + @DeleteMapping("/meetings/{meetingId}") @Operation(summary = "회의 삭제 API", description = """ 특정 회의를 삭제하는 API입니다. diff --git a/src/main/java/com/whylog/server/domain/meeting/dto/MeetingRequest.java b/src/main/java/com/whylog/server/domain/meeting/dto/MeetingRequest.java index 2fb09eb..63f655b 100644 --- a/src/main/java/com/whylog/server/domain/meeting/dto/MeetingRequest.java +++ b/src/main/java/com/whylog/server/domain/meeting/dto/MeetingRequest.java @@ -1,5 +1,6 @@ package com.whylog.server.domain.meeting.dto; +import com.whylog.server.global.external.fast.dto.response.TranscribeApplicationRunResponse; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.*; @@ -23,4 +24,24 @@ public static class MeetingCreateDTO { private LocalDateTime startDateTime; } + + @Getter + @NoArgsConstructor(access = AccessLevel.PROTECTED) + @AllArgsConstructor + @Builder + @Schema(description = "회의 분석 저장 테스트 요청") + public static class MeetingAnalysisTestDTO { + + @Schema(description = "요청 성공 여부", example = "true") + private Boolean isSuccess; + + @Schema(description = "응답 코드", example = "TRANSCRIBE_200") + private String code; + + @Schema(description = "응답 메시지", example = "비동기 실행 상태 조회에 성공했습니다.") + private String message; + + @Schema(description = "FastAPI 비동기 실행 상태 조회 결과") + private TranscribeApplicationRunResponse result; + } } diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java index 104d8c4..c69984a 100644 --- a/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingAnalysisService.java @@ -3,17 +3,27 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.whylog.server.domain.decision.entity.Application; +import com.whylog.server.domain.decision.entity.ApplicationBase; +import com.whylog.server.domain.decision.entity.ApplicationTimeline; import com.whylog.server.domain.decision.entity.Decision; +import com.whylog.server.domain.decision.entity.DecisionBase; +import com.whylog.server.domain.decision.entity.DecisionTimeline; +import com.whylog.server.domain.decision.repository.ApplicationBaseRepository; +import com.whylog.server.domain.decision.repository.ApplicationTimelineRepository; import com.whylog.server.domain.decision.repository.ApplicationRepository; +import com.whylog.server.domain.decision.repository.DecisionBaseRepository; +import com.whylog.server.domain.decision.repository.DecisionTimelineRepository; import com.whylog.server.domain.decision.repository.DecisionRepository; import com.whylog.server.domain.meeting.dto.MeetingResponse; import com.whylog.server.domain.meeting.entity.Dialogue; import com.whylog.server.domain.meeting.entity.Meeting; import com.whylog.server.domain.meeting.entity.MeetingAnalysis; import com.whylog.server.domain.meeting.entity.MeetingMember; +import com.whylog.server.domain.meeting.exception.MeetingInvalidMemberException; import com.whylog.server.domain.meeting.exception.MeetingAudioNotReadyException; import com.whylog.server.domain.meeting.repository.DialogueRepository; import com.whylog.server.domain.meeting.repository.MeetingAnalysisRepository; +import com.whylog.server.domain.meeting.repository.MeetingMemberRepository; import com.whylog.server.domain.user.entity.Member; import com.whylog.server.global.external.fast.client.FastApiTranscribeClient; import com.whylog.server.global.external.fast.dto.FastApiResponse; @@ -56,9 +66,14 @@ public class MeetingAnalysisService { private final MeetingAudioFileService meetingAudioFileService; private final FastApiTranscribeClient fastApiTranscribeClient; private final ApplicationRepository applicationRepository; + private final ApplicationBaseRepository applicationBaseRepository; + private final ApplicationTimelineRepository applicationTimelineRepository; + private final DecisionBaseRepository decisionBaseRepository; + private final DecisionTimelineRepository decisionTimelineRepository; private final DecisionRepository decisionRepository; private final MeetingAnalysisRepository meetingAnalysisRepository; private final DialogueRepository dialogueRepository; + private final MeetingMemberRepository meetingMemberRepository; private final TransactionTemplate transactionTemplate; private final ObjectMapper objectMapper; @@ -72,6 +87,23 @@ public void analyzeMeetingAudio(Long meetingId) { persistMeetingAnalysis(meeting, finalResponse); } + // FastAPI 응답 JSON을 받아 저장 로직을 테스트한다. + public void persistTestMeetingAnalysis(Long memberId, + Long meetingId, + com.whylog.server.domain.meeting.dto.MeetingRequest.MeetingAnalysisTestDTO request) { + if (!meetingMemberRepository.existsByMemberIdAndMeetingId(memberId, meetingId)) { + throw new MeetingInvalidMemberException(); + } + + Meeting meeting = meetingUseCase.findMeetingWithMembersById(meetingId); + TranscribeApplicationRunResponse response = request.getResult(); + if (response == null) { + throw new FastApiException(FastApiErrorCode.FAST_API_RESPONSE_EMPTY); + } + + persistMeetingAnalysis(meeting, response); + } + // 회의 오디오가 준비될 때까지 재시도하며 오디오 응답을 확보한다. private MeetingResponse.AudioDTO resolveAudioWithRetry(Meeting meeting) { MeetingAudioNotReadyException lastException = null; @@ -224,19 +256,97 @@ private Decision createDecisionIfAbsent(Meeting meeting) { private void replaceApplications(Long meetingId, Decision decision, List applications) { - List newApplications = applications.stream() - .map(TranscribeApplicationRunResponse.ApplicationResponse::applicationTitle) - .filter(title -> title != null && !title.isBlank()) - .map(title -> Application.create(decision, title.trim())) + List validApplications = applications.stream() + .filter(application -> application != null + && application.applicationTitle() != null + && !application.applicationTitle().isBlank()) + .toList(); + + List newApplications = validApplications.stream() + .map(application -> Application.create( + decision, + application.applicationTitle().trim() + )) .toList(); if (!newApplications.isEmpty()) { - applicationRepository.saveAll(newApplications); + List savedApplications = applicationRepository.saveAllAndFlush(newApplications); + persistApplicationDetails(savedApplications, validApplications); log.info("적용사항 저장 완료: meetingId={}, decisionId={}, applicationCount={}", meetingId, decision.getId(), newApplications.size()); } } + // 저장된 적용사항 엔티티에 reason/timeline 세부 정보를 순서대로 연결 저장한다. + private void persistApplicationDetails(List applications, + List applicationResponses) { + for (int index = 0; index < applications.size(); index++) { + Application application = applications.get(index); + TranscribeApplicationRunResponse.ApplicationResponse response = applicationResponses.get(index); + persistApplicationReasons(application, response.applicationReasons()); + persistApplicationTimelines(application, response.timeline()); + } + } + + // 적용사항 reason 목록을 DecisionBase/ApplicationBase로 분리 저장한다. + private void persistApplicationReasons(Application application, List reasons) { + List validReasons = safeStrings(reasons); + if (validReasons.isEmpty()) { + return; + } + + List decisionBases = validReasons.stream() + .map(reason -> DecisionBase.create(application.getDecision(), reason.trim())) + .toList(); + List savedDecisionBases = decisionBaseRepository.saveAllAndFlush(decisionBases); + + List applicationBases = savedDecisionBases.stream() + .map(decisionBase -> ApplicationBase.create(application, decisionBase)) + .toList(); + applicationBaseRepository.saveAllAndFlush(applicationBases); + } + + // 적용사항 timeline 목록을 DecisionTimeline/ApplicationTimeline으로 분리 저장한다. + private void persistApplicationTimelines(Application application, + List timelines) { + if (timelines == null || timelines.isEmpty()) { + return; + } + + List decisionTimelines = timelines.stream() + .filter(timeline -> timeline != null) + .map(timeline -> DecisionTimeline.create( + application.getDecision(), + timeline.timestamp(), + timeline.step(), + timeline.content(), + meetingUseCase.resolveMemberIdBySpeakerId(application.getDecision().getMeeting().getId(), timeline.speakerId()), + timeline.utterance() + )) + .toList(); + + if (decisionTimelines.isEmpty()) { + return; + } + + List savedDecisionTimelines = decisionTimelineRepository.saveAllAndFlush(decisionTimelines); + List applicationTimelines = savedDecisionTimelines.stream() + .map(decisionTimeline -> ApplicationTimeline.create(application, decisionTimeline)) + .toList(); + applicationTimelineRepository.saveAllAndFlush(applicationTimelines); + } + + // null 이거나 비어 있는 문자열을 제외한 값만 반환한다. + private List safeStrings(List values) { + if (values == null || values.isEmpty()) { + return List.of(); + } + + return values.stream() + .filter(value -> value != null && !value.isBlank()) + .toList(); + } + // OverallAnalysis를 MeetingAnalysis 저장용 payload로 변환한다. private MeetingAnalysis.MeetingAnalysisPayload buildMeetingAnalysisPayload( From 9e6c8c7febee9b73734d69d1ec864c14c9636200 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 12:36:47 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20=EC=A0=81=EC=9A=A9=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApplicationController.java | 16 ++- .../decision/dto/ApplicationResponse.java | 35 +++++- .../ApplicationNotFoundException.java | 10 ++ .../decision/exception/DecisionErrorCode.java | 39 +++++++ .../repository/ApplicationBaseRepository.java | 14 +++ .../ApplicationTimelineRepository.java | 14 +++ .../service/ApplicationQueryService.java | 102 ++++++++++++++++++ .../domain/user/service/MemberUseCase.java | 5 + 8 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/whylog/server/domain/decision/exception/ApplicationNotFoundException.java create mode 100644 src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java create mode 100644 src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java diff --git a/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java b/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java index a1cc92a..622c406 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/ApplicationController.java @@ -2,7 +2,12 @@ import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionRequest; +import com.whylog.server.domain.decision.exception.DecisionErrorCode; +import com.whylog.server.domain.decision.service.ApplicationQueryService; import com.whylog.server.global.apiPayload.ApiResponse; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples; +import com.whylog.server.global.apiPayload.code.status.ErrorStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -22,12 +27,18 @@ @Tag(name = "Application", description = "적용사항 관련 API") public class ApplicationController { + private final ApplicationQueryService applicationQueryService; + @GetMapping("/{applicationId}") - @Operation(summary = "적용사항 상세 조회 API", description = "특정 적용사항의 상세 정보를 조회하는 API입니다.") + @Operation(summary = "적용사항 상세 조회 API", description = "특정 적용사항의 상세 정보를 조회하는 API입니다. 적용사항 상세 조회 화면에서 적용사항 제목, 타임라인, 결정원문 맥락, 결정근거를 조회합니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "APPLICATION_NOT_FOUND") + }) public ApiResponse getApplication( @PathVariable Long applicationId) { - return ApiResponse.onSuccess(null); + return ApiResponse.onSuccess(applicationQueryService.getApplicationDetail(applicationId)); } @GetMapping("/{applicationId}/recommended-commits") @@ -60,4 +71,5 @@ public ApiResponse saveRecommendation( return ApiResponse.onSuccess(null); } + // TODO: 적용현황 조회 api 추가 } diff --git a/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java b/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java index 30e0d27..11444b2 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/ApplicationResponse.java @@ -45,12 +45,14 @@ public static class ApplicationDetailDTO { @Schema(description = "결정 타임라인 목록") private List decisionTimelines; - @Schema(description = "결정근거 목록") - private List decisionReasons; + @Schema(description = "결정 원문 맥락 목록") + private List decisionContexts; - @Schema(description = "적용현황 (커밋 정보) 목록") - private List applicationBases; + @Schema(description = "결정근거 개수", example = "3") + private Integer decisionReasonCount; + @Schema(description = "결정근거 목록") + private List decisionReasons; } @Getter @@ -63,12 +65,35 @@ public static class DecisionTimelineItemDTO { @Schema(description = "타임라인 시간", example = "2026-03-24T12:28:00") private String time; + @Schema(description = "타임라인 단계", example = "이슈제기") + private String step; + @Schema(description = "타임라인 내용", example = "장애 이슈 제기") private String content; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "결정 원문 맥락 항목") + public static class DecisionContextItemDTO { + + @Schema(description = "타임라인 시간", example = "2026-03-24T12:28:00") + private String time; + + @Schema(description = "발화자 ID", example = "1", nullable = true) + private Long memberId; + + @Schema(description = "발화자 이름", example = "김주뇽", nullable = true) + private String memberName; + + @Schema(description = "발화자 프로필 사진", example = "https://example.com/profile.jpg", nullable = true) + private String profileImage; + @Schema(description = "대화 내용", example = "아니 우리 이거 버그난다니까?!?@??@") private String dialogueContent; - } diff --git a/src/main/java/com/whylog/server/domain/decision/exception/ApplicationNotFoundException.java b/src/main/java/com/whylog/server/domain/decision/exception/ApplicationNotFoundException.java new file mode 100644 index 0000000..102259f --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/exception/ApplicationNotFoundException.java @@ -0,0 +1,10 @@ +package com.whylog.server.domain.decision.exception; + +import com.whylog.server.global.apiPayload.exception.GeneralException; + +public class ApplicationNotFoundException extends GeneralException { + + public ApplicationNotFoundException() { + super(DecisionErrorCode.APPLICATION_NOT_FOUND); + } +} diff --git a/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java b/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java new file mode 100644 index 0000000..3787764 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/exception/DecisionErrorCode.java @@ -0,0 +1,39 @@ +package com.whylog.server.domain.decision.exception; + +import com.whylog.server.global.apiPayload.code.BaseErrorCode; +import com.whylog.server.global.apiPayload.code.ErrorReasonDTO; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum DecisionErrorCode implements BaseErrorCode { + + DECISION_NOT_FOUND(HttpStatus.NOT_FOUND, "DECISION_404", "존재하지 않는 결정사항입니다."), + APPLICATION_NOT_FOUND(HttpStatus.NOT_FOUND, "APPLICATION_404", "존재하지 않는 적용사항입니다."), + ; + + private final HttpStatus httpStatus; + private final String code; + private final String message; + + @Override + public ErrorReasonDTO getReason() { + return ErrorReasonDTO.builder() + .isSuccess(false) + .code(code) + .message(message) + .build(); + } + + @Override + public ErrorReasonDTO getReasonHttpStatus() { + return ErrorReasonDTO.builder() + .httpStatus(httpStatus) + .isSuccess(false) + .code(code) + .message(message) + .build(); + } +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java index b67c0ed..c9309d6 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationBaseRepository.java @@ -3,6 +3,20 @@ import com.whylog.server.domain.decision.entity.ApplicationBase; import com.whylog.server.domain.decision.entity.ApplicationBaseId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ApplicationBaseRepository extends JpaRepository { + + // 적용사항에 연결된 결정근거 목록 조회 + @Query(""" + SELECT ab + FROM ApplicationBase ab + JOIN FETCH ab.decisionBase db + WHERE ab.application.id = :applicationId + ORDER BY db.id ASC + """) + List findByApplicationId(@Param("applicationId") Long applicationId); } diff --git a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java index 30ee47f..7cb8950 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/ApplicationTimelineRepository.java @@ -3,6 +3,20 @@ import com.whylog.server.domain.decision.entity.ApplicationTimeline; import com.whylog.server.domain.decision.entity.ApplicationTimelineId; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; public interface ApplicationTimelineRepository extends JpaRepository { + + // 적용사항에 연결된 결정 타임라인 목록을 조회한다. + @Query(""" + SELECT at + FROM ApplicationTimeline at + JOIN FETCH at.decisionTimeline dt + WHERE at.application.id = :applicationId + ORDER BY dt.timestamp ASC, dt.id ASC + """) + List findByApplicationId(@Param("applicationId") Long applicationId); } diff --git a/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java b/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java new file mode 100644 index 0000000..8ac93a0 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/service/ApplicationQueryService.java @@ -0,0 +1,102 @@ +package com.whylog.server.domain.decision.service; + +import com.whylog.server.domain.decision.dto.ApplicationResponse; +import com.whylog.server.domain.decision.entity.Application; +import com.whylog.server.domain.decision.entity.ApplicationBase; +import com.whylog.server.domain.decision.entity.ApplicationTimeline; +import com.whylog.server.domain.decision.exception.ApplicationNotFoundException; +import com.whylog.server.domain.decision.repository.ApplicationBaseRepository; +import com.whylog.server.domain.decision.repository.ApplicationRepository; +import com.whylog.server.domain.decision.repository.ApplicationTimelineRepository; +import com.whylog.server.domain.user.entity.Member; +import com.whylog.server.domain.user.service.MemberUseCase; +import java.util.Map; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ApplicationQueryService { + + private final ApplicationRepository applicationRepository; + private final ApplicationBaseRepository applicationBaseRepository; + private final ApplicationTimelineRepository applicationTimelineRepository; + private final MemberUseCase memberUseCase; + + // 적용사항 상세 조회에 필요한 제목, 타임라인, 원문 맥락, 결정근거를 조회 + @Transactional(readOnly = true) + public ApplicationResponse.ApplicationDetailDTO getApplicationDetail(Long applicationId) { + Application application = applicationRepository.findById(applicationId) + .orElseThrow(ApplicationNotFoundException::new); + + // 적용사항에 연결된 근거/타임라인 원본 엔티티를 각각 조회 + List applicationBases = applicationBaseRepository.findByApplicationId(applicationId); + List applicationTimelines = applicationTimelineRepository.findByApplicationId(applicationId); + Map membersById = findMembersById(applicationTimelines); + + return ApplicationResponse.ApplicationDetailDTO.builder() + .applicationId(application.getId()) + .name(application.getName()) + .decisionReasons(toDecisionReasonItems(applicationBases)) + .decisionTimelines(toDecisionTimelineItems(applicationTimelines)) + .decisionContexts(toDecisionContextItems(applicationTimelines, membersById)) + .decisionReasonCount(applicationBases.size()) + .build(); + } + + // 연결 테이블을 따라 적용사항에 속한 결정근거 목록을 응답 DTO로 변환 + private List toDecisionReasonItems(List applicationBases) { + return applicationBases.stream() + .map(applicationBase -> ApplicationResponse.DecisionReasonItemDTO.builder() + .reasonId(String.valueOf(applicationBase.getDecisionBase().getId())) + .title(applicationBase.getDecisionBase().getContent()) + .build()) + .toList(); + } + + // 타임라인 요약 정보 + private List toDecisionTimelineItems(List applicationTimelines) { + return applicationTimelines.stream() + .map(applicationTimeline -> ApplicationResponse.DecisionTimelineItemDTO.builder() + .time(applicationTimeline.getDecisionTimeline().getTimestamp()) + .step(applicationTimeline.getDecisionTimeline().getStep()) + .content(applicationTimeline.getDecisionTimeline().getContent()) + .build()) + .toList(); + } + + // 원문 맥락은 발화자 정보와 원문 발화를 함께 내려줌 + private List toDecisionContextItems(List applicationTimelines, + Map membersById) { + return applicationTimelines.stream() + .map(applicationTimeline -> { + Long memberId = applicationTimeline.getDecisionTimeline().getMemberId(); + Member member = memberId != null ? membersById.get(memberId) : null; + + return ApplicationResponse.DecisionContextItemDTO.builder() + .time(applicationTimeline.getDecisionTimeline().getTimestamp()) + .memberId(memberId) + .memberName(member != null ? member.getName() : null) + .profileImage(member != null ? member.getProfileImage() : null) + .dialogueContent(applicationTimeline.getDecisionTimeline().getUtterance()) + .build(); + }) + .toList(); + } + + // 타임라인에 포함된 발화자들을 한 번에 조회해 memberId 기준 맵으로 구성한다. + private Map findMembersById(List applicationTimelines) { + List memberIds = applicationTimelines.stream() + .map(applicationTimeline -> applicationTimeline.getDecisionTimeline().getMemberId()) + .filter(memberId -> memberId != null) + .distinct() + .toList(); + + return memberUseCase.findMembersByIds(memberIds).stream() + .collect(Collectors.toMap(Member::getId, Function.identity())); + } +} diff --git a/src/main/java/com/whylog/server/domain/user/service/MemberUseCase.java b/src/main/java/com/whylog/server/domain/user/service/MemberUseCase.java index bb2aa84..09cbed9 100644 --- a/src/main/java/com/whylog/server/domain/user/service/MemberUseCase.java +++ b/src/main/java/com/whylog/server/domain/user/service/MemberUseCase.java @@ -3,6 +3,7 @@ import com.whylog.server.domain.user.entity.Member; import com.whylog.server.domain.user.exception.MemberNotFoundException; import com.whylog.server.domain.user.repository.MemberRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,4 +24,8 @@ public Member findMemberByEmail(String email) { .orElseThrow(MemberNotFoundException::new); } + public List findMembersByIds(List memberIds) { + return memberRepository.findAllById(memberIds); + } + } From e6d503e6ee1e24539be20701504ecc32c3cee927 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 12:38:04 +0900 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DecisionController.java | 18 +++++ .../domain/decision/dto/DecisionResponse.java | 53 +++++++++++--- .../exception/DecisionNotFoundException.java | 10 +++ .../repository/DecisionRepository.java | 11 +++ .../service/DecisionQueryService.java | 73 +++++++++++++++++++ .../service/MeetingSpeakerResolver.java | 6 ++ .../meeting/service/MeetingUseCase.java | 6 +- 7 files changed, 164 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/whylog/server/domain/decision/exception/DecisionNotFoundException.java create mode 100644 src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java create mode 100644 src/main/java/com/whylog/server/domain/meeting/service/MeetingSpeakerResolver.java diff --git a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java index cb64fef..439ef61 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java @@ -2,7 +2,12 @@ import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionResponse; +import com.whylog.server.domain.decision.exception.DecisionErrorCode; +import com.whylog.server.domain.decision.service.DecisionQueryService; import com.whylog.server.global.apiPayload.ApiResponse; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample; +import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples; +import com.whylog.server.global.apiPayload.code.status.ErrorStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -19,6 +24,19 @@ @Tag(name = "Decision", description = "결정사항 관련 API") public class DecisionController { + private final DecisionQueryService decisionQueryService; + + @GetMapping("/{decisionId}") + @Operation(summary = "결정사항 상세 조회 API", description = "특정 결정사항의 상세 정보를 조회하는 API입니다. 적용사항 상세 조회 화면에서 상단의 결정사항 상세 정보를 조회합니다.") + @ApiErrorCodeExamples({ + @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), + @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "DECISION_NOT_FOUND") + }) + public ApiResponse getDecision( + @PathVariable Long decisionId) { + return ApiResponse.onSuccess(decisionQueryService.getDecisionDetail(decisionId)); + } + @GetMapping("/{decisionId}/reliability") @Operation(summary = "신뢰도 조회 API", description = "특정 결정사항의 신뢰도 정보를 조회하는 API입니다.") public ApiResponse getReliability( diff --git a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java index 386b671..01aa2e1 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java @@ -1,6 +1,7 @@ package com.whylog.server.domain.decision.dto; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDate; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; @@ -9,6 +10,49 @@ public class DecisionResponse { + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "결정사항 상세 조회 응답") + public static class DecisionDetailDTO { + + @Schema(description = "결정사항 ID", example = "1") + private Long decisionId; + + @Schema(description = "결정사항 이름", example = "백엔드 비상대책회의") + private String name; + + @Schema(description = "회의 날짜", example = "2026-05-05") + private LocalDate meetingDate; + + @Schema(description = "회의 소요 시간", example = "1시간 30분") + private String meetingTime; + + @Schema(description = "참여 인원 수", example = "4") + private Integer memberCount; + + @Schema(description = "참여자 목록") + private List members; + } + + @Getter + @NoArgsConstructor + @AllArgsConstructor + @Builder + @Schema(description = "결정사항 참여자 정보") + public static class DecisionParticipantDTO { + + @Schema(description = "멤버 id", example = "1") + private Long memberId; + + @Schema(description = "유저이름", example = "아무개") + private String name; + + @Schema(description = "프로필이미지", example = "https://example.com/profile/user-1.jpg") + private String profileImage; + } + @Getter @NoArgsConstructor @AllArgsConstructor @@ -35,15 +79,6 @@ public static class ReliabilityDTO { @Schema(description = "신뢰도 점수", example = "85") private Integer score; - - @Schema(description = "근거발언 개수", example = "42") - private Integer reasonSpeechCount; - - @Schema(description = "참여자 합의도", example = "HIGH") - private String participantConsensus; - - @Schema(description = "결정 구현 일치율", example = "92") - private Integer matchRatio; } } diff --git a/src/main/java/com/whylog/server/domain/decision/exception/DecisionNotFoundException.java b/src/main/java/com/whylog/server/domain/decision/exception/DecisionNotFoundException.java new file mode 100644 index 0000000..5491af8 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/exception/DecisionNotFoundException.java @@ -0,0 +1,10 @@ +package com.whylog.server.domain.decision.exception; + +import com.whylog.server.global.apiPayload.exception.GeneralException; + +public class DecisionNotFoundException extends GeneralException { + + public DecisionNotFoundException() { + super(DecisionErrorCode.DECISION_NOT_FOUND); + } +} diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java index e943acb..2dba8d0 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java @@ -11,6 +11,17 @@ public interface DecisionRepository extends JpaRepository { Optional findByMeetingId(Long meetingId); + // 결정사항 상세 조회에 필요한 회의와 참여자 정보를 함께 조회한다. + @Query(""" + SELECT DISTINCT d + FROM Decision d + JOIN FETCH d.meeting m + LEFT JOIN FETCH m.meetingMembers mm + LEFT JOIN FETCH mm.member + WHERE d.id = :decisionId + """) + Optional findDetailById(@Param("decisionId") Long decisionId); + @Modifying @Query("DELETE FROM Application a WHERE a.decision.meeting.team.id = :teamId") void deleteApplicationsByTeamId(@Param("teamId") Long teamId); diff --git a/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java b/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java new file mode 100644 index 0000000..96bb3e8 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java @@ -0,0 +1,73 @@ +package com.whylog.server.domain.decision.service; + +import com.whylog.server.domain.decision.dto.DecisionResponse; +import com.whylog.server.domain.decision.entity.Decision; +import com.whylog.server.domain.decision.exception.DecisionNotFoundException; +import com.whylog.server.domain.decision.repository.DecisionRepository; +import com.whylog.server.domain.meeting.entity.Meeting; +import com.whylog.server.domain.meeting.entity.MeetingMember; +import com.whylog.server.domain.user.entity.Member; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class DecisionQueryService { + + private final DecisionRepository decisionRepository; + + // 결정사항 상세 조회에 필요한 회의 정보와 참여자 정보를 조회 + @Transactional(readOnly = true) + public DecisionResponse.DecisionDetailDTO getDecisionDetail(Long decisionId) { + Decision decision = decisionRepository.findDetailById(decisionId) + .orElseThrow(DecisionNotFoundException::new); + + Meeting meeting = decision.getMeeting(); + + return DecisionResponse.DecisionDetailDTO.builder() + .decisionId(decision.getId()) + .name(meeting.getName()) + .meetingDate(meeting.getStartDateTime() != null ? meeting.getStartDateTime().toLocalDate() : null) + .meetingTime(buildMeetingDuration(meeting)) + .memberCount(meeting.getMeetingMembers().size()) + .members( + meeting.getMeetingMembers().stream() + .map(MeetingMember::getMember) + .map(this::toParticipant) + .toList() + ) + .build(); + } + + // 회의 소요 시간을 시간, 분 단위로 변환 + private String buildMeetingDuration(Meeting meeting) { + Long duration = meeting.getDuration(); + if (duration == null) { + return null; + } + + long hours = duration / 60; + long minutes = duration % 60; + + if (hours == 0) { + return minutes + "분"; + } + + if (minutes == 0) { + return hours + "시간"; + } + + return hours + "시간 " + minutes + "분"; + } + + // 멤버 엔티티를 결정사항 상세 참여자 응답으로 변환 + private DecisionResponse.DecisionParticipantDTO toParticipant(Member member) { + return DecisionResponse.DecisionParticipantDTO.builder() + .memberId(member.getId()) + .name(member.getName()) + .profileImage(member.getProfileImage()) + .build(); + } +} diff --git a/src/main/java/com/whylog/server/domain/meeting/service/MeetingSpeakerResolver.java b/src/main/java/com/whylog/server/domain/meeting/service/MeetingSpeakerResolver.java new file mode 100644 index 0000000..14b9fb5 --- /dev/null +++ b/src/main/java/com/whylog/server/domain/meeting/service/MeetingSpeakerResolver.java @@ -0,0 +1,6 @@ +package com.whylog.server.domain.meeting.service; + +public interface MeetingSpeakerResolver { + + Long resolveMemberIdBySpeakerId(Long meetingId, String speakerId); +} 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 f5916f9..b97cc47 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; @@ -13,10 +12,9 @@ @Service @RequiredArgsConstructor -public class MeetingUseCase { +public class MeetingUseCase implements MeetingSpeakerResolver { private final MeetingRepository meetingRepository; - private final MeetingAnalysisRepository meetingAnalysisRepository; public Meeting findMeetingById(Long id){ return meetingRepository.findById(id) @@ -48,7 +46,7 @@ public Meeting findWithAnalysisByMeetingId(Long meetingId) { return meetingRepository.findByMeetingId(meetingId) .orElseThrow(MeetingNotFoundException::new); } - + @Override public Long resolveMemberIdBySpeakerId(Long meetingId, String speakerId) { // TODO: 대화 내역을 기준으로 speakerId와 실제 memberId를 매칭한다. return 1L; From e7705c164457f0d959a19166b41a0b896d93a5d2 Mon Sep 17 00:00:00 2001 From: Yujin1219 Date: Tue, 5 May 2026 12:55:08 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20=EA=B2=B0=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=83=81=EC=84=B8=20=EC=A1=B0=ED=9A=8C=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20->=20=ED=9A=8C=EC=9D=98=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EC=A1=B0=ED=9A=8C=20API=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/DecisionController.java | 18 ----- .../domain/decision/dto/DecisionResponse.java | 44 ----------- .../repository/DecisionRepository.java | 11 --- .../service/DecisionQueryService.java | 73 ------------------- 4 files changed, 146 deletions(-) delete mode 100644 src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java diff --git a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java index 439ef61..cb64fef 100644 --- a/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java +++ b/src/main/java/com/whylog/server/domain/decision/controller/DecisionController.java @@ -2,12 +2,7 @@ import com.whylog.server.domain.decision.dto.ApplicationResponse; import com.whylog.server.domain.decision.dto.DecisionResponse; -import com.whylog.server.domain.decision.exception.DecisionErrorCode; -import com.whylog.server.domain.decision.service.DecisionQueryService; import com.whylog.server.global.apiPayload.ApiResponse; -import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExample; -import com.whylog.server.global.apiPayload.annotation.ApiErrorCodeExamples; -import com.whylog.server.global.apiPayload.code.status.ErrorStatus; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -24,19 +19,6 @@ @Tag(name = "Decision", description = "결정사항 관련 API") public class DecisionController { - private final DecisionQueryService decisionQueryService; - - @GetMapping("/{decisionId}") - @Operation(summary = "결정사항 상세 조회 API", description = "특정 결정사항의 상세 정보를 조회하는 API입니다. 적용사항 상세 조회 화면에서 상단의 결정사항 상세 정보를 조회합니다.") - @ApiErrorCodeExamples({ - @ApiErrorCodeExample(value = ErrorStatus.class, name = "_BAD_REQUEST"), - @ApiErrorCodeExample(value = DecisionErrorCode.class, name = "DECISION_NOT_FOUND") - }) - public ApiResponse getDecision( - @PathVariable Long decisionId) { - return ApiResponse.onSuccess(decisionQueryService.getDecisionDetail(decisionId)); - } - @GetMapping("/{decisionId}/reliability") @Operation(summary = "신뢰도 조회 API", description = "특정 결정사항의 신뢰도 정보를 조회하는 API입니다.") public ApiResponse getReliability( diff --git a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java index 01aa2e1..68e5f95 100644 --- a/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java +++ b/src/main/java/com/whylog/server/domain/decision/dto/DecisionResponse.java @@ -1,7 +1,6 @@ package com.whylog.server.domain.decision.dto; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.LocalDate; import java.util.List; import lombok.AllArgsConstructor; import lombok.Builder; @@ -10,49 +9,6 @@ public class DecisionResponse { - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - @Schema(description = "결정사항 상세 조회 응답") - public static class DecisionDetailDTO { - - @Schema(description = "결정사항 ID", example = "1") - private Long decisionId; - - @Schema(description = "결정사항 이름", example = "백엔드 비상대책회의") - private String name; - - @Schema(description = "회의 날짜", example = "2026-05-05") - private LocalDate meetingDate; - - @Schema(description = "회의 소요 시간", example = "1시간 30분") - private String meetingTime; - - @Schema(description = "참여 인원 수", example = "4") - private Integer memberCount; - - @Schema(description = "참여자 목록") - private List members; - } - - @Getter - @NoArgsConstructor - @AllArgsConstructor - @Builder - @Schema(description = "결정사항 참여자 정보") - public static class DecisionParticipantDTO { - - @Schema(description = "멤버 id", example = "1") - private Long memberId; - - @Schema(description = "유저이름", example = "아무개") - private String name; - - @Schema(description = "프로필이미지", example = "https://example.com/profile/user-1.jpg") - private String profileImage; - } - @Getter @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java index 2dba8d0..e943acb 100644 --- a/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java +++ b/src/main/java/com/whylog/server/domain/decision/repository/DecisionRepository.java @@ -11,17 +11,6 @@ public interface DecisionRepository extends JpaRepository { Optional findByMeetingId(Long meetingId); - // 결정사항 상세 조회에 필요한 회의와 참여자 정보를 함께 조회한다. - @Query(""" - SELECT DISTINCT d - FROM Decision d - JOIN FETCH d.meeting m - LEFT JOIN FETCH m.meetingMembers mm - LEFT JOIN FETCH mm.member - WHERE d.id = :decisionId - """) - Optional findDetailById(@Param("decisionId") Long decisionId); - @Modifying @Query("DELETE FROM Application a WHERE a.decision.meeting.team.id = :teamId") void deleteApplicationsByTeamId(@Param("teamId") Long teamId); diff --git a/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java b/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java deleted file mode 100644 index 96bb3e8..0000000 --- a/src/main/java/com/whylog/server/domain/decision/service/DecisionQueryService.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.whylog.server.domain.decision.service; - -import com.whylog.server.domain.decision.dto.DecisionResponse; -import com.whylog.server.domain.decision.entity.Decision; -import com.whylog.server.domain.decision.exception.DecisionNotFoundException; -import com.whylog.server.domain.decision.repository.DecisionRepository; -import com.whylog.server.domain.meeting.entity.Meeting; -import com.whylog.server.domain.meeting.entity.MeetingMember; -import com.whylog.server.domain.user.entity.Member; -import java.util.List; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -public class DecisionQueryService { - - private final DecisionRepository decisionRepository; - - // 결정사항 상세 조회에 필요한 회의 정보와 참여자 정보를 조회 - @Transactional(readOnly = true) - public DecisionResponse.DecisionDetailDTO getDecisionDetail(Long decisionId) { - Decision decision = decisionRepository.findDetailById(decisionId) - .orElseThrow(DecisionNotFoundException::new); - - Meeting meeting = decision.getMeeting(); - - return DecisionResponse.DecisionDetailDTO.builder() - .decisionId(decision.getId()) - .name(meeting.getName()) - .meetingDate(meeting.getStartDateTime() != null ? meeting.getStartDateTime().toLocalDate() : null) - .meetingTime(buildMeetingDuration(meeting)) - .memberCount(meeting.getMeetingMembers().size()) - .members( - meeting.getMeetingMembers().stream() - .map(MeetingMember::getMember) - .map(this::toParticipant) - .toList() - ) - .build(); - } - - // 회의 소요 시간을 시간, 분 단위로 변환 - private String buildMeetingDuration(Meeting meeting) { - Long duration = meeting.getDuration(); - if (duration == null) { - return null; - } - - long hours = duration / 60; - long minutes = duration % 60; - - if (hours == 0) { - return minutes + "분"; - } - - if (minutes == 0) { - return hours + "시간"; - } - - return hours + "시간 " + minutes + "분"; - } - - // 멤버 엔티티를 결정사항 상세 참여자 응답으로 변환 - private DecisionResponse.DecisionParticipantDTO toParticipant(Member member) { - return DecisionResponse.DecisionParticipantDTO.builder() - .memberId(member.getId()) - .name(member.getName()) - .profileImage(member.getProfileImage()) - .build(); - } -}