diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/command/controller/RestaurantController.java b/src/main/java/com/gdsc/jmt/domain/restaurant/command/controller/RestaurantController.java index ac08a53..fcbee7f 100644 --- a/src/main/java/com/gdsc/jmt/domain/restaurant/command/controller/RestaurantController.java +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/command/controller/RestaurantController.java @@ -3,6 +3,7 @@ import com.gdsc.jmt.domain.restaurant.command.controller.springdocs.CreateRecommendRestaurantSpringDocs; import com.gdsc.jmt.domain.restaurant.command.controller.springdocs.CreateRestaurantLocationSpringDocs; import com.gdsc.jmt.domain.restaurant.command.dto.request.CreateRecommendRestaurantRequestFromClient; +import com.gdsc.jmt.domain.restaurant.command.dto.request.CreateRestaurantReviewRequest; import com.gdsc.jmt.domain.restaurant.command.dto.request.ReportRecommendRestaurantRequest; import com.gdsc.jmt.domain.restaurant.command.dto.request.UpdateRecommendRestaurantRequest; import com.gdsc.jmt.domain.restaurant.command.dto.response.CreatedRestaurantResponse; @@ -62,4 +63,15 @@ public JMTApiResponse reportRecommendRestaurant(@PathVariable Long id, @Authe restaurantService.reportRecommendRestaurant(id, reporter, request); return JMTApiResponse.createResponseWithMessage(null, RestaurantMessage.RECOMMEND_RESTAURANT_REPORTED); } + + + @PostMapping(value = "/restaurant/{recommendRestaurantId}/review", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @Operation(summary = "맛집 후기 작성 API", description = "맛집 후기 작성") + @ResponseStatus(HttpStatus.CREATED) + public JMTApiResponse restaurantReview(@PathVariable Long recommendRestaurantId, + @AuthenticationPrincipal UserInfo user, + @ModelAttribute CreateRestaurantReviewRequest request) { + restaurantService.createRestaurantReview(recommendRestaurantId, user, request); + return JMTApiResponse.createResponseWithMessage(null, RestaurantMessage.RESTAURANT_REVIEW_CREATED); + } } diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/command/dto/request/CreateRestaurantReviewRequest.java b/src/main/java/com/gdsc/jmt/domain/restaurant/command/dto/request/CreateRestaurantReviewRequest.java new file mode 100644 index 0000000..b24fa5b --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/command/dto/request/CreateRestaurantReviewRequest.java @@ -0,0 +1,13 @@ +package com.gdsc.jmt.domain.restaurant.command.dto.request; + +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +@Data +public class CreateRestaurantReviewRequest { + private String reviewContent; + + private List reviewImages; +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/command/service/RestaurantService.java b/src/main/java/com/gdsc/jmt/domain/restaurant/command/service/RestaurantService.java index 2f60969..1909c89 100644 --- a/src/main/java/com/gdsc/jmt/domain/restaurant/command/service/RestaurantService.java +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/command/service/RestaurantService.java @@ -4,14 +4,10 @@ import com.gdsc.jmt.domain.category.query.repository.CategoryRepository; import com.gdsc.jmt.domain.group.entity.GroupEntity; import com.gdsc.jmt.domain.group.repository.GroupRepository; -import com.gdsc.jmt.domain.restaurant.command.dto.request.CreateRecommendRestaurantRequest; -import com.gdsc.jmt.domain.restaurant.command.dto.request.CreateRecommendRestaurantRequestFromClient; -import com.gdsc.jmt.domain.restaurant.command.dto.request.ReportRecommendRestaurantRequest; -import com.gdsc.jmt.domain.restaurant.command.dto.request.UpdateRecommendRestaurantRequest; +import com.gdsc.jmt.domain.restaurant.command.dto.request.*; import com.gdsc.jmt.domain.restaurant.command.dto.response.CreatedRestaurantResponse; import com.gdsc.jmt.domain.restaurant.query.entity.*; import com.gdsc.jmt.domain.restaurant.query.repository.*; -import com.gdsc.jmt.domain.restaurant.util.KakaoSearchDocument; import com.gdsc.jmt.domain.restaurant.util.KakaoSearchDocumentRequest; import com.gdsc.jmt.domain.user.query.entity.UserEntity; import com.gdsc.jmt.domain.user.query.repository.UserRepository; @@ -41,6 +37,9 @@ public class RestaurantService { private final ReportReasonRepository reportReasonRepository; private final GroupRepository groupRepository; + private final RestaurantReviewRepository restaurantReviewRepository; + private final RestaurantReviewPhotoRepository restaurantReviewPhotoRepository; + private final S3FileService s3FileService; @Transactional @@ -160,6 +159,13 @@ private RestaurantEntity validateRestaurant(final Long restaurantLocationId) { return restaurant.get(); } + private RecommendRestaurantEntity validateRecommendRestaurant(final Long recommendRestaurantId) { + Optional restaurant = recommendRestaurantRepository.findById(recommendRestaurantId); + if(restaurant.isEmpty()) + throw new ApiException(RestaurantMessage.RECOMMEND_RESTAURANT_NOT_FOUND); + return restaurant.get(); + } + private CategoryEntity validateCategory(final Long categoryId) { Optional category = categoryRepository.findById(categoryId); if(category.isEmpty()) @@ -176,7 +182,7 @@ private void validateConflict(RestaurantEntity restaurant) { private UserEntity validateUser(String email) { Optional result = userRepository.findByEmail(email); if(result.isEmpty()) - throw new ApiException(DefaultMessage.INTERNAL_SERVER_ERROR); + throw new ApiException(UserMessage.USER_NOT_FOUND); return result.get(); } @@ -198,4 +204,37 @@ private RecommendRestaurantEntity validateIsWriteRecommendRestaurantByUser(Long } return recommendRestaurant; } + + @Transactional + public void createRestaurantReview(Long recommendRestaurantId, UserInfo user, CreateRestaurantReviewRequest request) { + UserEntity userEntity = validateUser(user.getEmail()); + validateRecommendRestaurant(recommendRestaurantId); + + RestaurantReviewEntity restaurantReviewEntity = RestaurantReviewEntity + .builder() + .userId(userEntity.getId()) + .recommendRestaurantId(recommendRestaurantId) + .reviewContent(request.getReviewContent()) + .build(); + + restaurantReviewRepository.save(restaurantReviewEntity); + uploadReviewImages(restaurantReviewEntity.getId(), request.getReviewImages()); + } + + @Transactional + private void uploadReviewImages(Long restaurantReviewId, List images) { + for(MultipartFile image : images) { + try { + String imageUrl = s3FileService.upload(image,"restaurantReviewPhoto"); + RestaurantReviewPhotoEntity photoEntity = RestaurantReviewPhotoEntity.builder() + .imageUrl(imageUrl) + .restaurantReviewId(restaurantReviewId) + .build(); + restaurantReviewPhotoRepository.save(photoEntity); + } + catch (IOException e) { + throw new ApiException(RestaurantMessage.RESTAURANT_IMAGE_UPLOAD_FAIL); + } + } + } } diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/controller/RestaurantQueryController.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/controller/RestaurantQueryController.java index 3dceb4c..40e4a6d 100644 --- a/src/main/java/com/gdsc/jmt/domain/restaurant/query/controller/RestaurantQueryController.java +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/controller/RestaurantQueryController.java @@ -1,23 +1,22 @@ package com.gdsc.jmt.domain.restaurant.query.controller; +import com.gdsc.jmt.domain.restaurant.command.dto.request.CreateRestaurantReviewRequest; import com.gdsc.jmt.domain.restaurant.query.controller.springdocs.FindAllRestaurantSpringDocs; import com.gdsc.jmt.domain.restaurant.query.controller.springdocs.FindRestaurantsByUserIdSpringDocs; import com.gdsc.jmt.domain.restaurant.query.dto.request.RestaurantSearchInUserIdRequest; import com.gdsc.jmt.domain.restaurant.query.dto.request.RestaurantSearchRequest; -import com.gdsc.jmt.domain.restaurant.query.dto.response.FindReportReasonResponse; -import com.gdsc.jmt.domain.restaurant.query.dto.response.FindAllRestaurantResponse; +import com.gdsc.jmt.domain.restaurant.query.dto.response.*; import com.gdsc.jmt.domain.restaurant.query.controller.springdocs.CheckRecommendRestaurantExistingSpringDocs; import com.gdsc.jmt.domain.restaurant.query.controller.springdocs.FindRestaurantLocationSpringDocs; import com.gdsc.jmt.domain.restaurant.query.dto.request.FindRestaurantLocationListRequest; import com.gdsc.jmt.domain.restaurant.query.dto.request.RestaurantSearchMapRequest; -import com.gdsc.jmt.domain.restaurant.query.dto.response.FindDetailRestaurantItem; -import com.gdsc.jmt.domain.restaurant.query.dto.response.FindRestaurantResponse; import com.gdsc.jmt.domain.restaurant.query.service.RestaurantFilterService; import com.gdsc.jmt.domain.restaurant.query.service.RestaurantQueryService; import com.gdsc.jmt.domain.restaurant.util.KakaoSearchDocumentResponse; import com.gdsc.jmt.domain.user.command.controller.springdocs.FindDetailRestaurantSpringDocs; import com.gdsc.jmt.global.controller.FirstVersionRestController; import com.gdsc.jmt.global.dto.JMTApiResponse; +import com.gdsc.jmt.global.jwt.dto.UserInfo; import com.gdsc.jmt.global.messege.RestaurantMessage; import io.swagger.v3.oas.annotations.Operation; @@ -29,6 +28,9 @@ import org.springdoc.core.converters.models.PageableAsQueryParam; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -103,4 +105,12 @@ public JMTApiResponse> reportRecommendRestaurant( List reportReasonResponseList = restaurantQueryService.findAllReportReason(); return JMTApiResponse.createResponseWithMessage(reportReasonResponseList, RestaurantMessage.FIND_ALL_REPORT_REASON); } + + @GetMapping(value = "/restaurant/{recommendRestaurantId}/review") + @Operation(summary = "맛집 후기 조회 API", description = "맛집 후기 조회") + public JMTApiResponse restaurantReview(@PathVariable Long recommendRestaurantId, + @PageableDefault @Parameter(hidden = true) Pageable pageable) { + FindRestaurantReviewResponse response = restaurantQueryService.findAllReview(recommendRestaurantId, pageable); + return JMTApiResponse.createResponseWithMessage(response, RestaurantMessage.RESTAURANT_REVIEW_FIND_ALL); + } } diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReview.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReview.java new file mode 100644 index 0000000..e817473 --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReview.java @@ -0,0 +1,25 @@ +package com.gdsc.jmt.domain.restaurant.query.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Data +public class FindRestaurantReview { + + private Long reviewId; + + private Long recommendRestaurantId; + + private String userName; + + private String reviewContent; + + private List reviewImages; +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReviewResponse.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReviewResponse.java new file mode 100644 index 0000000..6ca3dd0 --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/dto/response/FindRestaurantReviewResponse.java @@ -0,0 +1,14 @@ +package com.gdsc.jmt.domain.restaurant.query.dto.response; + +import com.gdsc.jmt.global.dto.PageResponse; +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@AllArgsConstructor +@Data +public class FindRestaurantReviewResponse { + private List reviewList; + private PageResponse page; +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewEntity.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewEntity.java new file mode 100644 index 0000000..83b152d --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewEntity.java @@ -0,0 +1,53 @@ +package com.gdsc.jmt.domain.restaurant.query.entity; + + +import com.gdsc.jmt.domain.restaurant.query.dto.response.FindRestaurantReview; +import com.gdsc.jmt.domain.user.query.entity.UserEntity; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Entity +@Table(name = "tb_restaurant_review") +public class RestaurantReviewEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Getter + private Long id; + + @Column(name = "restaurant_id") + private Long recommendRestaurantId; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "review_content") + private String reviewContent; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "restaurant_review_id") + private List pictures; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", insertable = false, updatable = false) + private UserEntity user; + + public FindRestaurantReview toResponse() { + return FindRestaurantReview.builder() + .reviewId(this.id) + .recommendRestaurantId(this.recommendRestaurantId) + .userName(user.getNickname()) + .reviewContent(this.reviewContent) + .reviewImages(pictures.stream().map(RestaurantReviewPhotoEntity::getImageUrl).toList()) + .build(); + } +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewPhotoEntity.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewPhotoEntity.java new file mode 100644 index 0000000..62e104f --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/entity/RestaurantReviewPhotoEntity.java @@ -0,0 +1,27 @@ +package com.gdsc.jmt.domain.restaurant.query.entity; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@AllArgsConstructor +@Getter +@NoArgsConstructor +@Entity +@Table(name = "tb_restaurant_review_photo") +public class RestaurantReviewPhotoEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "image_url") + private String imageUrl; // 파일 저장 경로 + + @Column(name = "restaurant_review_id") + private Long restaurantReviewId; +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewPhotoRepository.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewPhotoRepository.java new file mode 100644 index 0000000..8df0ced --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewPhotoRepository.java @@ -0,0 +1,7 @@ +package com.gdsc.jmt.domain.restaurant.query.repository; + +import com.gdsc.jmt.domain.restaurant.query.entity.RestaurantReviewPhotoEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface RestaurantReviewPhotoRepository extends JpaRepository { +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewRepository.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewRepository.java new file mode 100644 index 0000000..5a17aa0 --- /dev/null +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/repository/RestaurantReviewRepository.java @@ -0,0 +1,13 @@ +package com.gdsc.jmt.domain.restaurant.query.repository; + +import com.gdsc.jmt.domain.restaurant.query.entity.RestaurantReviewEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface RestaurantReviewRepository extends JpaRepository { + + Page findByRecommendRestaurantId(Long recommendRestaurantId, Pageable pageable); +} diff --git a/src/main/java/com/gdsc/jmt/domain/restaurant/query/service/RestaurantQueryService.java b/src/main/java/com/gdsc/jmt/domain/restaurant/query/service/RestaurantQueryService.java index ae2b73a..c41aeaa 100644 --- a/src/main/java/com/gdsc/jmt/domain/restaurant/query/service/RestaurantQueryService.java +++ b/src/main/java/com/gdsc/jmt/domain/restaurant/query/service/RestaurantQueryService.java @@ -6,10 +6,12 @@ import com.gdsc.jmt.domain.restaurant.query.entity.RecommendRestaurantEntity; import com.gdsc.jmt.domain.restaurant.query.entity.ReportReasonEntity; import com.gdsc.jmt.domain.restaurant.query.entity.RestaurantEntity; +import com.gdsc.jmt.domain.restaurant.query.entity.RestaurantReviewEntity; import com.gdsc.jmt.domain.restaurant.query.entity.calculate.RecommendRestaurantWithDistanceDTO; import com.gdsc.jmt.domain.restaurant.query.repository.RecommendRestaurantRepository; import com.gdsc.jmt.domain.restaurant.query.repository.ReportReasonRepository; import com.gdsc.jmt.domain.restaurant.query.repository.RestaurantRepository; +import com.gdsc.jmt.domain.restaurant.query.repository.RestaurantReviewRepository; import com.gdsc.jmt.domain.restaurant.util.KakaoSearchDocument; import com.gdsc.jmt.domain.restaurant.util.KakaoSearchDocumentResponse; import com.gdsc.jmt.domain.restaurant.util.KakaoSearchResponse; @@ -39,6 +41,8 @@ public class RestaurantQueryService { private final RecommendRestaurantRepository recommendRestaurantRepository; private final ReportReasonRepository reportReasonRepository; + private final RestaurantReviewRepository restaurantReviewRepository; + private final RestaurantFilterService restaurantFilterService; private final RestaurantDynamicSearchService restaurantDynamicSearchService; @@ -144,4 +148,14 @@ private Page findRecommendRestaurantByUserId String userLocation = "POINT(" + request.userLocation().x() + " " + request.userLocation().y() + ")"; return recommendRestaurantRepository.findByUserId(userId, userLocation, pageable); } + + public FindRestaurantReviewResponse findAllReview(Long recommendRestaurantId, Pageable pageable) { + Page result = restaurantReviewRepository.findByRecommendRestaurantId(recommendRestaurantId, pageable); + + PageResponse pageResponse = new PageResponse(result); + return new FindRestaurantReviewResponse( + result.getContent().stream().map(RestaurantReviewEntity::toResponse).toList(), + pageResponse + ); + } } diff --git a/src/main/java/com/gdsc/jmt/global/messege/RestaurantMessage.java b/src/main/java/com/gdsc/jmt/global/messege/RestaurantMessage.java index dad11a8..995f6ec 100644 --- a/src/main/java/com/gdsc/jmt/global/messege/RestaurantMessage.java +++ b/src/main/java/com/gdsc/jmt/global/messege/RestaurantMessage.java @@ -23,6 +23,9 @@ public enum RestaurantMessage implements ResponseMessage { RESTAURANT_CREATED("맛집이 등록되었습니다." , HttpStatus.CREATED), RESTAURANT_FIND_ALL("맛집 리스트가 조회되었습니다.", HttpStatus.OK), + RESTAURANT_REVIEW_CREATED("맛집 후기가 등록되었습니다.", HttpStatus.CREATED), + RESTAURANT_REVIEW_FIND_ALL("맛집 후기가 조회되었습니다..", HttpStatus.OK), + RESTAURANT_LOCATION_NOT_FOUND("맛집 위치정보가 등록되지 않았습니다." , HttpStatus.NOT_FOUND), RESTAURANT_LOCATION_FIND("맛집 위치 정보를 조회하였습니다." , HttpStatus.OK), RESTAURANT_SEARCH_FIND("맛집 위치 정보를 조회하였습니다." , HttpStatus.OK),