From daf7442f2a757f2f66ac046a8faf3fe56d927207 Mon Sep 17 00:00:00 2001 From: ssggii Date: Mon, 15 Sep 2025 21:47:45 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[SCRUM-295]=20FIX:=20=EB=82=B4=20=EC=BD=94?= =?UTF-8?q?=EC=8A=A4=20=EC=83=9D=EC=84=B1=EC=97=90=EC=84=9C=20=EC=BD=94?= =?UTF-8?q?=EC=8A=A4=EB=AA=85=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0=20(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 내 코스 생성에 포함되어있던 코스명 중복 검사 로직을 제거했습니다. 해당 로직은 별도의 API로 구현할 예정입니다. --- .../domain/course/service/CourseService.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java index e56da12..6b7cb74 100644 --- a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java +++ b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java @@ -324,24 +324,15 @@ public boolean isInsideBusan(double longitude, double latitude) { */ @Transactional public Long createMemberCourse(Long memberId, CourseCreateRequestDto request) { - String newCourseName = request.startPointName().trim() + COURSE_NAME_DELIMITER + request.endPointName().trim(); - checkCourseNameDuplicated(newCourseName); Course newCourse = saveMemberCourse(memberId, request); courseDataService.updateCourseImage(newCourse.getId(), request.thumbnailImage()); publishCourseCreatedEvent(newCourse.getId(), request.isInsideBusan()); return newCourse.getId(); } - private void checkCourseNameDuplicated(String newCourseName) { - if (courseRepository.existsByName(newCourseName)) { - throw new BusinessException(DUPLICATE_COURSE_NAME); - } - } - private Course saveMemberCourse(Long memberId, CourseCreateRequestDto request) { Member member = memberRepository.findById(memberId).orElseThrow(() -> new BusinessException(MEMBER_NOT_FOUND)); - Course newCourse = courseDataService.createCourseToGpx( - new GpxCourseRequestDto(request.startPointName(), request.endPointName()), request.gpxFile()); + Course newCourse = courseDataService.createCourseToGpx(new GpxCourseRequestDto(request.startPointName(), request.endPointName()), request.gpxFile()); newCourse.setCreator(member); return newCourse; } @@ -352,6 +343,16 @@ private void publishCourseCreatedEvent(Long courseId, boolean isInsideBusan) { eventPublisher.publishEvent(event); } + /** + * 코스명 중복 여부를 검사합니다. + * + * @param newCourseName 사용자가 요청한 코스명 + * @return 코스명이 중복되면 true, 중복되지 않으면 false + */ + public boolean isCourseNameDuplicated(String newCourseName) { + return courseRepository.existsByName(newCourseName.trim()); + } + /** * 회원이 생성한 코스를 삭제합니다. * 코스에 저장된 즐길거리가 있는 경우 함께 삭제합니다. @@ -412,7 +413,9 @@ private void updateCourseName(Course course, String newStartPointName, String ne // 변경되었다면 중복 검사 후 이름 업데이트 if (isCourseNameChanged) { - checkCourseNameDuplicated(updatedCourseName); + if (isCourseNameDuplicated(updatedCourseName)) { // 코스명 중복 시 예외 발생 + throw new BusinessException(DUPLICATE_COURSE_NAME); + } course.updateName(updatedCourseName); } } From e2d3a318f164077919764426afd782a430548334 Mon Sep 17 00:00:00 2001 From: ssggii Date: Mon, 15 Sep 2025 22:10:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[SCRUM-295]=20FEAT:=20=EC=BD=94=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코스명 중복 검사 API의 엔드포인트를 구현했습니다. --- .../course/controller/CourseController.java | 21 +++++++++++++++---- .../domain/course/service/CourseService.java | 4 ++++ .../global/response/ResponseCode.java | 1 + 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/server/running_handai/domain/course/controller/CourseController.java b/src/main/java/com/server/running_handai/domain/course/controller/CourseController.java index 18edabc..421cb2e 100644 --- a/src/main/java/com/server/running_handai/domain/course/controller/CourseController.java +++ b/src/main/java/com/server/running_handai/domain/course/controller/CourseController.java @@ -6,7 +6,6 @@ import com.server.running_handai.domain.course.dto.*; import com.server.running_handai.domain.course.service.CourseService; -import com.server.running_handai.domain.spot.dto.SpotInfoDto; import com.server.running_handai.global.entity.SortBy; import com.server.running_handai.global.oauth.CustomOAuth2User; import com.server.running_handai.global.response.CommonResponse; @@ -18,6 +17,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import java.util.List; import lombok.RequiredArgsConstructor; @@ -26,10 +26,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -200,6 +197,22 @@ public ResponseEntity> isBusanCourse( return ResponseEntity.ok(CommonResponse.success(SUCCESS, isBusanCourse)); } + @Operation(summary = "코스명 중복 검사", description = "이미 존재하는 코스명인지 검사합니다." + + "
코스명이 중복되는 경우 true, 중복되지 않는 경우 false를 반환합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "성공"), + @ApiResponse(responseCode = "400", description = "요청 파라미터 오류") + }) + @GetMapping("/api/courses/name/exists") + public ResponseEntity> checkCourseNameDuplicated( + @Parameter(description = "코스 이름", required = true) + @RequestParam("name") String courseName + ) { + log.info("[코스명 중복 검사] courseName: {}", courseName); + boolean isCourseNameDuplicated = courseService.isCourseNameDuplicated(courseName); + return ResponseEntity.ok(CommonResponse.success(SUCCESS, isCourseNameDuplicated)); + } + @Operation(summary = "내 코스 생성", description = "회원의 코스를 생성합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "성공"), diff --git a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java index 6b7cb74..32eaf4c 100644 --- a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java +++ b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java @@ -4,6 +4,7 @@ import static com.server.running_handai.global.response.ResponseCode.COURSE_NOT_FOUND; import static com.server.running_handai.global.response.ResponseCode.DUPLICATE_COURSE_NAME; import static com.server.running_handai.global.response.ResponseCode.INVALID_AREA_PARAMETER; +import static com.server.running_handai.global.response.ResponseCode.INVALID_COURSE_NAME_PARAMETER; import static com.server.running_handai.global.response.ResponseCode.INVALID_THEME_PARAMETER; import static com.server.running_handai.global.response.ResponseCode.MEMBER_NOT_FOUND; import static com.server.running_handai.global.response.ResponseCode.NO_AUTHORITY_TO_DELETE_COURSE; @@ -350,6 +351,9 @@ private void publishCourseCreatedEvent(Long courseId, boolean isInsideBusan) { * @return 코스명이 중복되면 true, 중복되지 않으면 false */ public boolean isCourseNameDuplicated(String newCourseName) { + if (newCourseName == null || newCourseName.isBlank()) { + throw new BusinessException(INVALID_COURSE_NAME_PARAMETER); + } return courseRepository.existsByName(newCourseName.trim()); } diff --git a/src/main/java/com/server/running_handai/global/response/ResponseCode.java b/src/main/java/com/server/running_handai/global/response/ResponseCode.java index e3f3c1a..1c8481d 100644 --- a/src/main/java/com/server/running_handai/global/response/ResponseCode.java +++ b/src/main/java/com/server/running_handai/global/response/ResponseCode.java @@ -37,6 +37,7 @@ public enum ResponseCode { EMPTY_FILE(BAD_REQUEST, "파일이 누락되었습니다."), INVALID_POINT_NAME(BAD_REQUEST, "포인트 이름이 누락되었습니다."), DUPLICATE_COURSE_NAME(BAD_REQUEST, "이미 존재하는 코스 이름입니다."), + INVALID_COURSE_NAME_PARAMETER(BAD_REQUEST, "코스 이름은 필수 입력값입니다."), // UNAUTHORIZED (401) INVALID_ACCESS_TOKEN(UNAUTHORIZED, "유효하지 않은 액세스 토큰입니다."), From c2ffeebd412832d7abb397564b4cc8ceac69ecac Mon Sep 17 00:00:00 2001 From: ssggii Date: Mon, 15 Sep 2025 22:11:51 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[SCRUM-295]=20TEST:=20=EC=BD=94=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코스명 중복 검사 API의 테스트 코드를 구현했습니다. 코스명 중복 검사 로직이 제외된 내 코스 생성의 테스트 코드도 일부 수정했습니다. --- .../course/service/CourseServiceTest.java | 73 ++++++++++++++----- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java b/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java index b6a0883..5713c3d 100644 --- a/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java +++ b/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java @@ -760,7 +760,6 @@ void createMemberCourse_success() { Long courseId = 100L; Course newCourse = createMockCourse(courseId); - when(courseRepository.existsByName(COURSE_NAME)).thenReturn(false); // 중복된 이름 없음 when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); when(courseDataService.createCourseToGpx(any(GpxCourseRequestDto.class), any(MultipartFile.class))).thenReturn(newCourse); @@ -781,35 +780,16 @@ void createMemberCourse_success() { assertThat(capturedEvent.courseId()).isEqualTo(newCourse.getId()); assertThat(capturedEvent.isInsideBusan()).isTrue(); - verify(courseRepository).existsByName(COURSE_NAME); verify(memberRepository).findById(memberId); verify(courseDataService).createCourseToGpx(any(GpxCourseRequestDto.class), eq(gpxFile)); verify(courseDataService).updateCourseImage(newCourse.getId(), thumbnailImgFile); } - @Test - @DisplayName("실패 - 중복된 코스 이름") - void createMemberCourse_fail_duplicateCourseName() { - // given - Long memberId = 1L; - when(courseRepository.existsByName(COURSE_NAME)).thenReturn(true); // 코스 이름이 이미 존재함 - - // when, then - BusinessException exception = assertThrows(BusinessException.class, - () -> courseService.createMemberCourse(memberId, request)); - assertThat(exception.getResponseCode()).isEqualTo(DUPLICATE_COURSE_NAME); - - verify(memberRepository, never()).findById(anyLong()); - verify(courseDataService, never()).createCourseToGpx(any(), any()); - verify(courseDataService, never()).updateCourseImage(any(), any()); - } - @Test @DisplayName("실패 - 존재하지 않는 회원") void createMemberCourse_fail_memberNotFound() { // given Long nonExistentMemberId = 999L; - when(courseRepository.existsByName(COURSE_NAME)).thenReturn(false); // 중복은 통과 when(memberRepository.findById(nonExistentMemberId)).thenReturn(Optional.empty()); // 존재하지 않는 회원 // when, then @@ -822,6 +802,59 @@ void createMemberCourse_fail_memberNotFound() { } } + @Nested + @DisplayName("코스명 중복 검사") + class IsCourseNameDuplicatedTest { + + @Test + @DisplayName("성공 - 이미 존재하는 코스명일 경우 true 반환") + void returnsTrue_whenNameExists() { + // given + String existingName = "부산-바다"; + when(courseRepository.existsByName(existingName)).thenReturn(true); + + // when + boolean result = courseService.isCourseNameDuplicated(existingName); + + // then + assertThat(result).isTrue(); + verify(courseRepository).existsByName(existingName); + } + + @Test + @DisplayName("성공 - 존재하지 않는 코스명일 경우 false 반환") + void returnsFalse_whenNameDoesNotExist() { + // given + String newName = "서울-시티"; + when(courseRepository.existsByName(newName)).thenReturn(false); + + // when + boolean result = courseService.isCourseNameDuplicated(newName); + + // then + assertThat(result).isFalse(); + verify(courseRepository).existsByName(newName); + } + + // TODO 공백 처리에 대한 결정에 따라 수정 필요함 +// @Test +// @DisplayName("성공 - 앞뒤 공백이 포함된 코스명이 존재할 경우, 공백을 제거하고 비교하여 true 반환") +// void returnsTrue_whenTrimmedNameExists() { +// // given +// String nameWithSpaces = " 부산-바다 "; +// String trimmedName = "부산 바다 코스"; +// when(courseRepository.existsByName(trimmedName)).thenReturn(true); +// +// // when +// boolean result = courseService.isCourseNameDuplicated(nameWithSpaces); +// +// // then +// assertThat(result).isTrue(); +// // .trim()으로 정제된 이름으로 repository 메소드가 호출되었는지 검증 +// verify(courseRepository).existsByName(trimmedName); +// } + } + @Nested @DisplayName("내 코스 전체 조회 테스트") class GetMyAllCoursesTest { From ab94a39a2eb324742f27b082eb2bfc3fb9669dc4 Mon Sep 17 00:00:00 2001 From: ssggii Date: Tue, 16 Sep 2025 23:39:44 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[SCRUM-295]=20FIX:=20=EC=BD=94=EC=8A=A4?= =?UTF-8?q?=EB=AA=85=20=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=82=AC=20=EC=8B=9C?= =?UTF-8?q?=20=EA=B3=B5=EB=B0=B1=20=EB=AC=B4=EC=8B=9C=20(#139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코스명에 포함된 공백은 무시하고 공백 외의 문자로만 중복 여부를 판단하도록 코스명 중복 검사 로직을 수정했습니다. --- .../domain/course/service/CourseService.java | 2 +- .../course/service/CourseServiceTest.java | 49 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java index 32eaf4c..cd7cb02 100644 --- a/src/main/java/com/server/running_handai/domain/course/service/CourseService.java +++ b/src/main/java/com/server/running_handai/domain/course/service/CourseService.java @@ -354,7 +354,7 @@ public boolean isCourseNameDuplicated(String newCourseName) { if (newCourseName == null || newCourseName.isBlank()) { throw new BusinessException(INVALID_COURSE_NAME_PARAMETER); } - return courseRepository.existsByName(newCourseName.trim()); + return courseRepository.existsByName(newCourseName.replaceAll("\\s+", "")); } /** diff --git a/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java b/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java index 5713c3d..a489b2d 100644 --- a/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java +++ b/src/test/java/com/server/running_handai/domain/course/service/CourseServiceTest.java @@ -5,6 +5,7 @@ import static com.server.running_handai.domain.course.service.CourseService.MYSQL_POINT_FORMAT; import static com.server.running_handai.global.response.ResponseCode.COURSE_NOT_FOUND; import static com.server.running_handai.global.response.ResponseCode.DUPLICATE_COURSE_NAME; +import static com.server.running_handai.global.response.ResponseCode.INVALID_COURSE_NAME_PARAMETER; import static com.server.running_handai.global.response.ResponseCode.MEMBER_NOT_FOUND; import static com.server.running_handai.global.response.ResponseCode.NO_AUTHORITY_TO_DELETE_COURSE; import static org.assertj.core.api.Assertions.assertThat; @@ -836,23 +837,37 @@ void returnsFalse_whenNameDoesNotExist() { verify(courseRepository).existsByName(newName); } - // TODO 공백 처리에 대한 결정에 따라 수정 필요함 -// @Test -// @DisplayName("성공 - 앞뒤 공백이 포함된 코스명이 존재할 경우, 공백을 제거하고 비교하여 true 반환") -// void returnsTrue_whenTrimmedNameExists() { -// // given -// String nameWithSpaces = " 부산-바다 "; -// String trimmedName = "부산 바다 코스"; -// when(courseRepository.existsByName(trimmedName)).thenReturn(true); -// -// // when -// boolean result = courseService.isCourseNameDuplicated(nameWithSpaces); -// -// // then -// assertThat(result).isTrue(); -// // .trim()으로 정제된 이름으로 repository 메소드가 호출되었는지 검증 -// verify(courseRepository).existsByName(trimmedName); -// } + @Test + @DisplayName("성공 - 공백만 차이가 있는 코스명일 경우 true 반환") + void returnsTrue_whenTrimmedNameExists() { + // given + String nameWithSpaces = " 부 산-바 다 "; + String trimmedName = "부산-바다"; + when(courseRepository.existsByName(trimmedName)).thenReturn(true); + + // when + boolean result = courseService.isCourseNameDuplicated(nameWithSpaces); + + // then + assertThat(result).isTrue(); + verify(courseRepository).existsByName(trimmedName); + } + + @Test + @DisplayName("실패 - 코스명이 null이거나 blank인 경우 예외 발생") + void returnsFalse_whenTrimmedNameIsNull() { + // given + String[] courseNames = {null, "", " "}; + + // when, then + for (String name : courseNames) { + BusinessException exception = assertThrows(BusinessException.class, + () -> courseService.isCourseNameDuplicated(name)); + assertThat(exception.getResponseCode()).isEqualTo(INVALID_COURSE_NAME_PARAMETER); + verify(courseRepository, never()).existsByName(any()); + } + } + } @Nested