From fc729dc1dc21ce33bbd0fe87ca48a5e3593a9ecb Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Thu, 8 Jan 2026 01:11:45 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=C3=A3=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EC=A1=B0=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/exception/ErrorCode.java | 4 ++ .../application/service/ReviewService.java | 10 ++++ .../domain/review/domain/entity/Review.java | 52 +++++++++++++++++++ .../review/domain/entity/ReviewImages.java | 36 +++++++++++++ .../repository/ReviewImagesRepository.java | 8 +++ .../domain/repository/ReviewRepository.java | 8 +++ .../review/presentation/ReviewController.java | 22 ++++++++ .../presentation/ReviewResponseCode.java | 4 ++ 8 files changed, 144 insertions(+) create mode 100644 src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/entity/Review.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java create mode 100644 src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java create mode 100644 src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java new file mode 100644 index 00000000..b924a83e --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java @@ -0,0 +1,4 @@ +package com.campus.campus.domain.review.application.exception; + +public enum ErrorCode { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java new file mode 100644 index 00000000..9467e261 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -0,0 +1,10 @@ +package com.campus.campus.domain.review.application.service; + +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +public class ReviewService { +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java new file mode 100644 index 00000000..bfa0b006 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java @@ -0,0 +1,52 @@ +package com.campus.campus.domain.review.domain.entity; + +import com.campus.campus.domain.place.domain.entity.Place; +import com.campus.campus.domain.user.domain.entity.User; +import com.campus.campus.global.entity.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "reviews") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_id") + private Long reviewId; + + private int grade; //별점 + + @Column(name = "positive_points") + private String positivePoints; //좋았던 점 + + @Column(name = "negative_points") + private String negativePoints; //나빴던 점 + + private String keyword; + + @Column(name = "is_verified") + private Boolean isVerified; //영수증 OCR 인증 여부 + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "place_id", nullable = false) + private Place place; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "writer_id", nullable = false) + private User writer; + +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java new file mode 100644 index 00000000..e348490e --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java @@ -0,0 +1,36 @@ +package com.campus.campus.domain.review.domain.entity; + +import com.campus.campus.global.entity.BaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "review_images") +@Getter +@AllArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ReviewImages extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "review_images_id") + private Long ReviewImagesId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + + private String imgUrl; +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java new file mode 100644 index 00000000..365af6d6 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java @@ -0,0 +1,8 @@ +package com.campus.campus.domain.review.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.campus.campus.domain.review.domain.entity.ReviewImages; + +public interface ReviewImagesRepository extends JpaRepository { +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java new file mode 100644 index 00000000..7480dc0c --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -0,0 +1,8 @@ +package com.campus.campus.domain.review.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.campus.campus.domain.review.domain.entity.Review; + +public interface ReviewRepository extends JpaRepository { +} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java new file mode 100644 index 00000000..3b1174f7 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -0,0 +1,22 @@ +package com.campus.campus.domain.review.presentation; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.campus.campus.domain.review.application.service.ReviewService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + // @PostMapping(consumes= MediaType.MULTIPART_FORM_DATA_VALUE) + // @Operation(summary="리뷰 작성") + // public CommonResponse writeReview( + // + // ) +} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java new file mode 100644 index 00000000..587afff8 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java @@ -0,0 +1,4 @@ +package com.campus.campus.domain.review.presentation; + +public enum ReviewResponseCode { +} From 2e3099b68b2d64b3c318919e443d3cef3037122f Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:17:23 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20findOrCreatePlace=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/StudentCouncilPostService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java index 15ffbe11..4f89a8d3 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java @@ -72,7 +72,7 @@ public GetPostResponse create(Long councilId, PostRequest dto) { NormalizedDateTime normalized = dto.category().validateAndNormalize(dto); //Place 객체 생성 - Place place = placeService.findOrCreatePlace(dto); + Place place = placeService.findOrCreatePlace(dto.place()); StudentCouncilPost post = studentCouncilPostMapper.createStudentCouncilPost( writer, place, dto, normalized.startDateTime(), normalized.endDateTime() @@ -214,7 +214,7 @@ public GetPostResponse update(Long councilId, Long postId, PostRequest dto) { Place place = post.getPlace(); if (dto.place() != null && (place == null || !dto.place().placeName().equals(place.getPlaceName()))) { - place = placeService.findOrCreatePlace(dto); + place = placeService.findOrCreatePlace(dto.place()); } post.update( From 93253f55ca810b43a3691a20b7cb4761d403e9fc Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:42:35 +0900 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20api=20=EA=B5=AC=ED=98=84(=EC=98=81=EC=88=98?= =?UTF-8?q?=EC=A6=9D=20ocr=20=EA=B5=AC=ED=98=84=20=EC=A0=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/PlaceService.java | 4 +- .../dto/request/ReviewRequest.java | 47 ++++++++++++++ .../dto/response/ReviewResponse.java | 19 ++++++ .../application/mapper/ReviewMapper.java | 49 +++++++++++++++ .../application/service/ReviewService.java | 61 +++++++++++++++++++ .../domain/review/domain/entity/Review.java | 47 ++++++++++++++ .../review/domain/entity/ReviewImage.java | 34 +++++++++++ .../repository/ReviewImageRepository.java | 13 ++++ .../domain/repository/ReviewRepository.java | 8 +++ .../review/presentation/ReviewController.java | 34 +++++++++++ .../presentation/ReviewResponseCode.java | 19 ++++++ 11 files changed, 332 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/entity/Review.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java create mode 100644 src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java create mode 100644 src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java create mode 100644 src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java diff --git a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java index ef421082..bb3f99a2 100644 --- a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java +++ b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java @@ -14,7 +14,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; import com.campus.campus.domain.place.application.dto.response.LikeResponse; import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; import com.campus.campus.domain.place.application.dto.response.SearchCandidateResponse; @@ -113,8 +112,7 @@ public List search(double lat, double lng, String keyword) { } @Transactional - public Place findOrCreatePlace(PostRequest request) { - SavedPlaceInfo place = request.place(); + public Place findOrCreatePlace(SavedPlaceInfo place) { String placeKey = place.placeKey(); //이미 Place 존재하는지 확인 후 없으면 객체 생성 후 저장 diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java new file mode 100644 index 00000000..569a0d13 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java @@ -0,0 +1,47 @@ +package com.campus.campus.domain.review.application.dto.request; + +import java.util.List; + +import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record ReviewRequest( + + @NotNull + @Size(min = 20, message = "리뷰 내용은 최소 20자 이상이어야 합니다.") + @Schema(example = "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.") + String content, + + @NotNull + @Schema(example = "3.5") + double star, + + List imageUrls, + + @Schema(example = + """ + "placeName": "숙명여자대학교", + "placeKey": "string", + "address": "서울특별시 용산구 청파로47길 99", + "category": "교육,학문>대학교", + "link": "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90+%EC%A0%9C1%EC%BA%A0%ED%8D%BC%EC%8A%A4?c=37.545947,126.964578,15,0,0,0,dh", + "telephone": "010-1234-1234", + "coordinate": { + "latitude": 0.1, + "longitude": 0.1 + }, + "imgUrls": [ + "string" + ] + """, + description = "/search API에서 반환된 결과 중 하나를 선택") + @NotNull + @Valid + SavedPlaceInfo place + +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java new file mode 100644 index 00000000..c0a4a0b1 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java @@ -0,0 +1,19 @@ +package com.campus.campus.domain.review.application.dto.response; + +import java.time.LocalDate; +import java.util.List; + +import lombok.Builder; + +@Builder +public record ReviewResponse( + Long id, + Long userId, + String userName, + LocalDate createDate, + Long placeId, + String content, + double star, + List imageUrls +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java new file mode 100644 index 00000000..36f472f1 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -0,0 +1,49 @@ +package com.campus.campus.domain.review.application.mapper; + +import java.util.Collections; +import java.util.List; + +import org.springframework.stereotype.Component; + +import com.campus.campus.domain.place.domain.entity.Place; +import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.domain.entity.Review; +import com.campus.campus.domain.review.domain.entity.ReviewImage; +import com.campus.campus.domain.user.domain.entity.User; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class ReviewMapper { + + public Review createReview(ReviewRequest request, User user, Place place) { + return Review.builder() + .user(user) + .content(request.content()) + .star(request.star()) + .place(place) + .build(); + } + + public ReviewImage createReviewImage(Review review, String imageUrl) { + return ReviewImage.builder() + .review(review) + .imageUrl(imageUrl) + .build(); + } + + public ReviewResponse toReviewResponse(Review review, List imageUrls, User user) { + ReviewResponse.builder() + .id(review.getId()) + .userId(user.getId()) + .userName(user.getNickname()) + .createDate(review.getCreatedAt().toLocalDate()) + .placeId(review.getPlace().getPlaceId()) + .content(review.getContent()) + .star(review.getStar()) + .imageUrls(imageUrls != null ? imageUrls : Collections.emptyList()) + .build(); + } +} diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java new file mode 100644 index 00000000..038a0cc0 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -0,0 +1,61 @@ +package com.campus.campus.domain.review.application.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.campus.campus.domain.place.application.service.PlaceService; +import com.campus.campus.domain.place.domain.entity.Place; +import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.mapper.ReviewMapper; +import com.campus.campus.domain.review.domain.entity.Review; +import com.campus.campus.domain.review.domain.entity.ReviewImage; +import com.campus.campus.domain.review.domain.repository.ReviewImageRepository; +import com.campus.campus.domain.review.domain.repository.ReviewRepository; +import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; +import com.campus.campus.domain.user.domain.entity.User; +import com.campus.campus.domain.user.domain.repository.UserRepository; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ReviewService { + + private final UserRepository userRepository; + private final ReviewMapper reviewMapper; + private final ReviewRepository reviewRepository; + private final PlaceService placeService; + private final ReviewImageRepository reviewImageRepository; + + @Transactional + public ReviewResponse writeReview(ReviewRequest request, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFirstLoginException::new); + + //place 객체 생성 + Place place = placeService.findOrCreatePlace(request.place()); + + Review review = reviewMapper.createReview(request, user, place); + reviewRepository.save(review); + + if (request.imageUrls() != null) { + for (String imageUrl : request.imageUrls()) { + reviewImageRepository.save(reviewMapper.createReviewImage(review, imageUrl)); + } + } + + List imageUrls = reviewImageRepository + .findAllByReviewOrderbyIdAsc(review) + .stream() + .map(ReviewImage::getImageUrl) + .toList(); + + return reviewMapper.toReviewResponse(review, imageUrls, user); + } + +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java new file mode 100644 index 00000000..e0e005c5 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java @@ -0,0 +1,47 @@ +package com.campus.campus.domain.review.domain.entity; + +import com.campus.campus.domain.place.domain.entity.Place; +import com.campus.campus.domain.user.domain.entity.User; +import com.campus.campus.global.entity.BaseEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "reviews") +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String content; + + private double star; + + //영수증 제휴 인증 여부 + private boolean isVerified; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "place_id", nullable = false) + private Place place; +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java new file mode 100644 index 00000000..46b7713d --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java @@ -0,0 +1,34 @@ +package com.campus.campus.domain.review.domain.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Builder +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class ReviewImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String imageUrl; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java new file mode 100644 index 00000000..de1a1e62 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java @@ -0,0 +1,13 @@ +package com.campus.campus.domain.review.domain.repository; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.campus.campus.domain.review.domain.entity.Review; +import com.campus.campus.domain.review.domain.entity.ReviewImage; + +public interface ReviewImageRepository extends JpaRepository { + + List findAllByReviewOrderbyIdAsc(Review review); +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java new file mode 100644 index 00000000..f30d037f --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -0,0 +1,8 @@ +package com.campus.campus.domain.review.domain.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.campus.campus.domain.review.domain.entity.Review; + +public interface ReviewRepository extends JpaRepository { +} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java new file mode 100644 index 00000000..2727b553 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -0,0 +1,34 @@ +package com.campus.campus.domain.review.presentation; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.service.ReviewService; +import com.campus.campus.global.annotation.CurrentUserId; +import com.campus.campus.global.common.response.CommonResponse; + +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @PostMapping + @Operation(summary = "리뷰 작성") + public CommonResponse writeReview( + @Valid @RequestBody ReviewRequest request, + @CurrentUserId Long userId + ) { + ReviewResponse response = reviewService.writeReview(request, userId); + return CommonResponse.success(ReviewResponseCode.REVIEW_SAVE_SUCCESS, response); + } +} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java new file mode 100644 index 00000000..7491aa18 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java @@ -0,0 +1,19 @@ +package com.campus.campus.domain.review.presentation; + +import org.springframework.http.HttpStatus; + +import com.campus.campus.global.common.response.ResponseCodeInterface; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ReviewResponseCode implements ResponseCodeInterface { + + REVIEW_SAVE_SUCCESS(200, HttpStatus.OK, "리뷰 작성이 완료되었습니다."); + + private final int code; + private final HttpStatus status; + private final String message; +} From 6d03bb11b8eb7ff4da029b505411d7082bc8ee0f Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Sun, 11 Jan 2026 20:15:46 +0900 Subject: [PATCH 04/17] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=A1=B0=ED=9A=8C(=EB=8D=94=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/CursorPageReviewResponse.java | 16 + .../application/exception/ErrorCode.java | 20 ++ .../exception/NotUserWriterException.java | 9 + .../exception/ReviewNotFoundException.java | 9 + .../application/mapper/ReviewMapper.java | 19 +- .../application/service/ReviewService.java | 294 ++++++++++++++---- .../domain/review/domain/entity/Review.java | 10 + .../repository/ReviewImageRepository.java | 8 + .../domain/repository/ReviewRepository.java | 40 ++- .../review/presentation/ReviewController.java | 48 +++ .../presentation/ReviewResponseCode.java | 5 +- 11 files changed, 404 insertions(+), 74 deletions(-) create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java new file mode 100644 index 00000000..1046abbd --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/CursorPageReviewResponse.java @@ -0,0 +1,16 @@ +package com.campus.campus.domain.review.application.dto.response; + +import java.util.List; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class CursorPageReviewResponse { + + private List items; + private String nextCursorCreatedAt; + private Long nextCursorId; + private boolean hasNext; +} diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java new file mode 100644 index 00000000..8132089f --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java @@ -0,0 +1,20 @@ +package com.campus.campus.domain.review.application.exception; + +import org.springframework.http.HttpStatus; + +import com.campus.campus.global.common.exception.ErrorCodeInterface; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ErrorCode implements ErrorCodeInterface { + + REVIEW_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."), + NOT_REVIEW_WRITER(2701, HttpStatus.FORBIDDEN, "작성자만 해당 작업을 수행할 수 있습니다."); + + private final int code; + private final HttpStatus status; + private final String message; +} diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java b/src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java new file mode 100644 index 00000000..bda5d0f1 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/exception/NotUserWriterException.java @@ -0,0 +1,9 @@ +package com.campus.campus.domain.review.application.exception; + +import com.campus.campus.global.common.exception.ApplicationException; + +public class NotUserWriterException extends ApplicationException { + public NotUserWriterException() { + super(ErrorCode.NOT_REVIEW_WRITER); + } +} diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java b/src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java new file mode 100644 index 00000000..cc1c1a03 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ReviewNotFoundException.java @@ -0,0 +1,9 @@ +package com.campus.campus.domain.review.application.exception; + +import com.campus.campus.global.common.exception.ApplicationException; + +public class ReviewNotFoundException extends ApplicationException { + public ReviewNotFoundException() { + super(ErrorCode.REVIEW_NOT_FOUND); + } +} diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java index 36f472f1..cbd32510 100644 --- a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -7,6 +7,7 @@ import com.campus.campus.domain.place.domain.entity.Place; import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; import com.campus.campus.domain.review.domain.entity.Review; import com.campus.campus.domain.review.domain.entity.ReviewImage; @@ -34,11 +35,11 @@ public ReviewImage createReviewImage(Review review, String imageUrl) { .build(); } - public ReviewResponse toReviewResponse(Review review, List imageUrls, User user) { - ReviewResponse.builder() + public ReviewResponse toReviewResponse(Review review, List imageUrls) { + return ReviewResponse.builder() .id(review.getId()) - .userId(user.getId()) - .userName(user.getNickname()) + .userId(review.getUser().getId()) + .userName(review.getUser().getNickname()) .createDate(review.getCreatedAt().toLocalDate()) .placeId(review.getPlace().getPlaceId()) .content(review.getContent()) @@ -46,4 +47,14 @@ public ReviewResponse toReviewResponse(Review review, List imageUrls, Us .imageUrls(imageUrls != null ? imageUrls : Collections.emptyList()) .build(); } + + public CursorPageReviewResponse toCursorReviewResponse(List items, Review last, + boolean hasNext) { + return CursorPageReviewResponse.builder() + .items(items) + .nextCursorCreatedAt(last.getCreatedAt().toString()) + .nextCursorId(last.getId()) + .hasNext(hasNext) + .build(); + } } diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 038a0cc0..5055e96d 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -1,61 +1,233 @@ -package com.campus.campus.domain.review.application.service; - -import java.util.List; - -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.campus.campus.domain.place.application.service.PlaceService; -import com.campus.campus.domain.place.domain.entity.Place; -import com.campus.campus.domain.review.application.dto.request.ReviewRequest; -import com.campus.campus.domain.review.application.dto.response.ReviewResponse; -import com.campus.campus.domain.review.application.mapper.ReviewMapper; -import com.campus.campus.domain.review.domain.entity.Review; -import com.campus.campus.domain.review.domain.entity.ReviewImage; -import com.campus.campus.domain.review.domain.repository.ReviewImageRepository; -import com.campus.campus.domain.review.domain.repository.ReviewRepository; -import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; -import com.campus.campus.domain.user.domain.entity.User; -import com.campus.campus.domain.user.domain.repository.UserRepository; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - -@Service -@Slf4j -@RequiredArgsConstructor -public class ReviewService { - - private final UserRepository userRepository; - private final ReviewMapper reviewMapper; - private final ReviewRepository reviewRepository; - private final PlaceService placeService; - private final ReviewImageRepository reviewImageRepository; - - @Transactional - public ReviewResponse writeReview(ReviewRequest request, Long userId) { - User user = userRepository.findById(userId) - .orElseThrow(UserNotFirstLoginException::new); - - //place 객체 생성 - Place place = placeService.findOrCreatePlace(request.place()); - - Review review = reviewMapper.createReview(request, user, place); - reviewRepository.save(review); - - if (request.imageUrls() != null) { - for (String imageUrl : request.imageUrls()) { - reviewImageRepository.save(reviewMapper.createReviewImage(review, imageUrl)); - } - } - - List imageUrls = reviewImageRepository - .findAllByReviewOrderbyIdAsc(review) - .stream() - .map(ReviewImage::getImageUrl) - .toList(); - - return reviewMapper.toReviewResponse(review, imageUrls, user); - } - -} +package com.campus.campus.domain.review.application.service; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.campus.campus.domain.councilpost.application.exception.PostImageLimitExceededException; +import com.campus.campus.domain.councilpost.application.exception.PostOciImageDeleteFailedException; +import com.campus.campus.domain.place.application.service.PlaceService; +import com.campus.campus.domain.place.domain.entity.Place; +import com.campus.campus.domain.place.domain.repository.PlaceRepository; +import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.exception.NotUserWriterException; +import com.campus.campus.domain.review.application.exception.ReviewNotFoundException; +import com.campus.campus.domain.review.application.mapper.ReviewMapper; +import com.campus.campus.domain.review.domain.entity.Review; +import com.campus.campus.domain.review.domain.entity.ReviewImage; +import com.campus.campus.domain.review.domain.repository.ReviewImageRepository; +import com.campus.campus.domain.review.domain.repository.ReviewRepository; +import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; +import com.campus.campus.domain.user.domain.entity.User; +import com.campus.campus.domain.user.domain.repository.UserRepository; +import com.campus.campus.global.oci.application.service.PresignedUrlService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ReviewService { + + private final UserRepository userRepository; + private final ReviewMapper reviewMapper; + private final ReviewRepository reviewRepository; + private final PlaceService placeService; + private final ReviewImageRepository reviewImageRepository; + private final PresignedUrlService presignedUrlService; + private final PlaceRepository placeRepository; + + @Transactional + public ReviewResponse writeReview(ReviewRequest request, Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFirstLoginException::new); + + if (request.imageUrls() != null && request.imageUrls().size() > 10) { + throw new PostImageLimitExceededException(); + } + + //place 객체 생성 + Place place = placeService.findOrCreatePlace(request.place()); + + Review review = reviewMapper.createReview(request, user, place); + reviewRepository.save(review); + + if (request.imageUrls() != null) { + for (String imageUrl : request.imageUrls()) { + reviewImageRepository.save(reviewMapper.createReviewImage(review, imageUrl)); + } + } + + List imageUrls = reviewImageRepository + .findAllByReviewOrderbyIdAsc(review) + .stream() + .map(ReviewImage::getImageUrl) + .toList(); + + return reviewMapper.toReviewResponse(review, imageUrls); + } + + @Transactional(readOnly = true) + public ReviewResponse readReview(Long reviewId) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + + List imageUrls = reviewImageRepository + .findAllByReviewOrderbyIdAsc(review) + .stream() + .map(ReviewImage::getImageUrl) + .toList(); + + return reviewMapper.toReviewResponse(review, imageUrls); + } + + @Transactional + public void delete(Long userId, Long reviewId) { + + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + + if (!review.getUser().getId().equals(userId)) { + throw new NotUserWriterException(); + } + + List reviewImages = reviewImageRepository.findAllByReview(review); + + List deleted = new ArrayList<>(); + reviewImages.stream() + .map(ReviewImage::getImageUrl) + .forEach(deleted::add); + + reviewImageRepository.deleteAll(reviewImages); + reviewRepository.delete(review); + + for (String imageUrl : deleted) { + try { + presignedUrlService.deleteImage(imageUrl); + } catch (PostOciImageDeleteFailedException e) { + log.warn("OCI 파일 삭제 실패: {}", imageUrl, e); + } + } + } + + @Transactional + public ReviewResponse update(Long userId, Long reviewId, ReviewRequest request) { + + if (request.imageUrls() != null && request.imageUrls().size() > 10) { + throw new PostImageLimitExceededException(); + } + + Review review = reviewRepository.findById(reviewId) + .orElseThrow(ReviewNotFoundException::new); + + if (!review.getUser().getId().equals(userId)) { + throw new NotUserWriterException(); + } + + List oldImages = reviewImageRepository.findAllByReview(review); + review.update( + request.content(), + request.star() + ); + + reviewImageRepository.deleteByReview(review); + if (request.imageUrls() != null) { + for (String imageUrl : request.imageUrls()) { + reviewImageRepository.save(reviewMapper.createReviewImage(review, imageUrl)); + } + } + + cleanupUnusedImages(oldImages, request); + + List imageUrls = reviewImageRepository + .findAllByReview(review) + .stream() + .map(ReviewImage::getImageUrl) + .toList(); + + return reviewMapper.toReviewResponse(review, imageUrls); + } + + @Transactional(readOnly = true) + public CursorPageReviewResponse getReviewList( + Long placeId, + LocalDateTime cursorCreatedAt, + Long cursorId, + int size + ) { + + //size+1로 조회 -> 다음 페이지 여부(hasNext) 판단 + Pageable pageable = PageRequest.of(0, size + 1); + List fetched = reviewRepository.findByPlaceIdWithCursor( + placeId, cursorCreatedAt, cursorId, pageable + ); + + //다음 페이지가 있는지 판단, 실제로 내려줄 items는 size개만 자름 + boolean hasNext = fetched.size() > size; + List reviews = hasNext ? fetched.subList(0, size) : fetched; + + //리뷰 ID를 뽑아서 이미지들을 한 번에 조회 + List reviewIds = reviews.stream() + .map(Review::getId) + .toList(); + + //reviewId -> imageUrls로 그룹핑 + Map> imageMap = reviewImageRepository + .findAllByReviewIdInOrderByIdAsc(reviewIds) + .stream() + .collect(Collectors.groupingBy( + ri -> ri.getReview().getId(), + Collectors.mapping(ReviewImage::getImageUrl, Collectors.toList()) + )); + + List items = reviews.stream() + .map(review -> + reviewMapper.toReviewResponse( + review, + imageMap.get(review.getId()) + ) + ) + .toList(); + + Review last = reviews.get(reviews.size() - 1); + + return reviewMapper.toCursorReviewResponse(items, last, hasNext); + + } + + //이미지 삭제 + private void cleanupUnusedImages(List oldImages, ReviewRequest request) { + List newUrls = request.imageUrls() == null ? List.of() : request.imageUrls(); + List deleteTargets = new ArrayList<>(); + + // 본문 이미지 중 제거된 이미지 + oldImages.stream() + .map(ReviewImage::getImageUrl) + .filter(url -> !newUrls.contains(url)) + .forEach(deleteTargets::add); + + //삭제 + for (String imageUrl : deleteTargets) { + if (imageUrl == null || imageUrl.isBlank()) { + continue; + } + + try { + presignedUrlService.deleteImage(imageUrl); + } catch (PostOciImageDeleteFailedException e) { + log.warn("OCI 파일 삭제 실패 (파일이 없을 수 있음): {}", imageUrl, e); + } + } + } + +} diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java index e0e005c5..d8e652bc 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java @@ -4,6 +4,7 @@ import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.global.entity.BaseEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; @@ -35,6 +36,7 @@ public class Review extends BaseEntity { private double star; //영수증 제휴 인증 여부 + @Column(name = "is_verified") private boolean isVerified; @ManyToOne(fetch = FetchType.LAZY) @@ -44,4 +46,12 @@ public class Review extends BaseEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "place_id", nullable = false) private Place place; + + public void update( + String content, + double star + ) { + this.content = content; + this.star = star; + } } diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java index de1a1e62..bd8e03c4 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java @@ -3,6 +3,7 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; import com.campus.campus.domain.review.domain.entity.Review; import com.campus.campus.domain.review.domain.entity.ReviewImage; @@ -10,4 +11,11 @@ public interface ReviewImageRepository extends JpaRepository { List findAllByReviewOrderbyIdAsc(Review review); + + List findAllByReviewIdInOrderByIdAsc(@Param("reviewIds") List reviewIds); + + List findAllByReview(Review review); + + void deleteByReview(Review review); + } diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java index f30d037f..d1051697 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -1,8 +1,32 @@ -package com.campus.campus.domain.review.domain.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.campus.campus.domain.review.domain.entity.Review; - -public interface ReviewRepository extends JpaRepository { -} +package com.campus.campus.domain.review.domain.repository; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import com.campus.campus.domain.review.domain.entity.Review; + +public interface ReviewRepository extends JpaRepository { + + @Query(""" + SELECT r + FROM Review r + WHERE r.place.placeId = :placeId + AND ( + :cursorCreatedAt IS NULL + OR r.createdAt < :cursorCreatedAt + OR (r.createdAt = :cursorCreatedAt AND r.id < :cursorId) + ) + ORDER BY r.createdAt DESC, r.id DESC + """) + List findByPlaceIdWithCursor( + @Param("placeId") Long placeId, + @Param("cursorCreatedAt") LocalDateTime cursorCreatedAt, + @Param("cursorId") Long cursorId, + Pageable pageable + ); +} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index 2727b553..a44877a8 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -1,11 +1,20 @@ package com.campus.campus.domain.review.presentation; +import java.time.LocalDateTime; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import com.campus.campus.domain.review.application.dto.request.ReviewRequest; +import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; import com.campus.campus.domain.review.application.service.ReviewService; import com.campus.campus.global.annotation.CurrentUserId; @@ -31,4 +40,43 @@ public CommonResponse writeReview( ReviewResponse response = reviewService.writeReview(request, userId); return CommonResponse.success(ReviewResponseCode.REVIEW_SAVE_SUCCESS, response); } + + @GetMapping("/{reviewId}") + @Operation(summary = "리뷰 상세 조회") + public CommonResponse readReview(@PathVariable Long reviewId) { + ReviewResponse response = reviewService.readReview(reviewId); + return CommonResponse.success(ReviewResponseCode.REVIEW_SAVE_SUCCESS, response); + } + + @DeleteMapping("/{reviewId}") + @Operation(summary = "리뷰 삭제") + public CommonResponse deleteReview(@PathVariable Long reviewId, @CurrentUserId Long userId) { + reviewService.delete(userId, reviewId); + return CommonResponse.success(ReviewResponseCode.REVIEW_DELETE_SUCCESS); + } + + @PatchMapping("/{reviewId}") + @Operation(summary = "리뷰 수정") + public CommonResponse updateReview( + @CurrentUserId Long userId, + @PathVariable Long reviewId, + @RequestBody @Valid ReviewRequest request + ) { + ReviewResponse response = reviewService.update(userId, reviewId, request); + return CommonResponse.success(ReviewResponseCode.REVIEW_UPDATE_SUCCESS, response); + } + + @GetMapping("/list/{placeId}") + @Operation(summary = "리뷰 목록 조회 (더보기 이후)") + public CommonResponse> readAllReviews( + @PathVariable Long placeId, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime cursorCreatedAt, + @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Long cursorId, + @RequestParam(defaultValue = "10") int size + ) { + CursorPageReviewResponse response = reviewService.getReviewList(placeId, cursorCreatedAt, + cursorId, size); + return CommonResponse.success(ReviewResponseCode.GET_REVIEW_LIST_SUCCESS, response); + + } } diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java index 7491aa18..0123a79c 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java @@ -11,7 +11,10 @@ @AllArgsConstructor public enum ReviewResponseCode implements ResponseCodeInterface { - REVIEW_SAVE_SUCCESS(200, HttpStatus.OK, "리뷰 작성이 완료되었습니다."); + REVIEW_SAVE_SUCCESS(200, HttpStatus.OK, "리뷰 작성이 완료되었습니다."), + REVIEW_DELETE_SUCCESS(200, HttpStatus.OK, "리뷰 삭제가 완료되었습니다."), + REVIEW_UPDATE_SUCCESS(200, HttpStatus.OK, "리뷰 수정이 완료되었습니다."), + GET_REVIEW_LIST_SUCCESS(200, HttpStatus.OK, "리뷰 리스트 조회에 성공하였습니다.."); private final int code; private final HttpStatus status; From 532a1e9e478737a70a4d98d79b00bf4740993af8 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Sun, 11 Jan 2026 21:10:41 +0900 Subject: [PATCH 05/17] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=20=EC=9C=84=ED=95=B4=20api=20t=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/RankingScope.java | 10 ++++ .../dto/response/ReviewCreateResponse.java | 12 ++++ .../dto/response/ReviewCreateResult.java | 12 ++++ .../dto/response/ReviewRankingResponse.java | 8 +++ .../application/mapper/ReviewMapper.java | 34 +++++++++++ .../application/service/ReviewService.java | 58 +++++++++++++++++-- .../domain/repository/ReviewRepository.java | 10 ++++ .../review/presentation/ReviewController.java | 5 +- 8 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/RankingScope.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/RankingScope.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/RankingScope.java new file mode 100644 index 00000000..3e0d8196 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/RankingScope.java @@ -0,0 +1,10 @@ +package com.campus.campus.domain.review.application.dto.response; + +import lombok.Builder; + +@Builder +public record RankingScope( + String scope, + long rank //n번째 리뷰 +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java new file mode 100644 index 00000000..28d0e34c --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java @@ -0,0 +1,12 @@ +package com.campus.campus.domain.review.application.dto.response; + +import lombok.Builder; + +@Builder +public record ReviewCreateResponse( + ReviewResponse review, + ReviewCreateResult result, + ReviewRankingResponse ranking + +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java new file mode 100644 index 00000000..0581d042 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java @@ -0,0 +1,12 @@ +package com.campus.campus.domain.review.application.dto.response; + +import lombok.Builder; + +@Builder +public record ReviewCreateResult( + boolean isFirstReviewOfPlace, + int userReviewCountOfPlace, + // int NumberOfUserStamp, + String message +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java new file mode 100644 index 00000000..51e5b0c4 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewRankingResponse.java @@ -0,0 +1,8 @@ +package com.campus.campus.domain.review.application.dto.response; + +public record ReviewRankingResponse( + RankingScope major, + RankingScope college, + RankingScope school +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java index cbd32510..af0ff270 100644 --- a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -8,6 +8,10 @@ import com.campus.campus.domain.place.domain.entity.Place; import com.campus.campus.domain.review.application.dto.request.ReviewRequest; import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.RankingScope; +import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; +import com.campus.campus.domain.review.application.dto.response.ReviewCreateResult; +import com.campus.campus.domain.review.application.dto.response.ReviewRankingResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; import com.campus.campus.domain.review.domain.entity.Review; import com.campus.campus.domain.review.domain.entity.ReviewImage; @@ -57,4 +61,34 @@ public CursorPageReviewResponse toCursorReviewResponse(List oldImages, ReviewRequest requ } } + private ReviewCreateResult getCreateResult(Place place, User user) { + //해당 장소 리뷰 개수 + long totalReviewCountOfPlace = reviewRepository.countByPlaceId(place.getPlaceId()); + + //해당 장소에서 유저가 쓴 리뷰가 몇번째인지 + long userReviewCountOfPlace = reviewRepository.countByPlaceIdAndUserId(place.getPlaceId(), user.getId()); + boolean isFirstReviewOfPlace = totalReviewCountOfPlace == 1; + + return reviewMapper.toReviewCreateResult(isFirstReviewOfPlace, userReviewCountOfPlace); + + } + + private ReviewRankingResponse getRankingResult(Place place, User user) { + Long placeId = place.getPlaceId(); + Major major = user.getMajor(); + College college = user.getCollege(); + School school = user.getSchool(); + + Long majorId = major.getMajorId(); + Long collegeId = college.getCollegeId(); + Long schoolId = school.getSchoolId(); + + long majorRank = reviewRepository.countByPlace_PlaceIdAndUser_Major_MajorId( + placeId, + majorId + ); + + long collegeRank = reviewRepository.countByPlace_PlaceIdAndUser_College_CollegeId( + placeId, + collegeId + ); + + long schoolRank = reviewRepository.countByPlace_PlaceIdAndUser_School_SchoolId( + placeId, + schoolId + ); + + return reviewMapper.toReviewRankingResponse(major.getMajorName(), majorRank, college.getCollegeName(), + collegeRank, + school.getSchoolName(), schoolRank); + } + } diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java index d1051697..dc2048e8 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -29,4 +29,14 @@ List findByPlaceIdWithCursor( @Param("cursorId") Long cursorId, Pageable pageable ); + + long countByPlaceId(long placeId); + + long countByPlaceIdAndUserId(long placeId, long userId); + + long countByPlace_PlaceIdAndUser_Major_MajorId(long placeId, long majorId); + + long countByPlace_PlaceIdAndUser_College_CollegeId(Long placeId, long collegeId); + + long countByPlace_PlaceIdAndUser_School_SchoolId(Long placeId, long schoolId); } diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index a44877a8..34b08d23 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -15,6 +15,7 @@ import com.campus.campus.domain.review.application.dto.request.ReviewRequest; import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; import com.campus.campus.domain.review.application.service.ReviewService; import com.campus.campus.global.annotation.CurrentUserId; @@ -33,11 +34,11 @@ public class ReviewController { @PostMapping @Operation(summary = "리뷰 작성") - public CommonResponse writeReview( + public CommonResponse writeReview( @Valid @RequestBody ReviewRequest request, @CurrentUserId Long userId ) { - ReviewResponse response = reviewService.writeReview(request, userId); + ReviewCreateResponse response = reviewService.writeReview(request, userId); return CommonResponse.success(ReviewResponseCode.REVIEW_SAVE_SUCCESS, response); } From 84b8a2ee0e895511ed85d9b62af68a0ff47cb12f Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:16:01 +0900 Subject: [PATCH 06/17] =?UTF-8?q?feat:=20=EC=A0=9C=ED=9C=B4=20=EB=A7=A4?= =?UTF-8?q?=EC=9E=A5=20=C3=AB=EB=91=98=EB=9F=AC=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentCouncilPostRepository.java | 41 +++++++++++++++++ .../dto/response/SavedPlaceInfo.java | 3 +- .../dto/response/PlaceReviewRankResponse.java | 14 ++++++ .../application/mapper/ReviewMapper.java | 16 +++++++ .../application/service/ReviewService.java | 43 +++++++++++++++-- .../repository/ReviewImageRepository.java | 2 +- .../domain/repository/ReviewRepository.java | 4 +- .../review/presentation/ReviewController.java | 11 +++++ .../presentation/ReviewResponseCode.java | 3 +- .../service/KakaoOauthService.java | 46 ++++++++++++++----- .../user/presentation/AuthController.java | 10 ++-- 11 files changed, 167 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceReviewRankResponse.java diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index d14dfc7c..205c117e 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -275,4 +275,45 @@ List findPinsInBounds( @Param("now") LocalDateTime now ); + @Query(value = """ + SELECT scp.* + FROM student_council_post scp + JOIN student_councils sc + ON scp.writer_id = sc.student_council_id + JOIN places p + ON scp.place_id = p.place_id + LEFT JOIN reviews r + ON r.place_id = p.place_id + AND r.created_at >= :from + WHERE scp.start_date_time <= :now + AND scp.end_date_time >= :now + AND ( + (sc.council_type = 'MAJOR_COUNCIL' AND sc.major_id = :majorId) + OR (sc.council_type = 'COLLEGE_COUNCIL' AND sc.college_id = :collegeId) + OR (sc.council_type = 'SCHOOL_COUNCIL' AND sc.school_id = :schoolId) + ) + GROUP BY scp.id + ORDER BY COUNT(r.id) DESC + LIMIT 3; + + """, nativeQuery = true) + List findTop3RecommendedPartnershipPlaces( + @Param("majorId") Long majorId, + @Param("collegeId") Long collegeId, + @Param("schoolId") Long schoolId, + @Param("from") LocalDateTime from, + @Param("now") LocalDateTime now + ); + + @Query(""" + SELECT scp + FROM StudentCouncilPost scp + WHERE scp.place.placeId IN :placeIds + AND scp.writer.id = :writerId + ORDER BY scp.createdAt DESC + """) + List findRepresentativePosts( + @Param("placeIds") List placeIds + ); + } diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java index ef47fd0c..a095b140 100644 --- a/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public record SavedPlaceInfo( @@ -32,7 +33,7 @@ public record SavedPlaceInfo( String telephone, @Schema(description = "위도/경도") - @NotBlank + @NotNull Coordinate coordinate, @Schema(description = "이미지 url") diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceReviewRankResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceReviewRankResponse.java new file mode 100644 index 00000000..052075f5 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/PlaceReviewRankResponse.java @@ -0,0 +1,14 @@ +package com.campus.campus.domain.review.application.dto.response; + +import lombok.Builder; + +@Builder +public record PlaceReviewRankResponse( + Long placeId, + String placeName, + String category, + String partnership, + String thumbnailUrl + // double distance +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java index af0ff270..95455efc 100644 --- a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -5,9 +5,11 @@ import org.springframework.stereotype.Component; +import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; import com.campus.campus.domain.place.domain.entity.Place; import com.campus.campus.domain.review.application.dto.request.ReviewRequest; import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.PlaceReviewRankResponse; import com.campus.campus.domain.review.application.dto.response.RankingScope; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResult; @@ -91,4 +93,18 @@ public ReviewCreateResponse toReviewCreateResponse(ReviewResponse response, Revi .ranking(rankingResponse) .build(); } + + public PlaceReviewRankResponse toTopPartnershipResponse( + StudentCouncilPost post + ) { + return PlaceReviewRankResponse.builder() + .placeId(post.getPlace().getPlaceId()) + .placeName(post.getPlace().getPlaceName()) + .category(post.getPlace().getPlaceCategory()) + .partnership(post.getTitle()) + .thumbnailUrl(post.getThumbnailImageUrl()) + // .reviewCount(projection.getReviewCount()) + .build(); + } + } diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 2356ef82..8d41ab95 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -13,10 +13,13 @@ import com.campus.campus.domain.councilpost.application.exception.PostImageLimitExceededException; import com.campus.campus.domain.councilpost.application.exception.PostOciImageDeleteFailedException; +import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; +import com.campus.campus.domain.councilpost.domain.repository.StudentCouncilPostRepository; import com.campus.campus.domain.place.application.service.PlaceService; import com.campus.campus.domain.place.domain.entity.Place; import com.campus.campus.domain.review.application.dto.request.ReviewRequest; import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.PlaceReviewRankResponse; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResult; import com.campus.campus.domain.review.application.dto.response.ReviewRankingResponse; @@ -32,6 +35,7 @@ import com.campus.campus.domain.school.domain.entity.Major; import com.campus.campus.domain.school.domain.entity.School; import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; +import com.campus.campus.domain.user.application.exception.UserNotFoundException; import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; import com.campus.campus.global.oci.application.service.PresignedUrlService; @@ -50,6 +54,7 @@ public class ReviewService { private final PlaceService placeService; private final ReviewImageRepository reviewImageRepository; private final PresignedUrlService presignedUrlService; + private final StudentCouncilPostRepository studentCouncilPostRepository; @Transactional public ReviewCreateResponse writeReview(ReviewRequest request, Long userId) { @@ -73,7 +78,7 @@ public ReviewCreateResponse writeReview(ReviewRequest request, Long userId) { } List imageUrls = reviewImageRepository - .findAllByReviewOrderbyIdAsc(review) + .findAllByReviewOrderByIdAsc(review) .stream() .map(ReviewImage::getImageUrl) .toList(); @@ -91,7 +96,7 @@ public ReviewResponse readReview(Long reviewId) { .orElseThrow(ReviewNotFoundException::new); List imageUrls = reviewImageRepository - .findAllByReviewOrderbyIdAsc(review) + .findAllByReviewOrderByIdAsc(review) .stream() .map(ReviewImage::getImageUrl) .toList(); @@ -213,6 +218,35 @@ public CursorPageReviewResponse getReviewList( } + @Transactional(readOnly = true) + public List readPopularPartnerships(Long userId) { + User user = userRepository.findById(userId) + .orElseThrow(UserNotFoundException::new); + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime from = now.minusMonths(1); + + //리뷰가 가장 많은 top3 placeIds + List partnerships = + studentCouncilPostRepository.findTop3RecommendedPartnershipPlaces( + user.getMajor().getMajorId(), + user.getCollege().getCollegeId(), + user.getSchool().getSchoolId(), + from, + now + ); + + log.info("찾은 결과:{}", partnerships.stream().toList()); + + if (partnerships.isEmpty()) { + return List.of(); + } + + return partnerships.stream() + .map(reviewMapper::toTopPartnershipResponse) + .toList(); + } + //이미지 삭제 private void cleanupUnusedImages(List oldImages, ReviewRequest request) { List newUrls = request.imageUrls() == null ? List.of() : request.imageUrls(); @@ -240,10 +274,11 @@ private void cleanupUnusedImages(List oldImages, ReviewRequest requ private ReviewCreateResult getCreateResult(Place place, User user) { //해당 장소 리뷰 개수 - long totalReviewCountOfPlace = reviewRepository.countByPlaceId(place.getPlaceId()); + long totalReviewCountOfPlace = reviewRepository.countByPlace_PlaceId(place.getPlaceId()); //해당 장소에서 유저가 쓴 리뷰가 몇번째인지 - long userReviewCountOfPlace = reviewRepository.countByPlaceIdAndUserId(place.getPlaceId(), user.getId()); + long userReviewCountOfPlace = reviewRepository.countByPlace_PlaceIdAndUser_Id(place.getPlaceId(), + user.getId()); boolean isFirstReviewOfPlace = totalReviewCountOfPlace == 1; return reviewMapper.toReviewCreateResult(isFirstReviewOfPlace, userReviewCountOfPlace); diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java index bd8e03c4..26b51dc0 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImageRepository.java @@ -10,7 +10,7 @@ public interface ReviewImageRepository extends JpaRepository { - List findAllByReviewOrderbyIdAsc(Review review); + List findAllByReviewOrderByIdAsc(Review review); List findAllByReviewIdInOrderByIdAsc(@Param("reviewIds") List reviewIds); diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java index dc2048e8..b6af261b 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -30,9 +30,9 @@ List findByPlaceIdWithCursor( Pageable pageable ); - long countByPlaceId(long placeId); + long countByPlace_PlaceId(long placeId); - long countByPlaceIdAndUserId(long placeId, long userId); + long countByPlace_PlaceIdAndUser_Id(long placeId, long userId); long countByPlace_PlaceIdAndUser_Major_MajorId(long placeId, long majorId); diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index 34b08d23..f28e0dd7 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -1,6 +1,7 @@ package com.campus.campus.domain.review.presentation; import java.time.LocalDateTime; +import java.util.List; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.DeleteMapping; @@ -15,6 +16,7 @@ import com.campus.campus.domain.review.application.dto.request.ReviewRequest; import com.campus.campus.domain.review.application.dto.response.CursorPageReviewResponse; +import com.campus.campus.domain.review.application.dto.response.PlaceReviewRankResponse; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; import com.campus.campus.domain.review.application.service.ReviewService; @@ -80,4 +82,13 @@ public CommonResponse> readAllReviews( return CommonResponse.success(ReviewResponseCode.GET_REVIEW_LIST_SUCCESS, response); } + + @GetMapping("/partnership-list") + @Operation(summary = "제휴 매장 둘러보기", description = "최근 한달 간 제휴 이용수가 많았던 매장") + public CommonResponse> readAllPartnerships( + @CurrentUserId Long userId + ) { + List response = reviewService.readPopularPartnerships(userId); + return CommonResponse.success(ReviewResponseCode.GET_RANK_SUCCESS, response); + } } diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java index 0123a79c..ddfafd93 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java @@ -14,7 +14,8 @@ public enum ReviewResponseCode implements ResponseCodeInterface { REVIEW_SAVE_SUCCESS(200, HttpStatus.OK, "리뷰 작성이 완료되었습니다."), REVIEW_DELETE_SUCCESS(200, HttpStatus.OK, "리뷰 삭제가 완료되었습니다."), REVIEW_UPDATE_SUCCESS(200, HttpStatus.OK, "리뷰 수정이 완료되었습니다."), - GET_REVIEW_LIST_SUCCESS(200, HttpStatus.OK, "리뷰 리스트 조회에 성공하였습니다.."); + GET_REVIEW_LIST_SUCCESS(200, HttpStatus.OK, "리뷰 리스트 조회에 성공하였습니다."), + GET_RANK_SUCCESS(200, HttpStatus.OK, "최근 한달 리뷰 순에 따른 조회 가게 조회에 성공하였습니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java index 6a44d0b3..4122b8c0 100644 --- a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java +++ b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java @@ -3,30 +3,29 @@ import java.time.LocalDateTime; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; import com.campus.campus.domain.user.application.exception.NicknameNotMatchException; -import com.campus.campus.domain.user.application.exception.UserSignupForbiddenException; import com.campus.campus.domain.user.application.exception.UserNotFoundException; -import com.campus.campus.global.auth.application.mapper.LoginMapper; +import com.campus.campus.domain.user.application.exception.UserSignupForbiddenException; import com.campus.campus.domain.user.application.mapper.UserMapper; import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; import com.campus.campus.global.auth.application.dto.KakaoTokenResponse; import com.campus.campus.global.auth.application.dto.KakaoUserResponse; import com.campus.campus.global.auth.application.dto.OauthLoginResponse; +import com.campus.campus.global.auth.application.mapper.LoginMapper; import com.campus.campus.global.auth.application.property.KakaoOauthProperty; -import com.campus.campus.global.util.jwt.application.service.RedisTokenService; import com.campus.campus.global.util.jwt.JwtProvider; +import com.campus.campus.global.util.jwt.application.service.RedisTokenService; import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClient; - @Service @RequiredArgsConstructor public class KakaoOauthService { @@ -47,8 +46,9 @@ public class KakaoOauthService { private long refreshTokenExpirationSeconds; @Transactional - public OauthLoginResponse login(String kakaoAccessToken) { - KakaoUserResponse kakaoUser = getUserInfo(kakaoAccessToken); + public OauthLoginResponse login(String authorizationCode) { + KakaoTokenResponse kakaoToken = getToken(authorizationCode); + KakaoUserResponse kakaoUser = getUserInfo(kakaoToken.accessToken()); User user = findOrCreateUser(kakaoUser); @@ -133,4 +133,26 @@ private User findOrCreateUser(KakaoUserResponse kakaoUserResponse) { return userRepository.save(newUser); }); } -} + + private KakaoTokenResponse getToken(String authorizationCode) { + RestClient client = RestClient.create(); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("client_id", kakaoOauthProperty.getClientId()); + body.add("redirect_uri", kakaoOauthProperty.getRedirectUri()); + body.add("code", authorizationCode); + + if (kakaoOauthProperty.getClientSecret() != null && + !kakaoOauthProperty.getClientSecret().isBlank()) { + body.add("client_secret", kakaoOauthProperty.getClientSecret()); + } + + return client.post() + .uri(KAUTH_BASE_URL + "/oauth/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(body) + .retrieve() + .body(KakaoTokenResponse.class); + } +} \ No newline at end of file diff --git a/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java b/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java index a6012080..147fdac1 100644 --- a/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java +++ b/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java @@ -8,9 +8,9 @@ import org.springframework.web.bind.annotation.RestController; import com.campus.campus.domain.user.application.dto.request.UserWithdrawRequest; +import com.campus.campus.domain.user.application.service.KakaoOauthService; import com.campus.campus.global.annotation.CurrentUserId; import com.campus.campus.global.auth.application.dto.OauthLoginResponse; -import com.campus.campus.domain.user.application.service.KakaoOauthService; import com.campus.campus.global.common.response.CommonResponse; import io.swagger.v3.oas.annotations.Operation; @@ -24,9 +24,9 @@ public class AuthController { private final KakaoOauthService kakaoOauthService; @PostMapping("/login/kakao") - @Operation(summary = "카카오 로그인 (Native App 방식)") - public CommonResponse kakaoLogin(@RequestParam("token") String kakaoAccessToken) { - OauthLoginResponse response = kakaoOauthService.login(kakaoAccessToken); + @Operation(summary = "카카오 로그인") + public CommonResponse kakaoLogin(@RequestParam("code") String code) { + OauthLoginResponse response = kakaoOauthService.login(code); return CommonResponse.success(UserResponseCode.LOGIN_SUCCESS, response); } @@ -39,4 +39,4 @@ public CommonResponse withdraw(@CurrentUserId Long userId, return CommonResponse.success(UserResponseCode.WITHDRAW_SUCCESS); } -} +} \ No newline at end of file From f693364916dab8c0c8733483f06a06d799fd8a66 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:30:57 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=EB=A6=AC=EB=B7=B0=20=EB=AA=A9?= =?UTF-8?q?=EB=A1=9D=20=EC=A1=B0=ED=9A=8C(=EB=8D=94=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EC=9D=B4=ED=9B=84)=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .oci-keys/private-key.pem | 3 +++ .../domain/review/application/mapper/ReviewMapper.java | 9 +++++++++ .../domain/review/application/service/ReviewService.java | 4 ++++ 3 files changed, 16 insertions(+) create mode 100644 .oci-keys/private-key.pem diff --git a/.oci-keys/private-key.pem b/.oci-keys/private-key.pem new file mode 100644 index 00000000..4434433d --- /dev/null +++ b/.oci-keys/private-key.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUoD+dO30i51Ew m5wMqoIDkukWXrKd+2yHTM2VVGMyPhAPthC4Sf+1FnT+biG9AfyzOYSO/4MD3sxb X4TGHynEFX/d4OxWeTq/iceLd8xHF84g1KToryM8IrViiU4zfIpV7MsEIFzmFe1Q VPgLgtx7NF4wWbK9Q9TKonOoo0wgfRHbM3bp3WEHPijqJUfhpkVlNqsA9lwEgnNg r6FcaAI0jJIdpLqDnC4o6XgadpcT01dVBdiB3gJBmZJxnM8lA6yhZTuWf0CDKbYN aaYIzLNWJS0PsAMC+iMq67mXST5T65by2E07llzzklyJZfbXTRjRoMOeAY2L8fg/ DeQ4F1ATAgMBAAECggEAJqiCGGjF/jAXc2J/c1AW9mZiCaarDA039LuSj0l6BZsC GZEtJgySOM7983p7mN1BICbfgo5TOENsXVqXDdyBn/yWtHUeEzTUaRm4VZOw6OYb nJ185C9flSHsX59+P98vWaMFYFkv+sdWz+m69YDT7HSfgWP1mvK3wCXcKHeUMj2m RP6AT2dQIOVR+xb9a+JU4Gr1qBNF/+u4WH2bRLO9yc/+K9xY2uu5Ae1l+nkhsCU5 1tXV9KAqt1FViV9aR36YGj3VCQExOnc9QQFAWs0Ow8xK8DhcucGw1qSPZWZzHeBn fNXeDeLoBOgrueJWykNq64VjdMzNo0FZi/SpHYOSbQKBgQDtf8QVj2UyvtW/qi6t brauA0qfSASkUc3irB9lapsBteiVcrGLbujDdWkBFqhzFBGSvxxUXmJJjKMVnR2J X+Xrg+cdaBYh9CBGYg5NmdyYBrQ0wxVedlMh/bLh+5nHPaLYBxw4r0P7rf51R0LO saIoAYRUGxwZbpKRtfSJuJ47zQKBgQDlMHXD8sgzF0YMk+/Cnf8TfmoHrr2SogdO u3YEcXDShvW2/w0UOCYXrm73u2OSzM7XdWF1Y+b8pD8pmB30ZnfsWIXmbqUOc97I 1gZw9Pi7OTQWkh8xfGlquTJPYAEje76tg12SYMt4JWwDqOsZhk/UyZ24Q9KXZudv RfHEPdKbXwKBgF2A8egzHnqOG4VsMRhjAFUeQqDXL8yp0E8vyOSaxhA0WC+OikwF DGr3rLGbBBLakdiemT62MigW0JZP1zMgqFrDAZjYQ+52OMa9EDiJHQpViVlEfQNF vpEhWu4RqoeAwr4efoPqrO9Hn2j491p9IQRzJHAghD2XBOmNI4udPd75AoGAUMXA a1uQFPl51Yr5r9QxBnTB940tAJWcD7JfiHdWdMvdoy3GNT8IpcXzE9n14NHPf/29 aDraOGXGYOlcfTrzvtb/8dNC1pIdjRho0rxzQMCLS/0Zgz4+hL9aLMFABXpiHBK1 EpaMfv5pT+zjVm6QigS4Ui2M9ZDttbin2SqUo+8CgYEAs6VZcKDixtn+MRn/Jyv+ l/VHIxhDyM3yA0P1Vqc5T0iIDQu6WhjuMVX1xcIH1MFak3YxRX2yEbAQt/PmT+jL ZGt0kdt+4pZAN4Qn7ylpdEeqhm7I7tL4PRejLp3ZXcqlNqzfr9JuN/eCT5BwwNg0 +Yd1pJS8gh3ypGJ9Ln7ux3M= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java index 95455efc..2e938b1b 100644 --- a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -34,6 +34,15 @@ public Review createReview(ReviewRequest request, User user, Place place) { .build(); } + public CursorPageReviewResponse toEmptyCursorReviewResponse() { + return CursorPageReviewResponse.builder() + .items(List.of()) + .nextCursorCreatedAt(null) + .nextCursorId(null) + .hasNext(false) + .build(); + } + public ReviewImage createReviewImage(Review review, String imageUrl) { return ReviewImage.builder() .review(review) diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 8d41ab95..bc1a1765 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -189,6 +189,10 @@ public CursorPageReviewResponse getReviewList( boolean hasNext = fetched.size() > size; List reviews = hasNext ? fetched.subList(0, size) : fetched; + if (reviews.isEmpty()) { + return reviewMapper.toEmptyCursorReviewResponse(); + } + //리뷰 ID를 뽑아서 이미지들을 한 번에 조회 List reviewIds = reviews.stream() .map(Review::getId) From 233485e13def34790db0e5637151f94f67e64414 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:33:27 +0900 Subject: [PATCH 08/17] =?UTF-8?q?refactor:=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20(native)=20=C3=AC=C2=9B?= =?UTF-8?q?=EC=9B=90=EC=83=81=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .oci-keys/private-key.pem | 3 --- .../domain/user/application/service/KakaoOauthService.java | 5 ++--- .../campus/domain/user/presentation/AuthController.java | 6 +++--- 3 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 .oci-keys/private-key.pem diff --git a/.oci-keys/private-key.pem b/.oci-keys/private-key.pem deleted file mode 100644 index 4434433d..00000000 --- a/.oci-keys/private-key.pem +++ /dev/null @@ -1,3 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDUoD+dO30i51Ew m5wMqoIDkukWXrKd+2yHTM2VVGMyPhAPthC4Sf+1FnT+biG9AfyzOYSO/4MD3sxb X4TGHynEFX/d4OxWeTq/iceLd8xHF84g1KToryM8IrViiU4zfIpV7MsEIFzmFe1Q VPgLgtx7NF4wWbK9Q9TKonOoo0wgfRHbM3bp3WEHPijqJUfhpkVlNqsA9lwEgnNg r6FcaAI0jJIdpLqDnC4o6XgadpcT01dVBdiB3gJBmZJxnM8lA6yhZTuWf0CDKbYN aaYIzLNWJS0PsAMC+iMq67mXST5T65by2E07llzzklyJZfbXTRjRoMOeAY2L8fg/ DeQ4F1ATAgMBAAECggEAJqiCGGjF/jAXc2J/c1AW9mZiCaarDA039LuSj0l6BZsC GZEtJgySOM7983p7mN1BICbfgo5TOENsXVqXDdyBn/yWtHUeEzTUaRm4VZOw6OYb nJ185C9flSHsX59+P98vWaMFYFkv+sdWz+m69YDT7HSfgWP1mvK3wCXcKHeUMj2m RP6AT2dQIOVR+xb9a+JU4Gr1qBNF/+u4WH2bRLO9yc/+K9xY2uu5Ae1l+nkhsCU5 1tXV9KAqt1FViV9aR36YGj3VCQExOnc9QQFAWs0Ow8xK8DhcucGw1qSPZWZzHeBn fNXeDeLoBOgrueJWykNq64VjdMzNo0FZi/SpHYOSbQKBgQDtf8QVj2UyvtW/qi6t brauA0qfSASkUc3irB9lapsBteiVcrGLbujDdWkBFqhzFBGSvxxUXmJJjKMVnR2J X+Xrg+cdaBYh9CBGYg5NmdyYBrQ0wxVedlMh/bLh+5nHPaLYBxw4r0P7rf51R0LO saIoAYRUGxwZbpKRtfSJuJ47zQKBgQDlMHXD8sgzF0YMk+/Cnf8TfmoHrr2SogdO u3YEcXDShvW2/w0UOCYXrm73u2OSzM7XdWF1Y+b8pD8pmB30ZnfsWIXmbqUOc97I 1gZw9Pi7OTQWkh8xfGlquTJPYAEje76tg12SYMt4JWwDqOsZhk/UyZ24Q9KXZudv RfHEPdKbXwKBgF2A8egzHnqOG4VsMRhjAFUeQqDXL8yp0E8vyOSaxhA0WC+OikwF DGr3rLGbBBLakdiemT62MigW0JZP1zMgqFrDAZjYQ+52OMa9EDiJHQpViVlEfQNF vpEhWu4RqoeAwr4efoPqrO9Hn2j491p9IQRzJHAghD2XBOmNI4udPd75AoGAUMXA a1uQFPl51Yr5r9QxBnTB940tAJWcD7JfiHdWdMvdoy3GNT8IpcXzE9n14NHPf/29 aDraOGXGYOlcfTrzvtb/8dNC1pIdjRho0rxzQMCLS/0Zgz4+hL9aLMFABXpiHBK1 EpaMfv5pT+zjVm6QigS4Ui2M9ZDttbin2SqUo+8CgYEAs6VZcKDixtn+MRn/Jyv+ l/VHIxhDyM3yA0P1Vqc5T0iIDQu6WhjuMVX1xcIH1MFak3YxRX2yEbAQt/PmT+jL ZGt0kdt+4pZAN4Qn7ylpdEeqhm7I7tL4PRejLp3ZXcqlNqzfr9JuN/eCT5BwwNg0 +Yd1pJS8gh3ypGJ9Ln7ux3M= ------END PRIVATE KEY----- \ No newline at end of file diff --git a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java index 4122b8c0..42327e6a 100644 --- a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java +++ b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java @@ -46,9 +46,8 @@ public class KakaoOauthService { private long refreshTokenExpirationSeconds; @Transactional - public OauthLoginResponse login(String authorizationCode) { - KakaoTokenResponse kakaoToken = getToken(authorizationCode); - KakaoUserResponse kakaoUser = getUserInfo(kakaoToken.accessToken()); + public OauthLoginResponse login(String kakaoAccessToken) { + KakaoUserResponse kakaoUser = getUserInfo(kakaoAccessToken); User user = findOrCreateUser(kakaoUser); diff --git a/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java b/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java index 147fdac1..6b5c4f77 100644 --- a/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java +++ b/src/main/java/com/campus/campus/domain/user/presentation/AuthController.java @@ -24,9 +24,9 @@ public class AuthController { private final KakaoOauthService kakaoOauthService; @PostMapping("/login/kakao") - @Operation(summary = "카카오 로그인") - public CommonResponse kakaoLogin(@RequestParam("code") String code) { - OauthLoginResponse response = kakaoOauthService.login(code); + @Operation(summary = "카카오 로그인 (Native App 방식)") + public CommonResponse kakaoLogin(@RequestParam("token") String kakaoAccessToken) { + OauthLoginResponse response = kakaoOauthService.login(kakaoAccessToken); return CommonResponse.success(UserResponseCode.LOGIN_SUCCESS, response); } From 19a216f4a08ea9f68dcb7a34d2978eb6acfe5261 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:42:07 +0900 Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20errorcode=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../campus/domain/review/application/exception/ErrorCode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java index ba68bc96..0776753c 100644 --- a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java @@ -17,5 +17,5 @@ public enum ErrorCode implements ErrorCodeInterface { private final int code; private final HttpStatus status; private final String message; -public enum ErrorCode { + } From 09858300d8b7aa2f69b2468f5895879071ad437d Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:29:28 +0900 Subject: [PATCH 10/17] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=98=88=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/ReviewService.java | 3 +- .../review/presentation/ReviewController.java | 42 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index bc1a1765..cf35fbe7 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -174,6 +174,7 @@ public ReviewResponse update(Long userId, Long reviewId, ReviewRequest request) @Transactional(readOnly = true) public CursorPageReviewResponse getReviewList( Long placeId, + Double cursorStar, LocalDateTime cursorCreatedAt, Long cursorId, int size @@ -182,7 +183,7 @@ public CursorPageReviewResponse getReviewList( //size+1로 조회 -> 다음 페이지 여부(hasNext) 판단 Pageable pageable = PageRequest.of(0, size + 1); List fetched = reviewRepository.findByPlaceIdWithCursor( - placeId, cursorCreatedAt, cursorId, pageable + placeId, cursorStar, cursorCreatedAt, cursorId, pageable ); //다음 페이지가 있는지 판단, 실제로 내려줄 items는 size개만 자름 diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index f28e0dd7..a342cf71 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -35,7 +35,44 @@ public class ReviewController { private final ReviewService reviewService; @PostMapping - @Operation(summary = "리뷰 작성") + @Operation( + summary = "리뷰 작성", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + content = @io.swagger.v3.oas.annotations.media.Content( + mediaType = "application/json", + examples = @io.swagger.v3.oas.annotations.media.ExampleObject( + name = "리뷰 작성 요청 예시", + summary = "리뷰 작성 Request Body", + value = """ + { + "content": "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.", + "star": 3.5, + "imageUrls": [ + "https://image1.jpg", + "https://image2.jpg" + ], + "place": { + "placeName": "숙명여자대학교", + "placeKey": "string", + "address": "서울특별시 용산구 청파로47길 99", + "category": "교육,학문>대학교", + "link": "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90", + "telephone": "010-1234-1234", + "coordinate": { + "latitude": 37.545947, + "longitude": 126.964578 + }, + "imgUrls": [ + "https://place-image1.jpg" + ] + } + } + """ + ) + ) + ) + ) public CommonResponse writeReview( @Valid @RequestBody ReviewRequest request, @CurrentUserId Long userId @@ -70,7 +107,7 @@ public CommonResponse updateReview( } @GetMapping("/list/{placeId}") - @Operation(summary = "리뷰 목록 조회 (더보기 이후)") + @Operation(summary = "리뷰 목록 조회 - 최신순 (더보기 이후)") public CommonResponse> readAllReviews( @PathVariable Long placeId, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime cursorCreatedAt, @@ -80,7 +117,6 @@ public CommonResponse> readAllReviews( CursorPageReviewResponse response = reviewService.getReviewList(placeId, cursorCreatedAt, cursorId, size); return CommonResponse.success(ReviewResponseCode.GET_REVIEW_LIST_SUCCESS, response); - } @GetMapping("/partnership-list") From 6adb5b70acebfbf42aaf8ca6482dbd3549dd08fe Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 03:46:02 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../domain/repository/StudentCouncilPostRepository.java | 2 ++ .../domain/review/application/service/ReviewService.java | 3 +-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fe461d98..7dfc0eff 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ +src/main/resources/oci ### NetBeans ### /nbproject/private/ diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index 24fda98a..db951ea5 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -100,12 +100,14 @@ List findRandomActivePartnerships( WHERE w.councilType = :councilType AND s.schoolId = :schoolId AND (:category IS NULL OR p.category = :category) + AND (:excludePostId IS NULL OR p.id <> :excludePostId) AND w.deletedAt IS NULL """) Page findBySchoolId( @Param("schoolId") Long schoolId, @Param("category") PostCategory category, @Param("councilType") CouncilType councilType, + @Param("excludePostId") Long excludePostId, Pageable pageable ); diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index cf35fbe7..bc1a1765 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -174,7 +174,6 @@ public ReviewResponse update(Long userId, Long reviewId, ReviewRequest request) @Transactional(readOnly = true) public CursorPageReviewResponse getReviewList( Long placeId, - Double cursorStar, LocalDateTime cursorCreatedAt, Long cursorId, int size @@ -183,7 +182,7 @@ public CursorPageReviewResponse getReviewList( //size+1로 조회 -> 다음 페이지 여부(hasNext) 판단 Pageable pageable = PageRequest.of(0, size + 1); List fetched = reviewRepository.findByPlaceIdWithCursor( - placeId, cursorStar, cursorCreatedAt, cursorId, pageable + placeId, cursorCreatedAt, cursorId, pageable ); //다음 페이지가 있는지 판단, 실제로 내려줄 items는 size개만 자름 From 6bc5713fea1e536cefa71c53d38a32d80276c93a Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Mon, 12 Jan 2026 12:42:51 +0900 Subject: [PATCH 12/17] =?UTF-8?q?refactor:=201=EC=B0=A8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentCouncilPostRepository.java | 12 +------ .../partnership/PartnershipResponse.java | 2 +- .../dto/request/ReviewRequest.java | 2 +- .../dto/response/ReviewResponse.java | 2 +- .../application/exception/ErrorCode.java | 2 +- .../application/service/ReviewService.java | 6 ++-- .../domain/review/domain/entity/Review.java | 4 +-- .../review/domain/entity/ReviewImages.java | 36 ------------------- .../repository/ReviewImagesRepository.java | 8 ----- .../review/presentation/ReviewController.java | 4 +-- .../presentation/ReviewResponseCode.java | 3 +- 11 files changed, 14 insertions(+), 67 deletions(-) delete mode 100644 src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java delete mode 100644 src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index db951ea5..ee08fc2d 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -281,6 +281,7 @@ List findPinsInBounds( @Param("now") LocalDateTime now ); + @EntityGraph(attributePaths = {"place"}) @Query(value = """ SELECT scp.* FROM student_council_post scp @@ -311,15 +312,4 @@ List findTop3RecommendedPartnershipPlaces( @Param("now") LocalDateTime now ); - @Query(""" - SELECT scp - FROM StudentCouncilPost scp - WHERE scp.place.placeId IN :placeIds - AND scp.writer.id = :writerId - ORDER BY scp.createdAt DESC - """) - List findRepresentativePosts( - @Param("placeIds") List placeIds - ); - } diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java index 2353aad4..0fd54235 100644 --- a/src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/partnership/PartnershipResponse.java @@ -13,7 +13,7 @@ public record PartnershipResponse( Double longitude, String tag, //(ex.) 총학생회, 사회과학대학, IT공학과 boolean isLiked, - double star, //리뷰 평점 + Double star, //리뷰 평점 String partnerTitle, //제휴 제목 double distance, //거리(m) LocalDate endDate, //제휴 끝나는 시점 diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java index 569a0d13..f8778983 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java @@ -18,7 +18,7 @@ public record ReviewRequest( @NotNull @Schema(example = "3.5") - double star, + Double star, List imageUrls, diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java index c0a4a0b1..31ff73f5 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewResponse.java @@ -13,7 +13,7 @@ public record ReviewResponse( LocalDate createDate, Long placeId, String content, - double star, + Double star, List imageUrls ) { } diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java index 0776753c..70338c59 100644 --- a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java @@ -11,7 +11,7 @@ @AllArgsConstructor public enum ErrorCode implements ErrorCodeInterface { - REVIEW_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "게시글을 찾을 수 없습니다."), + REVIEW_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), NOT_REVIEW_WRITER(2701, HttpStatus.FORBIDDEN, "작성자만 해당 작업을 수행할 수 있습니다."); private final int code; diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index bc1a1765..2fde65f5 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -34,11 +34,11 @@ import com.campus.campus.domain.school.domain.entity.College; import com.campus.campus.domain.school.domain.entity.Major; import com.campus.campus.domain.school.domain.entity.School; -import com.campus.campus.domain.user.application.exception.UserNotFirstLoginException; import com.campus.campus.domain.user.application.exception.UserNotFoundException; import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; import com.campus.campus.global.oci.application.service.PresignedUrlService; +import com.campus.campus.global.oci.exception.OciObjectDeleteFailException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -59,7 +59,7 @@ public class ReviewService { @Transactional public ReviewCreateResponse writeReview(ReviewRequest request, Long userId) { User user = userRepository.findById(userId) - .orElseThrow(UserNotFirstLoginException::new); + .orElseThrow(UserNotFoundException::new); if (request.imageUrls() != null && request.imageUrls().size() > 10) { throw new PostImageLimitExceededException(); @@ -127,7 +127,7 @@ public void delete(Long userId, Long reviewId) { for (String imageUrl : deleted) { try { presignedUrlService.deleteImage(imageUrl); - } catch (PostOciImageDeleteFailedException e) { + } catch (OciObjectDeleteFailException e) { log.warn("OCI 파일 삭제 실패: {}", imageUrl, e); } } diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java index d8e652bc..8f61236f 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/Review.java @@ -33,7 +33,7 @@ public class Review extends BaseEntity { private String content; - private double star; + private Double star; //영수증 제휴 인증 여부 @Column(name = "is_verified") @@ -49,7 +49,7 @@ public class Review extends BaseEntity { public void update( String content, - double star + Double star ) { this.content = content; this.star = star; diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java deleted file mode 100644 index e348490e..00000000 --- a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImages.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.campus.campus.domain.review.domain.entity; - -import com.campus.campus.global.entity.BaseEntity; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "review_images") -@Getter -@AllArgsConstructor(access = AccessLevel.PROTECTED) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class ReviewImages extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "review_images_id") - private Long ReviewImagesId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "review_id") - private Review review; - - private String imgUrl; -} diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java deleted file mode 100644 index 365af6d6..00000000 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewImagesRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.campus.campus.domain.review.domain.repository; - -import org.springframework.data.jpa.repository.JpaRepository; - -import com.campus.campus.domain.review.domain.entity.ReviewImages; - -public interface ReviewImagesRepository extends JpaRepository { -} diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index a342cf71..defec4c6 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -85,7 +85,7 @@ public CommonResponse writeReview( @Operation(summary = "리뷰 상세 조회") public CommonResponse readReview(@PathVariable Long reviewId) { ReviewResponse response = reviewService.readReview(reviewId); - return CommonResponse.success(ReviewResponseCode.REVIEW_SAVE_SUCCESS, response); + return CommonResponse.success(ReviewResponseCode.GET_REVIEW_SUCCESS, response); } @DeleteMapping("/{reviewId}") @@ -111,7 +111,7 @@ public CommonResponse updateReview( public CommonResponse> readAllReviews( @PathVariable Long placeId, @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime cursorCreatedAt, - @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Long cursorId, + @RequestParam(required = false) Long cursorId, @RequestParam(defaultValue = "10") int size ) { CursorPageReviewResponse response = reviewService.getReviewList(placeId, cursorCreatedAt, diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java index ddfafd93..d18dbfed 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewResponseCode.java @@ -15,7 +15,8 @@ public enum ReviewResponseCode implements ResponseCodeInterface { REVIEW_DELETE_SUCCESS(200, HttpStatus.OK, "리뷰 삭제가 완료되었습니다."), REVIEW_UPDATE_SUCCESS(200, HttpStatus.OK, "리뷰 수정이 완료되었습니다."), GET_REVIEW_LIST_SUCCESS(200, HttpStatus.OK, "리뷰 리스트 조회에 성공하였습니다."), - GET_RANK_SUCCESS(200, HttpStatus.OK, "최근 한달 리뷰 순에 따른 조회 가게 조회에 성공하였습니다."); + GET_RANK_SUCCESS(200, HttpStatus.OK, "최근 한달 리뷰 순에 따른 조회 가게 조회에 성공하였습니다."), + GET_REVIEW_SUCCESS(200, HttpStatus.OK, "리뷰 상세 조회에 성공하였습니다."); private final int code; private final HttpStatus status; From 3e68965a7cf7175c1ccf788399cccc302f7072ce Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Tue, 13 Jan 2026 00:24:02 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix:=20PR=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../StudentCouncilPostRepository.java | 45 +++++++++---------- .../dto/response/SavedPlaceInfo.java | 2 - .../dto/response/ReviewCreateResponse.java | 2 +- .../dto/response/ReviewCreateResult.java | 3 +- .../dto/response/WriteReviewResponse.java | 18 ++++++++ .../application/exception/ErrorCode.java | 4 +- .../application/mapper/ReviewMapper.java | 17 ++++++- .../application/service/ReviewService.java | 34 ++++++-------- .../review/domain/entity/ReviewImage.java | 4 +- .../domain/repository/ReviewRepository.java | 4 +- 10 files changed, 79 insertions(+), 54 deletions(-) create mode 100644 src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index ee08fc2d..11dccc97 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -282,34 +282,33 @@ List findPinsInBounds( ); @EntityGraph(attributePaths = {"place"}) - @Query(value = """ - SELECT scp.* - FROM student_council_post scp - JOIN student_councils sc - ON scp.writer_id = sc.student_council_id - JOIN places p - ON scp.place_id = p.place_id - LEFT JOIN reviews r - ON r.place_id = p.place_id - AND r.created_at >= :from - WHERE scp.start_date_time <= :now - AND scp.end_date_time >= :now - AND ( - (sc.council_type = 'MAJOR_COUNCIL' AND sc.major_id = :majorId) - OR (sc.council_type = 'COLLEGE_COUNCIL' AND sc.college_id = :collegeId) - OR (sc.council_type = 'SCHOOL_COUNCIL' AND sc.school_id = :schoolId) - ) - GROUP BY scp.id - ORDER BY COUNT(r.id) DESC - LIMIT 3; - - """, nativeQuery = true) + @Query(""" + SELECT scp + FROM StudentCouncilPost scp + JOIN scp.writer sc + LEFT JOIN Review r + ON r.place = scp.place + AND r.createdAt >= :from + WHERE scp.startDateTime <= :now + AND scp.endDateTime >= :now + AND ( + (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.MAJOR_COUNCIL + AND sc.major.majorId = :majorId) + OR (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.COLLEGE_COUNCIL + AND sc.college.collegeId = :collegeId) + OR (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.SCHOOL_COUNCIL + AND sc.school.schoolId = :schoolId) + ) + GROUP BY scp + ORDER BY COUNT(r.id) DESC + """) List findTop3RecommendedPartnershipPlaces( @Param("majorId") Long majorId, @Param("collegeId") Long collegeId, @Param("schoolId") Long schoolId, @Param("from") LocalDateTime from, - @Param("now") LocalDateTime now + @Param("now") LocalDateTime now, + Pageable pageable ); } diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java index a095b140..f2ff9353 100644 --- a/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java @@ -6,7 +6,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; public record SavedPlaceInfo( @@ -33,7 +32,6 @@ public record SavedPlaceInfo( String telephone, @Schema(description = "위도/경도") - @NotNull Coordinate coordinate, @Schema(description = "이미지 url") diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java index 28d0e34c..26f12bd4 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResponse.java @@ -4,7 +4,7 @@ @Builder public record ReviewCreateResponse( - ReviewResponse review, + WriteReviewResponse review, ReviewCreateResult result, ReviewRankingResponse ranking diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java index 0581d042..4974b4d7 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/ReviewCreateResult.java @@ -5,8 +5,7 @@ @Builder public record ReviewCreateResult( boolean isFirstReviewOfPlace, - int userReviewCountOfPlace, + int userReviewCountOfPlace // int NumberOfUserStamp, - String message ) { } diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java b/src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java new file mode 100644 index 00000000..0105110f --- /dev/null +++ b/src/main/java/com/campus/campus/domain/review/application/dto/response/WriteReviewResponse.java @@ -0,0 +1,18 @@ +package com.campus.campus.domain.review.application.dto.response; + +import java.time.LocalDate; + +import lombok.Builder; + +@Builder +public record WriteReviewResponse( + Long id, + Long userId, + String userName, + LocalDate createDate, + Long placeId, + String content, + Double star, + String imageUrl +) { +} diff --git a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java index 70338c59..88a52ff4 100644 --- a/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java +++ b/src/main/java/com/campus/campus/domain/review/application/exception/ErrorCode.java @@ -11,8 +11,8 @@ @AllArgsConstructor public enum ErrorCode implements ErrorCodeInterface { - REVIEW_NOT_FOUND(2700, HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), - NOT_REVIEW_WRITER(2701, HttpStatus.FORBIDDEN, "작성자만 해당 작업을 수행할 수 있습니다."); + REVIEW_NOT_FOUND(2800, HttpStatus.NOT_FOUND, "리뷰를 찾을 수 없습니다."), + NOT_REVIEW_WRITER(2801, HttpStatus.FORBIDDEN, "작성자만 해당 작업을 수행할 수 있습니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java index 2e938b1b..9ab0cfc9 100644 --- a/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java +++ b/src/main/java/com/campus/campus/domain/review/application/mapper/ReviewMapper.java @@ -15,6 +15,7 @@ import com.campus.campus.domain.review.application.dto.response.ReviewCreateResult; import com.campus.campus.domain.review.application.dto.response.ReviewRankingResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.dto.response.WriteReviewResponse; import com.campus.campus.domain.review.domain.entity.Review; import com.campus.campus.domain.review.domain.entity.ReviewImage; import com.campus.campus.domain.user.domain.entity.User; @@ -63,6 +64,19 @@ public ReviewResponse toReviewResponse(Review review, List imageUrls) { .build(); } + public WriteReviewResponse toWriteReviewResponse(Review review, String imageUrl) { + return WriteReviewResponse.builder() + .id(review.getId()) + .userId(review.getUser().getId()) + .userName(review.getUser().getNickname()) + .createDate(review.getCreatedAt().toLocalDate()) + .placeId(review.getPlace().getPlaceId()) + .content(review.getContent()) + .star(review.getStar()) + .imageUrl(imageUrl) + .build(); + } + public CursorPageReviewResponse toCursorReviewResponse(List items, Review last, boolean hasNext) { return CursorPageReviewResponse.builder() @@ -78,7 +92,6 @@ public ReviewCreateResult toReviewCreateResult(boolean isFirstReviewOfPlace, lon .isFirstReviewOfPlace(isFirstReviewOfPlace) .userReviewCountOfPlace((int)userReviewCountOfPlace) //스탬프 추가 예정 - .message(isFirstReviewOfPlace ? "첫번째 리뷰 작성 완료!" : null) .build(); } @@ -94,7 +107,7 @@ public ReviewRankingResponse toReviewRankingResponse( ); } - public ReviewCreateResponse toReviewCreateResponse(ReviewResponse response, ReviewCreateResult createResult, + public ReviewCreateResponse toReviewCreateResponse(WriteReviewResponse response, ReviewCreateResult createResult, ReviewRankingResponse rankingResponse) { return ReviewCreateResponse.builder() .review(response) diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 2fde65f5..1111c261 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -12,7 +12,6 @@ import org.springframework.transaction.annotation.Transactional; import com.campus.campus.domain.councilpost.application.exception.PostImageLimitExceededException; -import com.campus.campus.domain.councilpost.application.exception.PostOciImageDeleteFailedException; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; import com.campus.campus.domain.councilpost.domain.repository.StudentCouncilPostRepository; import com.campus.campus.domain.place.application.service.PlaceService; @@ -24,6 +23,7 @@ import com.campus.campus.domain.review.application.dto.response.ReviewCreateResult; import com.campus.campus.domain.review.application.dto.response.ReviewRankingResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.dto.response.WriteReviewResponse; import com.campus.campus.domain.review.application.exception.NotUserWriterException; import com.campus.campus.domain.review.application.exception.ReviewNotFoundException; import com.campus.campus.domain.review.application.mapper.ReviewMapper; @@ -77,13 +77,10 @@ public ReviewCreateResponse writeReview(ReviewRequest request, Long userId) { } } - List imageUrls = reviewImageRepository - .findAllByReviewOrderByIdAsc(review) - .stream() - .map(ReviewImage::getImageUrl) - .toList(); + String imageUrl = + request.imageUrls() == null ? null : request.imageUrls().getFirst(); - ReviewResponse response = reviewMapper.toReviewResponse(review, imageUrls); + WriteReviewResponse response = reviewMapper.toWriteReviewResponse(review, imageUrl); ReviewCreateResult createResult = getCreateResult(place, user); ReviewRankingResponse rankingResponse = getRankingResult(place, user); @@ -134,7 +131,7 @@ public void delete(Long userId, Long reviewId) { } @Transactional - public ReviewResponse update(Long userId, Long reviewId, ReviewRequest request) { + public WriteReviewResponse update(Long userId, Long reviewId, ReviewRequest request) { if (request.imageUrls() != null && request.imageUrls().size() > 10) { throw new PostImageLimitExceededException(); @@ -162,13 +159,10 @@ public ReviewResponse update(Long userId, Long reviewId, ReviewRequest request) cleanupUnusedImages(oldImages, request); - List imageUrls = reviewImageRepository - .findAllByReview(review) - .stream() - .map(ReviewImage::getImageUrl) - .toList(); + String imageUrl = + request.imageUrls() == null ? null : request.imageUrls().getFirst(); - return reviewMapper.toReviewResponse(review, imageUrls); + return reviewMapper.toWriteReviewResponse(review, imageUrl); } @Transactional(readOnly = true) @@ -216,7 +210,7 @@ public CursorPageReviewResponse getReviewList( ) .toList(); - Review last = reviews.get(reviews.size() - 1); + Review last = reviews.getLast(); return reviewMapper.toCursorReviewResponse(items, last, hasNext); @@ -237,7 +231,8 @@ public List readPopularPartnerships(Long userId) { user.getCollege().getCollegeId(), user.getSchool().getSchoolId(), from, - now + now, + PageRequest.of(0, 3) ); log.info("찾은 결과:{}", partnerships.stream().toList()); @@ -270,7 +265,7 @@ private void cleanupUnusedImages(List oldImages, ReviewRequest requ try { presignedUrlService.deleteImage(imageUrl); - } catch (PostOciImageDeleteFailedException e) { + } catch (OciObjectDeleteFailException e) { log.warn("OCI 파일 삭제 실패 (파일이 없을 수 있음): {}", imageUrl, e); } } @@ -281,11 +276,10 @@ private ReviewCreateResult getCreateResult(Place place, User user) { long totalReviewCountOfPlace = reviewRepository.countByPlace_PlaceId(place.getPlaceId()); //해당 장소에서 유저가 쓴 리뷰가 몇번째인지 - long userReviewCountOfPlace = reviewRepository.countByPlace_PlaceIdAndUser_Id(place.getPlaceId(), - user.getId()); + long count = reviewRepository.countByPlaceAndUser(place, user); boolean isFirstReviewOfPlace = totalReviewCountOfPlace == 1; - return reviewMapper.toReviewCreateResult(isFirstReviewOfPlace, userReviewCountOfPlace); + return reviewMapper.toReviewCreateResult(isFirstReviewOfPlace, count); } diff --git a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java index 46b7713d..08ee39c8 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java +++ b/src/main/java/com/campus/campus/domain/review/domain/entity/ReviewImage.java @@ -1,5 +1,7 @@ package com.campus.campus.domain.review.domain.entity; +import com.campus.campus.global.entity.BaseEntity; + import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -19,7 +21,7 @@ @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class ReviewImage { +public class ReviewImage extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java index b6af261b..7b7cb900 100644 --- a/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java +++ b/src/main/java/com/campus/campus/domain/review/domain/repository/ReviewRepository.java @@ -8,7 +8,9 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import com.campus.campus.domain.place.domain.entity.Place; import com.campus.campus.domain.review.domain.entity.Review; +import com.campus.campus.domain.user.domain.entity.User; public interface ReviewRepository extends JpaRepository { @@ -32,7 +34,7 @@ List findByPlaceIdWithCursor( long countByPlace_PlaceId(long placeId); - long countByPlace_PlaceIdAndUser_Id(long placeId, long userId); + long countByPlaceAndUser(Place place, User user); long countByPlace_PlaceIdAndUser_Major_MajorId(long placeId, long majorId); From a41398ea965464b53b5a90c20dc9150c01674877 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:46:48 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20PR=202=EC=B0=A8=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/repository/StudentCouncilPostRepository.java | 7 ++++--- .../domain/place/application/service/PlaceService.java | 3 +-- .../domain/review/application/service/ReviewService.java | 6 ++++-- .../domain/review/presentation/ReviewController.java | 5 +++-- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index 679d9907..3dfc7864 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -293,13 +293,14 @@ List findPinsInBounds( WHERE scp.startDateTime <= :now AND scp.endDateTime >= :now AND ( - (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.MAJOR_COUNCIL + (sc.councilType = com.campus.campus.domain.council.domain.entity.CouncilType.MAJOR_COUNCIL AND sc.major.majorId = :majorId) - OR (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.COLLEGE_COUNCIL + OR (sc.councilType = com.campus.campus.domain.council.domain.entity.CouncilType.COLLEGE_COUNCIL AND sc.college.collegeId = :collegeId) - OR (sc.councilType = com.campus.campus.domain.studentcouncil.domain.CouncilType.SCHOOL_COUNCIL + OR (sc.councilType = com.campus.campus.domain.council.domain.entity.CouncilType.SCHOOL_COUNCIL AND sc.school.schoolId = :schoolId) ) + AND sc.deletedAt IS NULL GROUP BY scp ORDER BY COUNT(r.id) DESC """) diff --git a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java index 509e1eb3..7869c0e8 100644 --- a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java +++ b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java @@ -110,8 +110,7 @@ public List search(double lat, double lng, String keyword) { CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); return futures.stream().map(CompletableFuture::join).toList(); } - - @Transactional + public Place findOrCreatePlace(SavedPlaceInfo place) { String placeKey = place.placeKey(); diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 1111c261..62048717 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -78,7 +78,8 @@ public ReviewCreateResponse writeReview(ReviewRequest request, Long userId) { } String imageUrl = - request.imageUrls() == null ? null : request.imageUrls().getFirst(); + (request.imageUrls() == null || request.imageUrls().isEmpty()) + ? null : request.imageUrls().getFirst(); WriteReviewResponse response = reviewMapper.toWriteReviewResponse(review, imageUrl); ReviewCreateResult createResult = getCreateResult(place, user); @@ -160,7 +161,8 @@ public WriteReviewResponse update(Long userId, Long reviewId, ReviewRequest requ cleanupUnusedImages(oldImages, request); String imageUrl = - request.imageUrls() == null ? null : request.imageUrls().getFirst(); + (request.imageUrls() == null || request.imageUrls().isEmpty()) + ? null : request.imageUrls().getFirst(); return reviewMapper.toWriteReviewResponse(review, imageUrl); } diff --git a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java index defec4c6..6794ce85 100644 --- a/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java +++ b/src/main/java/com/campus/campus/domain/review/presentation/ReviewController.java @@ -19,6 +19,7 @@ import com.campus.campus.domain.review.application.dto.response.PlaceReviewRankResponse; import com.campus.campus.domain.review.application.dto.response.ReviewCreateResponse; import com.campus.campus.domain.review.application.dto.response.ReviewResponse; +import com.campus.campus.domain.review.application.dto.response.WriteReviewResponse; import com.campus.campus.domain.review.application.service.ReviewService; import com.campus.campus.global.annotation.CurrentUserId; import com.campus.campus.global.common.response.CommonResponse; @@ -97,12 +98,12 @@ public CommonResponse deleteReview(@PathVariable Long reviewId, @CurrentUs @PatchMapping("/{reviewId}") @Operation(summary = "리뷰 수정") - public CommonResponse updateReview( + public CommonResponse updateReview( @CurrentUserId Long userId, @PathVariable Long reviewId, @RequestBody @Valid ReviewRequest request ) { - ReviewResponse response = reviewService.update(userId, reviewId, request); + WriteReviewResponse response = reviewService.update(userId, reviewId, request); return CommonResponse.success(ReviewResponseCode.REVIEW_UPDATE_SUCCESS, response); } From 7fe95d49d821352290b4efaae0c635e5d1b8b79b Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:13:21 +0900 Subject: [PATCH 15/17] =?UTF-8?q?refactor:=20=EC=9D=B4=EB=AF=B8=EC=A7=80?= =?UTF-8?q?=20=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/application/service/ReviewService.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java index 62048717..f20f8d02 100644 --- a/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java +++ b/src/main/java/com/campus/campus/domain/review/application/service/ReviewService.java @@ -1,9 +1,10 @@ package com.campus.campus.domain.review.application.service; import java.time.LocalDateTime; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.springframework.data.domain.PageRequest; @@ -38,7 +39,6 @@ import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; import com.campus.campus.global.oci.application.service.PresignedUrlService; -import com.campus.campus.global.oci.exception.OciObjectDeleteFailException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -114,7 +114,7 @@ public void delete(Long userId, Long reviewId) { List reviewImages = reviewImageRepository.findAllByReview(review); - List deleted = new ArrayList<>(); + Set deleted = new HashSet<>(); reviewImages.stream() .map(ReviewImage::getImageUrl) .forEach(deleted::add); @@ -125,7 +125,7 @@ public void delete(Long userId, Long reviewId) { for (String imageUrl : deleted) { try { presignedUrlService.deleteImage(imageUrl); - } catch (OciObjectDeleteFailException e) { + } catch (Exception e) { log.warn("OCI 파일 삭제 실패: {}", imageUrl, e); } } @@ -251,7 +251,7 @@ public List readPopularPartnerships(Long userId) { //이미지 삭제 private void cleanupUnusedImages(List oldImages, ReviewRequest request) { List newUrls = request.imageUrls() == null ? List.of() : request.imageUrls(); - List deleteTargets = new ArrayList<>(); + Set deleteTargets = new HashSet<>(); // 본문 이미지 중 제거된 이미지 oldImages.stream() @@ -267,7 +267,7 @@ private void cleanupUnusedImages(List oldImages, ReviewRequest requ try { presignedUrlService.deleteImage(imageUrl); - } catch (OciObjectDeleteFailException e) { + } catch (Exception e) { log.warn("OCI 파일 삭제 실패 (파일이 없을 수 있음): {}", imageUrl, e); } } From 0c33ec3f4ab6e795c7535dba313a7fa6231a0b49 Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:41:23 +0900 Subject: [PATCH 16/17] =?UTF-8?q?refactor:=20=EC=B5=9C=EC=86=8C=20?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=EC=88=98=2010=EC=9E=90=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/review/application/dto/request/ReviewRequest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java index f8778983..bede918d 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java @@ -12,7 +12,7 @@ public record ReviewRequest( @NotNull - @Size(min = 20, message = "리뷰 내용은 최소 20자 이상이어야 합니다.") + @Size(min = 10, message = "리뷰 내용은 최소 20자 이상이어야 합니다.") @Schema(example = "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.") String content, From 3e2ce01f7b8ed06c7efc38afd1641a02029f3fcd Mon Sep 17 00:00:00 2001 From: minsikk <131092169+1224kang@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:21:36 +0900 Subject: [PATCH 17/17] =?UTF-8?q?refactor:=20PR=203=EC=B0=A8=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + .../dto/request/ReviewRequest.java | 6 ++++- .../service/KakaoOauthService.java | 23 ------------------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index 7dfc0eff..5e7af8e5 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ bin/ ### IntelliJ IDEA ### .env +.oci-keys/ .idea *.iws *.iml diff --git a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java index bede918d..5d6c11b2 100644 --- a/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java +++ b/src/main/java/com/campus/campus/domain/review/application/dto/request/ReviewRequest.java @@ -6,17 +6,21 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; public record ReviewRequest( @NotNull - @Size(min = 10, message = "리뷰 내용은 최소 20자 이상이어야 합니다.") + @Size(min = 10, message = "리뷰 내용은 최소 10자 이상이어야 합니다.") @Schema(example = "아주 정말 맛있습니다. 저의 완전 짱 또간집. 꼭꼮꼬꼬꼭 가세요.") String content, @NotNull + @DecimalMin(value = "0.0", inclusive = true) + @DecimalMax(value = "5.0", inclusive = true) @Schema(example = "3.5") Double star, diff --git a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java index 42327e6a..deacd8e6 100644 --- a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java +++ b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java @@ -16,7 +16,6 @@ import com.campus.campus.domain.user.application.mapper.UserMapper; import com.campus.campus.domain.user.domain.entity.User; import com.campus.campus.domain.user.domain.repository.UserRepository; -import com.campus.campus.global.auth.application.dto.KakaoTokenResponse; import com.campus.campus.global.auth.application.dto.KakaoUserResponse; import com.campus.campus.global.auth.application.dto.OauthLoginResponse; import com.campus.campus.global.auth.application.mapper.LoginMapper; @@ -132,26 +131,4 @@ private User findOrCreateUser(KakaoUserResponse kakaoUserResponse) { return userRepository.save(newUser); }); } - - private KakaoTokenResponse getToken(String authorizationCode) { - RestClient client = RestClient.create(); - - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("grant_type", "authorization_code"); - body.add("client_id", kakaoOauthProperty.getClientId()); - body.add("redirect_uri", kakaoOauthProperty.getRedirectUri()); - body.add("code", authorizationCode); - - if (kakaoOauthProperty.getClientSecret() != null && - !kakaoOauthProperty.getClientSecret().isBlank()) { - body.add("client_secret", kakaoOauthProperty.getClientSecret()); - } - - return client.post() - .uri(KAUTH_BASE_URL + "/oauth/token") - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(body) - .retrieve() - .body(KakaoTokenResponse.class); - } } \ No newline at end of file