Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -200,6 +197,22 @@ public ResponseEntity<CommonResponse<Boolean>> isBusanCourse(
return ResponseEntity.ok(CommonResponse.success(SUCCESS, isBusanCourse));
}

@Operation(summary = "코스명 중복 검사", description = "이미 존재하는 코스명인지 검사합니다."
+ "<br>코스명이 중복되는 경우 true, 중복되지 않는 경우 false를 반환합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "400", description = "요청 파라미터 오류")
})
@GetMapping("/api/courses/name/exists")
public ResponseEntity<CommonResponse<Boolean>> 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 = "성공"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -324,24 +325,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;
}
Expand All @@ -352,6 +344,19 @@ private void publishCourseCreatedEvent(Long courseId, boolean isInsideBusan) {
eventPublisher.publishEvent(event);
}

/**
* 코스명 중복 여부를 검사합니다.
*
* @param newCourseName 사용자가 요청한 코스명
* @return 코스명이 중복되면 true, 중복되지 않으면 false
*/
public boolean isCourseNameDuplicated(String newCourseName) {
if (newCourseName == null || newCourseName.isBlank()) {
throw new BusinessException(INVALID_COURSE_NAME_PARAMETER);
}
return courseRepository.existsByName(newCourseName.replaceAll("\\s+", ""));
}

/**
* 회원이 생성한 코스를 삭제합니다.
* 코스에 저장된 즐길거리가 있는 경우 함께 삭제합니다.
Expand Down Expand Up @@ -412,7 +417,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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "유효하지 않은 액세스 토큰입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -760,7 +761,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);

Expand All @@ -781,35 +781,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
Expand All @@ -822,6 +803,73 @@ 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);
}

@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
@DisplayName("내 코스 전체 조회 테스트")
class GetMyAllCoursesTest {
Expand Down