diff --git a/build.gradle b/build.gradle index 0e7b1b22..1f220ada 100644 --- a/build.gradle +++ b/build.gradle @@ -202,6 +202,7 @@ project(":layer-admin") { dependencies { implementation project(path: ':layer-event') + implementation project(path: ':layer-domain') compileOnly 'org.springframework.boot:spring-boot-starter-data-jpa' compileOnly 'org.springframework.boot:spring-boot-starter-web' diff --git a/layer-admin/src/main/java/org/layer/admin/retrospect/service/AdminRetrospectService.java b/layer-admin/src/main/java/org/layer/admin/retrospect/service/AdminRetrospectService.java index b3a8d980..1a1e4883 100644 --- a/layer-admin/src/main/java/org/layer/admin/retrospect/service/AdminRetrospectService.java +++ b/layer-admin/src/main/java/org/layer/admin/retrospect/service/AdminRetrospectService.java @@ -37,6 +37,12 @@ import org.layer.admin.retrospect.repository.dto.RetrospectAnswerCompletionDto; import org.layer.admin.retrospect.repository.dto.SpaceRetrospectCountDto; import org.layer.admin.space.repository.AdminSpaceRepository; +import org.layer.domain.retrospect.entity.Retrospect; +import org.layer.domain.retrospect.entity.RetrospectStatus; +import org.layer.domain.retrospect.repository.RetrospectRepository; +import org.layer.domain.space.entity.MemberSpaceRelation; +import org.layer.domain.space.entity.Team; +import org.layer.domain.space.repository.MemberSpaceRelationRepository; import org.layer.event.retrospect.ClickRetrospectEvent; import org.layer.event.retrospect.CreateRetrospectEvent; import org.layer.event.retrospect.AnswerRetrospectEndEvent; @@ -58,6 +64,8 @@ public class AdminRetrospectService { private final AdminRetrospectClickRepository adminRetrospectClickRepository; private final AdminMemberRepository adminMemberRepository; private final AdminSpaceRepository adminSpaceRepository; + private final RetrospectRepository retrospectRepository; + private final MemberSpaceRelationRepository memberSpaceRelationRepository; public MeaningfulRetrospectMemberResponse getAllMeaningfulRetrospect( LocalDateTime startTime, LocalDateTime endTime, int retrospectLength, int retrospectCount) { @@ -203,13 +211,60 @@ public RetrospectCompletionRateResponse getRetrospectCompletionRate(LocalDateTim List answerHistories = adminRetrospectAnswerRepository.findRetrospectAnswerCompletionStatsBetween( startTime, endTime); - // 회고별 완수율 계산 (단위: %) + if (answerHistories.isEmpty()) { + return new RetrospectCompletionRateResponse(0.0); + } + + // 필요한 회고/스페이스/팀 정보를 미리 한 번에 로딩해서 N+1 방지 + List retrospectIds = answerHistories.stream() + .map(RetrospectAnswerCompletionDto::retrospectId) + .distinct() + .toList(); + + List retrospects = retrospectRepository.findAllById(retrospectIds); + Map retrospectMap = retrospects.stream() + .collect(Collectors.toMap(Retrospect::getId, r -> r)); + + List spaceIds = retrospects.stream() + .map(Retrospect::getSpaceId) + .distinct() + .toList(); + + List allRelations = memberSpaceRelationRepository.findAllBySpaceIdIn(spaceIds); + Map teamBySpaceId = allRelations.stream() + .collect(Collectors.groupingBy( + relation -> relation.getSpace().getId(), + Collectors.collectingAndThen(Collectors.toList(), Team::new) + )); + + // 회고별 분모를 도메인 로직(Team, RetrospectStatus, deadline) 기반으로 계산 List completionRates = answerHistories.stream() - .filter(dto -> dto.targetAnswerCount() > 0) // division by zero 방지 - .map(dto -> (double) dto.actualAnswerCount() / dto.targetAnswerCount() * 100.0) + .map(dto -> { + Retrospect retrospect = retrospectMap.get(dto.retrospectId()); + if (retrospect == null) { + return null; + } + + Team team = teamBySpaceId.get(retrospect.getSpaceId()); + if (team == null) { + return null; + } + + long totalCount = team.getTeamMemberCount(); + if (retrospect.getRetrospectStatus().equals(RetrospectStatus.DONE)) { + // 회고가 종료된 경우, deadline 시점의 팀원 수를 분모로 사용 + totalCount = team.getTeamMemberCountBefore(retrospect.getDeadline()); + } + + if (totalCount == 0) { + return null; // division by zero 방지 + } + + return (double) dto.actualAnswerCount() / totalCount * 100.0; + }) + .filter(Objects::nonNull) .toList(); - // 평균 완수율 계산 double averageCompletionRate = completionRates.isEmpty() ? 0.0 : completionRates.stream() diff --git a/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java b/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java index 5d342016..81e6e6a8 100644 --- a/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java +++ b/layer-api/src/main/java/org/layer/domain/retrospect/service/RetrospectService.java @@ -135,14 +135,16 @@ public RetrospectListGetResponse getRetrospects(Long spaceId, Long memberId) { List retrospectDtos = retrospects.stream() .map(r -> { - long writeCount = team.getTeamMemberCount(); + long totalCount = team.getTeamMemberCount(); if (r.getRetrospectStatus().equals(RetrospectStatus.DONE)) { - writeCount = answers.getWriteCount(r.getId()); + // 회고가 종료된 경우, 해당 회고의 deadline 시점의 팀원 수를 totalCount로 설정한다. + // RetrospectStatus 가 DONE 으로 변경되면, deadline이 null 값이 될 수 없기 때문이다. + totalCount = team.getTeamMemberCountBefore(r.getDeadline()); } return RetrospectGetResponse.of(r.getSpaceId(), r.getId(), r.getTitle(), r.getIntroduction(), answers.getWriteStatus(memberId, r.getId()), r.getRetrospectStatus(), r.getAnalysisStatus(), - answers.getWriteCount(r.getId()), writeCount, r.getCreatedAt(), r.getDeadline()); + answers.getWriteCount(r.getId()), totalCount, r.getCreatedAt(), r.getDeadline()); }) .toList(); diff --git a/layer-domain/src/main/java/org/layer/domain/space/entity/Team.java b/layer-domain/src/main/java/org/layer/domain/space/entity/Team.java index 4ca88685..f8e71ab8 100644 --- a/layer-domain/src/main/java/org/layer/domain/space/entity/Team.java +++ b/layer-domain/src/main/java/org/layer/domain/space/entity/Team.java @@ -2,6 +2,7 @@ import static org.layer.global.exception.MemberSpaceRelationExceptionType.*; +import java.time.LocalDateTime; import java.util.List; import org.layer.domain.space.exception.MemberSpaceRelationException; @@ -30,4 +31,10 @@ public List getMemberIds(){ .map(MemberSpaceRelation::getMemberId) .toList(); } + + public long getTeamMemberCountBefore(LocalDateTime end) { + return memberSpaceRelations.stream() + .filter(memberSpaceRelation -> memberSpaceRelation.getCreatedAt().isBefore(end)) + .count(); + } } diff --git a/layer-domain/src/main/java/org/layer/domain/space/repository/MemberSpaceRelationRepository.java b/layer-domain/src/main/java/org/layer/domain/space/repository/MemberSpaceRelationRepository.java index 259a8fb9..99b38d0f 100644 --- a/layer-domain/src/main/java/org/layer/domain/space/repository/MemberSpaceRelationRepository.java +++ b/layer-domain/src/main/java/org/layer/domain/space/repository/MemberSpaceRelationRepository.java @@ -19,6 +19,9 @@ public interface MemberSpaceRelationRepository extends JpaRepository findAllBySpaceId(Long spaceId); + @Query("SELECT m FROM MemberSpaceRelation m WHERE m.space.id IN :spaceIds") + List findAllBySpaceIdIn(@Param("spaceIds") List spaceIds); + @Query("SELECT new org.layer.domain.retrospect.dto.SpaceMemberCount(m.space.id, COUNT(m)) " + "FROM MemberSpaceRelation m " + "WHERE m.space.id IN :spaceIds " +