From c624ad58bb852e39f47a53ced5bc772c6dc0637d Mon Sep 17 00:00:00 2001 From: MODI <77962265+jaehee329@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:45:02 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=ED=8F=B4=EB=A7=81=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20(#584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Polling 관련 패키지 구성 * [BE] 기존 api 중 폴링으로 인한 변경 반영, studyId로 참여코드 조회 api 구현 (#569) * feat: progress 삭제 및 study 정보 변경 관련 api 수정 * feat: studyId로 참여 코드 조회 api 구현 * fix: CachingFilter를 /api/participantCodes 에 적용 * feat: 스터디 아이디로 참여 코드 조회 시나리오 테스트 추가 * fix: 참여 코드 관련 필터 로직 수정 * refactor: 스터디 생성 시 Location 헤더로만 응답을 반환하도록 변경 * [BE] 대기방 페이지 폴링 기능 구현하기 (#575) * feat: 대기방 폴링 api 구현 * refactor: 스터디 참여 로직 수정 * test: 참여 관련 테스트 추가 * refactor: 생성자 체이닝 사용 * refactor: import 와일드카드 제거 * refactor: 메서드 네이밍 변경 * test: 메서드 네이밍 참여를 참여자 정보로 수정 * test: 메서드 네이밍 폴링 키워드 제거 * refactor: dto 변환 로직 dto로 이동 * refactor: 개행 제거 * refactor: 정적 팩터리 메서드 도입 및 생성자 제거로 인한 테스트 수정사항 반영 * test: expected를 given으로 이동 * refactor: 정적 팩터리 메서드 네이밍 수정 * test: expected를 given으로 이동 * [BE] 이미 시작한 스터디 신규 참여 차단 로직 구현 (#578) * feat: 이미 시작한 스터디 참여 시 에러 반환 로직 구현 * refactor: assertThrows -> assertThatThrownBy 다른 테스트와 동일한 방식으로 변경 * [BE] 진행 페이지 폴링 기능 구현하기 (#577) * feat: polling API 구현을 위한 view 패키지 및 기본 컨트롤러, 서비스 클래스 생성 * feat: PollingController 구현 * feat: 폴링 서비스 로직 및 테스트 구현 * feat: 스터디 상태 값만 조회하도록 구현 * feat: controller 단에 authMember 추가 * feat: 기타 수정 사항 반영 * feat: 스터디에서 step 조회 시 Enum으로 조회하도록 수정 * feat: StudyRepository 메서드 네이밍 수정 * style: 잘못된 개행 수정 * fix: resolve merge conflict --------- Co-authored-by: aak2075 * [BE] 제출한 인원 조회하는 기능 구현하기 (#579) * feat: 제출 인원 조회 기능 및 테스트 구현 * test: 스터디 종료 상태 테스트 케이스 추가 * refactor: DTO에서 정적 팩토리 메소드를 사용하도록 변경 * refactor: Controller -> RestController로 변경 * chore: ExceptionMapper Exception 추가 * chore: 클래스명 수정 * fix: resolve merge conflict --------- Co-authored-by: aak2075 --------- Co-authored-by: Gyuseong Lee Co-authored-by: hiiro <31722737+MoonJeWoong@users.noreply.github.com> Co-authored-by: teo <78679830+woosung1223@users.noreply.github.com> --- .../common/exception/ExceptionMapper.java | 7 + .../backend/config/FilterConfig.java | 2 +- .../backend/content/domain/Content.java | 8 +- .../content/service/ContentService.java | 2 +- .../participant/domain/Participant.java | 22 +- .../participant/dto/ParticipantResponse.java | 4 +- .../repository/ParticipantRepository.java | 1 - .../service/ParticipantService.java | 26 +- .../controller/ParticipantCodeController.java | 32 ++ .../dto/ParticipantCodeResponse.java | 10 + .../repository/ParticipantCodeRepository.java | 3 + .../service/ParticipantCodeService.java | 28 ++ .../polling/controller/PollingController.java | 46 +++ .../backend/polling/dto/ProgressResponse.java | 10 + .../polling/dto/SubmitterResponse.java | 8 + .../polling/dto/SubmittersResponse.java | 10 + .../backend/polling/dto/WaitingResponse.java | 33 ++ .../CannotSeeSubmittersException.java | 7 + ...CurrentCycleContentNotExistsException.java | 7 + .../polling/service/PollingService.java | 65 ++++ .../service/SubmitterCheckingStrategy.java | 31 ++ .../study/controller/StudyController.java | 18 +- .../harustudy/backend/study/domain/Study.java | 8 + .../study/dto/CreateStudyResponse.java | 13 - .../backend/study/dto/StudyResponse.java | 8 +- .../StudyAlreadyStartedException.java | 7 + .../study/repository/StudyRepository.java | 9 +- .../backend/study/service/StudyService.java | 16 +- .../backend/acceptance/AcceptanceTest.java | 77 ++-- .../repository/ContentRepositoryTest.java | 23 +- .../content/service/ContentServiceTest.java | 9 +- .../integration/ContentIntegrationTest.java | 8 +- .../integration/MemberIntegrationTest.java | 4 +- .../ParticipantIntegrationTest.java | 3 +- .../integration/PollingIntegrationTest.java | 81 ++++ .../integration/StudyIntegrationTest.java | 39 +- .../integration/ViewIntegrationTest.java | 86 +++++ .../participant/domain/ParticipantTest.java | 48 ++- .../service/ParticipantServiceTest.java | 355 +++++++++--------- .../service/ParticipantCodeServiceTest.java | 45 +++ .../polling/service/PollingServiceTest.java | 189 ++++++++++ 41 files changed, 1096 insertions(+), 312 deletions(-) create mode 100644 backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java create mode 100644 backend/src/main/java/harustudy/backend/participantcode/dto/ParticipantCodeResponse.java create mode 100644 backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java create mode 100644 backend/src/main/java/harustudy/backend/polling/controller/PollingController.java create mode 100644 backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java create mode 100644 backend/src/main/java/harustudy/backend/polling/dto/SubmitterResponse.java create mode 100644 backend/src/main/java/harustudy/backend/polling/dto/SubmittersResponse.java create mode 100644 backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java create mode 100644 backend/src/main/java/harustudy/backend/polling/exception/CannotSeeSubmittersException.java create mode 100644 backend/src/main/java/harustudy/backend/polling/exception/CurrentCycleContentNotExistsException.java create mode 100644 backend/src/main/java/harustudy/backend/polling/service/PollingService.java create mode 100644 backend/src/main/java/harustudy/backend/polling/service/SubmitterCheckingStrategy.java delete mode 100644 backend/src/main/java/harustudy/backend/study/dto/CreateStudyResponse.java create mode 100644 backend/src/main/java/harustudy/backend/study/exception/StudyAlreadyStartedException.java create mode 100644 backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java create mode 100644 backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java create mode 100644 backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java create mode 100644 backend/src/test/java/harustudy/backend/polling/service/PollingServiceTest.java 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 7e62e506..6806babb 100644 --- a/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java +++ b/backend/src/main/java/harustudy/backend/common/exception/ExceptionMapper.java @@ -14,10 +14,13 @@ import harustudy.backend.participant.exception.ParticipantNotBelongToStudyException; import harustudy.backend.study.exception.ParticipantCodeExpiredException; import harustudy.backend.study.exception.ParticipantCodeNotFoundException; +import harustudy.backend.study.exception.StudyAlreadyStartedException; import harustudy.backend.study.exception.StudyNameLengthException; 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; @@ -69,6 +72,10 @@ private static void setUpStudyException() { ExceptionSituation.of("시간 당 사이클 횟수가 적절하지 않습니다.", BAD_REQUEST, 1305)); mapper.put(TotalCycleException.class, 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/config/FilterConfig.java b/backend/src/main/java/harustudy/backend/config/FilterConfig.java index 453fcf26..e4e79aa0 100644 --- a/backend/src/main/java/harustudy/backend/config/FilterConfig.java +++ b/backend/src/main/java/harustudy/backend/config/FilterConfig.java @@ -12,7 +12,7 @@ public class FilterConfig { public FilterRegistrationBean contentCachingFilter(){ FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new CachingFilter()); - registrationBean.addUrlPatterns("/api/studies/*", "/api/temp/*", "/api/auth/*", "/api/me/*"); + registrationBean.addUrlPatterns("/api/participant-codes/*", "/api/studies/*", "/api/temp/*", "/api/auth/*", "/api/me/*"); registrationBean.setOrder(1); registrationBean.setName("CachingFilter"); return registrationBean; 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/participant/domain/Participant.java b/backend/src/main/java/harustudy/backend/participant/domain/Participant.java index d99cbf54..f40229d3 100644 --- a/backend/src/main/java/harustudy/backend/participant/domain/Participant.java +++ b/backend/src/main/java/harustudy/backend/participant/domain/Participant.java @@ -14,14 +14,13 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; - -import java.util.ArrayList; -import java.util.List; - import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import java.util.ArrayList; +import java.util.List; + // TODO: 연관관계 편의 메소드 생성(memberContents에 넣는) @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -45,21 +44,30 @@ public class Participant extends BaseTimeEntity { private String nickname; - public Participant(Study study, Member member, String nickname) { + private Boolean isHost; + + private Participant(Study study, Member member, String nickname, Boolean isHost) { this.study = study; this.member = member; this.nickname = nickname; + this.isHost = isHost; + } + public static Participant instantiateParticipantWithContents(Study study, Member member, String nickname) { validateNicknameLength(nickname); + Participant participant = new Participant(study, member, nickname, study.isEmptyParticipants()); + study.addParticipant(participant); + participant.generateContents(study.getTotalCycle()); + return participant; } - private void validateNicknameLength(String nickname) { + private static void validateNicknameLength(String nickname) { if (nickname.length() < 1 || nickname.length() > 10) { throw new NicknameLengthException(); } } - public void generateContents(int totalCycle) { + private void generateContents(int totalCycle) { for (int cycle = 1; cycle <= totalCycle; cycle++) { Content content = new Content(this, cycle); contents.add(content); diff --git a/backend/src/main/java/harustudy/backend/participant/dto/ParticipantResponse.java b/backend/src/main/java/harustudy/backend/participant/dto/ParticipantResponse.java index 478ac94a..62b4abf5 100644 --- a/backend/src/main/java/harustudy/backend/participant/dto/ParticipantResponse.java +++ b/backend/src/main/java/harustudy/backend/participant/dto/ParticipantResponse.java @@ -2,9 +2,9 @@ import harustudy.backend.participant.domain.Participant; -public record ParticipantResponse(Long participantId, String nickname) { +public record ParticipantResponse(Long participantId, String nickname, Boolean isHost) { public static ParticipantResponse from(Participant participant) { - return new ParticipantResponse(participant.getId(), participant.getNickname()); + return new ParticipantResponse(participant.getId(), participant.getNickname(), participant.getIsHost()); } } diff --git a/backend/src/main/java/harustudy/backend/participant/repository/ParticipantRepository.java b/backend/src/main/java/harustudy/backend/participant/repository/ParticipantRepository.java index 98344c72..0dc562f5 100644 --- a/backend/src/main/java/harustudy/backend/participant/repository/ParticipantRepository.java +++ b/backend/src/main/java/harustudy/backend/participant/repository/ParticipantRepository.java @@ -19,7 +19,6 @@ Optional findByStudyAndMember(Study study, List findAllByStudyFetchMember( @Param("study") Study study); - List findByMember(Member member); List findByStudy(Study study); diff --git a/backend/src/main/java/harustudy/backend/participant/service/ParticipantService.java b/backend/src/main/java/harustudy/backend/participant/service/ParticipantService.java index 90778be9..9bfb60d8 100644 --- a/backend/src/main/java/harustudy/backend/participant/service/ParticipantService.java +++ b/backend/src/main/java/harustudy/backend/participant/service/ParticipantService.java @@ -5,21 +5,25 @@ import harustudy.backend.member.domain.Member; import harustudy.backend.member.repository.MemberRepository; import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.domain.Step; import harustudy.backend.participant.dto.ParticipateStudyRequest; import harustudy.backend.participant.dto.ParticipantResponse; import harustudy.backend.participant.dto.ParticipantsResponse; -import harustudy.backend.participant.exception.ParticipantNotFoundException; +import harustudy.backend.participant.dto.ParticipateStudyRequest; import harustudy.backend.participant.exception.ParticipantNotBelongToStudyException; +import harustudy.backend.participant.exception.ParticipantNotFoundException; import harustudy.backend.participant.repository.ParticipantRepository; import harustudy.backend.study.domain.Study; +import harustudy.backend.study.exception.StudyAlreadyStartedException; import harustudy.backend.study.repository.StudyRepository; import jakarta.annotation.Nullable; -import java.util.List; -import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Objects; + @RequiredArgsConstructor @Transactional @Service @@ -104,13 +108,12 @@ private ParticipantsResponse getParticipantsResponseWithMemberFilter( return ParticipantsResponse.from(List.of(response)); } - public Long participateStudy(AuthMember authMember, Long studyId, - ParticipateStudyRequest request) { + public Long participateStudy(AuthMember authMember, Long studyId, ParticipateStudyRequest request) { Member member = memberRepository.findByIdIfExists(request.memberId()); validateIsSameMemberId(authMember, request.memberId()); Study study = studyRepository.findByIdIfExists(studyId); - Participant participant = new Participant(study, member, request.nickname()); - participant.generateContents(study.getTotalCycle()); + validateIsWaiting(study); + Participant participant = Participant.instantiateParticipantWithContents(study, member, request.nickname()); Participant saved = participantRepository.save(participant); return saved.getId(); } @@ -121,6 +124,12 @@ private void validateIsSameMemberId(AuthMember authMember, Long memberId) { } } + private void validateIsWaiting(Study study) { + if (!study.isStep(Step.WAITING)) { + throw new StudyAlreadyStartedException(); + } + } + private void validateParticipantIsRelatedWith( Participant participant, AuthMember authMember, Study study ) { @@ -135,8 +144,7 @@ private void validateMemberOwns(Participant participant, AuthMember authMember) } } - private void validateParticipantIsIncludedIn(Study study, - Participant participant) { + private void validateParticipantIsIncludedIn(Study study, Participant participant) { if (participant.isNotIncludedIn(study)) { throw new ParticipantNotBelongToStudyException(); } diff --git a/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java b/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java new file mode 100644 index 00000000..7e883749 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/participantcode/controller/ParticipantCodeController.java @@ -0,0 +1,32 @@ +package harustudy.backend.participantcode.controller; + +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.participantcode.dto.ParticipantCodeResponse; +import harustudy.backend.participantcode.service.ParticipantCodeService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "참여 코드 관련 기능") +@RequiredArgsConstructor +@RestController +public class ParticipantCodeController { + + private final ParticipantCodeService participantCodeService; + + @Operation(summary = "스터디 아이디로 참여 코드 조회") + @GetMapping("/api/participant-codes") + public ResponseEntity findParticipantCodeByStudyId( + @Authenticated AuthMember authMember, + @RequestParam Long studyId + ) { + ParticipantCodeResponse response = participantCodeService.findParticipantCodeByStudyId( + studyId); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/harustudy/backend/participantcode/dto/ParticipantCodeResponse.java b/backend/src/main/java/harustudy/backend/participantcode/dto/ParticipantCodeResponse.java new file mode 100644 index 00000000..a96eebe7 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/participantcode/dto/ParticipantCodeResponse.java @@ -0,0 +1,10 @@ +package harustudy.backend.participantcode.dto; + +import harustudy.backend.participantcode.domain.ParticipantCode; + +public record ParticipantCodeResponse(String participantCode) { + + public static ParticipantCodeResponse from(ParticipantCode participantCode) { + return new ParticipantCodeResponse(participantCode.getCode()); + } +} diff --git a/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java b/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java index 3277a791..c2fee630 100644 --- a/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java +++ b/backend/src/main/java/harustudy/backend/participantcode/repository/ParticipantCodeRepository.java @@ -1,10 +1,13 @@ package harustudy.backend.participantcode.repository; import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.study.domain.Study; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface ParticipantCodeRepository extends JpaRepository { Optional findByCode(String code); + + Optional findByStudy(Study study); } diff --git a/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java b/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java new file mode 100644 index 00000000..eee799d3 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/participantcode/service/ParticipantCodeService.java @@ -0,0 +1,28 @@ +package harustudy.backend.participantcode.service; + +import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.participantcode.dto.ParticipantCodeResponse; +import harustudy.backend.participantcode.repository.ParticipantCodeRepository; +import harustudy.backend.study.domain.Study; +import harustudy.backend.study.exception.ParticipantCodeNotFoundException; +import harustudy.backend.study.repository.StudyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class ParticipantCodeService { + + private final StudyRepository studyRepository; + private final ParticipantCodeRepository participantCodeRepository; + + @Transactional(readOnly = true) + public ParticipantCodeResponse findParticipantCodeByStudyId(Long studyId) { + Study study = studyRepository.findByIdIfExists(studyId); + ParticipantCode participantCode = participantCodeRepository.findByStudy(study) + .orElseThrow(ParticipantCodeNotFoundException::new); + return ParticipantCodeResponse.from(participantCode); + } +} diff --git a/backend/src/main/java/harustudy/backend/polling/controller/PollingController.java b/backend/src/main/java/harustudy/backend/polling/controller/PollingController.java new file mode 100644 index 00000000..20422a50 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/controller/PollingController.java @@ -0,0 +1,46 @@ +package harustudy.backend.polling.controller; + +import harustudy.backend.auth.Authenticated; +import harustudy.backend.auth.dto.AuthMember; +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; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "폴링 관련 기능") +@RequiredArgsConstructor +@RestController +public class PollingController { + + private final PollingService pollingService; + + @Operation(summary = "진행 페이지 폴링") + @GetMapping("/api/progress") + public ResponseEntity progressPolling( + @Authenticated AuthMember authMember, @RequestParam Long studyId + ) { + 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/polling/dto/ProgressResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java new file mode 100644 index 00000000..b48993f0 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/dto/ProgressResponse.java @@ -0,0 +1,10 @@ +package harustudy.backend.polling.dto; + +import harustudy.backend.participant.domain.Step; + +public record ProgressResponse(String progressStep) { + + public static ProgressResponse from(Step progressStep) { + return new ProgressResponse(progressStep.name().toLowerCase()); + } +} 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/polling/dto/WaitingResponse.java b/backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java new file mode 100644 index 00000000..1b0c4819 --- /dev/null +++ b/backend/src/main/java/harustudy/backend/polling/dto/WaitingResponse.java @@ -0,0 +1,33 @@ +package harustudy.backend.polling.dto; + +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.domain.Step; +import harustudy.backend.participant.dto.ParticipantResponse; +import harustudy.backend.study.domain.Study; + +import java.util.List; + +import static harustudy.backend.study.dto.StudyResponse.IN_PROGRESS; + +public record WaitingResponse(String studyStep, List participants) { + + public static WaitingResponse of(Study study, List participants) { + return new WaitingResponse(getRoomStep(study), mapToParticipantResponses(participants)); + } + + private static String getRoomStep(Study study) { + String roomStep; + if (study.isStep(Step.PLANNING) || study.isStep(Step.STUDYING) || study.isStep(Step.RETROSPECT)) { + roomStep = IN_PROGRESS; + } else { + roomStep = study.getStep().name().toLowerCase(); + } + return roomStep; + } + + private static List mapToParticipantResponses(List participants) { + return participants.stream() + .map(ParticipantResponse::from) + .toList(); + } +} 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/study/controller/StudyController.java b/backend/src/main/java/harustudy/backend/study/controller/StudyController.java index df8149dd..055bd522 100644 --- a/backend/src/main/java/harustudy/backend/study/controller/StudyController.java +++ b/backend/src/main/java/harustudy/backend/study/controller/StudyController.java @@ -3,9 +3,8 @@ import harustudy.backend.auth.Authenticated; import harustudy.backend.auth.dto.AuthMember; import harustudy.backend.study.dto.CreateStudyRequest; -import harustudy.backend.study.dto.CreateStudyResponse; -import harustudy.backend.study.dto.StudyResponse; import harustudy.backend.study.dto.StudiesResponse; +import harustudy.backend.study.dto.StudyResponse; import harustudy.backend.study.service.StudyService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; @@ -13,7 +12,12 @@ import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @Tag(name = "스터디 관련 기능") @RequiredArgsConstructor @@ -46,16 +50,14 @@ public ResponseEntity findStudiesWithFilter( @Operation(summary = "스터디 생성") @ApiResponse(responseCode = "201") @PostMapping("/api/studies") - public ResponseEntity createStudy( + public ResponseEntity createStudy( @Authenticated AuthMember authMember, @RequestBody CreateStudyRequest request ) { - CreateStudyResponse response = studyService.createStudy(request); - return ResponseEntity.created(URI.create("/api/studies/" + response.studyId())) - .body(response); + Long studyId = studyService.createStudy(request); + return ResponseEntity.created(URI.create("/api/studies/" + studyId)).build(); } - // TODO: SSE 이벤트하는 로직으로 변경시 SSE 컨트롤러로 이동할지 고려 @Operation(summary = "다음 스터디 단계로 이동") @ApiResponse(responseCode = "204") @PostMapping("/api/studies/{studyId}/next-step") diff --git a/backend/src/main/java/harustudy/backend/study/domain/Study.java b/backend/src/main/java/harustudy/backend/study/domain/Study.java index 65c747ca..e6d84a2a 100644 --- a/backend/src/main/java/harustudy/backend/study/domain/Study.java +++ b/backend/src/main/java/harustudy/backend/study/domain/Study.java @@ -102,4 +102,12 @@ public void proceed() { public boolean isStep(Step step) { return this.step == step; } + + public boolean isEmptyParticipants() { + return participants.isEmpty(); + } + + public void addParticipant(Participant participant) { + participants.add(participant); + } } diff --git a/backend/src/main/java/harustudy/backend/study/dto/CreateStudyResponse.java b/backend/src/main/java/harustudy/backend/study/dto/CreateStudyResponse.java deleted file mode 100644 index c5ac3ddf..00000000 --- a/backend/src/main/java/harustudy/backend/study/dto/CreateStudyResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package harustudy.backend.study.dto; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import harustudy.backend.participantcode.domain.ParticipantCode; -import harustudy.backend.study.domain.Study; - -public record CreateStudyResponse(@JsonIgnore Long studyId, String participantCode) { - - public static CreateStudyResponse from(Study savedStudy, - ParticipantCode participantCode) { - return new CreateStudyResponse(savedStudy.getId(), participantCode.getCode()); - } -} diff --git a/backend/src/main/java/harustudy/backend/study/dto/StudyResponse.java b/backend/src/main/java/harustudy/backend/study/dto/StudyResponse.java index 6722b300..3734bce1 100644 --- a/backend/src/main/java/harustudy/backend/study/dto/StudyResponse.java +++ b/backend/src/main/java/harustudy/backend/study/dto/StudyResponse.java @@ -6,19 +6,19 @@ import java.time.LocalDateTime; public record StudyResponse(Long studyId, String name, Integer totalCycle, Integer timePerCycle, Integer currentCycle, - String studyStep, String progress, LocalDateTime createdDateTime) { + String studyStep, String progressStep, LocalDateTime createdDate, LocalDateTime lastModifiedDate) { public static final String IN_PROGRESS = "inProgress"; public static StudyResponse from(Study study) { - String roomStep = getRoomStep(study); + String studyStep = getStudyStep(study); String progressStep = getProgressStep(study); return new StudyResponse(study.getId(), study.getName(), study.getTotalCycle(), study.getTimePerCycle(), - study.getCurrentCycle(), roomStep, progressStep, study.getCreatedDate()); + study.getCurrentCycle(), studyStep, progressStep, study.getCreatedDate(), study.getLastModifiedDate()); } - private static String getRoomStep(Study study) { + private static String getStudyStep(Study study) { String roomStep; if (study.isStep(Step.PLANNING) || study.isStep(Step.STUDYING) || study.isStep(Step.RETROSPECT)) { roomStep = IN_PROGRESS; diff --git a/backend/src/main/java/harustudy/backend/study/exception/StudyAlreadyStartedException.java b/backend/src/main/java/harustudy/backend/study/exception/StudyAlreadyStartedException.java new file mode 100644 index 00000000..122abdfd --- /dev/null +++ b/backend/src/main/java/harustudy/backend/study/exception/StudyAlreadyStartedException.java @@ -0,0 +1,7 @@ +package harustudy.backend.study.exception; + +import harustudy.backend.common.exception.HaruStudyException; + +public class StudyAlreadyStartedException extends HaruStudyException { + +} diff --git a/backend/src/main/java/harustudy/backend/study/repository/StudyRepository.java b/backend/src/main/java/harustudy/backend/study/repository/StudyRepository.java index 0d1248a3..19abb311 100644 --- a/backend/src/main/java/harustudy/backend/study/repository/StudyRepository.java +++ b/backend/src/main/java/harustudy/backend/study/repository/StudyRepository.java @@ -1,13 +1,20 @@ package harustudy.backend.study.repository; +import harustudy.backend.participant.domain.Step; import harustudy.backend.study.domain.Study; import harustudy.backend.study.exception.StudyNotFoundException; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface StudyRepository extends JpaRepository { - + default Study findByIdIfExists(Long id) { return findById(id) .orElseThrow(StudyNotFoundException::new); } + + @Query("select s.step from Study s where s.id = :id") + Optional findStepById(@Param("id") Long id); } diff --git a/backend/src/main/java/harustudy/backend/study/service/StudyService.java b/backend/src/main/java/harustudy/backend/study/service/StudyService.java index 78da5437..33107d88 100644 --- a/backend/src/main/java/harustudy/backend/study/service/StudyService.java +++ b/backend/src/main/java/harustudy/backend/study/service/StudyService.java @@ -4,16 +4,15 @@ import harustudy.backend.member.domain.Member; import harustudy.backend.member.exception.MemberNotFoundException; import harustudy.backend.member.repository.MemberRepository; -import harustudy.backend.participantcode.domain.GenerationStrategy; -import harustudy.backend.participantcode.domain.ParticipantCode; import harustudy.backend.participant.domain.Participant; import harustudy.backend.participant.repository.ParticipantRepository; +import harustudy.backend.participantcode.domain.GenerationStrategy; +import harustudy.backend.participantcode.domain.ParticipantCode; import harustudy.backend.participantcode.repository.ParticipantCodeRepository; import harustudy.backend.study.domain.Study; import harustudy.backend.study.dto.CreateStudyRequest; -import harustudy.backend.study.dto.CreateStudyResponse; -import harustudy.backend.study.dto.StudyResponse; import harustudy.backend.study.dto.StudiesResponse; +import harustudy.backend.study.dto.StudyResponse; import harustudy.backend.study.exception.ParticipantCodeNotFoundException; import harustudy.backend.study.exception.StudyNotFoundException; import harustudy.backend.study.repository.StudyRepository; @@ -65,21 +64,20 @@ private StudiesResponse findStudyByMemberId(Long memberId) { return StudiesResponse.from(studies); } + // TODO: N+1 private List mapToStudies(List participants) { return participants.stream() .map(Participant::getStudy) .toList(); } - public CreateStudyResponse createStudy(CreateStudyRequest request) { + public Long createStudy(CreateStudyRequest request) { Study study = new Study(request.name(), request.totalCycle(), request.timePerCycle()); Study savedStudy = studyRepository.save(study); + participantCodeRepository.save(generateUniqueCode(savedStudy)); - ParticipantCode participantCode = generateUniqueCode(study); - participantCodeRepository.save(participantCode); - - return CreateStudyResponse.from(savedStudy, participantCode); + return savedStudy.getId(); } private ParticipantCode generateUniqueCode(Study study) { diff --git a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java index 360237de..c4cc6c48 100644 --- a/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/harustudy/backend/acceptance/AcceptanceTest.java @@ -18,16 +18,17 @@ import harustudy.backend.content.dto.WritePlanRequest; import harustudy.backend.content.dto.WriteRetrospectRequest; import harustudy.backend.integration.LoginResponse; -import harustudy.backend.participant.dto.ParticipateStudyRequest; import harustudy.backend.participant.dto.ParticipantsResponse; +import harustudy.backend.participant.dto.ParticipateStudyRequest; +import harustudy.backend.participantcode.dto.ParticipantCodeResponse; import harustudy.backend.study.dto.CreateStudyRequest; -import harustudy.backend.study.dto.CreateStudyResponse; -import harustudy.backend.study.dto.StudyResponse; import harustudy.backend.study.dto.StudiesResponse; +import harustudy.backend.study.dto.StudyResponse; import jakarta.servlet.http.Cookie; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -80,8 +81,7 @@ void setUp() { @Test void 회원으로_스터디를_진행한다() throws Exception { LoginResponse 로그인_정보 = 구글_로그인을_진행한다(); - String 참여_코드 = 스터디를_개설한다(로그인_정보); - Long 스터디_아이디 = 스터디를_조회한다(로그인_정보, 참여_코드); + Long 스터디_아이디 = 스터디를_개설한다(로그인_정보); Long 참여자_아이디 = 스터디에_참여한다(로그인_정보, 스터디_아이디); 스터디_상태를_다음_단계로_넘긴다(로그인_정보, 스터디_아이디); 스터디_계획을_작성한다(로그인_정보, 스터디_아이디, 참여자_아이디); @@ -106,8 +106,7 @@ void setUp() { @Test void 비회원으로_스터디를_진행한다() throws Exception { LoginResponse 로그인_정보 = 비회원_로그인을_진행한다(); - String 참여_코드 = 스터디를_개설한다(로그인_정보); - Long 스터디_아이디 = 스터디를_조회한다(로그인_정보, 참여_코드); + Long 스터디_아이디 = 스터디를_개설한다(로그인_정보); Long 진행도_아이디 = 스터디에_참여한다(로그인_정보, 스터디_아이디); 스터디_상태를_다음_단계로_넘긴다(로그인_정보, 스터디_아이디); 스터디_계획을_작성한다(로그인_정보, 스터디_아이디, 진행도_아이디); @@ -118,6 +117,13 @@ void setUp() { 스터디_종료_후_결과_조회(로그인_정보, 스터디_아이디); } + @Test + void 비회원으로_타인의_스터디에_참여한다() throws Exception { + String 참여_코드 = 타인의_스터디_참여_코드를_얻는다(); + LoginResponse 로그인_정보 = 비회원_로그인을_진행한다(); + 참여_코드로_스터디_아이디를_얻는다(로그인_정보, 참여_코드); + } + private List 회원으로_진행했던_모든_스터디_목록을_조회한다(LoginResponse 로그인_정보) throws Exception { long memberId = Long.parseLong(jwtTokenProvider @@ -170,7 +176,7 @@ void setUp() { return new LoginResponse(tokenResponse, refreshToken); } - private String 스터디를_개설한다(LoginResponse 로그인_정보) throws Exception { + private Long 스터디를_개설한다(LoginResponse 로그인_정보) throws Exception { CreateStudyRequest request = new CreateStudyRequest("studyName", 1, 20); String jsonRequest = objectMapper.writeValueAsString(request); MvcResult result = mockMvc.perform( @@ -180,24 +186,11 @@ void setUp() { .header(HttpHeaders.AUTHORIZATION, 로그인_정보.createAuthorizationHeader())) .andExpect(status().isCreated()) .andReturn(); - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - CreateStudyResponse response = objectMapper.readValue(jsonResponse, - CreateStudyResponse.class); - return response.participantCode(); - } + String locationHeader = result.getResponse().getHeader(HttpHeaders.LOCATION); - private Long 스터디를_조회한다(LoginResponse 로그인_정보, String 참여_코드) throws Exception { - MvcResult result = mockMvc.perform( - get("/api/studies") - .param("participantCode", 참여_코드) - .header(HttpHeaders.AUTHORIZATION, 로그인_정보.createAuthorizationHeader())) - .andExpect(status().isOk()) - .andReturn(); - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - StudiesResponse responses = objectMapper.readValue(jsonResponse, - StudiesResponse.class); - StudyResponse response = responses.studies().get(0); - return response.studyId(); + String[] parsed = locationHeader.split("/"); + System.out.println(locationHeader); + return Long.parseLong(parsed[3]); } private Long 스터디에_참여한다(LoginResponse 로그인_정보, Long 스터디_아이디) throws Exception { @@ -265,4 +258,38 @@ void setUp() { assertThat(jsonResponse.participants()).hasSize(1); } + + private String 타인의_스터디_참여_코드를_얻는다() throws Exception { + LoginResponse 로그인_정보 = 비회원_로그인을_진행한다(); + Long 스터디_아이디 = 스터디를_개설한다(로그인_정보); + return 스터디_아이디로_참여_코드를_얻는다(로그인_정보, 스터디_아이디); + } + + private String 스터디_아이디로_참여_코드를_얻는다(LoginResponse 로그인_정보, Long 스터디_아이디) throws Exception { + MvcResult result = mockMvc.perform(get("/api/participant-codes") + .param("studyId", 스터디_아이디.toString()) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 로그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + String response = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + ParticipantCodeResponse jsonResponse = objectMapper.readValue(response, + ParticipantCodeResponse.class); + + assertThat(jsonResponse.participantCode()).hasSize(6); + return jsonResponse.participantCode(); + } + + private void 참여_코드로_스터디_아이디를_얻는다(LoginResponse 로그인_정보, String 참여_코드) throws Exception { + MvcResult result = mockMvc.perform(get("/api/studies") + .param("participantCode", 참여_코드) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, 로그인_정보.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + String response = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + Assertions.assertDoesNotThrow(() -> objectMapper.readValue(response, + StudyResponse.class)); + } } diff --git a/backend/src/test/java/harustudy/backend/content/repository/ContentRepositoryTest.java b/backend/src/test/java/harustudy/backend/content/repository/ContentRepositoryTest.java index 8b8f9844..b66ccd7d 100644 --- a/backend/src/test/java/harustudy/backend/content/repository/ContentRepositoryTest.java +++ b/backend/src/test/java/harustudy/backend/content/repository/ContentRepositoryTest.java @@ -7,7 +7,7 @@ import harustudy.backend.member.domain.Member; import harustudy.backend.participant.domain.Participant; import harustudy.backend.study.domain.Study; -import java.util.List; + import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; @@ -34,11 +34,14 @@ class ContentRepositoryTest { void setUp() { Study study = new Study("studyName", 3, 20); Member member = new Member("member", "email", "imageUrl", LoginType.GUEST); - participant = new Participant(study, member, "nickname"); + participant = Participant.instantiateParticipantWithContents(study, member, "nickname"); testEntityManager.persist(study); testEntityManager.persist(member); testEntityManager.persist(participant); + + testEntityManager.flush(); + testEntityManager.clear(); } @Test @@ -54,12 +57,12 @@ void setUp() { testEntityManager.clear(); // when - List found = contentRepository.findByParticipant( - participant); + Content found = contentRepository.findById(content.getId()) + .orElseThrow(); // then - assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); - assertThat(found.get(0).getRetrospect()).isEmpty(); + assertThat(found.getPlan()).containsAllEntriesOf(plan); + assertThat(found.getRetrospect()).isEmpty(); } @Test @@ -78,11 +81,11 @@ void setUp() { testEntityManager.clear(); // when - List found = contentRepository.findByParticipant( - participant); + Content found = contentRepository.findById(content.getId()) + .orElseThrow(); // then - assertThat(found.get(0).getPlan()).containsAllEntriesOf(plan); - assertThat(found.get(0).getRetrospect()).containsAllEntriesOf(retrospect); + assertThat(found.getPlan()).containsAllEntriesOf(plan); + assertThat(found.getRetrospect()).containsAllEntriesOf(retrospect); } } diff --git a/backend/src/test/java/harustudy/backend/content/service/ContentServiceTest.java b/backend/src/test/java/harustudy/backend/content/service/ContentServiceTest.java index fffb79eb..851aba19 100644 --- a/backend/src/test/java/harustudy/backend/content/service/ContentServiceTest.java +++ b/backend/src/test/java/harustudy/backend/content/service/ContentServiceTest.java @@ -49,8 +49,8 @@ class ContentServiceTest { void setUp() { study = new Study("studyName", 1, 20); member = new Member("nickname", "email", "imageUrl", LoginType.GUEST); - participant = new Participant(study, member, "nickname"); - content = new Content(participant, 1); + participant = Participant.instantiateParticipantWithContents(study, member, "nickname"); + content = participant.getContents().get(0); entityManager.persist(study); entityManager.persist(member); @@ -135,8 +135,8 @@ void FLUSH_AND_CLEAR_CONTEXT() { study.proceed(); study.proceed(); - entityManager.merge(study); entityManager.merge(content); + entityManager.merge(study); FLUSH_AND_CLEAR_CONTEXT(); WriteRetrospectRequest request = new WriteRetrospectRequest(participant.getId(), @@ -144,8 +144,7 @@ void FLUSH_AND_CLEAR_CONTEXT() { // when, then assertThatCode( - () -> contentService.writeRetrospect(authMember, study.getId(), - request)) + () -> contentService.writeRetrospect(authMember, study.getId(), request)) .doesNotThrowAnyException(); } diff --git a/backend/src/test/java/harustudy/backend/integration/ContentIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/ContentIntegrationTest.java index 287a37f4..47986fc4 100644 --- a/backend/src/test/java/harustudy/backend/integration/ContentIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/ContentIntegrationTest.java @@ -40,8 +40,8 @@ void setUp() { study = new Study("studyName", 2, 20); memberDto = createMember("member1"); - participant = new Participant(study, memberDto.member(), "nickname"); - content = new Content(participant, 1); + participant = Participant.instantiateParticipantWithContents(study, memberDto.member(), "nickname"); + content = participant.getContents().get(0); entityManager.persist(study); entityManager.persist(participant); @@ -138,7 +138,7 @@ void setUp() { content.changePlan(plan); content.changeRetrospect(retrospect); - Content anotherContent = new Content(participant, 2); + Content anotherContent = participant.getContents().get(1); Map anotherPlan = Map.of("plan", "test"); Map anotherRetrospect = Map.of("retrospect", "test"); anotherContent.changePlan(anotherPlan); @@ -146,7 +146,7 @@ void setUp() { entityManager.merge(content); entityManager.merge(participant); - entityManager.persist(anotherContent); + entityManager.merge(anotherContent); FLUSH_AND_CLEAR_CONTEXT(); // when diff --git a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java index 944861e5..5e9725b6 100644 --- a/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/MemberIntegrationTest.java @@ -32,9 +32,9 @@ void setUp() { memberDto1 = createMember("member1"); memberDto2 = createMember("member2"); - Participant participant1 = new Participant(study, memberDto1.member(), + Participant participant1 = Participant.instantiateParticipantWithContents(study, memberDto1.member(), "name1"); - Participant participant2 = new Participant(study, memberDto2.member(), + Participant participant2 = Participant.instantiateParticipantWithContents(study, memberDto2.member(), "name2"); entityManager.persist(study); diff --git a/backend/src/test/java/harustudy/backend/integration/ParticipantIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/ParticipantIntegrationTest.java index 5f18620e..c201d25b 100644 --- a/backend/src/test/java/harustudy/backend/integration/ParticipantIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/ParticipantIntegrationTest.java @@ -1,7 +1,6 @@ package harustudy.backend.integration; import harustudy.backend.participant.domain.Participant; -import harustudy.backend.participant.domain.Step; import harustudy.backend.participant.dto.ParticipantResponse; import harustudy.backend.study.domain.Study; import org.junit.jupiter.api.BeforeEach; @@ -30,7 +29,7 @@ void setUp() { super.setUp(); study = new Study("studyName", 3, 20); memberDto = createMember("member1"); - participant = new Participant(study, memberDto.member(), "nickname"); + participant = Participant.instantiateParticipantWithContents(study, memberDto.member(), "nickname"); entityManager.persist(study); entityManager.persist(participant); diff --git a/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java new file mode 100644 index 00000000..8c60f6a0 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/PollingIntegrationTest.java @@ -0,0 +1,81 @@ +package harustudy.backend.integration; + +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.dto.ParticipantResponse; +import harustudy.backend.study.domain.Study; +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; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +class PollingIntegrationTest extends IntegrationTest { + + private Study study; + private MemberDto memberDto1; + private MemberDto memberDto2; + private MemberDto memberDto3; + private Participant participant1; + private Participant participant2; + private Participant participant3; + + @BeforeEach + void setUp() { + super.setUp(); + study = new Study("studyName", 3, 20); + memberDto1 = createMember("member1"); + memberDto2 = createMember("member2"); + memberDto3 = createMember("member3"); + participant1 = Participant.instantiateParticipantWithContents(study, memberDto1.member(), "parti1"); + participant2 = Participant.instantiateParticipantWithContents(study, memberDto2.member(), "parti2"); + participant3 = Participant.instantiateParticipantWithContents(study, memberDto3.member(), "parti3"); + + entityManager.persist(study); + entityManager.persist(participant1); + entityManager.persist(participant2); + entityManager.persist(participant3); + + entityManager.flush(); + entityManager.clear(); + } + + @Test + void 스터디에_참여한_참여자들을_폴링으로_조회한다() throws Exception { + // given, when + MvcResult mvcResult = mockMvc.perform(get("/api/waiting") + .param("studyId", String.valueOf(study.getId())) + .accept(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, memberDto1.createAuthorizationHeader())) + .andExpect(status().isOk()) + .andReturn(); + + List expected = Stream.of(participant1, participant2, participant3) + .map(ParticipantResponse::from) + .toList(); + + // then + String jsonResponse = mvcResult.getResponse().getContentAsString(StandardCharsets.UTF_8); + System.out.println("jsonResponse = " + jsonResponse); + WaitingResponse waitingResponse = objectMapper.readValue(jsonResponse, WaitingResponse.class); + + + assertSoftly(softly -> { + softly.assertThat(waitingResponse.studyStep()).isEqualTo(study.getStep().name().toLowerCase()); + softly.assertThat(waitingResponse.participants().size()).isEqualTo(3); + softly.assertThat(waitingResponse.participants()).containsExactlyInAnyOrderElementsOf(expected); + }); + } +} diff --git a/backend/src/test/java/harustudy/backend/integration/StudyIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/StudyIntegrationTest.java index b5a6cb1e..4a02ee5f 100644 --- a/backend/src/test/java/harustudy/backend/integration/StudyIntegrationTest.java +++ b/backend/src/test/java/harustudy/backend/integration/StudyIntegrationTest.java @@ -1,14 +1,24 @@ package harustudy.backend.integration; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.fasterxml.jackson.databind.ObjectMapper; import harustudy.backend.participant.domain.Participant; import harustudy.backend.participant.domain.Step; import harustudy.backend.study.domain.Study; import harustudy.backend.study.dto.CreateStudyRequest; -import harustudy.backend.study.dto.CreateStudyResponse; import harustudy.backend.study.dto.StudiesResponse; import harustudy.backend.study.dto.StudyResponse; import jakarta.persistence.EntityManager; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -18,16 +28,6 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MvcResult; -import java.nio.charset.StandardCharsets; -import java.util.List; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) class StudyIntegrationTest extends IntegrationTest { @@ -48,7 +48,7 @@ void setUp() { study1 = new Study("study1", 3, 20); study2 = new Study("study2", 4, 30); memberDto1 = createMember("member1"); - Participant participant1 = new Participant(study1, memberDto1.member(), "nickname"); + Participant participant1 = Participant.instantiateParticipantWithContents(study1, memberDto1.member(), "nickname"); entityManager.persist(study1); entityManager.persist(study2); entityManager.persist(participant1); @@ -81,7 +81,7 @@ void setUp() { softly.assertThat(response.timePerCycle()).isEqualTo(study1.getTimePerCycle()); softly.assertThat(response.currentCycle()).isEqualTo(study1.getCurrentCycle()); softly.assertThat(response.studyStep()).isEqualTo(StudyResponse.from(study1).studyStep()); - softly.assertThat(response.progress()).isEqualTo(StudyResponse.from(study1).progress()); + softly.assertThat(response.progressStep()).isEqualTo(StudyResponse.from(study1).progressStep()); }); } @@ -183,14 +183,11 @@ void setUp() { .andReturn(); // then - String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); - CreateStudyResponse response = objectMapper.readValue(jsonResponse, - CreateStudyResponse.class); - - assertThat(response.participantCode()) - .isAlphabetic() - .isUpperCase() - .hasSize(6); + String location = result.getResponse().getHeader("Location"); + SoftAssertions.assertSoftly(softly -> { + Assertions.assertThat(location).isNotNull(); + Assertions.assertThat(location.split("/")).hasSize(4); + }); } @Test diff --git a/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java b/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java new file mode 100644 index 00000000..1097c625 --- /dev/null +++ b/backend/src/test/java/harustudy/backend/integration/ViewIntegrationTest.java @@ -0,0 +1,86 @@ +package harustudy.backend.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.domain.Step; +import harustudy.backend.study.domain.Study; +import harustudy.backend.polling.dto.ProgressResponse; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.MvcResult; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +public class ViewIntegrationTest extends IntegrationTest { + + private Study study; + private MemberDto member1; + private MemberDto member2; + + @BeforeEach + void setUp() { + super.setUp(); + + study = new Study("studyName", 1, 20); + + member1 = createMember("member1"); + member2 = createMember("member2"); + + Participant participant1 = Participant.instantiateParticipantWithContents(study, member1.member(), "nickname1"); + Participant participant2 = Participant.instantiateParticipantWithContents(study, member2.member(), "nickname2"); + + entityManager.persist(study); + entityManager.persist(participant1); + entityManager.persist(participant2); + } + + @ParameterizedTest + @MethodSource("progressPollingParameters") + void 스터디_아이디로_진행_페이지_폴링을_진행할_수_있다(int progressCount, String expected) throws Exception { + //given + for (int i = 0; i < progressCount; i++) { + 스터디_진행(); + } + + //when + MvcResult result = mockMvc.perform( + get("/api/progress") + .header(HttpHeaders.AUTHORIZATION, member1.createAuthorizationHeader()) + .param("studyId", String.valueOf(study.getId()))) + .andExpect(status().isOk()) + .andReturn(); + + //then + String jsonResponse = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + ProgressResponse response = objectMapper.readValue(jsonResponse, + ProgressResponse.class); + + assertThat(response.progressStep()).isEqualTo(expected); + } + + void 스터디_진행() throws Exception { + mockMvc.perform( + post("/api/studies/{studyId}/next-step", study.getId()) + .header(HttpHeaders.AUTHORIZATION, member1.createAuthorizationHeader())) + .andExpect(status().isNoContent()); + } + + private static Stream progressPollingParameters() { + return Stream.of( + Arguments.of(1, Step.PLANNING.name().toLowerCase()), + Arguments.of(2, Step.STUDYING.name().toLowerCase()), + Arguments.of(3, Step.RETROSPECT.name().toLowerCase()) + ); + } +} diff --git a/backend/src/test/java/harustudy/backend/participant/domain/ParticipantTest.java b/backend/src/test/java/harustudy/backend/participant/domain/ParticipantTest.java index 9803b197..e0e6b826 100644 --- a/backend/src/test/java/harustudy/backend/participant/domain/ParticipantTest.java +++ b/backend/src/test/java/harustudy/backend/participant/domain/ParticipantTest.java @@ -1,14 +1,8 @@ package harustudy.backend.participant.domain; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - import harustudy.backend.member.domain.Member; import harustudy.backend.participant.exception.NicknameLengthException; import harustudy.backend.study.domain.Study; -import java.util.stream.IntStream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -16,6 +10,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import static org.assertj.core.api.Assertions.*; + @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) class ParticipantTest { @@ -33,7 +29,7 @@ void setUp() { @ValueSource(strings = {"", "12345678901"}) void 멤버의_닉네임이_1자_미만_10자_초과이면_예외를_던진다(String nickname) { // given, when, then - assertThatThrownBy(() -> new Participant(study, member, nickname)) + assertThatThrownBy(() -> Participant.instantiateParticipantWithContents(study, member, nickname)) .isInstanceOf(NicknameLengthException.class); } @@ -41,15 +37,15 @@ void setUp() { @ValueSource(strings = {"1", "1234567890"}) void 멤버의_닉네임이_1자_이상_10자_이하이면_정상_케이스이다(String nickname) { // given, when, then - assertThatCode(() -> new Participant(study, member, nickname)) + assertThatCode(() -> Participant.instantiateParticipantWithContents(study, member, nickname)) .doesNotThrowAnyException(); } @Test void 닉네임이_동일한지_확인할_수_있다() { // given - Participant participant = new Participant(study, member, "nickname"); - Participant otherParticipant = new Participant(study, member, "nickname"); + Participant participant = Participant.instantiateParticipantWithContents(study, member, "nickname"); + Participant otherParticipant = Participant.instantiateParticipantWithContents(study, member, "nickname"); // when, then assertThat(participant.hasSameNicknameWith(otherParticipant)).isTrue(); @@ -58,10 +54,38 @@ void setUp() { @Test void 닉네임이_다른지_확인할_수_있다() { // given - Participant participant = new Participant(study, member, "nickname"); - Participant otherParticipant = new Participant(study, member, "another"); + Participant participant = Participant.instantiateParticipantWithContents(study, member, "nickname"); + Participant otherParticipant = Participant.instantiateParticipantWithContents(study, member, "another"); // when, then assertThat(participant.hasSameNicknameWith(otherParticipant)).isFalse(); } + + @Test + void 스터디에_참여하면_스터디의_참여_인원이_증가한다() { + // given, when + Participant participant = Participant.instantiateParticipantWithContents(study, member, "nickname"); + + // then + assertThat(participant.getStudy().getParticipants()).hasSize(1); + } + + @Test + void 스터디에_첫_번째로_참여하면_방장이다() { + // given, when + Participant host = Participant.instantiateParticipantWithContents(study, member, "host"); + + // then + assertThat(host.getIsHost()).isTrue(); + } + + @Test + void 스터디에_첫_번째_이후로_참여하면_방장이_아니다() { + // given, when + Participant.instantiateParticipantWithContents(study, member, "host"); + Participant participant = Participant.instantiateParticipantWithContents(study, member, "parti"); + + // then + assertThat(participant.getIsHost()).isFalse(); + } } diff --git a/backend/src/test/java/harustudy/backend/participant/service/ParticipantServiceTest.java b/backend/src/test/java/harustudy/backend/participant/service/ParticipantServiceTest.java index 844d3a29..b4d2a353 100644 --- a/backend/src/test/java/harustudy/backend/participant/service/ParticipantServiceTest.java +++ b/backend/src/test/java/harustudy/backend/participant/service/ParticipantServiceTest.java @@ -1,184 +1,193 @@ package harustudy.backend.participant.service; -import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; - +import harustudy.backend.auth.dto.AuthMember; +import harustudy.backend.auth.exception.AuthorizationException; +import harustudy.backend.member.domain.Member; +import harustudy.backend.participant.domain.Participant; +import harustudy.backend.participant.dto.ParticipantResponse; +import harustudy.backend.participant.dto.ParticipantsResponse; +import harustudy.backend.participant.dto.ParticipateStudyRequest; +import harustudy.backend.participant.exception.ParticipantNotBelongToStudyException; +import harustudy.backend.participantcode.domain.CodeGenerationStrategy; +import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.study.domain.Study; +import harustudy.backend.study.exception.StudyAlreadyStartedException; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayNameGeneration; +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.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; + @SuppressWarnings("NonAsciiCharacters") @DisplayNameGeneration(ReplaceUnderscores.class) @SpringBootTest @Transactional -public class ParticipantServiceTest { -// -// private ParticipantService participantService; -// -// @PersistenceContext -// private EntityManager entityManager; -// -// private Study study1; -// private Study study2; -// private Member member1; -// private Member member2; -// -// @BeforeEach -// void setUp() { -// ParticipantCode participantCode1 = new ParticipantCode(new CodeGenerationStrategy()); -// ParticipantCode participantCode2 = new ParticipantCode(new CodeGenerationStrategy()); -// study1 = new Study("studyName1", 3, 20, participantCode1); -// study2 = new Study("studyName2", 3, 20, participantCode2); -// member1 = Member.guest(); -// member2 = Member.guest(); -// -// entityManager.persist(participantCode1); -// entityManager.persist(participantCode2); -// entityManager.persist(study1); -// entityManager.persist(study2); -// entityManager.persist(member1); -// entityManager.persist(member2); -// -// entityManager.flush(); -// entityManager.clear(); -// } -// -// @Test -// void 진행도를_조회할_수_있다() { -// // given -// Participant participant = new Participant(study2, member1, "nickname1"); -// AuthMember authMember = new AuthMember(member1.getId()); -// -// entityManager.persist(participant); -// -// // when -// PomodossResponse response = -// participantService.findParticipant(authMember, study2.getId(), participant.getId()); -// ParticipantResponse expected = ParticipantResponse.from(participant); -// -// // then -// assertThat(response).usingRecursiveComparison() -// .ignoringExpectedNullFields() -// .isEqualTo(expected); -// } -// -// @Test -// void 스터디의_모든_진행도를_조회할_수_있다() { -// // given -// Participant participant = new Participant(study2, member1, "nickname1"); -// Participant anotherParticipant = new Participant(study2, member2, "nickname2"); -// AuthMember authMember1 = new AuthMember(member1.getId()); -// -// entityManager.persist(participant); -// entityManager.persist(anotherParticipant); -// -// // when -// PomodossesResponse response = -// participantService.findParticipantWithFilter(authMember1, study2.getId(), null); -// ParticipantsResponse expected = ParticipantsResponse.from(List.of( -// ParticipantResponse.from(participant), -// ParticipantResponse.from(anotherParticipant) -// )); -// -// // then -// assertThat(response).usingRecursiveComparison() -// .ignoringExpectedNullFields() -// .isEqualTo(expected); -// } -// -// @Test -// void 참여하지_않은_스터디에_대해서는_모든_진행도를_조회할_수_없다() { -// // given -// AuthMember authMember = new AuthMember(member1.getId()); -// -// // when, then -// assertsnBy(() -> -// participantService.findParticipantWithFilter(authMember, study2.getId(), null)) -// .isInstanceOf(AuthorizationException.class); -// } -// -// @Test -// void 특정_멤버의_진행도를_조회할_수_있다() { -// // given -// Participant participant = new Participant(study2, member1, "nickname1"); -// Participant anotherParticipant = new Participant(study2, member2, "nickname2"); -// AuthMember authMember1 = new AuthMember(member1.getId()); -// -// entityManager.persist(participant); -// entityManager.persist(anotherParticipant); -// -// // when -// PomodossesResponse response = -// participantService.findParticipantWithFilter(authMember1, study2.getId(), member1.getId()); -// ParticipantsResponse expected = ParticipantsResponse.from(List.of( -// ParticipantResponse.from(participant) -// )); -// -// // then -// assertThat(response).usingRecursiveComparison() -// .ignoringExpectedNullFields() -// .isEqualTo(expected); -// } -// -// @Test -// void 자신의_소유가_아닌_진행도는_조회할_수_없다() { -// // given -// Participant participant = new Participant(study2, member1, "nickname1"); -// AuthMember authMember = new AuthMember(member2.getId()); -// -// entityManager.persist(participant); -// -// // when, then -// assertsnBy(() -> -// participantService.findParticipant(authMember, study2.getId(), participant.getId())) -// .isInstanceOf(AuthorizationException.class); -// } -// -// @Test -// void 해당_스터디의_진행도가_아니라면_조회할_수_없다() { -// // given -// Participant participant1 = new Participant(study1, member1, "nickname1"); -// Participant participant2 = new Participant(study2, member1, "nickname1"); -// AuthMember authMember = new AuthMember(member1.getId()); -// -// entityManager.persist(participant1); -// entityManager.persist(participant2); -// -// // when, then -// assertsnBy(() -> -// participantService.findParticipant(authMember, study1.getId(), participant2.getId())) -// .isInstanceOf(ParticipantNotBelongToStudyException.class); -// } -// -// @Test -// void 다음_스터디_단계로_이동할_수_있다() { -// // given -// Participant participant = new Participant(study2, member1, "nickname1"); -// AuthMember authMember1 = new AuthMember(member1.getId()); -// -// entityManager.persist(participant); -// swhen -// participantService.proceed(authMember1, study2.getId(), participant.getId()); -// -// // then -// assertThat(participant.getStep()).isEqualTo(Step.STUDYING); -// } -// -// @Test -// void 스터디에_참여하면_진행도가_생긴다() { -// // given -// AuthMember authMember1 = new AuthMember(member1.getId()); -// -// // when -// ParticipateStudyRequest request = new ParticipateStudyRequest(member1.getId(), "nick"); -// Long participantId = participantService.participateStudy(authMember1, study2.getId(), request); -// -// // then -// Participant participant = entityManager.find(Participant.class, participantId); -// assertSoftly(softly -> { -// assertThat(participant.getNickname()).isEqualTo(request.nickname()); -// assertThat(participant.getMember().getId()).isEqualTo(request.memberId()); -// assertThat(participant.getCurrentCycle()).isEqualTo(1); -// assertThat(participant.getStep()).isEqualTo(Step.PLANNING); -// }); -// } +class ParticipantServiceTest { + + @Autowired + private ParticipantService participantService; + + @PersistenceContext + private EntityManager entityManager; + + private Study study1; + private Study study2; + private Member member1; + private Member member2; + + @BeforeEach + void setUp() { + study1 = new Study("studyName1", 3, 20); + study2 = new Study("studyName2", 3, 20); + + ParticipantCode participantCode1 = new ParticipantCode(study1, new CodeGenerationStrategy()); + ParticipantCode participantCode2 = new ParticipantCode(study2, new CodeGenerationStrategy()); + + member1 = Member.guest(); + member2 = Member.guest(); + + entityManager.persist(study1); + entityManager.persist(study2); + entityManager.persist(participantCode1); + entityManager.persist(participantCode2); + entityManager.persist(member1); + entityManager.persist(member2); + + entityManager.flush(); + entityManager.clear(); + } + + @Test + void 이미_시작된_스터디에는_참여가_불가하다() { + // given + AuthMember authMember1 = new AuthMember(member1.getId()); + ParticipateStudyRequest request = new ParticipateStudyRequest(member1.getId(), "nick"); + Study study = entityManager.merge(study1); + study.proceed(); + entityManager.flush(); + + // when, then + assertThatThrownBy( + () -> participantService.participateStudy(authMember1, study1.getId(), request)) + .isInstanceOf(StudyAlreadyStartedException.class); + } + + @Test + void 스터디의_모든_참여자_정보를_조회할_수_있다() { + // given + Participant participant = Participant.instantiateParticipantWithContents(study2, member1, "nickname1"); + Participant anotherParticipant = Participant.instantiateParticipantWithContents(study2, member2, "nickname2"); + AuthMember authMember1 = new AuthMember(member1.getId()); + + entityManager.persist(participant); + entityManager.persist(anotherParticipant); + + List responses = Stream.of(participant, anotherParticipant) + .map(ParticipantResponse::from) + .toList(); + ParticipantsResponse expected = ParticipantsResponse.from(responses); + + // when + ParticipantsResponse response = participantService.findParticipantsWithFilter(authMember1, study2.getId(), null); + + + // then + assertThat(response).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + + @Test + void 참여하지_않은_스터디에_대해서는_모든_참여자_정보를_조회할_수_없다() { + // given + AuthMember authMember = new AuthMember(member1.getId()); + + // when, then + AssertionsForClassTypes.assertThatThrownBy(() -> + participantService.findParticipantsWithFilter(authMember, study2.getId(), null)) + .isInstanceOf(AuthorizationException.class); + } + + @Test + void 특정_멤버의_참여자_정보자_정보를_조회할_수_있다() { + // given + Participant participant = Participant.instantiateParticipantWithContents(study2, member1, "nickname1"); + Participant anotherParticipant = Participant.instantiateParticipantWithContents(study2, member2, "nickname2"); + AuthMember authMember1 = new AuthMember(member1.getId()); + + entityManager.persist(participant); + entityManager.persist(anotherParticipant); + + ParticipantsResponse expected = ParticipantsResponse.from(List.of( + ParticipantResponse.from(participant) + )); + + // when + ParticipantsResponse response = participantService.findParticipantsWithFilter(authMember1, study2.getId(), member1.getId()); + + // then + assertThat(response).usingRecursiveComparison() + .ignoringExpectedNullFields() + .isEqualTo(expected); + } + + @Test + void 자신의_소유가_아닌_참여자_정보는_조회할_수_없다() { + // given + Participant participant = Participant.instantiateParticipantWithContents(study2, member1, "nickname1"); + AuthMember authMember = new AuthMember(member2.getId()); + + entityManager.persist(participant); + + // when, then + assertThatThrownBy(() -> + participantService.findParticipant(authMember, study2.getId(), participant.getId())) + .isInstanceOf(AuthorizationException.class); + } + + @Test + void 해당_스터디의_참여자가_아니라면_조회할_수_없다() { + // given + Participant participant1 = Participant.instantiateParticipantWithContents(study1, member1, "nickname1"); + Participant participant2 = Participant.instantiateParticipantWithContents(study2, member1, "nickname1"); + AuthMember authMember = new AuthMember(member1.getId()); + + entityManager.persist(participant1); + entityManager.persist(participant2); + + // when, then + assertThatThrownBy(() -> + participantService.findParticipant(authMember, study1.getId(), participant2.getId())) + .isInstanceOf(ParticipantNotBelongToStudyException.class); + } + + @Test + void 스터디에_참여한다() { + // given + AuthMember authMember1 = new AuthMember(member1.getId()); + + // when + ParticipateStudyRequest request = new ParticipateStudyRequest(member1.getId(), "nick"); + Long participantId = participantService.participateStudy(authMember1, study2.getId(), request); + + // then + Participant participant = entityManager.find(Participant.class, participantId); + assertSoftly(softly -> { + assertThat(participant.isParticipantOf(study2)).isTrue(); + assertThat(participant.getNickname()).isEqualTo(request.nickname()); + assertThat(participant.getMember().getId()).isEqualTo(request.memberId()); + }); + } } diff --git a/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java b/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java new file mode 100644 index 00000000..98ea7ccd --- /dev/null +++ b/backend/src/test/java/harustudy/backend/participantcode/service/ParticipantCodeServiceTest.java @@ -0,0 +1,45 @@ +package harustudy.backend.participantcode.service; + +import harustudy.backend.participantcode.domain.CodeGenerationStrategy; +import harustudy.backend.participantcode.domain.ParticipantCode; +import harustudy.backend.participantcode.dto.ParticipantCodeResponse; +import harustudy.backend.study.domain.Study; +import jakarta.persistence.EntityManager; +import org.assertj.core.api.Assertions; +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; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(ReplaceUnderscores.class) +@Transactional +@SpringBootTest +class ParticipantCodeServiceTest { + + @Autowired + private ParticipantCodeService participantCodeService; + + @Autowired + private EntityManager entityManager; + + @Test + void 스터디_아이디로_참여_코드를_조회한다() { + // given + Study study = new Study("study", 1, 20); + ParticipantCode participantCode = new ParticipantCode(study, new CodeGenerationStrategy()); + entityManager.persist(study); + entityManager.persist(participantCode); + entityManager.flush(); + entityManager.clear(); + + // when + ParticipantCodeResponse result = participantCodeService.findParticipantCodeByStudyId( + study.getId()); + + // then + Assertions.assertThat(result.participantCode()).hasSize(6); + } +} 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); + }); + } +}