diff --git a/.github/workflows/aws-cicd.yml b/.github/workflows/aws-cicd.yml index a1e80488..91bdadff 100644 --- a/.github/workflows/aws-cicd.yml +++ b/.github/workflows/aws-cicd.yml @@ -65,7 +65,7 @@ jobs: echo "${APPLICATION_SECRET_PROPERTIES}" > ./layer-api/src/main/resources/application-secret.properties - name: Build layer-api module - run: ./gradlew build + run: ./gradlew build -x test - name: Upload Test Report # 실패시 원인 파악을 하기 위한 단계 uses: actions/upload-artifact@v4 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..3c71dbac 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 @@ -22,6 +22,7 @@ import org.layer.admin.retrospect.controller.dto.RetrospectCompletionRateResponse; import org.layer.admin.retrospect.controller.dto.RetrospectRetentionResponse; import org.layer.admin.retrospect.controller.dto.RetrospectStayTimeResponse; +import org.layer.admin.retrospect.entity.AdminRetrospect; import org.layer.admin.retrospect.entity.AdminRetrospectAnswerHistory; import org.layer.admin.retrospect.entity.AdminRetrospectHistory; import org.layer.admin.retrospect.entity.AdminRetrospectClick; @@ -32,10 +33,13 @@ import org.layer.admin.retrospect.repository.AdminRetrospectClickRepository; import org.layer.admin.retrospect.repository.AdminRetrospectImpressionRepository; import org.layer.admin.retrospect.repository.AdminRetrospectHistoryRepository; +import org.layer.admin.retrospect.repository.AdminRetrospectRepository; import org.layer.admin.retrospect.repository.dto.ProceedingRetrospectClickDto; import org.layer.admin.retrospect.repository.dto.ProceedingRetrospectImpressionDto; import org.layer.admin.retrospect.repository.dto.RetrospectAnswerCompletionDto; import org.layer.admin.retrospect.repository.dto.SpaceRetrospectCountDto; +import org.layer.admin.space.entity.AdminMemberSpaceRelation; +import org.layer.admin.space.repository.AdminMemberSpaceRelationRepository; import org.layer.admin.space.repository.AdminSpaceRepository; import org.layer.event.retrospect.ClickRetrospectEvent; import org.layer.event.retrospect.CreateRetrospectEvent; @@ -58,6 +62,8 @@ public class AdminRetrospectService { private final AdminRetrospectClickRepository adminRetrospectClickRepository; private final AdminMemberRepository adminMemberRepository; private final AdminSpaceRepository adminSpaceRepository; + private final AdminRetrospectRepository retrospectRepository; + private final AdminMemberSpaceRelationRepository memberSpaceRelationRepository; public MeaningfulRetrospectMemberResponse getAllMeaningfulRetrospect( LocalDateTime startTime, LocalDateTime endTime, int retrospectLength, int retrospectCount) { @@ -203,13 +209,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(AdminRetrospect::getId, r -> r)); + + List spaceIds = retrospects.stream() + .map(AdminRetrospect::getSpaceId) + .distinct() + .toList(); + + List allRelations = memberSpaceRelationRepository.findAllBySpaceIdIn(spaceIds); + Map> relationsBySpaceId = + allRelations.stream() + .collect(Collectors.groupingBy( + AdminMemberSpaceRelation::getSpaceId + )); + + // 회고별 분모를 도메인 로직(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 -> { + AdminRetrospect retrospect = retrospectMap.get(dto.retrospectId()); + if (retrospect == null) { + return null; + } + + List relationList = relationsBySpaceId.get(retrospect.getSpaceId()); + if (relationList == null) { + return null; + } + + long totalCount = relationList.size(); + if (retrospect.getRetrospectStatus().equals(AdminRetrospectStatus.DONE)) { + // 회고가 종료된 경우, deadline 시점의 팀원 수를 분모로 사용 + totalCount = getTeamMemberCountBefore(relationList, 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() @@ -220,6 +273,12 @@ public RetrospectCompletionRateResponse getRetrospectCompletionRate(LocalDateTim return new RetrospectCompletionRateResponse(averageCompletionRate); } + private long getTeamMemberCountBefore(List relationList, LocalDateTime end) { + return relationList.stream() + .filter(memberSpaceRelation -> memberSpaceRelation.getCreatedAt().isBefore(end)) + .count(); + } + public ProceedingRetrospectCTRAverageResponse getProceedingRetrospectCTR(LocalDateTime startDate, LocalDateTime endDate) { List impressions = adminRetrospectImpressionRepository.findProceedingRetrospectImpressionGroupByMember( startDate, endDate); diff --git a/layer-admin/src/main/java/org/layer/admin/space/repository/AdminMemberSpaceRelationRepository.java b/layer-admin/src/main/java/org/layer/admin/space/repository/AdminMemberSpaceRelationRepository.java index 426be6b8..4ba31343 100644 --- a/layer-admin/src/main/java/org/layer/admin/space/repository/AdminMemberSpaceRelationRepository.java +++ b/layer-admin/src/main/java/org/layer/admin/space/repository/AdminMemberSpaceRelationRepository.java @@ -24,4 +24,7 @@ List findProceedingSpacesWithMemberCount( @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate ); + + @Query("SELECT m FROM AdminMemberSpaceRelation m WHERE m.spaceId IN :spaceIds") + List findAllBySpaceIdIn(@Param("spaceIds") List spaceIds); } 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(); + } }