From d36839dee9a661663c443269b7cbfa96087057ac Mon Sep 17 00:00:00 2001 From: Bojun Kim Date: Wed, 9 Aug 2023 13:53:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=8B=89=EB=84=A4=EC=9E=84=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20(#255)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 닉네임 중복을 조회하는 기능 추가 * refactor: 예상하지 못한 예외에 대한 핸들링 메시지를 프로필에 따라 분리 * test: 컨트롤러 테스트에 InternalServerErrorMessageConverter MockBean 추가 --- .../backend/application/MemberService.java | 8 ++++++++ .../com/yigongil/backend/config/WebConfig.java | 1 + .../backend/domain/member/MemberRepository.java | 2 ++ .../yigongil/backend/ui/MemberController.java | 7 +++++++ .../ApiExceptionHandler.java | 10 ++++++++-- .../DevInternalServerErrorMessageConverter.java | 14 ++++++++++++++ .../InternalServerErrorMessageConverter.java | 6 ++++++ .../ProdInternalServerErrorMessageConverter.java | 14 ++++++++++++++ .../backend/acceptance/steps/MemberSteps.java | 16 ++++++++++++++++ .../backend/ui/MemberControllerTest.java | 16 ++++++++++++++++ .../yigongil/backend/ui/StudyControllerTest.java | 8 +++++--- .../yigongil/backend/ui/TodoControllerTest.java | 4 ++++ .../resources/features/update-profile.feature | 9 +++++++++ 13 files changed, 110 insertions(+), 5 deletions(-) rename backend/src/main/java/com/yigongil/backend/ui/{ => exceptionhandler}/ApiExceptionHandler.java (77%) create mode 100644 backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/DevInternalServerErrorMessageConverter.java create mode 100644 backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/InternalServerErrorMessageConverter.java create mode 100644 backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ProdInternalServerErrorMessageConverter.java diff --git a/backend/src/main/java/com/yigongil/backend/application/MemberService.java b/backend/src/main/java/com/yigongil/backend/application/MemberService.java index dfedc234d..313e64abd 100644 --- a/backend/src/main/java/com/yigongil/backend/application/MemberService.java +++ b/backend/src/main/java/com/yigongil/backend/application/MemberService.java @@ -2,12 +2,14 @@ import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.domain.member.MemberRepository; +import com.yigongil.backend.domain.member.Nickname; import com.yigongil.backend.domain.study.Study; import com.yigongil.backend.domain.studymember.StudyMember; import com.yigongil.backend.domain.studymember.StudyMemberRepository; import com.yigongil.backend.exception.MemberNotFoundException; import com.yigongil.backend.request.ProfileUpdateRequest; import com.yigongil.backend.response.FinishedStudyResponse; +import com.yigongil.backend.response.NicknameValidationResponse; import com.yigongil.backend.response.ProfileResponse; import java.util.List; import org.springframework.stereotype.Service; @@ -89,4 +91,10 @@ private int calculateNumberOfSuccessRounds(Member member) { public void update(Member member, ProfileUpdateRequest request) { member.updateProfile(request.nickname(), request.introduction()); } + + @Transactional(readOnly = true) + public NicknameValidationResponse existsByNickname(String nickname) { + boolean exists = memberRepository.existsByNickname(new Nickname(nickname)); + return new NicknameValidationResponse(exists); + } } diff --git a/backend/src/main/java/com/yigongil/backend/config/WebConfig.java b/backend/src/main/java/com/yigongil/backend/config/WebConfig.java index a0bfab8ae..75546f816 100644 --- a/backend/src/main/java/com/yigongil/backend/config/WebConfig.java +++ b/backend/src/main/java/com/yigongil/backend/config/WebConfig.java @@ -28,6 +28,7 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/v1/**") .excludePathPatterns("/v1/login/**") .excludePathPatterns("/v1/members/{id:[0-9]\\d*}") + .excludePathPatterns("/v1/members/exists") .excludePathPatterns("/v1/studies/recruiting") .order(2); diff --git a/backend/src/main/java/com/yigongil/backend/domain/member/MemberRepository.java b/backend/src/main/java/com/yigongil/backend/domain/member/MemberRepository.java index 5c3ff5373..9fe64ee46 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/member/MemberRepository.java +++ b/backend/src/main/java/com/yigongil/backend/domain/member/MemberRepository.java @@ -20,4 +20,6 @@ public interface MemberRepository extends Repository { on r.id = :id """) List findMembersByRoundId(@Param("id") Long id); + + boolean existsByNickname(Nickname nickname); } diff --git a/backend/src/main/java/com/yigongil/backend/ui/MemberController.java b/backend/src/main/java/com/yigongil/backend/ui/MemberController.java index 1a4387fde..191ec1502 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/MemberController.java +++ b/backend/src/main/java/com/yigongil/backend/ui/MemberController.java @@ -5,6 +5,7 @@ import com.yigongil.backend.domain.member.Member; import com.yigongil.backend.request.ProfileUpdateRequest; import com.yigongil.backend.response.MyProfileResponse; +import com.yigongil.backend.response.NicknameValidationResponse; import com.yigongil.backend.response.ProfileResponse; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -12,6 +13,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/v1/members") @@ -54,4 +56,9 @@ public ResponseEntity updateProfile(@Authorization Member member, @Request return ResponseEntity.ok().build(); } + @GetMapping(path = "/exists") + public ResponseEntity existsByNickname(@RequestParam String nickname) { + NicknameValidationResponse response = memberService.existsByNickname(nickname); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/yigongil/backend/ui/ApiExceptionHandler.java b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ApiExceptionHandler.java similarity index 77% rename from backend/src/main/java/com/yigongil/backend/ui/ApiExceptionHandler.java rename to backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ApiExceptionHandler.java index 8a521ce47..172edcc0c 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/ApiExceptionHandler.java +++ b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ApiExceptionHandler.java @@ -1,4 +1,4 @@ -package com.yigongil.backend.ui; +package com.yigongil.backend.ui.exceptionhandler; import com.yigongil.backend.exception.HttpException; import io.jsonwebtoken.ExpiredJwtException; @@ -13,6 +13,12 @@ @RestControllerAdvice public class ApiExceptionHandler { + private final InternalServerErrorMessageConverter internalServerErrorMessageConverter; + + public ApiExceptionHandler(InternalServerErrorMessageConverter internalServerErrorMessageConverter) { + this.internalServerErrorMessageConverter = internalServerErrorMessageConverter; + } + @ExceptionHandler public ResponseEntity handleHttpException(HttpException e) { log.error("예외 발생: ", e); @@ -38,6 +44,6 @@ public ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotVali public ResponseEntity handleException(Exception e) { log.error("예상치 못한 예외 발생: ", e); return ResponseEntity.internalServerError() - .body("서버 에러 발생"); + .body(internalServerErrorMessageConverter.convert(e)); } } diff --git a/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/DevInternalServerErrorMessageConverter.java b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/DevInternalServerErrorMessageConverter.java new file mode 100644 index 000000000..0791a8d52 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/DevInternalServerErrorMessageConverter.java @@ -0,0 +1,14 @@ +package com.yigongil.backend.ui.exceptionhandler; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(value = "!prod") +@Component +public class DevInternalServerErrorMessageConverter implements InternalServerErrorMessageConverter { + + @Override + public String convert(Exception e) { + return e.getClass().getSimpleName() + ": " + e.getLocalizedMessage(); + } +} diff --git a/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/InternalServerErrorMessageConverter.java b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/InternalServerErrorMessageConverter.java new file mode 100644 index 000000000..452e5ee89 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/InternalServerErrorMessageConverter.java @@ -0,0 +1,6 @@ +package com.yigongil.backend.ui.exceptionhandler; + +public interface InternalServerErrorMessageConverter { + + String convert(Exception e); +} diff --git a/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ProdInternalServerErrorMessageConverter.java b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ProdInternalServerErrorMessageConverter.java new file mode 100644 index 000000000..995f912e8 --- /dev/null +++ b/backend/src/main/java/com/yigongil/backend/ui/exceptionhandler/ProdInternalServerErrorMessageConverter.java @@ -0,0 +1,14 @@ +package com.yigongil.backend.ui.exceptionhandler; + +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile(value = "prod") +@Component +public class ProdInternalServerErrorMessageConverter implements InternalServerErrorMessageConverter { + + @Override + public String convert(Exception e) { + return "서버 에러 발생"; + } +} diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java index 42bd79875..41df83e4a 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/MemberSteps.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yigongil.backend.config.oauth.JwtTokenProvider; import com.yigongil.backend.request.ProfileUpdateRequest; +import com.yigongil.backend.response.NicknameValidationResponse; import com.yigongil.backend.response.ProfileResponse; import com.yigongil.backend.response.TokenResponse; import io.cucumber.java.en.Given; @@ -16,6 +17,7 @@ import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; public class MemberSteps { @@ -73,4 +75,18 @@ public MemberSteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTo () -> assertThat(response.introduction()).isEqualTo(introduction) ); } + + @Then("{string}은 중복된 닉네임인 것을 확인할 수 있다.") + public void 중복_닉네임_확인(String nickname) { + ExtractableResponse response = given() + .when() + .get("/v1/members/exists?nickname=" + nickname) + .then().log().all() + .extract(); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(response.as(NicknameValidationResponse.class).exists()).isTrue() + ); + } } diff --git a/backend/src/test/java/com/yigongil/backend/ui/MemberControllerTest.java b/backend/src/test/java/com/yigongil/backend/ui/MemberControllerTest.java index d44fb05a6..df9fa8c7d 100644 --- a/backend/src/test/java/com/yigongil/backend/ui/MemberControllerTest.java +++ b/backend/src/test/java/com/yigongil/backend/ui/MemberControllerTest.java @@ -21,7 +21,9 @@ import com.yigongil.backend.domain.member.MemberRepository; import com.yigongil.backend.fixture.MemberFixture; import com.yigongil.backend.request.ProfileUpdateRequest; +import com.yigongil.backend.response.NicknameValidationResponse; import com.yigongil.backend.response.ProfileResponse; +import com.yigongil.backend.ui.exceptionhandler.InternalServerErrorMessageConverter; import com.yigongil.backend.utils.querycounter.ApiQueryCounter; import java.util.Collections; import java.util.Optional; @@ -59,6 +61,9 @@ class MemberControllerTest { @MockBean private ApiQueryCounter apiQueryCounter; + @MockBean + private InternalServerErrorMessageConverter internalServerErrorMessageConverter; + @Test void 프로필_정보를_조회한다() throws Exception { Member member = MemberFixture.김진우.toMember(); @@ -104,4 +109,15 @@ class MemberControllerTest { verify(memberService, only()).update(MemberFixture.김진우.toMember(), request); } + + @Test + void 중복된_닉네임이_있는지_확인한다() throws Exception { + final String existNickname = "jinwoo"; + given(memberService.existsByNickname(existNickname)).willReturn(new NicknameValidationResponse(true)); + + mockMvc.perform(get("/v1/members/exists?nickname={nickname}", existNickname)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.exists").value(true)); + } } diff --git a/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java b/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java index 38f6a7ae7..a0d8441e4 100644 --- a/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java +++ b/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java @@ -16,8 +16,7 @@ import com.yigongil.backend.domain.member.MemberRepository; import com.yigongil.backend.fixture.MemberFixture; import com.yigongil.backend.request.StudyCreateRequest; -import com.yigongil.backend.request.TodoCreateRequest; -import com.yigongil.backend.request.TodoUpdateRequest; +import com.yigongil.backend.ui.exceptionhandler.InternalServerErrorMessageConverter; import com.yigongil.backend.utils.querycounter.ApiQueryCounter; import java.time.LocalDate; import java.time.temporal.ChronoUnit; @@ -58,13 +57,16 @@ class StudyControllerTest { @MockBean private ApiQueryCounter apiQueryCounter; + @MockBean + private InternalServerErrorMessageConverter internalServerErrorMessageConverter; + @BeforeEach void setUp() { given(memberRepository.findById(anyLong())).willReturn(Optional.of(MemberFixture.김진우.toMember())); } @Test - void 프로필_정보를_업데이트_한다() throws Exception { + void 스터디를_개설한다() throws Exception { LocalDate startAt = LocalDate.now().plus(5L, ChronoUnit.MONTHS); StudyCreateRequest request = new StudyCreateRequest( "자바", diff --git a/backend/src/test/java/com/yigongil/backend/ui/TodoControllerTest.java b/backend/src/test/java/com/yigongil/backend/ui/TodoControllerTest.java index 2029d2c28..8cdcb31dd 100644 --- a/backend/src/test/java/com/yigongil/backend/ui/TodoControllerTest.java +++ b/backend/src/test/java/com/yigongil/backend/ui/TodoControllerTest.java @@ -20,6 +20,7 @@ import com.yigongil.backend.fixture.MemberFixture; import com.yigongil.backend.request.TodoCreateRequest; import com.yigongil.backend.request.TodoUpdateRequest; +import com.yigongil.backend.ui.exceptionhandler.InternalServerErrorMessageConverter; import com.yigongil.backend.utils.querycounter.ApiQueryCounter; import java.util.Optional; import org.apache.http.HttpHeaders; @@ -55,6 +56,9 @@ class TodoControllerTest { @MockBean private ApiQueryCounter apiQueryCounter; + @MockBean + private InternalServerErrorMessageConverter internalServerErrorMessageConverter; + @BeforeEach void setUp() { given(memberRepository.findById(anyLong())).willReturn(Optional.of(MemberFixture.김진우.toMember())); diff --git a/backend/src/test/resources/features/update-profile.feature b/backend/src/test/resources/features/update-profile.feature index 3a9739a04..b8478c3c6 100644 --- a/backend/src/test/resources/features/update-profile.feature +++ b/backend/src/test/resources/features/update-profile.feature @@ -8,3 +8,12 @@ Feature: 프로필 정보를 업데이트한다 Examples: | nickname | introduction | | 김김진진우우 | 간단 소개입니다 | + + Scenario Outline: 프로필 정보를 업데이트할 때 닉네임이 중복되면 실패한다 + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + When "jinwoo"가 닉네임 ""과 간단 소개""으로 수정한다. + Then ""은 중복된 닉네임인 것을 확인할 수 있다. + + Examples: + | nickname | introduction | + | 김김진진우우 | 간단 소개입니다 | From cde64f901f1b8f04cad0725dad1f31f73c01f35f Mon Sep 17 00:00:00 2001 From: kevstevie <109793396+kevstevie@users.noreply.github.com> Date: Wed, 9 Aug 2023 14:05:00 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#256)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 검색 기능 추가 --- .../backend/application/StudyService.java | 20 +++++++++++++++-- .../yigongil/backend/config/WebConfig.java | 2 +- .../backend/domain/study/PageStrategy.java | 2 +- .../backend/domain/study/StudyRepository.java | 10 ++------- .../yigongil/backend/ui/StudyController.java | 10 +++++++++ .../backend/acceptance/steps/StudySteps.java | 22 +++++++++++++++++++ ...create-and-find-recruiting-studies.feature | 16 ++++++++++++++ .../features/create-and-find-todo.feature | 0 .../resources/features/create-todo.feature | 0 ...ure => find-participating-studies.feature} | 0 10 files changed, 70 insertions(+), 12 deletions(-) delete mode 100644 backend/src/test/resources/features/create-and-find-todo.feature delete mode 100644 backend/src/test/resources/features/create-todo.feature rename backend/src/test/resources/features/{find-studies.feature => find-participating-studies.feature} (100%) diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index 71f1f1e7a..f57090635 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -1,6 +1,6 @@ package com.yigongil.backend.application; -import static com.yigongil.backend.domain.study.PageStrategy.CREATED_AT_DESC; +import static com.yigongil.backend.domain.study.PageStrategy.ID_DESC; import com.yigongil.backend.application.studyevent.StudyStartedEvent; import com.yigongil.backend.domain.member.Member; @@ -76,9 +76,25 @@ public Long create(Member member, StudyCreateRequest request) { @Transactional(readOnly = true) public List findRecruitingStudies(int page) { - Pageable pageable = PageRequest.of(page, CREATED_AT_DESC.getSize(), CREATED_AT_DESC.getSort()); + Pageable pageable = PageRequest.of(page, ID_DESC.getSize(), ID_DESC.getSort()); + Page studies = studyRepository.findAllByProcessingStatus(ProcessingStatus.RECRUITING, pageable); + return toRecruitingStudyResponse(studies); + + } + + @Transactional(readOnly = true) + public List findRecruitingStudiesWithSearch(int page, String word) { + Pageable pageable = PageRequest.of(page, ID_DESC.getSize(), ID_DESC.getSort()); + Page studies = studyRepository.findAllByProcessingStatusAndNameContainingIgnoreCase( + ProcessingStatus.RECRUITING, + word, + pageable + ); + return toRecruitingStudyResponse(studies); + } + private List toRecruitingStudyResponse(Page studies) { return studies.get() .map(RecruitingStudyResponse::from) .toList(); diff --git a/backend/src/main/java/com/yigongil/backend/config/WebConfig.java b/backend/src/main/java/com/yigongil/backend/config/WebConfig.java index 75546f816..da07ac1fc 100644 --- a/backend/src/main/java/com/yigongil/backend/config/WebConfig.java +++ b/backend/src/main/java/com/yigongil/backend/config/WebConfig.java @@ -29,7 +29,7 @@ public void addInterceptors(InterceptorRegistry registry) { .excludePathPatterns("/v1/login/**") .excludePathPatterns("/v1/members/{id:[0-9]\\d*}") .excludePathPatterns("/v1/members/exists") - .excludePathPatterns("/v1/studies/recruiting") + .excludePathPatterns("/v1/studies/recruiting/**") .order(2); registry.addInterceptor(loggingInterceptor) diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/PageStrategy.java b/backend/src/main/java/com/yigongil/backend/domain/study/PageStrategy.java index 6fd6c3097..4742ccc00 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/PageStrategy.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/PageStrategy.java @@ -6,7 +6,7 @@ @Getter public enum PageStrategy { - CREATED_AT_DESC(Constants.PAGE_SIZE, Sort.by("createdAt").descending()); + ID_DESC(Constants.PAGE_SIZE, Sort.by("id").descending()); private final int size; private final Sort sort; diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/StudyRepository.java b/backend/src/main/java/com/yigongil/backend/domain/study/StudyRepository.java index 1c6da39a5..26547021a 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/StudyRepository.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/StudyRepository.java @@ -19,6 +19,8 @@ public interface StudyRepository extends Repository { Page findAllByProcessingStatus(ProcessingStatus processingStatus, Pageable pageable); + Page findAllByProcessingStatusAndNameContainingIgnoreCase(ProcessingStatus processingStatus, String word, Pageable pageable); + List findAllByProcessingStatus(ProcessingStatus processingStatus); @Query(""" @@ -35,12 +37,4 @@ public interface StudyRepository extends Repository { and s.processingStatus = :processingStatus """) List findByMemberAndProcessingStatus(@Param("member") Member member, @Param("processingStatus") ProcessingStatus processingStatus); - - @Query(""" - select distinct s from Study s - join StudyMember sm - on s = sm.study - where sm.member = :member - """) - List findStudiesOfMember(@Param("member") Member member); } diff --git a/backend/src/main/java/com/yigongil/backend/ui/StudyController.java b/backend/src/main/java/com/yigongil/backend/ui/StudyController.java index 495bfbf4d..9867a6715 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/StudyController.java +++ b/backend/src/main/java/com/yigongil/backend/ui/StudyController.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RequestMapping("/v1/studies") @@ -74,6 +75,15 @@ public ResponseEntity> findRecruitingStudies(int p return ResponseEntity.ok(response); } + @GetMapping("/recruiting/search") + public ResponseEntity> findRecruitingStudiesWithSearch( + int page, + @RequestParam(name = "q") String word + ) { + List response = studyService.findRecruitingStudiesWithSearch(page, word); + return ResponseEntity.ok(response); + } + @GetMapping("/{id}/applicants") public ResponseEntity> findApplicantOfStudy(@PathVariable Long id, @Authorization Member master) { List applicants = studyService.findApplicantsOfStudy(id, master); diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java index 60e1420ab..39585a2a6 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java @@ -219,4 +219,26 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok assertThat(homeResponse.studies().get(0).nextDate()).isNotNull(); } + + @When("{string}를 검색한다.") + public void 검색한다(String search) { + ExtractableResponse response = given().log().all() + .when() + .get("/v1/studies/recruiting/search?page=0&q=" + search) + .then().log().all().extract(); + + sharedContext.setResponse(response); + } + + @Then("결과가 모두 {string}를 포함하고 {int} 개가 조회된다.") + public void 결과가_모두_검색어를_포함한다(String search, int number) { + List responses = sharedContext.getResponse() + .jsonPath() + .getList(".", RecruitingStudyResponse.class); + + assertAll( + () -> assertThat(responses).map(RecruitingStudyResponse::name).allMatch(name -> name.contains(search)), + () -> assertThat(responses).hasSize(number) + ); + } } diff --git a/backend/src/test/resources/features/create-and-find-recruiting-studies.feature b/backend/src/test/resources/features/create-and-find-recruiting-studies.feature index effa387d0..eca2c34e0 100644 --- a/backend/src/test/resources/features/create-and-find-recruiting-studies.feature +++ b/backend/src/test/resources/features/create-and-find-recruiting-studies.feature @@ -6,3 +6,19 @@ Feature: 스터디를 만들면 모집 중인 스터디로 생성된다. Given "jinwoo"가 제목-"자바2", 정원-"8"명, 예상시작일-"3"일 뒤, 총 회차-"3"회, 주기-"3d", 소개-"스터디소개2"로 스터디를 개설한다. When 모집 중인 스터디 탭을 클릭한다. Then 모집 중인 스터디를 2개 중 2개를 확인할 수 있다. + + Scenario: 스터디를 검색한다. + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + Given "jinwoo"가 제목-"자바1", 정원-"6"명, 예상시작일-"1"일 뒤, 총 회차-"2"회, 주기-"1w", 소개-"스터디소개1"로 스터디를 개설한다. + Given "jinwoo"가 제목-"2자바1", 정원-"6"명, 예상시작일-"1"일 뒤, 총 회차-"2"회, 주기-"1w", 소개-"스터디소개1"로 스터디를 개설한다. + Given "jinwoo"가 제목-"자3바", 정원-"8"명, 예상시작일-"3"일 뒤, 총 회차-"3"회, 주기-"3d", 소개-"스터디소개2"로 스터디를 개설한다. + When "자바"를 검색한다. + Then 결과가 모두 "자바"를 포함하고 2 개가 조회된다. + + Scenario: 스터디 검색 결과가 없다. + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + Given "jinwoo"가 제목-"자바1", 정원-"6"명, 예상시작일-"1"일 뒤, 총 회차-"2"회, 주기-"1w", 소개-"스터디소개1"로 스터디를 개설한다. + Given "jinwoo"가 제목-"2자바1", 정원-"6"명, 예상시작일-"1"일 뒤, 총 회차-"2"회, 주기-"1w", 소개-"스터디소개1"로 스터디를 개설한다. + Given "jinwoo"가 제목-"자3바", 정원-"8"명, 예상시작일-"3"일 뒤, 총 회차-"3"회, 주기-"3d", 소개-"스터디소개2"로 스터디를 개설한다. + When "아무개"를 검색한다. + Then 결과가 모두 "아무개"를 포함하고 0 개가 조회된다. diff --git a/backend/src/test/resources/features/create-and-find-todo.feature b/backend/src/test/resources/features/create-and-find-todo.feature deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/test/resources/features/create-todo.feature b/backend/src/test/resources/features/create-todo.feature deleted file mode 100644 index e69de29bb..000000000 diff --git a/backend/src/test/resources/features/find-studies.feature b/backend/src/test/resources/features/find-participating-studies.feature similarity index 100% rename from backend/src/test/resources/features/find-studies.feature rename to backend/src/test/resources/features/find-participating-studies.feature From 58e87e610581229a11e5d97d0ffe47020d3a332f Mon Sep 17 00:00:00 2001 From: Jae Min Yu Date: Wed, 9 Aug 2023 14:41:54 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=8A=A4=ED=84=B0=EB=94=94=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#257)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 스터디 정보 수정 기능 구현 * refactor: 스터디 생성/수정 Request DTO 통일 및 방장 검증 로직 객체 내부로 삽입 --- .../backend/application/StudyService.java | 18 +++++- .../yigongil/backend/domain/study/Study.java | 42 ++++++++++---- ...teRequest.java => StudyUpdateRequest.java} | 2 +- .../yigongil/backend/ui/StudyController.java | 15 ++++- .../backend/acceptance/steps/StudySteps.java | 58 ++++++++++++++++--- .../backend/config/JacksonConfigTest.java | 4 +- .../backend/domain/study/StudyTest.java | 22 +++++++ .../backend/ui/StudyControllerTest.java | 4 +- .../resources/features/study-detail.feature | 4 +- .../resources/features/update-study.feature | 8 +++ 10 files changed, 146 insertions(+), 31 deletions(-) rename backend/src/main/java/com/yigongil/backend/request/{StudyCreateRequest.java => StudyUpdateRequest.java} (96%) create mode 100644 backend/src/test/resources/features/update-study.feature diff --git a/backend/src/main/java/com/yigongil/backend/application/StudyService.java b/backend/src/main/java/com/yigongil/backend/application/StudyService.java index f57090635..f3e3f346b 100644 --- a/backend/src/main/java/com/yigongil/backend/application/StudyService.java +++ b/backend/src/main/java/com/yigongil/backend/application/StudyService.java @@ -15,7 +15,7 @@ import com.yigongil.backend.exception.ApplicantAlreadyExistException; import com.yigongil.backend.exception.ApplicantNotFoundException; import com.yigongil.backend.exception.StudyNotFoundException; -import com.yigongil.backend.request.StudyCreateRequest; +import com.yigongil.backend.request.StudyUpdateRequest; import com.yigongil.backend.response.MyStudyResponse; import com.yigongil.backend.response.RecruitingStudyResponse; import com.yigongil.backend.response.StudyDetailResponse; @@ -49,7 +49,7 @@ public StudyService( } @Transactional - public Long create(Member member, StudyCreateRequest request) { + public Long create(Member member, StudyUpdateRequest request) { Study study = Study.initializeStudyOf( request.name(), request.introduction(), @@ -253,6 +253,20 @@ public void startStudy(Member member, Long studyId) { publisher.publishEvent(new StudyStartedEvent(study)); } + @Transactional + public void update(Member member, Long studyId, StudyUpdateRequest request) { + Study study = findStudyById(studyId); + study.updateInformation( + member, + request.name(), + request.numberOfMaximumMembers(), + request.startAt().atStartOfDay(), + request.totalRoundCount(), + request.periodOfRound(), + request.introduction() + ); + } + private Study findStudyById(Long studyId) { return studyRepository.findById(studyId) .orElseThrow(() -> new StudyNotFoundException("해당 스터디를 찾을 수 없습니다", studyId)); diff --git a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java index f8ac55024..fd36aa984 100644 --- a/backend/src/main/java/com/yigongil/backend/domain/study/Study.java +++ b/backend/src/main/java/com/yigongil/backend/domain/study/Study.java @@ -189,17 +189,6 @@ public void addMember(Member member) { } } - private void validateStudyProcessingStatus() { - if (!isRecruiting()) { - throw new InvalidProcessingStatusException("모집 중인 스터디가 아니기 때문에 신청을 수락할 수 없습니다.", - processingStatus.name()); - } - } - - public boolean isRecruiting() { - return this.processingStatus == ProcessingStatus.RECRUITING; - } - private void validateMemberSize() { if (sizeOfCurrentMembers() >= numberOfMaximumMembers) { throw new InvalidMemberSizeException("스터디 정원이 가득 찼습니다.", numberOfMaximumMembers); @@ -266,4 +255,35 @@ public String findPeriodOfRoundToString() { public Member getMaster() { return currentRound.getMaster(); } + + public void updateInformation( + Member member, + String name, + Integer numberOfMaximumMembers, + LocalDateTime startAt, + Integer totalRoundCount, + String periodOfRound, + String introduction + ) { + validateMaster(member); + validateStudyProcessingStatus(); + this.name = name; + this.numberOfMaximumMembers = numberOfMaximumMembers; + this.startAt = startAt; + this.totalRoundCount = totalRoundCount; + this.periodOfRound = PeriodUnit.getPeriodNumber(periodOfRound); + this.periodUnit = PeriodUnit.getPeriodUnit(periodOfRound); + this.introduction = introduction; + } + + private void validateStudyProcessingStatus() { + if (!isRecruiting()) { + throw new InvalidProcessingStatusException("현재 스터디의 상태가 모집중이 아닙니다.", + processingStatus.name()); + } + } + + public boolean isRecruiting() { + return this.processingStatus == ProcessingStatus.RECRUITING; + } } diff --git a/backend/src/main/java/com/yigongil/backend/request/StudyCreateRequest.java b/backend/src/main/java/com/yigongil/backend/request/StudyUpdateRequest.java similarity index 96% rename from backend/src/main/java/com/yigongil/backend/request/StudyCreateRequest.java rename to backend/src/main/java/com/yigongil/backend/request/StudyUpdateRequest.java index 5960dde1c..814eb0b3f 100644 --- a/backend/src/main/java/com/yigongil/backend/request/StudyCreateRequest.java +++ b/backend/src/main/java/com/yigongil/backend/request/StudyUpdateRequest.java @@ -5,7 +5,7 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.Positive; -public record StudyCreateRequest( +public record StudyUpdateRequest( @NotBlank(message = "스터디 이름이 공백입니다.") String name, @Positive(message = "스터디 멤버 정원은 음수일 수 없습니다.") diff --git a/backend/src/main/java/com/yigongil/backend/ui/StudyController.java b/backend/src/main/java/com/yigongil/backend/ui/StudyController.java index 9867a6715..0a02a37e3 100644 --- a/backend/src/main/java/com/yigongil/backend/ui/StudyController.java +++ b/backend/src/main/java/com/yigongil/backend/ui/StudyController.java @@ -3,7 +3,7 @@ import com.yigongil.backend.application.StudyService; import com.yigongil.backend.config.auth.Authorization; import com.yigongil.backend.domain.member.Member; -import com.yigongil.backend.request.StudyCreateRequest; +import com.yigongil.backend.request.StudyUpdateRequest; import com.yigongil.backend.response.MyStudyResponse; import com.yigongil.backend.response.RecruitingStudyResponse; import com.yigongil.backend.response.StudyDetailResponse; @@ -17,6 +17,7 @@ import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -35,12 +36,22 @@ public StudyController(StudyService studyService) { @PostMapping public ResponseEntity createStudy( @Authorization Member member, - @RequestBody @Valid StudyCreateRequest request + @RequestBody @Valid StudyUpdateRequest request ) { Long studyId = studyService.create(member, request); return ResponseEntity.created(URI.create("/v1/studies/" + studyId)).build(); } + @PutMapping("/{studyId}") + public ResponseEntity updateStudy( + @Authorization Member member, + @PathVariable Long studyId, + @RequestBody @Valid StudyUpdateRequest request + ) { + studyService.update(member, studyId, request); + return ResponseEntity.ok().build(); + } + @PostMapping("/{studyId}/applicants") public ResponseEntity applyStudy(@Authorization Member member, @PathVariable Long studyId) { studyService.apply(member, studyId); diff --git a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java index 39585a2a6..a08fdaab2 100644 --- a/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java +++ b/backend/src/test/java/com/yigongil/backend/acceptance/steps/StudySteps.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yigongil.backend.config.oauth.JwtTokenProvider; import com.yigongil.backend.domain.study.ProcessingStatus; -import com.yigongil.backend.request.StudyCreateRequest; +import com.yigongil.backend.request.StudyUpdateRequest; import com.yigongil.backend.response.HomeResponse; import com.yigongil.backend.response.RecruitingStudyResponse; import com.yigongil.backend.response.RoundNumberResponse; @@ -52,7 +52,7 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok String introduction ) throws JsonProcessingException { LocalDate startAt = LocalDate.now().plus(Long.parseLong(leftDays), ChronoUnit.DAYS); - StudyCreateRequest request = new StudyCreateRequest( + StudyUpdateRequest request = new StudyUpdateRequest( name, Integer.parseInt(numberOfMaximumMembers), startAt, @@ -112,14 +112,16 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok sharedContext.setResponse(response); } - @Then("스터디 상세조회가 입력한 대로 되어있다.") - public void 스터디상세_조회에서_해당_스터디를_확인할_수_있다() { + @Then("스터디 상세조회 결과가 제목-{string}, 정원-{string}로 조회된다.") + public void 스터디상세_조회에서_해당_스터디를_확인할_수_있다(String studyName, String maximumNumber) { StudyDetailResponse response = sharedContext.getResponse() .as(StudyDetailResponse.class); assertAll( - () -> assertThat(response.name()).isEqualTo("자바"), - () -> assertThat(response.numberOfMaximumMembers()).isEqualTo(5) + () -> assertThat(response.name()).isEqualTo(studyName), + () -> assertThat( + response.numberOfMaximumMembers()).isEqualTo(Integer.parseInt(maximumNumber) + ) ); } @@ -145,7 +147,8 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok .as(RoundResponse.class); assertAll( - () -> assertThat(round.masterId()).isEqualTo(tokenProvider.parseToken((String) sharedContext.getParameter(masterGithubId))), + () -> assertThat(round.masterId()).isEqualTo(tokenProvider.parseToken( + (String) sharedContext.getParameter(masterGithubId))), () -> assertThat(round.id()).isEqualTo(sharedContext.getParameter("roundId")) ); } @@ -157,7 +160,8 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok String studyId = (String) sharedContext.getParameter(studyName); StudyDetailResponse studyDetailResponse = given().log().all() - .header(HttpHeaders.AUTHORIZATION, memberId) + .header(HttpHeaders.AUTHORIZATION, + memberId) .when() .get("/v1/studies/" + studyId) .then().log().all() @@ -166,7 +170,8 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok Long roundId = studyDetailResponse.rounds() .stream() - .filter(round -> Objects.equals(round.number(), roundNumber)) + .filter(round -> Objects.equals(round.number(), + roundNumber)) .findFirst() .map(RoundNumberResponse::id) .get(); @@ -211,6 +216,41 @@ public StudySteps(ObjectMapper objectMapper, SharedContext sharedContext, JwtTok sharedContext.setResponse(response); } + @Given("{string}가 {string} 스터디의 정보를 제목-{string}, 정원-{string}명, 예상시작일-{string}일 뒤, 총 회차-{string}회, 주기-{string}, 소개-{string}로 수정한다.") + public void 스터디_정보_수정( + String masterGithubId, + String originalStudyName, + String updateStudyName, + String updateNumberOfMaximumMembers, + String updateStartAt, + String updateTotalRoundCount, + String updatePeriodOfRound, + String updateIntroduction + ) { + String memberId = (String) sharedContext.getParameter(masterGithubId); + String studyId = (String) sharedContext.getParameter(originalStudyName); + + StudyUpdateRequest request = new StudyUpdateRequest( + updateStudyName, + Integer.parseInt(updateNumberOfMaximumMembers), + LocalDate.now().plus(Long.parseLong(updateStartAt), ChronoUnit.DAYS), + Integer.parseInt(updateTotalRoundCount), + updatePeriodOfRound, + updateIntroduction + ); + + given().log().all() + .header(HttpHeaders.AUTHORIZATION, memberId) + .contentType( + MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when() + .put("/v1/studies/{studyId}", studyId) + .then().log().all(); + + sharedContext.setParameter(updateStudyName, studyId); + } + @Then("스터디의 남은 날짜가 null이 아니다.") public void 스터디_회차_업데이트_검증() { ExtractableResponse response = sharedContext.getResponse(); diff --git a/backend/src/test/java/com/yigongil/backend/config/JacksonConfigTest.java b/backend/src/test/java/com/yigongil/backend/config/JacksonConfigTest.java index c4ac417ae..38ea70983 100644 --- a/backend/src/test/java/com/yigongil/backend/config/JacksonConfigTest.java +++ b/backend/src/test/java/com/yigongil/backend/config/JacksonConfigTest.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.yigongil.backend.request.StudyCreateRequest; +import com.yigongil.backend.request.StudyUpdateRequest; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; @@ -38,7 +38,7 @@ class JacksonConfigTest { """.formatted(startAtInput); // when - StudyCreateRequest request = objectMapper.readValue(json, StudyCreateRequest.class); + StudyUpdateRequest request = objectMapper.readValue(json, StudyUpdateRequest.class); // then assertThat(request.startAt()).isEqualTo(startAt); diff --git a/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java b/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java index 462c93b51..276375296 100644 --- a/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java +++ b/backend/src/test/java/com/yigongil/backend/domain/study/StudyTest.java @@ -11,6 +11,7 @@ import com.yigongil.backend.fixture.MemberFixture; import com.yigongil.backend.fixture.StudyFixture; import java.time.LocalDateTime; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.junit.jupiter.api.Test; class StudyTest { @@ -123,4 +124,25 @@ class StudyTest { assertThatThrownBy(() -> study.addMember(member2)) .isInstanceOf(InvalidMemberSizeException.class); } + + @Test + void 스터디의_상태가_모집중이_아닐_때_정보를_수정하면_예외가_발생한다() { + // given + Study study = StudyFixture.자바_스터디_진행중.toStudy(); + + // when + ThrowingCallable throwable = () -> study.updateInformation( + study.getMaster(), + "이름 수정", + 5, + LocalDateTime.now(), + 5, + "3d", + "소개" + ); + + // then + assertThatThrownBy(throwable) + .isInstanceOf(InvalidProcessingStatusException.class); + } } diff --git a/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java b/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java index a0d8441e4..88ba68cc2 100644 --- a/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java +++ b/backend/src/test/java/com/yigongil/backend/ui/StudyControllerTest.java @@ -15,7 +15,7 @@ import com.yigongil.backend.config.oauth.JwtTokenProvider; import com.yigongil.backend.domain.member.MemberRepository; import com.yigongil.backend.fixture.MemberFixture; -import com.yigongil.backend.request.StudyCreateRequest; +import com.yigongil.backend.request.StudyUpdateRequest; import com.yigongil.backend.ui.exceptionhandler.InternalServerErrorMessageConverter; import com.yigongil.backend.utils.querycounter.ApiQueryCounter; import java.time.LocalDate; @@ -68,7 +68,7 @@ void setUp() { @Test void 스터디를_개설한다() throws Exception { LocalDate startAt = LocalDate.now().plus(5L, ChronoUnit.MONTHS); - StudyCreateRequest request = new StudyCreateRequest( + StudyUpdateRequest request = new StudyUpdateRequest( "자바", 5, startAt, diff --git a/backend/src/test/resources/features/study-detail.feature b/backend/src/test/resources/features/study-detail.feature index adddc5856..beb0d9697 100644 --- a/backend/src/test/resources/features/study-detail.feature +++ b/backend/src/test/resources/features/study-detail.feature @@ -7,7 +7,7 @@ Feature: 스터디를 생성하고 조회한다. Given 깃허브 아이디가 "noiman"인 멤버가 이름이 "자바"스터디에 신청할 수 있다. Given "jinwoo"가 "noiman"의 "자바" 스터디 신청을 수락한다. When "noiman"가 스터디 상세 조회에서 이름이 "자바"인 스터디를 조회한다. - Then 스터디 상세조회가 입력한 대로 되어있다. + Then 스터디 상세조회 결과가 제목-"자바", 정원-"5"로 조회된다. Scenario: 스터디를 정상 생성하고 조회한다. Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. @@ -15,4 +15,4 @@ Feature: 스터디를 생성하고 조회한다. Given "noiman"의 깃허브 아이디로 회원가입을 한다. Given 깃허브 아이디가 "noiman"인 멤버가 이름이 "자바"스터디에 신청할 수 있다. When "noiman"가 스터디 상세 조회에서 이름이 "자바"인 스터디를 조회한다. - Then 스터디 상세조회가 입력한 대로 되어있다. + Then 스터디 상세조회 결과가 제목-"자바", 정원-"5"로 조회된다. diff --git a/backend/src/test/resources/features/update-study.feature b/backend/src/test/resources/features/update-study.feature new file mode 100644 index 000000000..ea17cb2ee --- /dev/null +++ b/backend/src/test/resources/features/update-study.feature @@ -0,0 +1,8 @@ +Feature: 스터디의 정보를 수정한다. + + Scenario: 스터디 정보를 정상적으로 수정한다. + Given "jinwoo"의 깃허브 아이디로 회원가입을 한다. + Given "jinwoo"가 제목-"자바", 정원-"6"명, 예상시작일-"5"일 뒤, 총 회차-"5"회, 주기-"1w", 소개-"수정 전 스터디 소개"로 스터디를 개설한다. + Given "jinwoo"가 "자바" 스터디의 정보를 제목-"자바스크립트", 정원-"8"명, 예상시작일-"3"일 뒤, 총 회차-"6"회, 주기-"5d", 소개-"수정 후 스터디 소개"로 수정한다. + When "jinwoo"가 스터디 상세 조회에서 이름이 "자바스크립트"인 스터디를 조회한다. + Then 스터디 상세조회 결과가 제목-"자바스크립트", 정원-"8"로 조회된다.