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,6 +1,8 @@
package com.server.running_handai.domain.bookmark.controller;

import com.server.running_handai.domain.bookmark.dto.BookmarkedCourseInfoDto;
import com.server.running_handai.domain.bookmark.service.BookmarkService;
import com.server.running_handai.domain.course.entity.Area;
import com.server.running_handai.global.oauth.CustomOAuth2User;
import com.server.running_handai.global.response.CommonResponse;
import com.server.running_handai.global.response.ResponseCode;
Expand All @@ -9,20 +11,21 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
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.DeleteMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/courses/{courseId}/bookmarks")
@Tag(name = "Bookmark", description = "북마크 관련 API")
public class BookmarkController {

Expand All @@ -35,7 +38,7 @@ public class BookmarkController {
@ApiResponse(responseCode = "400", description = "실패 (이미 북마크한 코스)"),
@ApiResponse(responseCode = "401", description = "실패 (인증 실패)")
})
@PostMapping
@PostMapping("/api/courses/{courseId}/bookmarks")
public ResponseEntity<CommonResponse<?>> registerBookmark(
@Parameter(description = "북마크 대상 코스", required = true) @PathVariable Long courseId,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
Expand All @@ -54,7 +57,7 @@ public ResponseEntity<CommonResponse<?>> registerBookmark(
@ApiResponse(responseCode = "404", description = "실패 (존재하지 않는 북마크)"),
@ApiResponse(responseCode = "401", description = "실패 (인증 실패)")
})
@DeleteMapping
@DeleteMapping("/api/courses/{courseId}/bookmarks")
public ResponseEntity<CommonResponse<?>> deleteBookmark(
@Parameter(description = "북마크 대상 코스", required = true) @PathVariable Long courseId,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
Expand All @@ -66,4 +69,25 @@ public ResponseEntity<CommonResponse<?>> deleteBookmark(
return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS_BOOKMARK_DELETE, null));
}

