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
@@ -1,20 +1,32 @@
package com.server.running_handai.domain.course.dto;

import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.review.dto.ReviewInfoDto;
import com.server.running_handai.domain.review.dto.ReviewInfoListDto;
import com.server.running_handai.domain.spot.dto.SpotInfoDto;

import java.util.List;

public record CourseSummaryDto(
double distance,
int duration,
double maxElevation,
ReviewInfoListDto reviewInfoListDto //TODO 즐길거리 dto 추가
int reviewCount,
double starAverage,
List<ReviewInfoDto> reviews,
List<SpotInfoDto> spots
) {
public static CourseSummaryDto from(Course course, ReviewInfoListDto reviewInfoListDto) {
public static CourseSummaryDto from(Course course, int reviewCount, double starAverage,
List<ReviewInfoDto> reviewInfoDtos, List<SpotInfoDto> spotInfoDtos) {

return new CourseSummaryDto(
course.getDistance(),
course.getDuration(),
course.getMaxElevation(),
reviewInfoListDto
reviewCount,
starAverage,
reviewInfoDtos,
spotInfoDtos
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
import com.server.running_handai.domain.course.repository.CourseRepository;
import com.server.running_handai.domain.course.repository.TrackPointRepository;
import com.server.running_handai.domain.review.dto.ReviewInfoDto;
import com.server.running_handai.domain.review.dto.ReviewInfoListDto;
import com.server.running_handai.domain.review.repository.ReviewRepository;
import com.server.running_handai.domain.review.service.ReviewService;
import com.server.running_handai.domain.spot.dto.SpotInfoDto;
import com.server.running_handai.domain.spot.repository.SpotRepository;
import com.server.running_handai.global.response.exception.BusinessException;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -50,9 +51,9 @@ public class CourseService {
private final TrackPointRepository trackPointRepository;
private final BookmarkRepository bookmarkRepository;
private final GeometryFactory geometryFactory;
private final SpotRepository spotRepository;
private final ReviewRepository reviewRepository;
private final ReviewService reviewService;
private final CourseDataService courseDataService;

@Value("${course.simplification.distance-tolerance}")
private double distanceTolerance;
Expand Down Expand Up @@ -210,12 +211,13 @@ public CourseSummaryDto getCourseSummary(Long courseId, Long memberId) {

// 리뷰 조회
List<ReviewInfoDto> reviewInfoDtos = reviewService.convertToReviewInfoDtos(
reviewRepository.findRandom2ByCourseId(courseId), memberId);
double averageStars = reviewService.calculateAverageStars(courseId);
ReviewInfoListDto reviewInfoListDto = ReviewInfoListDto.from(averageStars, reviewInfoDtos);
reviewRepository.findRecent2ByCourseId(courseId), memberId);
int reviewCount = (int) reviewRepository.countByCourseId(courseId); // 리뷰 전체 개수
double starAverage = reviewService.calculateAverageStars(courseId); // 리뷰 전체 평점

// TODO 즐길거리 조회
// 즐길거리 조회
List<SpotInfoDto> spotInfoDtos = spotRepository.findRandom3ByCourseId(course.getId());

return CourseSummaryDto.from(course, reviewInfoListDto);
return CourseSummaryDto.from(course, reviewCount, starAverage, reviewInfoDtos, spotInfoDtos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public record ReviewInfoListDto(
int reviewCount,
List<ReviewInfoDto> reviewInfoDtos
) {
public static ReviewInfoListDto from(double starAverage, List<ReviewInfoDto> reviewInfoDtos) {
return new ReviewInfoListDto(starAverage, reviewInfoDtos.size(), reviewInfoDtos);
public static ReviewInfoListDto from(double starAverage, int reviewCount, List<ReviewInfoDto> reviewInfoDtos) {
return new ReviewInfoListDto(starAverage, reviewCount, reviewInfoDtos);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ public interface ReviewRepository extends JpaRepository<Review, Long> {
boolean existsByIdAndWriterId(Long reviewId, Long writerId);

/**
* courseId로 리뷰를 랜덤으로 2개만 조회
* courseId로 최신순 리뷰 2개 조회
*/
@Query(value = "SELECT * FROM review r WHERE r.course_id = :courseId ORDER BY RAND() LIMIT 2", nativeQuery = true)
List<Review> findRandom2ByCourseId(@Param("courseId") Long courseId);
@Query(value = "SELECT * FROM review r WHERE r.course_id = :courseId ORDER BY created_at DESC LIMIT 2", nativeQuery = true)
List<Review> findRecent2ByCourseId(@Param("courseId") Long courseId);

/**
* courseId에 해당하는 모든 리뷰의 평점(stars) 평균을 계산
*/
@Query("SELECT AVG(r.stars) FROM Review r WHERE r.course.id = :courseId")
Double findAverageStarsByCourseId(@Param("courseId") Long courseId);

/**
* courseId로 조회한 리뷰의 전체 개수 조회
*/
long countByCourseId(Long courseId);

/**
* 특정 회원이 작성한 리뷰 조회 (연관 엔티티 동시 조회)
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public ReviewInfoListDto findAllReviewsByCourse(Long courseId, Long memberId) {

double averageStars = calculateAverageStars(courseId);
List<ReviewInfoDto> reviewInfoDtos = convertToReviewInfoDtos(reviewRepository.findAllByCourseId(courseId), memberId);
return ReviewInfoListDto.from(averageStars, reviewInfoDtos);
return ReviewInfoListDto.from(averageStars, reviewInfoDtos.size(), reviewInfoDtos);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.server.running_handai.domain.spot.controller;

import com.server.running_handai.domain.spot.dto.SpotDetailDto;
import com.server.running_handai.domain.spot.service.SpotService;
import com.server.running_handai.global.oauth.CustomOAuth2User;
import com.server.running_handai.global.response.CommonResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.server.running_handai.global.response.ResponseCode.SUCCESS;

@Slf4j
@RestController
@RequestMapping("/api/courses")
@RequiredArgsConstructor
public class SpotController {
private final SpotService spotService;

@Operation(summary = "즐길거리 전체 조회", description = "특정 코스의 즐길거리 전체 정보를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "404", description = "실패 (존재하지 않는 코스)"),
})
@GetMapping("/{courseId}/spots")
public ResponseEntity<CommonResponse<SpotDetailDto>> getSpotDetails(
@Parameter(description = "조회하려는 코스 ID", required = true) @PathVariable("courseId") Long courseId,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
Long memberId = (customOAuth2User != null) ? customOAuth2User.getMember().getId() : null;
log.info("[즐길거리 전체 조회] courseId: {}, memberId: {}", courseId, memberId);
SpotDetailDto spotDetailDto = spotService.getSpotDetails(courseId);
return ResponseEntity.ok(CommonResponse.success(SUCCESS, spotDetailDto));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.server.running_handai.domain.spot.dto;

import java.util.List;

public record SpotDetailDto (
long courseId,
int spotCount,
List<SpotInfoDto> spots
) {
public static SpotDetailDto from(long courseId, List<SpotInfoDto> spots) {
return new SpotDetailDto(
courseId,
spots.size(),
spots
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.server.running_handai.domain.spot.dto;

import com.server.running_handai.domain.spot.entity.Spot;

public record SpotInfoDto(
long spotId,
String name,
String description,
String imageUrl
) {
public static SpotInfoDto from(Spot spot) {
return new SpotInfoDto(
spot.getId(),
spot.getName(),
spot.getDescription(),
spot.getSpotImage() != null ? spot.getSpotImage().getImgUrl() : null
);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,43 @@
package com.server.running_handai.domain.spot.repository;

import com.server.running_handai.domain.spot.dto.SpotInfoDto;
import com.server.running_handai.domain.spot.entity.Spot;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.Collection;
import java.util.List;

public interface SpotRepository extends JpaRepository<Spot, Long> {
/**
* DB에 동일한 ExternalId를 가진 Spot이 있다면 해당 Spot을 가져옵니다.
*/
List<Spot> findByExternalIdIn(Collection<String> externalIds);

/**
* CourseId와 일치하는 Spot을 SpotImage와 함께 가져옵니다.
*/
@Query("SELECT s " +
"FROM Spot s " +
"LEFT JOIN FETCH s.spotImage " +
"JOIN CourseSpot cs ON cs.spot = s " +
"WHERE cs.course.id = :courseId")
List<Spot> findByCourseIdWithSpotImage(@Param("courseId") Long courseId);

/**
* CourseId와 일치하는 Spot을 SpotImage와 함께 랜덤으로 3개 가져옵니다.
*/
@Query(value = "SELECT " +
" s.spot_id AS spotId, " +
" s.name, " +
" s.description, " +
" si.img_url As imageUrl " +
"FROM spot s " +
"LEFT JOIN spot_image si ON s.spot_id = si.spot_id " +
"JOIN course_spot cs ON cs.spot_id = s.spot_id " +
"WHERE cs.course_id = :courseId " +
"ORDER BY RAND() LIMIT 3",
nativeQuery = true)
List<SpotInfoDto> findRandom3ByCourseId(@Param("courseId") Long courseId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.server.running_handai.domain.spot.service;

import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.course.repository.CourseRepository;
import com.server.running_handai.domain.spot.dto.SpotDetailDto;
import com.server.running_handai.domain.spot.dto.SpotInfoDto;
import com.server.running_handai.domain.spot.entity.Spot;
import com.server.running_handai.domain.spot.repository.SpotRepository;
import com.server.running_handai.global.response.ResponseCode;
import com.server.running_handai.global.response.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SpotService {
private final CourseRepository courseRepository;
private final SpotRepository spotRepository;

/**
* 코스에 해당되는 즐길거리를 전체 조회합니다.
*
* @param courseId 조회하려는 코스의 ID
* @return 조회된 즐길거리 목록 DTO
*/
public SpotDetailDto getSpotDetails(Long courseId) {
if (!courseRepository.existsById(courseId)) {
throw new BusinessException(ResponseCode.COURSE_NOT_FOUND);
}
List<Spot> spots = spotRepository.findByCourseIdWithSpotImage(courseId);

List<SpotInfoDto> spotInfoDtos = spots.stream()
.map(SpotInfoDto::from)
.toList();

return SpotDetailDto.from(courseId, spotInfoDtos);
}
}
3 changes: 2 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ spring:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 5MB

server:
port: ${PORT:8080}
Expand Down Expand Up @@ -132,4 +133,4 @@ app:
swagger:
server:
local: http://localhost:8080
prod: https://api.runninghandai.com
prod: https://api.runninghandai.com
Loading