diff --git a/src/main/java/com/semosan/api/common/status/SuccessStatus.java b/src/main/java/com/semosan/api/common/status/SuccessStatus.java index 6040440f..a25334e9 100644 --- a/src/main/java/com/semosan/api/common/status/SuccessStatus.java +++ b/src/main/java/com/semosan/api/common/status/SuccessStatus.java @@ -43,6 +43,7 @@ public enum SuccessStatus implements BaseStatus { MOUNTAIN_MAP_SUCCESS(HttpStatus.OK, "MTN_200_7", "지도 영역 내 산 조회에 성공했습니다."), MOUNTAIN_RECOMMENDATION_SUCCESS(HttpStatus.OK, "MTN_200_8", "레벨 맞춤 산 추천 조회에 성공했습니다."), COURSE_DETAIL_SUCCESS(HttpStatus.OK, "MTN_200_9", "코스 상세 정보 조회에 성공했습니다."), + COURSE_LIKE_TOGGLE_SUCCESS(HttpStatus.OK, "MTN_200_10", "코스 좋아요 처리에 성공했습니다."), /** * Hiking Record diff --git a/src/main/java/com/semosan/api/domain/mountain/controller/CourseController.java b/src/main/java/com/semosan/api/domain/mountain/controller/CourseController.java index 04d19048..108ca7ba 100644 --- a/src/main/java/com/semosan/api/domain/mountain/controller/CourseController.java +++ b/src/main/java/com/semosan/api/domain/mountain/controller/CourseController.java @@ -4,12 +4,15 @@ import com.semosan.api.common.status.SuccessStatus; import com.semosan.api.domain.mountain.controller.docs.CourseControllerDocs; import com.semosan.api.domain.mountain.dto.response.CourseDetailResponse; +import com.semosan.api.domain.mountain.dto.response.CourseLikeToggleResponse; +import com.semosan.api.domain.mountain.service.CourseLikeService; import com.semosan.api.domain.mountain.service.CourseService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -19,6 +22,7 @@ public class CourseController implements CourseControllerDocs { private final CourseService courseService; + private final CourseLikeService courseLikeService; @GetMapping("/{courseId}") @Override @@ -26,7 +30,19 @@ public ResponseEntity> getCourseDetail( @AuthenticationPrincipal Long userId, @PathVariable Long courseId ) { - CourseDetailResponse response = courseService.getCourseDetail(courseId); + CourseDetailResponse response = courseService.getCourseDetail(userId, courseId); return ApiResponse.success(SuccessStatus.COURSE_DETAIL_SUCCESS, response); } + + @PostMapping("/{courseId}/like") + @Override + public ResponseEntity> toggleCourseLike( + @AuthenticationPrincipal Long userId, + @PathVariable Long courseId + ) { + return ApiResponse.success( + SuccessStatus.COURSE_LIKE_TOGGLE_SUCCESS, + courseLikeService.toggleCourseLike(userId, courseId) + ); + } } diff --git a/src/main/java/com/semosan/api/domain/mountain/controller/docs/CourseControllerDocs.java b/src/main/java/com/semosan/api/domain/mountain/controller/docs/CourseControllerDocs.java index e503b0b1..32c03935 100644 --- a/src/main/java/com/semosan/api/domain/mountain/controller/docs/CourseControllerDocs.java +++ b/src/main/java/com/semosan/api/domain/mountain/controller/docs/CourseControllerDocs.java @@ -2,6 +2,7 @@ import com.semosan.api.common.response.ApiResponse; import com.semosan.api.domain.mountain.dto.response.CourseDetailResponse; +import com.semosan.api.domain.mountain.dto.response.CourseLikeToggleResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -36,4 +37,25 @@ ResponseEntity> getCourseDetail( @Parameter(description = "코스 ID", required = true) @PathVariable Long courseId ); + + @Operation( + summary = "코스 좋아요 토글", + description = "로그인한 사용자가 코스 좋아요를 누르거나 취소합니다." + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "코스 좋아요 토글 성공" + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "404", + description = "코스를 찾을 수 없음 (MTN_404_3)", + content = @Content(schema = @Schema(implementation = ApiResponse.class)) + ) + }) + ResponseEntity> toggleCourseLike( + @AuthenticationPrincipal Long userId, + @Parameter(description = "코스 ID", required = true) + @PathVariable Long courseId + ); } diff --git a/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseDetailResponse.java b/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseDetailResponse.java index 169fdfaa..b522c396 100644 --- a/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseDetailResponse.java +++ b/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseDetailResponse.java @@ -12,6 +12,7 @@ */ public record CourseDetailResponse( Long id, + Long mountainId, String name, Difficulty difficulty, Double distance, @@ -21,12 +22,14 @@ public record CourseDetailResponse( Double ascent, Double descent, Double maxAltitude, + boolean likedByMe, @JsonRawValue String polyline, @JsonRawValue String altitudes ) { - public static CourseDetailResponse from(CourseDetailProjection p) { + public static CourseDetailResponse from(CourseDetailProjection p, boolean likedByMe) { return new CourseDetailResponse( p.getId(), + p.getMountainId(), p.getName(), p.getDifficulty() == null ? null : Difficulty.valueOf(p.getDifficulty()), p.getDistance(), @@ -36,6 +39,7 @@ public static CourseDetailResponse from(CourseDetailProjection p) { p.getAscent(), p.getDescent(), p.getMaxAltitude(), + likedByMe, p.getPolyline(), p.getAltitudes() ); diff --git a/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseLikeToggleResponse.java b/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseLikeToggleResponse.java new file mode 100644 index 00000000..8c9f20dd --- /dev/null +++ b/src/main/java/com/semosan/api/domain/mountain/dto/response/CourseLikeToggleResponse.java @@ -0,0 +1,6 @@ +package com.semosan.api.domain.mountain.dto.response; + +public record CourseLikeToggleResponse( + boolean liked +) { +} diff --git a/src/main/java/com/semosan/api/domain/mountain/entity/CourseLike.java b/src/main/java/com/semosan/api/domain/mountain/entity/CourseLike.java new file mode 100644 index 00000000..a0a84618 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/mountain/entity/CourseLike.java @@ -0,0 +1,58 @@ +package com.semosan.api.domain.mountain.entity; + +import com.semosan.api.common.base.BaseEntity; +import com.semosan.api.domain.user.entity.User; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table( + name = "course_likes", + uniqueConstraints = { + @UniqueConstraint( + name = "uk_course_likes_user_id_course_id", + columnNames = {"user_id", "course_id"} + ) + }, + indexes = { + @Index(name = "idx_course_likes_user_id_created_at", columnList = "user_id, created_at DESC") + } +) +@Getter +@Entity +@Builder(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CourseLike extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "course_id", nullable = false) + private Course course; + + public static CourseLike create(User user, Course course) { + return CourseLike.builder() + .user(user) + .course(course) + .build(); + } +} diff --git a/src/main/java/com/semosan/api/domain/mountain/repository/CourseLikeRepository.java b/src/main/java/com/semosan/api/domain/mountain/repository/CourseLikeRepository.java new file mode 100644 index 00000000..f88c1312 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/mountain/repository/CourseLikeRepository.java @@ -0,0 +1,20 @@ +package com.semosan.api.domain.mountain.repository; + +import com.semosan.api.domain.mountain.entity.CourseLike; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; + +public interface CourseLikeRepository extends JpaRepository { + + boolean existsByUser_IdAndCourse_Id(Long userId, Long courseId); + + Optional findByUser_IdAndCourse_Id(Long userId, Long courseId); + + @Modifying(clearAutomatically = true) + @Query("DELETE FROM CourseLike cl WHERE cl.user.id = :userId") + void deleteByUser_Id(@Param("userId") Long userId); +} diff --git a/src/main/java/com/semosan/api/domain/mountain/repository/CourseRepository.java b/src/main/java/com/semosan/api/domain/mountain/repository/CourseRepository.java index 2d5bf7b0..d6248488 100644 --- a/src/main/java/com/semosan/api/domain/mountain/repository/CourseRepository.java +++ b/src/main/java/com/semosan/api/domain/mountain/repository/CourseRepository.java @@ -29,6 +29,7 @@ public interface CourseRepository extends JpaRepository { value = """ SELECT c.id AS id, + c.mountain_id AS mountainId, c.name AS name, c.difficulty AS difficulty, c.distance AS distance, diff --git a/src/main/java/com/semosan/api/domain/mountain/repository/projection/CourseDetailProjection.java b/src/main/java/com/semosan/api/domain/mountain/repository/projection/CourseDetailProjection.java index 1b4b4fa9..1a1edecf 100644 --- a/src/main/java/com/semosan/api/domain/mountain/repository/projection/CourseDetailProjection.java +++ b/src/main/java/com/semosan/api/domain/mountain/repository/projection/CourseDetailProjection.java @@ -7,6 +7,7 @@ */ public interface CourseDetailProjection { Long getId(); + Long getMountainId(); String getName(); String getDifficulty(); Double getDistance(); diff --git a/src/main/java/com/semosan/api/domain/mountain/service/CourseLikeService.java b/src/main/java/com/semosan/api/domain/mountain/service/CourseLikeService.java new file mode 100644 index 00000000..f33ae4b9 --- /dev/null +++ b/src/main/java/com/semosan/api/domain/mountain/service/CourseLikeService.java @@ -0,0 +1,59 @@ +package com.semosan.api.domain.mountain.service; + +import com.semosan.api.common.exception.GeneralException; +import com.semosan.api.common.status.ErrorStatus; +import com.semosan.api.domain.mountain.dto.response.CourseLikeToggleResponse; +import com.semosan.api.domain.mountain.entity.Course; +import com.semosan.api.domain.mountain.entity.CourseLike; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; +import com.semosan.api.domain.mountain.repository.CourseRepository; +import com.semosan.api.domain.user.entity.User; +import com.semosan.api.domain.user.service.UserReader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CourseLikeService { + + private final CourseLikeRepository courseLikeRepository; + private final CourseRepository courseRepository; + private final UserReader userReader; + + @Transactional(noRollbackFor = DataIntegrityViolationException.class) + public CourseLikeToggleResponse toggleCourseLike(Long userId, Long courseId) { + boolean liked = toggle(userId, courseId); + return new CourseLikeToggleResponse(liked); + } + + private boolean toggle(Long userId, Long courseId) { + User user = userReader.findCompletedOnboardingUserById(userId); + Course course = findCourseById(courseId); + + return courseLikeRepository.findByUser_IdAndCourse_Id(userId, courseId) + .map(existing -> { + courseLikeRepository.delete(existing); + return false; + }) + .orElseGet(() -> createCourseLike(user, course, userId, courseId)); + } + + private boolean createCourseLike(User user, Course course, Long userId, Long courseId) { + try { + courseLikeRepository.save(CourseLike.create(user, course)); + return true; + } catch (DataIntegrityViolationException e) { + log.warn("CourseLike 동시 요청 감지: courseId={}, userId={}", courseId, userId); + return true; + } + } + + private Course findCourseById(Long courseId) { + return courseRepository.findById(courseId) + .orElseThrow(() -> new GeneralException(ErrorStatus.COURSE_NOT_FOUND)); + } +} diff --git a/src/main/java/com/semosan/api/domain/mountain/service/CourseService.java b/src/main/java/com/semosan/api/domain/mountain/service/CourseService.java index fe846ef2..bd4e8973 100644 --- a/src/main/java/com/semosan/api/domain/mountain/service/CourseService.java +++ b/src/main/java/com/semosan/api/domain/mountain/service/CourseService.java @@ -3,7 +3,9 @@ import com.semosan.api.common.exception.GeneralException; import com.semosan.api.common.status.ErrorStatus; import com.semosan.api.domain.mountain.dto.response.CourseDetailResponse; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; import com.semosan.api.domain.mountain.repository.CourseRepository; +import com.semosan.api.domain.mountain.repository.projection.CourseDetailProjection; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -14,10 +16,12 @@ public class CourseService { private final CourseRepository courseRepository; + private final CourseLikeRepository courseLikeRepository; - public CourseDetailResponse getCourseDetail(Long courseId) { - return courseRepository.findCourseDetailById(courseId) - .map(CourseDetailResponse::from) + public CourseDetailResponse getCourseDetail(Long userId, Long courseId) { + CourseDetailProjection course = courseRepository.findCourseDetailById(courseId) .orElseThrow(() -> new GeneralException(ErrorStatus.COURSE_NOT_FOUND)); + boolean likedByMe = userId != null && courseLikeRepository.existsByUser_IdAndCourse_Id(userId, courseId); + return CourseDetailResponse.from(course, likedByMe); } } diff --git a/src/main/java/com/semosan/api/domain/user/service/UserService.java b/src/main/java/com/semosan/api/domain/user/service/UserService.java index e3a60c9d..a5387c7e 100644 --- a/src/main/java/com/semosan/api/domain/user/service/UserService.java +++ b/src/main/java/com/semosan/api/domain/user/service/UserService.java @@ -4,6 +4,7 @@ import com.semosan.api.common.status.ErrorStatus; import com.semosan.api.domain.hiking.repository.HikingMemberRepository; import com.semosan.api.domain.hiking.repository.HikingRecordRepository; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; import com.semosan.api.domain.mountain.repository.MountainLikeRepository; import com.semosan.api.domain.notification.repository.NotificationRepository; import com.semosan.api.domain.review.repository.ReviewRepository; @@ -33,6 +34,7 @@ public class UserService { private final UserNotificationSettingRepository userNotificationSettingRepository; private final UserOnboardingRepository userOnboardingRepository; private final MountainLikeRepository mountainLikeRepository; + private final CourseLikeRepository courseLikeRepository; private final ReviewRepository reviewRepository; private final HikingMemberRepository hikingMemberRepository; private final HikingRecordRepository hikingRecordRepository; @@ -143,6 +145,7 @@ public void withdrawUser(User user) { private void deleteUserChildRecords(Long userId) { mountainLikeRepository.deleteByUser_Id(userId); + courseLikeRepository.deleteByUser_Id(userId); reviewRepository.deleteByUser_Id(userId); List recordIdsToDelete = hikingRecordRepository.findRecordIdsOnlyParticipatedByUser(userId); hikingMemberRepository.deleteByUser_Id(userId); diff --git a/src/main/resources/db/migration/V26__create_course_likes.sql b/src/main/resources/db/migration/V26__create_course_likes.sql new file mode 100644 index 00000000..015b557f --- /dev/null +++ b/src/main/resources/db/migration/V26__create_course_likes.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS course_likes +( + id bigint generated by default as identity + primary key, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + user_id bigint not null + constraint fk_course_likes_user + references users, + course_id bigint not null + constraint fk_course_likes_course + references courses, + constraint uk_course_likes_user_id_course_id + unique (user_id, course_id) +); + +CREATE INDEX IF NOT EXISTS idx_course_likes_user_id_created_at + on course_likes (user_id asc, created_at desc); diff --git a/src/test/java/com/semosan/api/domain/mountain/service/CourseLikeServiceTest.java b/src/test/java/com/semosan/api/domain/mountain/service/CourseLikeServiceTest.java new file mode 100644 index 00000000..7c0b4f61 --- /dev/null +++ b/src/test/java/com/semosan/api/domain/mountain/service/CourseLikeServiceTest.java @@ -0,0 +1,101 @@ +package com.semosan.api.domain.mountain.service; + +import com.semosan.api.domain.mountain.dto.response.CourseLikeToggleResponse; +import com.semosan.api.domain.mountain.entity.Course; +import com.semosan.api.domain.mountain.entity.CourseLike; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; +import com.semosan.api.domain.mountain.repository.CourseRepository; +import com.semosan.api.domain.user.entity.User; +import com.semosan.api.domain.user.enums.user.DeviceType; +import com.semosan.api.domain.user.service.UserReader; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.util.ReflectionTestUtils; + +import java.lang.reflect.Constructor; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CourseLikeServiceTest { + + @Mock + private CourseLikeRepository courseLikeRepository; + + @Mock + private CourseRepository courseRepository; + + @Mock + private UserReader userReader; + + @InjectMocks + private CourseLikeService courseLikeService; + + @Test + void toggleCourseLikeCreatesLikeWhenNotLiked() throws Exception { + User user = user(1L); + Course course = course(10L); + + when(userReader.findCompletedOnboardingUserById(1L)).thenReturn(user); + when(courseRepository.findById(10L)).thenReturn(Optional.of(course)); + when(courseLikeRepository.findByUser_IdAndCourse_Id(1L, 10L)).thenReturn(Optional.empty()); + + CourseLikeToggleResponse response = courseLikeService.toggleCourseLike(1L, 10L); + + assertThat(response.liked()).isTrue(); + verify(courseLikeRepository).save(any(CourseLike.class)); + } + + @Test + void toggleCourseLikeDeletesLikeWhenAlreadyLiked() throws Exception { + User user = user(1L); + Course course = course(10L); + CourseLike courseLike = CourseLike.create(user, course); + + when(userReader.findCompletedOnboardingUserById(1L)).thenReturn(user); + when(courseRepository.findById(10L)).thenReturn(Optional.of(course)); + when(courseLikeRepository.findByUser_IdAndCourse_Id(1L, 10L)).thenReturn(Optional.of(courseLike)); + + CourseLikeToggleResponse response = courseLikeService.toggleCourseLike(1L, 10L); + + assertThat(response.liked()).isFalse(); + verify(courseLikeRepository).delete(courseLike); + } + + @Test + void toggleCourseLikeReturnsLikedWhenConcurrentDuplicateDetected() throws Exception { + User user = user(1L); + Course course = course(10L); + + when(userReader.findCompletedOnboardingUserById(1L)).thenReturn(user); + when(courseRepository.findById(10L)).thenReturn(Optional.of(course)); + when(courseLikeRepository.findByUser_IdAndCourse_Id(1L, 10L)).thenReturn(Optional.empty()); + when(courseLikeRepository.save(any(CourseLike.class))).thenThrow(new DataIntegrityViolationException("duplicate")); + + CourseLikeToggleResponse response = courseLikeService.toggleCourseLike(1L, 10L); + + assertThat(response.liked()).isTrue(); + } + + private User user(Long id) { + User user = User.createTestUser("user-" + id, DeviceType.IOS); + ReflectionTestUtils.setField(user, "id", id); + return user; + } + + private Course course(Long id) throws Exception { + Constructor constructor = Course.class.getDeclaredConstructor(); + constructor.setAccessible(true); + Course course = constructor.newInstance(); + ReflectionTestUtils.setField(course, "id", id); + return course; + } +} diff --git a/src/test/java/com/semosan/api/domain/mountain/service/CourseServiceTest.java b/src/test/java/com/semosan/api/domain/mountain/service/CourseServiceTest.java new file mode 100644 index 00000000..d12b6194 --- /dev/null +++ b/src/test/java/com/semosan/api/domain/mountain/service/CourseServiceTest.java @@ -0,0 +1,74 @@ +package com.semosan.api.domain.mountain.service; + +import com.semosan.api.common.exception.GeneralException; +import com.semosan.api.common.status.ErrorStatus; +import com.semosan.api.domain.mountain.dto.response.CourseDetailResponse; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; +import com.semosan.api.domain.mountain.repository.CourseRepository; +import com.semosan.api.domain.mountain.repository.projection.CourseDetailProjection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CourseServiceTest { + + @Mock + private CourseRepository courseRepository; + + @Mock + private CourseLikeRepository courseLikeRepository; + + @Mock + private CourseDetailProjection projection; + + @InjectMocks + private CourseService courseService; + + @Test + void getCourseDetailIncludesLikedByMe() { + when(courseRepository.findCourseDetailById(10L)).thenReturn(Optional.of(projection)); + when(courseLikeRepository.existsByUser_IdAndCourse_Id(1L, 10L)).thenReturn(true); + stubProjection(); + + CourseDetailResponse response = courseService.getCourseDetail(1L, 10L); + + assertThat(response.id()).isEqualTo(10L); + assertThat(response.mountainId()).isEqualTo(100L); + assertThat(response.likedByMe()).isTrue(); + } + + @Test + void getCourseDetailThrowsWhenCourseNotFound() { + when(courseRepository.findCourseDetailById(10L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> courseService.getCourseDetail(1L, 10L)) + .isInstanceOf(GeneralException.class) + .hasMessage(ErrorStatus.COURSE_NOT_FOUND.getMessage()); + } + + private void stubProjection() { + when(projection.getId()).thenReturn(10L); + when(projection.getMountainId()).thenReturn(100L); + when(projection.getName()).thenReturn("가리산 코스 1"); + when(projection.getDifficulty()).thenReturn("EASY"); + when(projection.getDistance()).thenReturn(3300.0); + when(projection.getDuration()).thenReturn(55); + when(projection.getStartName()).thenReturn("가리산자연휴양림"); + when(projection.getEndName()).thenReturn("갈림길"); + when(projection.getAscent()).thenReturn(59.0); + when(projection.getDescent()).thenReturn(593.0); + when(projection.getMaxAltitude()).thenReturn(930.0); + when(projection.getPolyline()).thenReturn(null); + when(projection.getAltitudes()).thenReturn(null); + } + +} diff --git a/src/test/java/com/semosan/api/domain/user/service/UserServiceTest.java b/src/test/java/com/semosan/api/domain/user/service/UserServiceTest.java index df72fc22..a3bbeffc 100644 --- a/src/test/java/com/semosan/api/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/semosan/api/domain/user/service/UserServiceTest.java @@ -2,6 +2,7 @@ import com.semosan.api.domain.hiking.repository.HikingMemberRepository; import com.semosan.api.domain.hiking.repository.HikingRecordRepository; +import com.semosan.api.domain.mountain.repository.CourseLikeRepository; import com.semosan.api.domain.mountain.repository.MountainLikeRepository; import com.semosan.api.domain.notification.repository.NotificationRepository; import com.semosan.api.domain.review.repository.ReviewRepository; @@ -44,6 +45,9 @@ class UserServiceTest { @Mock private MountainLikeRepository mountainLikeRepository; + @Mock + private CourseLikeRepository courseLikeRepository; + @Mock private ReviewRepository reviewRepository;