@Operation(summary = "북마크한 코스 조회", description = "회원이 북마크한 코스를 조회합니다. 코스의 지역으로 조건 검색이 가능합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "200", description = "성공 (북마크한 코스 없음)"),
@ApiResponse(responseCode = "400", description = "실패 (요청 파라미터 오류)"),
@ApiResponse(responseCode = "401", description = "실패 (인증 실패)")
})
@GetMapping("/api/members/me/courses/bookmarks")
public ResponseEntity<CommonResponse<?>> getBookmarkedCourses(
@Parameter(description = "지역 조건 (전체 보기인 경우 null)")
@RequestParam(required = false) Area area,
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
Long memberId = customOAuth2User.getMember().getId();
log.info("[북마크한 코스 조회] memberId={}, area={}", memberId, (area != null) ? area.name() : null);
List<BookmarkedCourseInfoDto> responseData = bookmarkService.getBookmarkedCoursesByMemberAndArea(memberId, area);
if (responseData.isEmpty()) {
return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS_EMPTY_BOOKMARKS, responseData));
}
return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, responseData));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.server.running_handai.domain.bookmark.dto;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({
"bookmarkId",
"courseId",
"thumbnailUrl",
"distance",
"duration",
"maxElevation",
"isBookmarked",
"bookmarkCount"
})
public interface BookmarkedCourseInfoDto {
long getBookmarkId();
long getCourseId();
String getThumbnailUrl();
double getDistance();
int getDuration();

@JsonIgnore
double getRawMaxElevation(); // JPA 전용

default int getMaxElevation() { // 클라이언트 전용
return (int) getRawMaxElevation();
}

boolean getIsBookmarked();
int getBookmarkCount();
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.server.running_handai.domain.bookmark.repository;

import com.server.running_handai.domain.bookmark.dto.BookmarkedCourseInfoDto;
import com.server.running_handai.domain.bookmark.entity.Bookmark;
import com.server.running_handai.domain.course.dto.BookmarkCountDto;
import com.server.running_handai.domain.course.entity.Area;
import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.member.entity.Member;
import java.util.List;
Expand Down Expand Up @@ -30,8 +32,44 @@ public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {
+ "GROUP BY b.course.id")
List<BookmarkCountDto> countByCourseIdIn(@Param("courseIds") List<Long> courseIds);

// 사용자가 북마크한 코스 ID 목록 조회
// 코스 목록 중에서 사용자가 북마크한 코스 조회
@Query("SELECT b.course.id FROM Bookmark b WHERE b.course.id IN :courseIds AND b.member.id = :memberId")
Set<Long> findBookmarkedCourseIdsByMember(@Param("courseIds") List<Long> courseIds, @Param("memberId") Long memberId);

// 사용자가 북마크한 모든 코스 조회
@Query("SELECT "
+ "b.id AS bookmarkId, "
+ "c.id AS courseId, "
+ "ci.imgUrl AS thumbnailUrl, "
+ "c.distance AS distance, "
+ "c.duration AS duration, "
+ "c.maxElevation AS rawMaxElevation, "
+ "true AS isBookmarked, "
+ "(SELECT count(b2.id) FROM Bookmark b2 WHERE b2.course.id = c.id) AS bookmarkCount " // 총 북마크 수 계산
+ "FROM Bookmark b "
+ "LEFT JOIN b.course c "
+ "LEFT JOIN c.courseImage ci "
+ "WHERE b.member.id = :memberId "
+ "ORDER BY b.createdAt DESC "
)
List<BookmarkedCourseInfoDto> findBookmarkedCoursesByMemberId(Long memberId);

// 사용자가 북마크한 코스 중에서 특정 지역의 코스만 조회
@Query("SELECT "
+ "b.id AS bookmarkId, "
+ "c.id AS courseId, "
+ "ci.imgUrl AS thumbnailUrl, "
+ "c.distance AS distance, "
+ "c.duration AS duration, "
+ "c.maxElevation AS rawMaxElevation, "
+ "true AS isBookmarked, "
+ "(SELECT count(b2.id) FROM Bookmark b2 WHERE b2.course.id = c.id) AS bookmarkCount " // 총 북마크 수 계산
+ "FROM Bookmark b "
+ "LEFT JOIN b.course c "
+ "LEFT JOIN c.courseImage ci "
+ "WHERE b.member.id = :memberId "
+ "AND c.area = :area "
+ "ORDER BY b.createdAt DESC "
)
List<BookmarkedCourseInfoDto> findBookmarkedCoursesByMemberIdAndArea(Long memberId, Area area);
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package com.server.running_handai.domain.bookmark.service;

import com.server.running_handai.domain.bookmark.dto.BookmarkedCourseInfoDto;
import com.server.running_handai.domain.bookmark.entity.Bookmark;
import com.server.running_handai.domain.bookmark.repository.BookmarkRepository;
import com.server.running_handai.domain.course.entity.Area;
import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.course.repository.CourseRepository;
import com.server.running_handai.global.response.ResponseCode;
import com.server.running_handai.global.response.exception.BusinessException;
import com.server.running_handai.domain.member.entity.Member;
import com.server.running_handai.domain.member.repository.MemberRepository;
import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BookmarkService {

private final BookmarkRepository bookmarkRepository;
Expand Down Expand Up @@ -53,4 +58,25 @@ public void deleteBookmark(Long memberId, Long courseId) {
// 북마크 삭제
bookmarkRepository.delete(bookmark);
}

/**
* 회원이 북마크한 코스를 조회합니다.
*
* @param memberId 요청한 회원의 ID
* @return 북마크한 코스 정보 DTO
*/
public List<BookmarkedCourseInfoDto> getBookmarkedCoursesByMemberAndArea(Long memberId, Area area) {
List<BookmarkedCourseInfoDto> bookmarkedCourseInfoDtos;
if (area == null) { // 지역 전체인 경우
bookmarkedCourseInfoDtos = bookmarkRepository.findBookmarkedCoursesByMemberId(memberId);
} else { // 특정 지역 필터링한 경우
bookmarkedCourseInfoDtos = bookmarkRepository.findBookmarkedCoursesByMemberIdAndArea(memberId, area);
}

if (bookmarkedCourseInfoDtos.isEmpty()) {
return Collections.emptyList();
}

return bookmarkedCourseInfoDtos;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.server.running_handai.domain.review.controller;

import com.server.running_handai.domain.review.dto.MyReviewInfoDto;
import com.server.running_handai.domain.review.dto.ReviewCreateResponseDto;
import com.server.running_handai.domain.review.dto.ReviewInfoListDto;
import com.server.running_handai.domain.review.dto.ReviewCreateRequestDto;
Expand All @@ -14,9 +15,9 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
Expand Down Expand Up @@ -104,4 +105,22 @@ public ResponseEntity<CommonResponse<?>> deleteReview(
return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, null));
}

@Operation(summary = "내 리뷰 조회", description = "회원이 작성한 리뷰를 조회합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "200", description = "성공 (리뷰 없음)")
})
@GetMapping("/api/me/reviews")
public ResponseEntity<CommonResponse<List<MyReviewInfoDto>>> getMyReviews(
@AuthenticationPrincipal CustomOAuth2User customOAuth2User
) {
List<MyReviewInfoDto> responseData = reviewService.getMyReviews(customOAuth2User.getMember().getId());

if (responseData.isEmpty()) {
return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS_EMPTY_REVIEWS, responseData));
}

