diff --git a/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java b/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java index 70014775..6806babb 100644 --- a/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java @@ -19,6 +19,8 @@ import harustudy.backend.study.exception.TimePerCycleException; import harustudy.backend.study.exception.TotalCycleException; import harustudy.backend.study.exception.StudyNotFoundException; +import harustudy.backend.polling.exception.CannotSeeSubmittersException; + import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -72,6 +74,8 @@ private static void setUpStudyException() { ExceptionSituation.of("총 사이클 횟수가 적절하지 않습니다.", BAD_REQUEST, 1306)); mapper.put(StudyAlreadyStartedException.class, ExceptionSituation.of("이미 시작된 스터디입니다.", BAD_REQUEST, 1307)); + mapper.put(CannotSeeSubmittersException.class, + ExceptionSituation.of("해당 단계에서는 제출 여부를 조회할 수 없습니다.", BAD_REQUEST, 1308)); } private static void setUpAuthenticationException() { diff --git a/backend/src/main/java/harustudy/backend/content/domain/Content.java b/backend/src/main/java/harustudy/backend/content/domain/Content.java index 48ca40fa..68ad88fe 100644 --- a/backend/src/main/java/harustudy/backend/content/domain/Content.java +++ b/backend/src/main/java/harustudy/backend/content/domain/Content.java @@ -63,7 +63,11 @@ public boolean hasSameCycleWith(Study study) { return cycle.equals(study.getCurrentCycle()); } - public boolean hasEmptyPlan() { - return plan.isEmpty(); + public boolean isPlanWritten() { + return !plan.isEmpty(); + } + + public boolean isRetrospectWritten() { + return !retrospect.isEmpty(); } } diff --git a/backend/src/main/java/harustudy/backend/content/service/ContentService.java b/backend/src/main/java/harustudy/backend/content/service/ContentService.java index 377d8e0f..c8ff9a67 100644 --- a/backend/src/main/java/harustudy/backend/content/service/ContentService.java +++ b/backend/src/main/java/harustudy/backend/content/service/ContentService.java @@ -158,7 +158,7 @@ private void validateStudyIsRetrospect(Study study) { } private void validateIsPlanFilled(Content recentContent) { - if (recentContent.hasEmptyPlan()) { + if (!recentContent.isPlanWritten()) { throw new StudyStepException(); } } diff --git a/backend/src/main/java/harustudy/backend/view/controller/PollingController.java b/backend/src/main/java/harustudy/backend/polling/controller/PollingController.java similarity index 67% rename from backend/src/main/java/harustudy/backend/view/controller/PollingController.java rename to backend/src/main/java/harustudy/backend/polling/controller/PollingController.java index fc964a7d..20422a50 100644 --- a/backend/src/main/java/harustudy/backend/view/controller/PollingController.java +++ b/backend/src/main/java/harustudy/backend/polling/controller/PollingController.java @@ -1,10 +1,11 @@ -package harustudy.backend.view.controller; +package harustudy.backend.polling.controller; import harustudy.backend.auth.Authenticated; import harustudy.backend.auth.dto.AuthMember; -import harustudy.backend.view.dto.ProgressResponse; -import harustudy.backend.view.dto.WaitingResponse; -import harustudy.backend.view.service.PollingService; +import harustudy.backend.polling.dto.ProgressResponse; +import harustudy.backend.polling.dto.SubmittersResponse; +import harustudy.backend.polling.dto.WaitingResponse; +import harustudy.backend.polling.service.PollingService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -28,10 +29,18 @@ public ResponseEntity progressPolling( ProgressResponse progressResponse = pollingService.pollProgress(studyId); return ResponseEntity.ok(progressResponse); } - @GetMapping("/api/waiting") public ResponseEntity pollWaiting(@Authenticated AuthMember authMember, @RequestParam Long studyId) { WaitingResponse waitingResponse = pollingService.pollWaiting(studyId); return ResponseEntity.ok(waitingResponse); } + + @Operation(summary = "스터디원 별 제출 여부 조회") + @GetMapping("/api/submitted") + public ResponseEntity findSubmitters( + @Authenticated AuthMember authMember, + @RequestParam Long studyId + ) { + return ResponseEntity.ok(pollingService.findSubmitters(studyId)); + } } diff --git a/backend/src/main/java/harustudy/backend/view/dto/ProgressResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java similarity index 86% rename from backend/src/main/java/harustudy/backend/view/dto/ProgressResponse.java rename to backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java index 973d3091..b48993f0 100644 --- a/backend/src/main/java/harustudy/backend/view/dto/ProgressResponse.java +++ b/backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java @@ -1,4 +1,4 @@ -package harustudy.backend.view.dto; +package harustudy.backend.polling.dto; import harustudy.backend.participant.domain.Step; diff --git a/backend/src/main/java/harustudy/backend/polling/dto/SubmitterResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/SubmitterResponse.java new file mode 100644 index 00000000..becfc35c --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/dto/SubmitterResponse.java @@ -0,0 +1,8 @@ +package harustudy.backend.polling.dto; + +public record SubmitterResponse(String nickname, Boolean submitted) { + + public static SubmitterResponse of(String nickname, Boolean submitted) { + return new SubmitterResponse(nickname, submitted); + } +} diff --git a/backend/src/main/java/harustudy/backend/polling/dto/SubmittersResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/SubmittersResponse.java new file mode 100644 index 00000000..18b4711a --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/dto/SubmittersResponse.java @@ -0,0 +1,10 @@ +package harustudy.backend.polling.dto; + +import java.util.List; + +public record SubmittersResponse(List status) { + + public static SubmittersResponse from(List submitterResponses) { + return new SubmittersResponse(submitterResponses); + } +} diff --git a/backend/src/main/java/harustudy/backend/view/dto/WaitingResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java similarity index 96% rename from backend/src/main/java/harustudy/backend/view/dto/WaitingResponse.java rename to backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java index 5229c982..1b0c4819 100644 --- a/backend/src/main/java/harustudy/backend/view/dto/WaitingResponse.java +++ b/backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java @@ -1,4 +1,4 @@ -package harustudy.backend.view.dto; +package harustudy.backend.polling.dto; import harustudy.backend.participant.domain.Participant; import harustudy.backend.participant.domain.Step; diff --git a/backend/src/main/java/harustudy/backend/polling/exception/CannotSeeSubmittersException.java b/backend/src/main/java/harustudy/backend/polling/exception/CannotSeeSubmittersException.java new file mode 100644 index 00000000..c570f423 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/exception/CannotSeeSubmittersException.java @@ -0,0 +1,7 @@ +package harustudy.backend.polling.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class CannotSeeSubmittersException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/polling/exception/CurrentCycleContentNotExistsException.java b/backend/src/main/java/harustudy/backend/polling/exception/CurrentCycleContentNotExistsException.java new file mode 100644 index 00000000..396be974 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/exception/CurrentCycleContentNotExistsException.java @@ -0,0 +1,7 @@ +package harustudy.backend.polling.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class CurrentCycleContentNotExistsException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/polling/service/PollingService.java b/backend/src/main/java/harustudy/backend/polling/service/PollingService.java new file mode 100644 index 00000000..94230081 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/service/PollingService.java @@ -0,0 +1,65 @@ +package harustudy.backend.polling.service; + +import harustudy.backend.content.domain.Content; +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.domain.Step; +import harustudy.backend.participant.repository.ParticipantRepository; +import harustudy.backend.study.domain.Study; +import harustudy.backend.study.exception.StudyNotFoundException; +import harustudy.backend.study.repository.StudyRepository; +import harustudy.backend.polling.dto.ProgressResponse; +import harustudy.backend.polling.dto.SubmitterResponse; +import harustudy.backend.polling.dto.SubmittersResponse; +import harustudy.backend.polling.dto.WaitingResponse; +import harustudy.backend.polling.exception.CurrentCycleContentNotExistsException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class PollingService { + + private final StudyRepository studyRepository; + private final ParticipantRepository participantRepository; + + public WaitingResponse pollWaiting(Long studyId) { + Study study = studyRepository.findByIdIfExists(studyId); + return WaitingResponse.of(study, study.getParticipants()); + } + public ProgressResponse pollProgress(Long studyId) { + Step step = studyRepository.findStepById(studyId) + .orElseThrow(StudyNotFoundException::new); + return ProgressResponse.from(step); + } + + public SubmittersResponse findSubmitters(Long studyId) { + Study study = studyRepository.findByIdIfExists(studyId); + List participants = participantRepository.findByStudy(study); + return generateSubmitterResponses(study, participants); + } + + private SubmittersResponse generateSubmitterResponses(Study study, List participants) { + List submitterResponses = new ArrayList<>(); + + for (Participant participant : participants) { + Content currentCycleContent = extractCurrentCycleContent(study, participant); + + submitterResponses.add(SubmitterResponse.of( + participant.getNickname(), + SubmitterCheckingStrategy.isSubmitted(study.getStep(), currentCycleContent))); + } + return SubmittersResponse.from(submitterResponses); + } + + private Content extractCurrentCycleContent(Study study, Participant participant) { + return participant.getContents().stream() + .filter(content -> content.hasSameCycleWith(study)) + .findFirst() + .orElseThrow(CurrentCycleContentNotExistsException::new); + } +} diff --git a/backend/src/main/java/harustudy/backend/polling/service/SubmitterCheckingStrategy.java b/backend/src/main/java/harustudy/backend/polling/service/SubmitterCheckingStrategy.java new file mode 100644 index 00000000..ffa99b45 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/service/SubmitterCheckingStrategy.java @@ -0,0 +1,31 @@ +package harustudy.backend.polling.service; + +import harustudy.backend.content.domain.Content; +import harustudy.backend.participant.domain.Step; +import harustudy.backend.polling.exception.CannotSeeSubmittersException; + +import java.util.Arrays; +import java.util.function.Function; + +public enum SubmitterCheckingStrategy { + + PLANNING(Step.PLANNING, Content::isPlanWritten), + RETROSPECT(Step.RETROSPECT, Content::isRetrospectWritten); + + private final Step step; + private final Function strategy; + + SubmitterCheckingStrategy(Step step, Function strategy) { + this.step = step; + this.strategy = strategy; + } + + public static boolean isSubmitted(Step step, Content content) { + return Arrays.stream(values()) + .filter(each -> each.step.equals(step)) + .map(each -> each.strategy) + .findFirst() + .orElseThrow(CannotSeeSubmittersException::new) + .apply(content); + } +} diff --git a/backend/src/main/java/harustudy/backend/view/service/PollingService.java b/backend/src/main/java/harustudy/backend/view/service/PollingService.java deleted file mode 100644 index 3c98b3e2..00000000 --- a/backend/src/main/java/harustudy/backend/view/service/PollingService.java +++ /dev/null @@ -1,30 +0,0 @@ -package harustudy.backend.view.service; - -import harustudy.backend.participant.domain.Step; -import harustudy.backend.study.domain.Study; -import harustudy.backend.study.exception.StudyNotFoundException; -import harustudy.backend.study.repository.StudyRepository; -import harustudy.backend.view.dto.ProgressResponse; -import harustudy.backend.view.dto.WaitingResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@RequiredArgsConstructor -@Transactional(readOnly = true) -@Service -public class PollingService { - - private final StudyRepository studyRepository; - - public WaitingResponse pollWaiting(Long studyId) { - Study study = studyRepository.findByIdIfExists(studyId); - return WaitingResponse.of(study, study.getParticipants()); - } - - public ProgressResponse pollProgress(Long studyId) { - Step step = studyRepository.findStepById(studyId) - .orElseThrow(StudyNotFoundException::new); - return ProgressResponse.from(step); - } -} diff --git a/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java index 3a0d0857..8c60f6a0 100644 --- a/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java @@ -3,7 +3,7 @@ import harustudy.backend.participant.domain.Participant; import harustudy.backend.participant.dto.ParticipantResponse; import harustudy.backend.study.domain.Study; -import harustudy.backend.view.dto.WaitingResponse; +import harustudy.backend.polling.dto.WaitingResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; diff --git a/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java index 67b64483..1097c625 100644 --- a/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java @@ -8,7 +8,7 @@ import harustudy.backend.participant.domain.Participant; import harustudy.backend.participant.domain.Step; import harustudy.backend.study.domain.Study; -import harustudy.backend.view.dto.ProgressResponse; +import harustudy.backend.polling.dto.ProgressResponse; import java.nio.charset.StandardCharsets; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; diff --git a/backend/src/test/java/harustudy/backend/polling/service/PollingServiceTest.java b/backend/src/test/java/harustudy/backend/polling/service/PollingServiceTest.java new file mode 100644 index 00000000..da4fd432 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/polling/service/PollingServiceTest.java @@ -0,0 +1,189 @@ +package harustudy.backend.polling.service; + +import harustudy.backend.content.domain.Content; +import harustudy.backend.member.domain.LoginType; +import harustudy.backend.member.domain.Member; +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.dto.ParticipantResponse; +import harustudy.backend.study.domain.Study; +import harustudy.backend.polling.dto.SubmitterResponse; +import harustudy.backend.polling.dto.SubmittersResponse; +import harustudy.backend.polling.dto.WaitingResponse; +import harustudy.backend.polling.exception.CannotSeeSubmittersException; +import jakarta.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@Transactional +@SpringBootTest +class PollingServiceTest { + + @Autowired + private PollingService pollingService; + + @Autowired + private EntityManager entityManager; + + private Member member1; + private Member member2; + private Member member3; + private Participant participant1; + private Participant participant2; + private Participant participant3; + private Study study; + private Content content1; + private Content content2; + + @BeforeEach + void setUp() { + study = new Study("studyName", 1, 20); + + member1 = new Member("member1", "email", "url", LoginType.GUEST); + member2 = new Member("member2", "email", "url", LoginType.GUEST); + member3 = new Member("member3", "email", "url", LoginType.GUEST); + + participant1 = Participant.instantiateParticipantWithContents(study, member1, "parti1"); + participant2 = Participant.instantiateParticipantWithContents(study, member2, "parti2"); + participant3 = Participant.instantiateParticipantWithContents(study, member3, "parti3"); + + content1 = participant1.getContents().get(0); + content2 = participant2.getContents().get(0); + + study.addParticipant(participant1); + study.addParticipant(participant2); + study.addParticipant(participant3); + + entityManager.persist(study); + entityManager.persist(member1); + entityManager.persist(member2); + entityManager.persist(member3); + entityManager.persist(participant1); + entityManager.persist(participant2); + entityManager.persist(participant3); + } + + @Test + void 대기_상태에서_제출_인원을_확인하려_하면_예외가_발생한다() { + // given, when, then + entityManager.flush(); + entityManager.clear(); + + assertThatThrownBy(() -> pollingService.findSubmitters(study.getId())) + .isInstanceOf(CannotSeeSubmittersException.class); + } + + @Test + void 계획_단계에서는_제출_인원을_확인할_수_있다() { + // given + study.proceed(); + content1.changePlan(Map.of("content", "written")); + + entityManager.merge(content1); + entityManager.merge(study); + entityManager.flush(); + entityManager.clear(); + + // when + SubmittersResponse submitters = pollingService.findSubmitters(study.getId()); + + // then + SubmittersResponse expected = new SubmittersResponse(List.of( + new SubmitterResponse("parti1", true), + new SubmitterResponse("parti2", false), + new SubmitterResponse("parti3", false) + )); + + assertThat(submitters).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void 진행_단계에서는_제출_인원을_확인하려_하면_예외가_발생한다() { + // given + study.proceed(); + study.proceed(); + + entityManager.flush(); + entityManager.clear(); + + // when, then + assertThatThrownBy(() -> pollingService.findSubmitters(study.getId())) + .isInstanceOf(CannotSeeSubmittersException.class); + } + + @Test + void 회고_단계에서는_제출_인원을_확인할_수_있다() { + study.proceed(); + study.proceed(); + study.proceed(); + content1.changeRetrospect(Map.of("content", "written")); + content2.changeRetrospect(Map.of("content", "written")); + + entityManager.flush(); + entityManager.clear(); + + // when + SubmittersResponse submitters = pollingService.findSubmitters(study.getId()); + + // then + SubmittersResponse expected = new SubmittersResponse(List.of( + new SubmitterResponse("parti1", true), + new SubmitterResponse("parti2", true), + new SubmitterResponse("parti3", false) + )); + + assertThat(submitters).usingRecursiveComparison() + .isEqualTo(expected); + } + + @Test + void 스터디가_종료한_뒤에는_제출_인원을_확인하려_하면_예외가_발생한다() { + // given + study.proceed(); + study.proceed(); + study.proceed(); + study.proceed(); + + entityManager.flush(); + entityManager.clear(); + + // when, then + assertThatThrownBy(() -> pollingService.findSubmitters(study.getId())) + .isInstanceOf(CannotSeeSubmittersException.class); + } + + @Test + void 스터디에_참여한_참여자들을_조회한다() { + // given, when + entityManager.flush(); + entityManager.clear(); + + WaitingResponse response = pollingService.pollWaiting(study.getId()); + + // then + List expected = Stream.of(participant1, participant2, participant3) + .map(ParticipantResponse::from) + .toList(); + + assertSoftly(softly -> { + softly.assertThat(response.studyStep()).isEqualTo(study.getStep().name().toLowerCase()); + softly.assertThat(response.participants().size()).isEqualTo(3); + softly.assertThat(response.participants()).containsExactlyInAnyOrderElementsOf(expected); + }); + } +} diff --git a/backend/src/test/java/harustudy/backend/view/service/PollingServiceTest.java b/backend/src/test/java/harustudy/backend/view/service/PollingServiceTest.java deleted file mode 100644 index 5c9c743b..00000000 --- a/backend/src/test/java/harustudy/backend/view/service/PollingServiceTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package harustudy.backend.view.service; - -import harustudy.backend.member.domain.LoginType; -import harustudy.backend.member.domain.Member; -import harustudy.backend.participant.domain.Participant; -import harustudy.backend.participant.dto.ParticipantResponse; -import harustudy.backend.study.domain.Study; -import harustudy.backend.view.dto.WaitingResponse; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayNameGeneration; -import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Stream; - -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -@SuppressWarnings("NonAsciiCharacters") -@DisplayNameGeneration(ReplaceUnderscores.class) -@Transactional -@SpringBootTest -class PollingServiceTest { - - @Autowired - private PollingService pollingService; - - @Autowired - private EntityManager entityManager; - - private Study study; - private Member member1; - private Member member2; - private Member member3; - private Participant participant1; - private Participant participant2; - private Participant participant3; - - @BeforeEach - void setUp() { - study = new Study("studyName", 3, 20); - - member1 = new Member("member1", "email", "url", LoginType.GUEST); - member2 = new Member("member2", "email", "url", LoginType.GUEST); - member3 = new Member("member3", "email", "url", LoginType.GUEST); - - participant1 = Participant.instantiateParticipantWithContents(study, member1, "parti1"); - participant2 = Participant.instantiateParticipantWithContents(study, member2, "parti2"); - participant3 = Participant.instantiateParticipantWithContents(study, member3, "parti3"); - - entityManager.persist(study); - entityManager.persist(member1); - entityManager.persist(member2); - entityManager.persist(member3); - entityManager.persist(participant1); - entityManager.persist(participant2); - entityManager.persist(participant3); - - entityManager.flush(); - entityManager.clear(); - } - - @Test - void 스터디에_참여한_참여자들을_조회한다() { - // given, when - WaitingResponse response = pollingService.pollWaiting(study.getId()); - - // then - List expected = Stream.of(participant1, participant2, participant3) - .map(ParticipantResponse::from) - .toList(); - - assertSoftly(softly -> { - softly.assertThat(response.studyStep()).isEqualTo(study.getStep().name().toLowerCase()); - softly.assertThat(response.participants().size()).isEqualTo(3); - softly.assertThat(response.participants()).containsExactlyInAnyOrderElementsOf(expected); - }); - } -}