return ResponseEntity.ok(CommonResponse.success(ResponseCode.SUCCESS, responseData));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.server.running_handai.domain.review.dto;

import com.server.running_handai.domain.course.entity.Course;
import com.server.running_handai.domain.review.entity.Review;
import java.time.format.DateTimeFormatter;

public record MyReviewInfoDto(
long reviewId,
long courseId,
String courseName,
String thumbnailUrl,
String area,
double distance,
int duration,
int maxElevation,
double stars,
String contents,
String createdAt
) {
public static MyReviewInfoDto from(Review review) {
Course course = review.getCourse();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formattedCreatedAt = review.getCreatedAt().format(formatter);

return new MyReviewInfoDto(
review.getId(),
course.getId(),
course.getName(),
(course.getCourseImage() != null) ? course.getCourseImage().getImgUrl() : null,
course.getArea().name(),
course.getDistance(),
course.getDuration(),
(int) course.getMaxElevation().doubleValue(),
review.getStars(),
review.getContents(),
formattedCreatedAt
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ public interface ReviewRepository extends JpaRepository<Review, Long> {
*/
@Query("SELECT AVG(r.stars) FROM Review r WHERE r.course.id = :courseId")
Double findAverageStarsByCourseId(@Param("courseId") Long courseId);

/**
* 특정 회원이 작성한 리뷰 조회 (연관 엔티티 동시 조회)
*/
@Query("SELECT r FROM Review r " +
"LEFT JOIN FETCH r.course c " +
"LEFT JOIN FETCH c.courseImage ci " +
"WHERE r.writer.id = :memberId")
List<Review> findReviewsWithDetailsByMemberId(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.server.running_handai.domain.course.repository.CourseRepository;
import com.server.running_handai.domain.member.entity.Member;
import com.server.running_handai.domain.member.repository.MemberRepository;
import com.server.running_handai.domain.review.dto.MyReviewInfoDto;
import com.server.running_handai.domain.review.dto.ReviewCreateResponseDto;
import com.server.running_handai.domain.review.dto.ReviewInfoDto;
import com.server.running_handai.domain.review.dto.ReviewInfoListDto;
Expand All @@ -14,8 +15,10 @@
import com.server.running_handai.domain.review.repository.ReviewRepository;
import com.server.running_handai.global.response.ResponseCode;
import com.server.running_handai.global.response.exception.BusinessException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -163,4 +166,22 @@ public void deleteReview(Long reviewId, Long memberId) {
reviewRepository.delete(review);
}

/**
* 회원이 작성한 리뷰를 조회합니다.
*
* @param memberId 요청한 회원의 ID
* @return 내 리뷰 조회용 DTO
*/
public List<MyReviewInfoDto> getMyReviews(Long memberId) {
List<Review> reviews = reviewRepository.findReviewsWithDetailsByMemberId(memberId);

if (reviews.isEmpty()) {
return Collections.emptyList();
}

return reviews.stream()
.map(MyReviewInfoDto::from)
.collect(Collectors.toList());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public enum ResponseCode {
SUCCESS_BOOKMARK_CREATE(OK, "북마크 등록 완료했습니다."),
SUCCESS_BOOKMARK_DELETE(OK, "북마크 해제 완료했습니다."),
SUCCESS_EMPTY_REVIEWS(OK, "리뷰 조회 결과가 없습니다."),
SUCCESS_EMPTY_BOOKMARKS(OK, "북마크한 코스가 없습니다."),

/** 비즈니스 에러 코드 */
// BAD_REQUEST (400)
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ external:
durunubi:
base-url: http://apis.data.go.kr/B551011/Durunubi
service-key: ${DURUNUBI_SERVICE_KEY}
spot:
base-url: http://apis.data.go.kr/B551011/KorService2
service-key: ${SPOT_SERVICE_KEY}
radius: 50000 # [국문 관광정보] 위치기반 관광정보 조회 API 거리 반경 (50000m = 5km)

springdoc:
default-produces-media-type: application/json
Expand Down
Loading