From d903815eb12ad72fcee3023e301043121ed50032 Mon Sep 17 00:00:00 2001 From: yejin Date: Mon, 12 Jan 2026 17:00:15 +0900 Subject: [PATCH 01/11] =?UTF-8?q?Feat:=20=ED=8E=B8=EC=9D=98=EC=8B=9C?= =?UTF-8?q?=EC=84=A4=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20=EB=A6=AC=EB=B7=B0=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/place/entity/FoodType.java | 32 +++++++++++++ .../teamcback/domain/place/entity/Place.java | 4 ++ .../review/entity/PlaceReviewTagMap.java | 27 +++++++++++ .../domain/review/entity/Review.java | 44 +++++++++++++++++ .../domain/review/entity/ReviewTag.java | 22 +++++++++ .../domain/review/entity/ReviewTagMap.java | 24 ++++++++++ .../domain/review/entity/TagType.java | 5 ++ .../PlaceReviewTagMapRepository.java | 11 +++++ .../review/repository/ReviewRepository.java | 8 ++++ .../repository/ReviewTagMapRepository.java | 7 +++ .../repository/ReviewTagRepository.java | 7 +++ .../dto/response/SearchFacilityRes.java | 10 ++++ .../search/dto/response/SearchPlaceRes.java | 9 ++++ .../dto/response/SearchPlaceReviewTagRes.java | 25 ++++++++++ .../dto/response/SearchRoomDetailRes.java | 7 +++ .../domain/search/service/SearchService.java | 47 +++++++++++++++++-- .../teamcback/global/response/ResultCode.java | 5 +- 17 files changed, 290 insertions(+), 4 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/entity/Review.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTag.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/entity/TagType.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceReviewTagRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java b/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java new file mode 100644 index 00000000..d7522b1c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java @@ -0,0 +1,32 @@ +package devkor.com.teamcback.domain.place.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FoodType { + + KOREAN("한식"), + WESTERN("양식"), + JAPANESE("일식"), + CHINESE("중식"), + ASIAN("아시안"), + FUSION("퓨전"), + CAFE("카페"), + BAKERY("베이커리"), + SALAD("샐러드"), + PORRIDGE("죽"), + BBQ("고기요리"), + BUNSIK("분식"), + CHICKEN("치킨"), + PIZZA("피자"), + BURGER("햄버거"), + SEAFOOD("해산물"), + NOODLES("면요리"), + BISTRO("요리주점"), + BAR("바"); + + private final String type; + +} diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java index 0d2e8c41..295a59a3 100644 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java @@ -73,6 +73,10 @@ public class Place extends BaseEntity { @Column private String contact; // 연락처 등 + @Column + @Enumerated(EnumType.STRING) + private FoodType foodType; + @ManyToOne @JoinColumn(name = "building_id") private Building building; diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java b/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java new file mode 100644 index 00000000..6e0419b0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java @@ -0,0 +1,27 @@ +package devkor.com.teamcback.domain.review.entity; + +import devkor.com.teamcback.domain.place.entity.Place; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "tb_place_review_tag_map") +@NoArgsConstructor +public class PlaceReviewTagMap { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "place_id") + private Place place; + + @ManyToOne + @JoinColumn(name = "review_tag_id") + private ReviewTag reviewTag; + + @Column(nullable = false) + private int num = 0; +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java new file mode 100644 index 00000000..57114c09 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java @@ -0,0 +1,44 @@ +package devkor.com.teamcback.domain.review.entity; + +import devkor.com.teamcback.domain.common.entity.BaseEntity; +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.user.entity.User; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Table(name = "tb_review") +@NoArgsConstructor +public class Review extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private String fileUuid; // 3장까지 업로드 가능 + + @Column(nullable = false) + private double score = 0; + + @Column(nullable = false) + private boolean isRevisit; // 재방문 여부 + + @Column(nullable = false, length = 500) + private String comment = ""; // 500자까지 작성 가능 + + @ManyToOne + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne + @JoinColumn(name = "place_id") + private Place place; // 식당, 카페 등의 장소 + + @OneToMany(mappedBy = "review", cascade = CascadeType.ALL) + private List reviewTagMaps = new ArrayList<>(); // 선택한 후기 태그 +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTag.java b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTag.java new file mode 100644 index 00000000..55ea1d5a --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTag.java @@ -0,0 +1,22 @@ +package devkor.com.teamcback.domain.review.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "tb_review_tag") +@NoArgsConstructor +public class ReviewTag { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private TagType type; + + @Column(nullable = false) + private String tag; +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java new file mode 100644 index 00000000..79503679 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java @@ -0,0 +1,24 @@ +package devkor.com.teamcback.domain.review.entity; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@Table(name = "tb_review_tag_map") +@NoArgsConstructor +public class ReviewTagMap { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + // 하나의 리뷰에 태그 최대 5개 + @ManyToOne + @JoinColumn(name = "review_id") + private Review review; + + @ManyToOne + @JoinColumn(name = "review_tag_id") + private ReviewTag reviewTag; +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/TagType.java b/src/main/java/devkor/com/teamcback/domain/review/entity/TagType.java new file mode 100644 index 00000000..0da4cb27 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/TagType.java @@ -0,0 +1,5 @@ +package devkor.com.teamcback.domain.review.entity; + +public enum TagType { + TASTE, MOOD; +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java new file mode 100644 index 00000000..58b13b2c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java @@ -0,0 +1,11 @@ +package devkor.com.teamcback.domain.review.repository; + +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface PlaceReviewTagMapRepository extends JpaRepository { + List findByPlaceOrderByNumDesc(Place place); +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java new file mode 100644 index 00000000..46816efa --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java @@ -0,0 +1,8 @@ +package devkor.com.teamcback.domain.review.repository; + +import devkor.com.teamcback.domain.review.entity.Review; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewRepository extends JpaRepository { + +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java new file mode 100644 index 00000000..3c9e7fc4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.review.repository; + +import devkor.com.teamcback.domain.review.entity.ReviewTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewTagMapRepository extends JpaRepository { +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagRepository.java new file mode 100644 index 00000000..216d6a01 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagRepository.java @@ -0,0 +1,7 @@ +package devkor.com.teamcback.domain.review.repository; + +import devkor.com.teamcback.domain.review.entity.ReviewTag; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReviewTagRepository extends JpaRepository { +} diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java index 5c875df7..588cbe35 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java @@ -4,6 +4,10 @@ import devkor.com.teamcback.domain.place.entity.PlaceType; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; @Schema(description = "편의시설 정보") @Getter @@ -44,6 +48,11 @@ public class SearchFacilityRes { private String description; @Schema(description = "편의시설 별점", example = "NaN 또는 3.6666666666666665") private String starAverage; + @Schema(description = "식당인 경우 음식 카테고리", example = "한식") + private String foodTypeName; + @Setter + @Schema(description = "식당인 경우 리뷰 태그 목록") + private List tagList; public SearchFacilityRes(Place place, String imageUrl) { this.id = place.getId(); @@ -64,5 +73,6 @@ public SearchFacilityRes(Place place, String imageUrl) { this.floor = place.getFloor().intValue(); this.description = place.getDescription(); this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); + this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java index 54e7228f..eb7e1a1e 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java @@ -6,6 +6,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; @Schema(description = "건물 및 강의실 조회 결과") @Getter @@ -55,6 +58,11 @@ public class SearchPlaceRes { private String description; @Schema(description = "편의시설 별점", example = "NaN 또는 3.6666666666666665") private String starAverage; + @Schema(description = "식당인 경우 음식 카테고리", example = "한식") + private String foodTypeName; + @Setter + @Schema(description = "식당인 경우 리뷰 태그 목록") + private List tagList; public SearchPlaceRes(Place place, String imageUrl) { this.id = place.getId(); @@ -79,5 +87,6 @@ public SearchPlaceRes(Place place, String imageUrl) { this.placeType = place.getType(); this.description = place.getDescription(); this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); + this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceReviewTagRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceReviewTagRes.java new file mode 100644 index 00000000..4a064ff0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceReviewTagRes.java @@ -0,0 +1,25 @@ +package devkor.com.teamcback.domain.search.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Schema(description = "장소의 리뷰 태그 목록") +@Getter +@NoArgsConstructor +public class SearchPlaceReviewTagRes { + @Schema(description = "리뷰 태그 id", example = "1") + private Long id; + + @Schema(description = "태그 내용", example = "맛있어요") + private String tag; + + @Schema(description = "태그 개수", example = "5") + private int num; + + public SearchPlaceReviewTagRes(Long id, String tag, int num) { + this.id = id; + this.tag = tag; + this.num = num; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java index 13bbc735..c15e1af8 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java @@ -7,8 +7,11 @@ import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.place.entity.PlaceType; import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.List; import java.util.regex.Pattern; import lombok.Getter; +import lombok.Setter; @Getter public class SearchRoomDetailRes { @@ -33,6 +36,9 @@ public class SearchRoomDetailRes { private String description; private String starAverage; private String nextPlaceTime; + private String foodTypeName; + @Setter + private List tagList; public SearchRoomDetailRes(Place place, String imageUrl) { this.id = place.getId(); @@ -62,5 +68,6 @@ else if(place.isOperating()) { // 운영 중이면 종료 시간 else { // 운영 종료인 경우 여는 시간 this.nextPlaceTime = place.getOperatingTime().substring(0, 5); } + this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java index e4287db0..f9823118 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java +++ b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java @@ -15,6 +15,12 @@ import devkor.com.teamcback.domain.place.repository.PlaceImageRepository; import devkor.com.teamcback.domain.place.repository.PlaceNicknameRepository; import devkor.com.teamcback.domain.place.repository.PlaceRepository; +import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; +import devkor.com.teamcback.domain.review.entity.ReviewTag; +import devkor.com.teamcback.domain.review.entity.ReviewTagMap; +import devkor.com.teamcback.domain.review.repository.PlaceReviewTagMapRepository; +import devkor.com.teamcback.domain.review.repository.ReviewTagMapRepository; +import devkor.com.teamcback.domain.review.repository.ReviewTagRepository; import devkor.com.teamcback.domain.routes.repository.NodeRepository; import devkor.com.teamcback.domain.search.dto.request.SaveSearchLogReq; import devkor.com.teamcback.domain.search.dto.response.*; @@ -56,6 +62,8 @@ public class SearchService { private final BookmarkRepository bookmarkRepository; private final PlaceImageRepository placeImageRepository; private final NodeRepository nodeRepository; + private final PlaceReviewTagMapRepository placeReviewTagMapRepository; + private final ReviewTagRepository reviewTagRepository; private final LogUtil logUtil; private final FileUtil fileUtil; @@ -196,7 +204,15 @@ public SearchBuildingFacilityListRes searchBuildingFacilityByType(Long buildingI if(place.getFileUuid() != null) { imageUrl = fileUtil.getThumbnail(place.getFileUuid()); } - map.get(place.getFloor()).add(new SearchFacilityRes(place, imageUrl)); + + SearchFacilityRes facilityRes = new SearchFacilityRes(place, imageUrl); + + // 리뷰 내용 추가 + if(place.getType() == PlaceType.CAFE || place.getType() == PlaceType.CAFETERIA) { + facilityRes.setTagList(findPlaceReviewTagList(place)); + } + + map.get(place.getFloor()).add(facilityRes); } res.setFacilities(map); @@ -236,7 +252,14 @@ public SearchFloorInfoRes searchPlaceByBuildingFloor(Long buildingId, int floor) imageUrl = fileUtil.getThumbnail(place.getFileUuid()); } - placeResList.add(new SearchRoomDetailRes(place, imageUrl)); + SearchRoomDetailRes detailRes = new SearchRoomDetailRes(place, imageUrl); + + // 리뷰 내용 추가 + if(place.getType() == PlaceType.CAFE || place.getType() == PlaceType.CAFETERIA) { + detailRes.setTagList(findPlaceReviewTagList(place)); + } + + placeResList.add(detailRes); } List nodeList = nodeRepository.findAllByBuildingAndFloorAndTypeIn(building, floor, List.of(ENTRANCE, STAIR, ELEVATOR)) @@ -276,7 +299,14 @@ public SearchFacilityListRes searchFacilitiesWithType(PlaceType placeType) { imageUrl = fileUtil.getThumbnail(place.getFileUuid()); } - placeResList.add(new SearchPlaceRes(place, imageUrl)); + SearchPlaceRes placeRes = new SearchPlaceRes(place, imageUrl); + + // 리뷰 내용 추가 + if(place.getType() == PlaceType.CAFE || place.getType() == PlaceType.CAFETERIA) { + placeRes.setTagList(findPlaceReviewTagList(place)); + } + + placeResList.add(placeRes); } logUtil.logClick(null, null, null, placeType.toString()); return new SearchFacilityListRes(placeResList); @@ -705,4 +735,15 @@ private Building findBuilding(Long buildingId) { private Place findPlace(Long placeId) { return placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(NOT_FOUND_PLACE)); } + + private List findPlaceReviewTagList(Place place) { + List reviewTagMaps = placeReviewTagMapRepository.findByPlaceOrderByNumDesc(place); + + List tagResList = new ArrayList<>(); + for(PlaceReviewTagMap reviewTagMap : reviewTagMaps) { + tagResList.add(new SearchPlaceReviewTagRes(reviewTagMap.getReviewTag().getId(), reviewTagMap.getReviewTag().getTag(), reviewTagMap.getNum())); + } + + return tagResList; + } } diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 504d7e7b..73368048 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -91,7 +91,10 @@ public enum ResultCode { EXISTING_PLACE_FOR_DEVICE(HttpStatus.CONFLICT, 13004, "중복되는 device placeId입니다."), // 강의 14000번대 - NOT_FOUND_COURSE(HttpStatus.NOT_FOUND, 14000, "강의를 찾을 수 없습니다.") + NOT_FOUND_COURSE(HttpStatus.NOT_FOUND, 14000, "강의를 찾을 수 없습니다."), + + // 리뷰 15000번대 + NOT_FOUND_REVIEW_TAG(HttpStatus.NOT_FOUND, 15000, "리뷰 태그를 찾을 수 없습니다.") ; private final HttpStatus status; From 1aac47f281674d2f1d3ad4a6d0fca23154400f8a Mon Sep 17 00:00:00 2001 From: yejin Date: Fri, 23 Jan 2026 14:27:38 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Fix:=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=8C=8C=EC=9D=BC=EB=AA=85=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 --- .../teamcback/domain/common/util/FileUtil.java | 6 +++++- .../domain/search/service/SearchService.java | 10 +++++----- .../devkor/com/teamcback/infra/s3/S3Util.java | 15 ++++----------- src/main/resources/application.yml | 2 +- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java b/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java index 53334460..4e0d2c31 100644 --- a/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java +++ b/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java @@ -137,7 +137,7 @@ public void uploadThumb(File savedFile, MultipartFile file, Integer width, Integ InputStream inputStream = new ByteArrayInputStream(os.toByteArray()); // 저장 - String thumbSavedName = s3Util.uploadFile(inputStream, savedFile.getFileUuid(), filePath, file.getContentType()); + String thumbSavedName = s3Util.uploadFile(inputStream, savedFile.getFileOriginalName(), filePath, file.getContentType()); savedFile.setThumbSavedName(thumbSavedName); } @@ -167,6 +167,10 @@ public void deleteFile(String fileUuid) { fileRepository.deleteAll(savedFileList); } + public List getFiles(String fileUuid) { + return fileRepository.findAllByFileUuid(fileUuid); + } + public List getOriginalFiles(String fileUuid) { List fileList = fileRepository.findAllByFileUuid(fileUuid).stream().map(File::getFileSavedName).toList(); diff --git a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java index f9823118..363b7f30 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java +++ b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java @@ -427,14 +427,14 @@ public SearchPlaceDetailRes searchPlaceDetail(Long userId, Long placeId) { // 장소 사진 // TODO: 나중에 수정 String imageUrl = null; - List placeImageList; + List placeImageList = new ArrayList<>(); if(place.getFileUuid() != null) { - placeImageList = fileUtil.getThumbnailFiles(place.getFileUuid()).stream().map(image -> new SearchPlaceImageRes(0L, image)).toList(); + placeImageList = fileUtil.getFiles(place.getFileUuid()).stream().map(file -> new SearchPlaceImageRes(file.getId(), file.getThumbSavedName())).toList(); imageUrl = placeImageList.isEmpty() ? null : placeImageList.get(0).getImage(); } - else { - placeImageList = new ArrayList<>();placeImageRepository.findAllByPlace(place).stream().map(SearchPlaceImageRes::new).toList(); - } +/* else { + placeImageList = placeImageRepository.findAllByPlace(place).stream().map(SearchPlaceImageRes::new).toList(); + }*/ // 즐겨찾기 if(bookmarkRepository.existsByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(place.getId(), LocationType.PLACE, categories)) { diff --git a/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java b/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java index 64280467..156c1aae 100644 --- a/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java +++ b/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java @@ -50,12 +50,8 @@ private static ObjectMetadata setObjectMetadata(InputStream inputStream, String return metadata; } - // TODO: 추후 정리 - public String uploadFile(MultipartFile file, FilePath filePath) { - return uploadFile(file, filePath, UUID.randomUUID().toString()); - } - public String uploadFile(MultipartFile multipartFile, FilePath filePath, String fileUuid) { + public String uploadFile(MultipartFile multipartFile, FilePath filePath) { // 업로드할 파일이 존재하지 않거나 비어있으면 null 반환 if (multipartFile == null || multipartFile.isEmpty()) { return null; @@ -74,7 +70,7 @@ public String uploadFile(MultipartFile multipartFile, FilePath filePath, String } // 파일명을 UTF-8로 디코딩 - String fileName = URLDecoder.decode(fileUuid, StandardCharsets.UTF_8); + String fileName = URLDecoder.decode(multipartFile.getName(), StandardCharsets.UTF_8); // 업로드할 파일의 메타데이터 생성 ObjectMetadata metadata = setObjectMetadata(multipartFile); @@ -91,9 +87,9 @@ public String uploadFile(MultipartFile multipartFile, FilePath filePath, String return getFileUrl(fileName, filePath); } - public String uploadFile(InputStream inputStream, String fileUuid, FilePath filePath, String contentType) { + public String uploadFile(InputStream inputStream, String originalFileName, FilePath filePath, String contentType) { // 파일명을 UTF-8로 디코딩 - String fileName = URLDecoder.decode(fileUuid, StandardCharsets.UTF_8); + String fileName = URLDecoder.decode(originalFileName, StandardCharsets.UTF_8); try { // 업로드할 파일의 메타데이터 생성 @@ -139,7 +135,6 @@ public void deleteFile(String fileUrl, String filePath) { amazonS3Client.deleteObject(bucketName, filePath + fileName); } - // TODO: 추후 정리 public boolean exists(String fileUrl, FilePath filePath) { // 주어진 파일 URL로부터 파일명을 추출 String fileName = getFileNameFromFileUrl(fileUrl, filePath); @@ -166,7 +161,6 @@ public boolean exists(String fileUrl, String filePath) { return true; } - // TODO: 추후 정리 private String getFileUrl(String fileName, FilePath filePath) { // AWS S3 클라이언트를 사용하여 주어진 버킷, 파일 경로 및 파일명에 해당하는 파일의 URL을 얻어옴 return amazonS3Client.getUrl(bucketName, filePath.getPath() + fileName).toString(); @@ -177,7 +171,6 @@ private String getFileUrl(String fileName, String filePath) { return amazonS3Client.getUrl(bucketName, filePath+ fileName).toString(); } - // TODO: 추후 정리 private String getFileNameFromFileUrl(String fileUrl, FilePath filePath) { // 파일 URL에서 파일 경로 다음의 문자열부터 파일명의 끝까지 추출하여 반환 return fileUrl.substring(fileUrl.lastIndexOf(filePath.getPath()) + filePath.getPath().length()); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 625c517d..0922602f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -26,7 +26,7 @@ spring: servlet: multipart: - location: /app/temp + location: /tmp max-file-size: 5MB max-request-size: 50MB From e58a5cc44404f8489fe59cd4cad065464d18f0ef Mon Sep 17 00:00:00 2001 From: yejin Date: Fri, 23 Jan 2026 16:02:04 +0900 Subject: [PATCH 03/11] =?UTF-8?q?Feat:=20=EC=9E=A5=EC=86=8C=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=81=EC=84=B8=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/repository/FileRepository.java | 6 ++ .../domain/common/util/FileUtil.java | 12 +++ .../teamcback/domain/place/entity/Place.java | 4 +- .../domain/place/entity/PlaceImage.java | 30 ------ .../repository/PlaceImageRepository.java | 11 --- .../review/controller/ReviewController.java | 39 ++++++++ .../dto/response/GetReviewPlaceDetailRes.java | 61 ++++++++++++ .../dto/response/SearchPlaceReviewRes.java | 45 +++++++++ .../dto/response/SearchReviewImageRes.java | 18 ++++ .../review/repository/ReviewRepository.java | 5 + .../domain/review/service/ReviewService.java | 95 +++++++++++++++++++ .../dto/response/SearchPlaceImageRes.java | 6 -- .../domain/search/service/SearchService.java | 5 - 13 files changed, 283 insertions(+), 54 deletions(-) delete mode 100644 src/main/java/devkor/com/teamcback/domain/place/entity/PlaceImage.java delete mode 100644 src/main/java/devkor/com/teamcback/domain/place/repository/PlaceImageRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchReviewImageRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java diff --git a/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java b/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java index 807eb29d..d70d6f44 100644 --- a/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java @@ -8,5 +8,11 @@ public interface FileRepository extends JpaRepository { List findAllByFileUuid(String fileUuid); + List findTop10AllByFileUuidOrderBySortNumAsc(String fileUuid); + + List findTop5AllByFileUuidOrderBySortNumAsc(String fileUuid); + + List findTop3AllByFileUuidOrderBySortNumAsc(String fileUuid); + File findByFileUuidAndSortNum(String fileUuid, Long sortNum); } diff --git a/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java b/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java index 4e0d2c31..f2734f7a 100644 --- a/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java +++ b/src/main/java/devkor/com/teamcback/domain/common/util/FileUtil.java @@ -171,6 +171,18 @@ public List getFiles(String fileUuid) { return fileRepository.findAllByFileUuid(fileUuid); } + public List getTop10Files(String fileUuid) { + return fileRepository.findTop10AllByFileUuidOrderBySortNumAsc(fileUuid); + } + + public List getTop5Files(String fileUuid) { + return fileRepository.findTop5AllByFileUuidOrderBySortNumAsc(fileUuid); + } + + public List getTop3Files(String fileUuid) { + return fileRepository.findTop3AllByFileUuidOrderBySortNumAsc(fileUuid); + } + public List getOriginalFiles(String fileUuid) { List fileList = fileRepository.findAllByFileUuid(fileUuid).stream().map(File::getFileSavedName).toList(); diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java index 295a59a3..96a8c84f 100644 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java @@ -65,10 +65,10 @@ public class Place extends BaseEntity { private Integer maskIndex; @Column(nullable = false) - private Integer starSum = 0; + private double starSum = 0; @Column(nullable = false) - private Integer starNum = 0; + private int starNum = 0; @Column private String contact; // 연락처 등 diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/PlaceImage.java b/src/main/java/devkor/com/teamcback/domain/place/entity/PlaceImage.java deleted file mode 100644 index 42bddecf..00000000 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/PlaceImage.java +++ /dev/null @@ -1,30 +0,0 @@ -package devkor.com.teamcback.domain.place.entity; - -import devkor.com.teamcback.domain.common.entity.BaseEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -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.Getter; -import lombok.NoArgsConstructor; - -@Entity -@Getter -@Table(name = "tb_place_image") -@NoArgsConstructor -public class PlaceImage extends BaseEntity { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String image; // 추후 삭제 - - @ManyToOne - @JoinColumn(name = "place_id") - private Place place; -} diff --git a/src/main/java/devkor/com/teamcback/domain/place/repository/PlaceImageRepository.java b/src/main/java/devkor/com/teamcback/domain/place/repository/PlaceImageRepository.java deleted file mode 100644 index 677dbe73..00000000 --- a/src/main/java/devkor/com/teamcback/domain/place/repository/PlaceImageRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package devkor.com.teamcback.domain.place.repository; - -import devkor.com.teamcback.domain.place.entity.Place; -import devkor.com.teamcback.domain.place.entity.PlaceImage; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface PlaceImageRepository extends JpaRepository { - - List findAllByPlace(Place place); -} diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java new file mode 100644 index 00000000..3386e002 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -0,0 +1,39 @@ +package devkor.com.teamcback.domain.review.controller; + +import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; +import devkor.com.teamcback.domain.review.service.ReviewService; +import devkor.com.teamcback.global.response.CommonResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/reviews") +public class ReviewController { + + private final ReviewService reviewService; + + @Operation(summary = "리뷰가 있는 장소 상세 검색", + description = "식당, 카페") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/places/{placeId}") + public CommonResponse getReviewPlaceDetail( + @Parameter(name = "placeId", description = "장소 ID") @RequestParam Long placeId) { + + return CommonResponse.success(reviewService.getReviewPlaceDetail(placeId)); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java new file mode 100644 index 00000000..94e48411 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java @@ -0,0 +1,61 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.entity.PlaceType; +import devkor.com.teamcback.domain.search.dto.response.SearchPlaceImageRes; +import devkor.com.teamcback.domain.search.dto.response.SearchPlaceReviewTagRes; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +public class GetReviewPlaceDetailRes { + + private Long placeId; + private Long nodeId; + private Integer maskIndex; + private PlaceType type; + private String name; + private String detail; + private String operatingTime; + private boolean availability; + private boolean plugAvailability; + private boolean isOperating; + private String description; + + @Schema(description = "별점 평균") + private String starAverage; + @Schema(description = "음식 카테고리", example = "한식") + private String foodTypeName; + + @Schema(description = "장소 대표 사진 목록") + private List placeImages; + @Schema(description = "리뷰 태그 목록") + private List tagList; + @Schema(description = "리뷰 목록") + private List reviewList; + @Schema(description = "리뷰 사진 목록") + private List reviewImageList; + + public GetReviewPlaceDetailRes(Place place, List placeImageList, List reviewTagList, List reviewList, List reviewImageList) { + this.placeId = place.getId(); + this.nodeId = place.getNode().getId(); + this.maskIndex = place.getMaskIndex(); + this.type = place.getType(); + this.name = place.getName(); + this.detail = place.getDetail(); + this.operatingTime = place.getOperatingTime(); + this.availability = place.isAvailability(); + this.plugAvailability = place.isPlugAvailability(); + this.isOperating = place.isOperating(); + this.description = place.getDescription(); + this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); + this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); + this.placeImages = placeImageList; + this.tagList = reviewTagList; + this.reviewList = reviewList; + this.reviewImageList = reviewImageList; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java new file mode 100644 index 00000000..2e4afcd4 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java @@ -0,0 +1,45 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import devkor.com.teamcback.domain.review.entity.Review; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +@Getter +public class SearchPlaceReviewRes { + + @Schema(description = "후기 사용자 id") + private Long userId; + + @Schema(description = "후기 id") + private Long reviewId; + + @Schema(description = "재방문 여부") + private boolean isRevisit; + + @Schema(description = "한줄평") + private String comment; + + @Schema(description = "리뷰 작성 시기") + private String createdAt; + + @Schema(description = "리뷰별 사진 목록") + private List reviewImageRes; + + public SearchPlaceReviewRes(Review review, List reviewImageRes) { + this.userId = review.getUser() != null ? review.getUser().getUserId() : null; + this.reviewId = review.getId(); + this.isRevisit = review.isRevisit(); + this.comment = review.getComment(); + + // 작성일 변환 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy.MM.dd(E)", Locale.KOREAN); + this.createdAt = review.getCreatedAt() != null ? review.getCreatedAt().format(formatter): ""; + + // 리뷰별 사진 + this.reviewImageRes = reviewImageRes; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchReviewImageRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchReviewImageRes.java new file mode 100644 index 00000000..dd06776b --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchReviewImageRes.java @@ -0,0 +1,18 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class SearchReviewImageRes { + private Long imageId; + private String image; + + public SearchReviewImageRes(Long imageId, String image) { + this.imageId = imageId; + this.image = image; + } +} + + diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java index 46816efa..cdb794a8 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewRepository.java @@ -1,8 +1,13 @@ package devkor.com.teamcback.domain.review.repository; +import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ReviewRepository extends JpaRepository { + List findAllByPlaceOrderByCreatedAtDesc(Place place); + } diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java new file mode 100644 index 00000000..cca59d27 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -0,0 +1,95 @@ +package devkor.com.teamcback.domain.review.service; + +import devkor.com.teamcback.domain.common.entity.File; +import devkor.com.teamcback.domain.common.util.FileUtil; +import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.place.entity.PlaceType; +import devkor.com.teamcback.domain.place.repository.PlaceRepository; +import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; +import devkor.com.teamcback.domain.review.dto.response.SearchPlaceReviewRes; +import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; +import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; +import devkor.com.teamcback.domain.review.entity.Review; +import devkor.com.teamcback.domain.review.repository.PlaceReviewTagMapRepository; +import devkor.com.teamcback.domain.review.repository.ReviewRepository; +import devkor.com.teamcback.domain.search.dto.response.SearchPlaceImageRes; +import devkor.com.teamcback.domain.search.dto.response.SearchPlaceReviewTagRes; +import devkor.com.teamcback.global.exception.exception.GlobalException; +import devkor.com.teamcback.global.response.ResultCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final PlaceRepository placeRepository; + private final ReviewRepository reviewRepository; + private final PlaceReviewTagMapRepository placeReviewTagMapRepository; + private final FileUtil fileUtil; + + /** + * 리뷰 기능있는 장소 상세 조회 + */ + @Transactional(readOnly = true) + public GetReviewPlaceDetailRes getReviewPlaceDetail(Long placeId) { + // 장소 검색 + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + + // 식당, 카페만 조회 가능하도록 제한 + if(place.getType() != PlaceType.CAFETERIA && place.getType() != PlaceType.CAFE) { + throw new GlobalException(ResultCode.NOT_SUPPORTED_PLACE_TYPE); + } + + // 장소 대표 사진 조회 (사진 5장 제한 - 원본 사진) + List placeImageList = new ArrayList<>(); + if(place.getFileUuid() != null) { + placeImageList = fileUtil.getTop5Files(place.getFileUuid()).stream().map(file -> new SearchPlaceImageRes(file.getId(), file.getFileSavedName())).toList(); + } + + // 장소 리뷰 태그 조회 + List reviewTagList = findPlaceReviewTagList(place); + + // 리뷰 최신순 조회 + List reviewList = reviewRepository.findAllByPlaceOrderByCreatedAtDesc(place); + + // 리뷰 전체 이미지 조회(원본) + List totalReviewImageList = new ArrayList<>(); + // 리뷰별 이미지 조회(썸네일) + List reviewImageList = new ArrayList<>(); + + // 리뷰별 이미지 조회 + for(Review review : reviewList) { + // 리뷰별 이미지 + List reviewFiles = fileUtil.getFiles(review.getFileUuid()); + + // 썸네일 이미지 추출 + List imageResList = reviewFiles.stream().map(file -> new SearchReviewImageRes(file.getId(), file.getThumbSavedName())).toList(); + reviewImageList.add(new SearchPlaceReviewRes(review, imageResList)); + + // 리뷰 전체 이미지(원본으로 총 10장까지만) + int idx = 0; + while(totalReviewImageList.size() < 10 && idx < reviewFiles.size()) { + totalReviewImageList.add(new SearchReviewImageRes(reviewFiles.get(idx).getId(), reviewFiles.get(idx).getFileSavedName())); + idx++; + } + } + + return new GetReviewPlaceDetailRes(place, placeImageList, reviewTagList, reviewImageList, totalReviewImageList); + } + + private List findPlaceReviewTagList(Place place) { + List reviewTagMaps = placeReviewTagMapRepository.findByPlaceOrderByNumDesc(place); + + List tagResList = new ArrayList<>(); + for(PlaceReviewTagMap reviewTagMap : reviewTagMaps) { + tagResList.add(new SearchPlaceReviewTagRes(reviewTagMap.getReviewTag().getId(), reviewTagMap.getReviewTag().getTag(), reviewTagMap.getNum())); + } + + return tagResList; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceImageRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceImageRes.java index 1ad73458..8c8ad4f2 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceImageRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceImageRes.java @@ -1,6 +1,5 @@ package devkor.com.teamcback.domain.search.dto.response; -import devkor.com.teamcback.domain.place.entity.PlaceImage; import lombok.Getter; import lombok.NoArgsConstructor; @@ -10,11 +9,6 @@ public class SearchPlaceImageRes { private Long imageId; private String image; - public SearchPlaceImageRes(PlaceImage placeImage) { - this.imageId = placeImage.getId(); - this.image = placeImage.getImage(); - } - public SearchPlaceImageRes(Long imageId, String image) { this.imageId = imageId; this.image = image; diff --git a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java index 363b7f30..699c8fa4 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java +++ b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java @@ -12,14 +12,10 @@ import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.place.entity.PlaceNickname; import devkor.com.teamcback.domain.place.entity.PlaceType; -import devkor.com.teamcback.domain.place.repository.PlaceImageRepository; import devkor.com.teamcback.domain.place.repository.PlaceNicknameRepository; import devkor.com.teamcback.domain.place.repository.PlaceRepository; import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; -import devkor.com.teamcback.domain.review.entity.ReviewTag; -import devkor.com.teamcback.domain.review.entity.ReviewTagMap; import devkor.com.teamcback.domain.review.repository.PlaceReviewTagMapRepository; -import devkor.com.teamcback.domain.review.repository.ReviewTagMapRepository; import devkor.com.teamcback.domain.review.repository.ReviewTagRepository; import devkor.com.teamcback.domain.routes.repository.NodeRepository; import devkor.com.teamcback.domain.search.dto.request.SaveSearchLogReq; @@ -60,7 +56,6 @@ public class SearchService { private final UserRepository userRepository; private final CategoryRepository categoryRepository; private final BookmarkRepository bookmarkRepository; - private final PlaceImageRepository placeImageRepository; private final NodeRepository nodeRepository; private final PlaceReviewTagMapRepository placeReviewTagMapRepository; private final ReviewTagRepository reviewTagRepository; From 16efdc131f8646f68951e7ac2b3cf6e307b75702 Mon Sep 17 00:00:00 2001 From: yejin Date: Fri, 23 Jan 2026 16:07:44 +0900 Subject: [PATCH 04/11] =?UTF-8?q?Fix:=20=EA=B1=B4=EB=AC=BC,=20=EC=9E=A5?= =?UTF-8?q?=EC=86=8C=EC=9D=98=20=EB=8C=80=ED=91=9C=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=8D=B8=EB=84=A4=EC=9D=BC=20->=20=EC=9B=90?= =?UTF-8?q?=EB=B3=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teamcback/domain/search/service/SearchService.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java index 699c8fa4..00f1c16d 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java +++ b/src/main/java/devkor/com/teamcback/domain/search/service/SearchService.java @@ -370,7 +370,7 @@ public SearchBuildingDetailRes searchBuildingDetail(Long userId, Long buildingId // 건물 대표 이미지 확인 String imageUrl = null; if(building.getFileUuid() != null) { - imageUrl = fileUtil.getThumbnail(building.getFileUuid()); + imageUrl = fileUtil.getOriginalFile(building.getFileUuid()); } logUtil.logClick(building.getName(), null, null, null); return new SearchBuildingDetailRes(res, containPlaceTypes, building, imageUrl, bookmarked); @@ -420,16 +420,12 @@ public SearchPlaceDetailRes searchPlaceDetail(Long userId, Long placeId) { Place place = findPlace(placeId); // 장소 사진 - // TODO: 나중에 수정 String imageUrl = null; List placeImageList = new ArrayList<>(); if(place.getFileUuid() != null) { - placeImageList = fileUtil.getFiles(place.getFileUuid()).stream().map(file -> new SearchPlaceImageRes(file.getId(), file.getThumbSavedName())).toList(); + placeImageList = fileUtil.getFiles(place.getFileUuid()).stream().map(file -> new SearchPlaceImageRes(file.getId(), file.getFileSavedName())).toList(); imageUrl = placeImageList.isEmpty() ? null : placeImageList.get(0).getImage(); } -/* else { - placeImageList = placeImageRepository.findAllByPlace(place).stream().map(SearchPlaceImageRes::new).toList(); - }*/ // 즐겨찾기 if(bookmarkRepository.existsByLocationIdAndLocationTypeAndCategoryBookmarkList_CategoryIn(place.getId(), LocationType.PLACE, categories)) { From 56b4aa0e38c9b6866a9b02772b809d3ec5a257ba Mon Sep 17 00:00:00 2001 From: yejin Date: Fri, 23 Jan 2026 16:51:24 +0900 Subject: [PATCH 05/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC?= =?UTF-8?q?=EC=A7=84=20=EC=B6=94=EA=B0=80=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/repository/FileRepository.java | 3 +- .../review/controller/ReviewController.java | 25 ++++++++++--- .../repository/CustomFileRepository.java | 9 +++++ .../repository/CustomFileRepositoryImpl.java | 37 +++++++++++++++++++ .../domain/review/service/ReviewService.java | 35 +++++++++++++----- .../devkor/com/teamcback/infra/s3/S3Util.java | 7 +++- 6 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepository.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepositoryImpl.java diff --git a/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java b/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java index d70d6f44..801044f1 100644 --- a/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/common/repository/FileRepository.java @@ -1,11 +1,12 @@ package devkor.com.teamcback.domain.common.repository; import devkor.com.teamcback.domain.common.entity.File; +import devkor.com.teamcback.domain.review.repository.CustomFileRepository; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; -public interface FileRepository extends JpaRepository { +public interface FileRepository extends JpaRepository, CustomFileRepository { List findAllByFileUuid(String fileUuid); List findTop10AllByFileUuidOrderBySortNumAsc(String fileUuid); diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index 3386e002..edf3e5ae 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -1,6 +1,7 @@ package devkor.com.teamcback.domain.review.controller; import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; +import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; import devkor.com.teamcback.domain.review.service.ReviewService; import devkor.com.teamcback.global.response.CommonResponse; import io.swagger.v3.oas.annotations.Operation; @@ -10,10 +11,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.List; @RestController @@ -32,8 +32,23 @@ public class ReviewController { }) @GetMapping("/places/{placeId}") public CommonResponse getReviewPlaceDetail( - @Parameter(name = "placeId", description = "장소 ID") @RequestParam Long placeId) { + @Parameter(name = "placeId", description = "장소 ID") @PathVariable Long placeId) { return CommonResponse.success(reviewService.getReviewPlaceDetail(placeId)); } + + @Operation(summary = "리뷰가 있는 장소 상세 검색 - 무한스크롤로 리뷰 사진 추가 조회", + description = "식당, 카페") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/places/{placeId}/images") + public CommonResponse> getReviewPlaceDetailImages( + @Parameter(name = "placeId", description = "장소 ID") @PathVariable Long placeId, + @Parameter(name = "lastFileId", description = "마지막 조회한 사진 id") @RequestParam Long lastFileId) { + + return CommonResponse.success(reviewService.getReviewPlaceDetailImages(placeId, lastFileId)); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepository.java new file mode 100644 index 00000000..8d670bcd --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepository.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.review.repository; + +import devkor.com.teamcback.domain.common.entity.File; + +import java.util.List; + +public interface CustomFileRepository { + List getReviewFilesByPlaceWithPage(Long placeId, Long lastFileId, int size); +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepositoryImpl.java b/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepositoryImpl.java new file mode 100644 index 00000000..c4706ecb --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/CustomFileRepositoryImpl.java @@ -0,0 +1,37 @@ +package devkor.com.teamcback.domain.review.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import devkor.com.teamcback.domain.common.entity.File; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static devkor.com.teamcback.domain.common.entity.QFile.file; +import static devkor.com.teamcback.domain.review.entity.QReview.review; + +@Repository +@RequiredArgsConstructor +public class CustomFileRepositoryImpl implements CustomFileRepository { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List getReviewFilesByPlaceWithPage(Long placeId, Long lastFileId, int size) { + return jpaQueryFactory + .select(file) + .from(file) + .join(review).on(file.fileUuid.eq(review.fileUuid)) + .where( + review.place.id.eq(placeId), + file.id.gt(lastFileId) + ) + .orderBy( + review.createdAt.desc(), + file.sortNum.asc() + ) + .limit(size) + .fetch(); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index cca59d27..ef6bddd6 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -1,6 +1,7 @@ package devkor.com.teamcback.domain.review.service; import devkor.com.teamcback.domain.common.entity.File; +import devkor.com.teamcback.domain.common.repository.FileRepository; import devkor.com.teamcback.domain.common.util.FileUtil; import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.place.entity.PlaceType; @@ -31,6 +32,7 @@ public class ReviewService { private final ReviewRepository reviewRepository; private final PlaceReviewTagMapRepository placeReviewTagMapRepository; private final FileUtil fileUtil; + private final FileRepository fileRepository; /** * 리뷰 기능있는 장소 상세 조회 @@ -57,8 +59,6 @@ public GetReviewPlaceDetailRes getReviewPlaceDetail(Long placeId) { // 리뷰 최신순 조회 List reviewList = reviewRepository.findAllByPlaceOrderByCreatedAtDesc(place); - // 리뷰 전체 이미지 조회(원본) - List totalReviewImageList = new ArrayList<>(); // 리뷰별 이미지 조회(썸네일) List reviewImageList = new ArrayList<>(); @@ -70,18 +70,35 @@ public GetReviewPlaceDetailRes getReviewPlaceDetail(Long placeId) { // 썸네일 이미지 추출 List imageResList = reviewFiles.stream().map(file -> new SearchReviewImageRes(file.getId(), file.getThumbSavedName())).toList(); reviewImageList.add(new SearchPlaceReviewRes(review, imageResList)); - - // 리뷰 전체 이미지(원본으로 총 10장까지만) - int idx = 0; - while(totalReviewImageList.size() < 10 && idx < reviewFiles.size()) { - totalReviewImageList.add(new SearchReviewImageRes(reviewFiles.get(idx).getId(), reviewFiles.get(idx).getFileSavedName())); - idx++; - } } + // 리뷰 전체 이미지 조회(원본 - 10장까지) + List totalReviewImageList = fileRepository.getReviewFilesByPlaceWithPage(placeId, 0L, 10).stream().map(file -> new SearchReviewImageRes(file.getId(), file.getFileSavedName())).toList(); + return new GetReviewPlaceDetailRes(place, placeImageList, reviewTagList, reviewImageList, totalReviewImageList); } + /** + * 리뷰 기능있는 장소 상세 조회 - 리뷰 사진 추가 조회 + */ + @Transactional(readOnly = true) + public List getReviewPlaceDetailImages(Long placeId, Long lastFileId) { + // 장소 검색 + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + + // 식당, 카페만 조회 가능하도록 제한 + if(place.getType() != PlaceType.CAFETERIA && place.getType() != PlaceType.CAFE) { + throw new GlobalException(ResultCode.NOT_SUPPORTED_PLACE_TYPE); + } + + // 리뷰 전체 이미지 조회(원본 - 10장까지) + return fileRepository.getReviewFilesByPlaceWithPage(placeId, lastFileId, 10).stream().map(file -> new SearchReviewImageRes(file.getId(), file.getFileSavedName())).toList(); + + } + + /** + * 장소별 리뷰 태그 조회 + */ private List findPlaceReviewTagList(Place place) { List reviewTagMaps = placeReviewTagMapRepository.findByPlaceOrderByNumDesc(place); diff --git a/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java b/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java index 156c1aae..a2dd731f 100644 --- a/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java +++ b/src/main/java/devkor/com/teamcback/infra/s3/S3Util.java @@ -69,8 +69,13 @@ public String uploadFile(MultipartFile multipartFile, FilePath filePath) { throw new GlobalException(MAXIMUM_UPLOAD_FILE_SIZE); } + String originalName = multipartFile.getOriginalFilename(); + if(originalName == null || originalName.isEmpty()) { + originalName = UUID.randomUUID().toString(); + } + // 파일명을 UTF-8로 디코딩 - String fileName = URLDecoder.decode(multipartFile.getName(), StandardCharsets.UTF_8); + String fileName = URLDecoder.decode(originalName, StandardCharsets.UTF_8); // 업로드할 파일의 메타데이터 생성 ObjectMetadata metadata = setObjectMetadata(multipartFile); From 04b41cb046185154154996effb9b123a33331618 Mon Sep 17 00:00:00 2001 From: yejin Date: Sat, 24 Jan 2026 17:16:38 +0900 Subject: [PATCH 06/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AdminPlaceController.java | 4 +- .../review/controller/ReviewController.java | 22 +++++++ .../review/dto/request/CreateReviewReq.java | 35 ++++++++++ .../review/dto/response/CreateReviewRes.java | 14 ++++ .../review/entity/PlaceReviewTagMap.java | 8 +++ .../domain/review/entity/Review.java | 13 ++++ .../domain/review/entity/ReviewTagMap.java | 5 ++ .../PlaceReviewTagMapRepository.java | 3 + .../repository/ReviewTagMapRepository.java | 4 +- .../domain/review/service/ReviewService.java | 64 +++++++++++++++++++ .../teamcback/global/response/ResultCode.java | 1 + .../global/security/SecurityConfig.java | 1 + .../com/teamcback/infra/s3/FilePath.java | 3 +- 13 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/request/CreateReviewReq.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/CreateReviewRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/place/controller/AdminPlaceController.java b/src/main/java/devkor/com/teamcback/domain/place/controller/AdminPlaceController.java index e53d939a..2f6a3c1c 100644 --- a/src/main/java/devkor/com/teamcback/domain/place/controller/AdminPlaceController.java +++ b/src/main/java/devkor/com/teamcback/domain/place/controller/AdminPlaceController.java @@ -1,6 +1,5 @@ package devkor.com.teamcback.domain.place.controller; -import devkor.com.teamcback.domain.building.dto.response.SaveBuildingMainImageRes; import devkor.com.teamcback.domain.place.dto.request.CreatePlaceReq; import devkor.com.teamcback.domain.place.dto.request.ModifyPlaceReq; import devkor.com.teamcback.domain.place.dto.response.CreatePlaceRes; @@ -18,7 +17,6 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -92,7 +90,7 @@ public CommonResponse deletePlace( return CommonResponse.success(adminPlaceService.deletePlace(placeId)); } - @PostMapping(value = "/{placeId}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) +// @PostMapping(value = "/{placeId}/image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @Operation(summary = "장소 사진 저장", description = "장소 사진 저장(첫 사진이 대표 사진)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index edf3e5ae..ab24edff 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -1,16 +1,22 @@ package devkor.com.teamcback.domain.review.controller; +import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; +import devkor.com.teamcback.domain.review.dto.response.CreateReviewRes; import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; import devkor.com.teamcback.domain.review.service.ReviewService; import devkor.com.teamcback.global.response.CommonResponse; +import devkor.com.teamcback.global.security.UserDetailsImpl; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -51,4 +57,20 @@ public CommonResponse> getReviewPlaceDetailImages( return CommonResponse.success(reviewService.getReviewPlaceDetailImages(placeId, lastFileId)); } + + @Operation(summary = "리뷰 작성", + description = "식당, 카페에 대한 리뷰를 작성") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PostMapping(value = "/places/{placeId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResponse createReview( + @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail, + @Parameter(name = "placeId", description = "장소 ID") @PathVariable Long placeId, + @Parameter(description = "리뷰 작성 내용", required = true) @Valid @ModelAttribute CreateReviewReq createReviewReq) { + + return CommonResponse.success(reviewService.createReview(userDetail.getUser().getUserId(), placeId, createReviewReq)); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/request/CreateReviewReq.java b/src/main/java/devkor/com/teamcback/domain/review/dto/request/CreateReviewReq.java new file mode 100644 index 00000000..1dbc376c --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/request/CreateReviewReq.java @@ -0,0 +1,35 @@ +package devkor.com.teamcback.domain.review.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "저장할 리뷰 정보") +@Getter +@Setter +public class CreateReviewReq { + + @Schema(description = "별점", example = "1.5") + @DecimalMin(value = "0.5", message = "별점을 입력해주세요.") + private double score; + + @Schema(description = "재방문 여부", example = "false") + private boolean isRevisit = false; + + @Schema(description = "리뷰 태그 리스트") + @Size(max = 5, message = "태그는 최대 5개까지 가능합니다.") + private List tagIds = new ArrayList<>(); + + @Schema(description = "한줄평", example = "맛있고 좋아요.") + private String comment; + + @Size(max = 3) + @Schema(description = "첨부 사진") + private List images = new ArrayList<>(); +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/CreateReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/CreateReviewRes.java new file mode 100644 index 00000000..14fc29e0 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/CreateReviewRes.java @@ -0,0 +1,14 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Schema(description = "리뷰 생성 응답 dto") +@Getter +public class CreateReviewRes { + private Long reviewId; + + public CreateReviewRes(Long id) { + this.reviewId = id; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java b/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java index 6e0419b0..e94ddc17 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/PlaceReviewTagMap.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity @Getter @@ -22,6 +23,13 @@ public class PlaceReviewTagMap { @JoinColumn(name = "review_tag_id") private ReviewTag reviewTag; + @Setter @Column(nullable = false) private int num = 0; + + public PlaceReviewTagMap(Place place, ReviewTag tag) { + this.place = place; + this.reviewTag = tag; + this.num = 1; + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java index 57114c09..04b84a75 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java @@ -2,13 +2,16 @@ import devkor.com.teamcback.domain.common.entity.BaseEntity; import devkor.com.teamcback.domain.place.entity.Place; +import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; import devkor.com.teamcback.domain.user.entity.User; import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.util.ArrayList; import java.util.List; +import java.util.UUID; @Entity @Getter @@ -39,6 +42,16 @@ public class Review extends BaseEntity { @JoinColumn(name = "place_id") private Place place; // 식당, 카페 등의 장소 + @Setter @OneToMany(mappedBy = "review", cascade = CascadeType.ALL) private List reviewTagMaps = new ArrayList<>(); // 선택한 후기 태그 + + public Review(CreateReviewReq createReviewReq, User user, Place place) { + this.fileUuid = UUID.randomUUID().toString(); + this.score = createReviewReq.getScore(); + this.isRevisit = createReviewReq.isRevisit(); + this.comment = createReviewReq.getComment(); + this.user = user; + this.place = place; + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java index 79503679..87e87af1 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/ReviewTagMap.java @@ -21,4 +21,9 @@ public class ReviewTagMap { @ManyToOne @JoinColumn(name = "review_tag_id") private ReviewTag reviewTag; + + public ReviewTagMap(Review savedReview, ReviewTag tag) { + this.review = savedReview; + this.reviewTag = tag; + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java index 58b13b2c..9a8f8741 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/PlaceReviewTagMapRepository.java @@ -2,10 +2,13 @@ import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; +import devkor.com.teamcback.domain.review.entity.ReviewTag; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface PlaceReviewTagMapRepository extends JpaRepository { List findByPlaceOrderByNumDesc(Place place); + + PlaceReviewTagMap findByPlaceAndReviewTag(Place place, ReviewTag reviewTag); } diff --git a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java index 3c9e7fc4..8cfdbfa2 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java +++ b/src/main/java/devkor/com/teamcback/domain/review/repository/ReviewTagMapRepository.java @@ -1,7 +1,7 @@ package devkor.com.teamcback.domain.review.repository; -import devkor.com.teamcback.domain.review.entity.ReviewTag; +import devkor.com.teamcback.domain.review.entity.ReviewTagMap; import org.springframework.data.jpa.repository.JpaRepository; -public interface ReviewTagMapRepository extends JpaRepository { +public interface ReviewTagMapRepository extends JpaRepository { } diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index ef6bddd6..efdd6ac8 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -6,17 +6,26 @@ import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.place.entity.PlaceType; import devkor.com.teamcback.domain.place.repository.PlaceRepository; +import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; +import devkor.com.teamcback.domain.review.dto.response.CreateReviewRes; import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; import devkor.com.teamcback.domain.review.dto.response.SearchPlaceReviewRes; import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; import devkor.com.teamcback.domain.review.entity.Review; +import devkor.com.teamcback.domain.review.entity.ReviewTag; +import devkor.com.teamcback.domain.review.entity.ReviewTagMap; import devkor.com.teamcback.domain.review.repository.PlaceReviewTagMapRepository; import devkor.com.teamcback.domain.review.repository.ReviewRepository; +import devkor.com.teamcback.domain.review.repository.ReviewTagMapRepository; +import devkor.com.teamcback.domain.review.repository.ReviewTagRepository; import devkor.com.teamcback.domain.search.dto.response.SearchPlaceImageRes; import devkor.com.teamcback.domain.search.dto.response.SearchPlaceReviewTagRes; +import devkor.com.teamcback.domain.user.entity.User; +import devkor.com.teamcback.domain.user.repository.UserRepository; import devkor.com.teamcback.global.exception.exception.GlobalException; import devkor.com.teamcback.global.response.ResultCode; +import devkor.com.teamcback.infra.s3.FilePath; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,9 +39,12 @@ public class ReviewService { private final PlaceRepository placeRepository; private final ReviewRepository reviewRepository; + private final ReviewTagRepository reviewTagRepository; + private final ReviewTagMapRepository reviewTagMapRepository; private final PlaceReviewTagMapRepository placeReviewTagMapRepository; private final FileUtil fileUtil; private final FileRepository fileRepository; + private final UserRepository userRepository; /** * 리뷰 기능있는 장소 상세 조회 @@ -96,6 +108,58 @@ public List getReviewPlaceDetailImages(Long placeId, Long } + /** + * 리뷰 작성 + */ + @Transactional + public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq createReviewReq) { + // 사용자 검색 + User user = userRepository.findById(userId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + + // 장소 검색 + Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + + // 첨부파일 검사 (최대 3개) + if(createReviewReq.getImages().size() > 3) { + throw new GlobalException(ResultCode.EXCEEDED_MAXIMUM_FILE_COUNT); + } + + // 리뷰 저장 + Review savedReview = reviewRepository.save(new Review(createReviewReq, user, place)); + + // 사진 저장 + fileUtil.upload(createReviewReq.getImages(), savedReview.getFileUuid(), null, FilePath.REVIEW); + + // 리뷰 태그 저장 + List reviewTagMaps = new ArrayList<>(); + for(Long tagId : createReviewReq.getTagIds()) { + ReviewTag tag = reviewTagRepository.findById(tagId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW_TAG)); + + // 리뷰 태그 저장 + ReviewTagMap reviewTagMap = reviewTagMapRepository.save(new ReviewTagMap(savedReview, tag)); + reviewTagMaps.add(reviewTagMap); + + // 장소 태그 저장 + PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(place, tag); + + if(placeReviewTagMap != null) { + placeReviewTagMap.setNum(placeReviewTagMap.getNum() + 1); + } + else { + placeReviewTagMapRepository.save(new PlaceReviewTagMap(place, tag)); + } + } + + // 리뷰에 태그 목록 저장 + savedReview.setReviewTagMaps(reviewTagMaps); + + // 장소 별점 추가 + place.setStarNum(place.getStarNum() + 1); + place.setStarSum(place.getStarSum() + createReviewReq.getScore()); + + return new CreateReviewRes(savedReview.getId()); + } + /** * 장소별 리뷰 태그 조회 */ diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 73368048..1046f403 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -24,6 +24,7 @@ public enum ResultCode { FORBIDDEN(HttpStatus.FORBIDDEN, 1013, "권한이 없는 사용자입니다."), DB_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 1014, "DB 데이터 문제가 발생했습니다."), UNSUPPORTED_REQUEST(HttpStatus.NOT_FOUND, 1015, "존재하지 않는 요청입니다."), + EXCEEDED_MAXIMUM_FILE_COUNT(HttpStatus.BAD_REQUEST, 1016, "최대 파일 개수를 초과했습니다."), // 사용자 2000번대 NOT_FOUND_USER(HttpStatus.NOT_FOUND, 2000, "사용자를 찾을 수 없습니다."), diff --git a/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java b/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java index c9b1a62c..ab990384 100644 --- a/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java +++ b/src/main/java/devkor/com/teamcback/global/security/SecurityConfig.java @@ -90,6 +90,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/api/admin/**").hasRole("ADMIN") // 관리자인 경우에만 허용 .requestMatchers("/api/categories/**").authenticated() .requestMatchers("/api/bookmarks/**").authenticated() + .requestMatchers(HttpMethod.POST, "/api/reviews/**").authenticated() // 리뷰는 로그인 필요 .anyRequest().permitAll() ).exceptionHandling(ex -> ex .accessDeniedHandler(customAccessDeniedHandler()) // 인가 실패 시 diff --git a/src/main/java/devkor/com/teamcback/infra/s3/FilePath.java b/src/main/java/devkor/com/teamcback/infra/s3/FilePath.java index 18d7c776..4c75cbbe 100644 --- a/src/main/java/devkor/com/teamcback/infra/s3/FilePath.java +++ b/src/main/java/devkor/com/teamcback/infra/s3/FilePath.java @@ -10,7 +10,8 @@ public enum FilePath { // 파일 경로를 나타내는 상수를 정의 BUILDING("building/"), PLACE("place/"), BUILDING_IMAGE("buildingImage/"), - SUGGESTION("suggestion/"); + SUGGESTION("suggestion/"), + REVIEW("review/"); private final String path; // 경로를 저장하는 final 필드 } From d66af74fbbab920d96474eab7f5ac98b37237fbe Mon Sep 17 00:00:00 2001 From: yejin Date: Sun, 25 Jan 2026 15:16:24 +0900 Subject: [PATCH 07/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 19 +++++ .../review/dto/request/ModifyReviewReq.java | 35 ++++++++ .../review/dto/response/ModifyReviewRes.java | 14 ++++ .../domain/review/entity/Review.java | 8 ++ .../domain/review/service/ReviewService.java | 79 ++++++++++++++++--- .../teamcback/global/response/ResultCode.java | 3 +- 6 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/request/ModifyReviewReq.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/ModifyReviewRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index ab24edff..8963cb9d 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -1,8 +1,10 @@ package devkor.com.teamcback.domain.review.controller; import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; +import devkor.com.teamcback.domain.review.dto.request.ModifyReviewReq; import devkor.com.teamcback.domain.review.dto.response.CreateReviewRes; import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; +import devkor.com.teamcback.domain.review.dto.response.ModifyReviewRes; import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; import devkor.com.teamcback.domain.review.service.ReviewService; import devkor.com.teamcback.global.response.CommonResponse; @@ -58,6 +60,7 @@ public CommonResponse> getReviewPlaceDetailImages( return CommonResponse.success(reviewService.getReviewPlaceDetailImages(placeId, lastFileId)); } + // TODO: 리뷰 작성 시 포인트 부여 @Operation(summary = "리뷰 작성", description = "식당, 카페에 대한 리뷰를 작성") @ApiResponses(value = { @@ -73,4 +76,20 @@ public CommonResponse createReview( return CommonResponse.success(reviewService.createReview(userDetail.getUser().getUserId(), placeId, createReviewReq)); } + + @Operation(summary = "리뷰 수정", + description = "식당, 카페에 대한 리뷰를 수정") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @PostMapping(value = "/{reviewId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public CommonResponse modifyReview( + @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail, + @Parameter(name = "reviewId", description = "리뷰 ID") @PathVariable Long reviewId, + @Parameter(description = "리뷰 작성 내용", required = true) @Valid @ModelAttribute ModifyReviewReq modifyReviewReq) { + + return CommonResponse.success(reviewService.modifyReview(userDetail.getUser().getUserId(), reviewId, modifyReviewReq)); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/request/ModifyReviewReq.java b/src/main/java/devkor/com/teamcback/domain/review/dto/request/ModifyReviewReq.java new file mode 100644 index 00000000..77818cae --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/request/ModifyReviewReq.java @@ -0,0 +1,35 @@ +package devkor.com.teamcback.domain.review.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "수정할 리뷰 정보") +@Getter +@Setter +public class ModifyReviewReq { + + @Schema(description = "별점", example = "1.5") + @DecimalMin(value = "0.5", message = "별점을 입력해주세요.") + private double score; + + @Schema(description = "재방문 여부", example = "false") + private boolean isRevisit = false; + + @Schema(description = "리뷰 태그 리스트") + @Size(max = 5, message = "태그는 최대 5개까지 가능합니다.") + private List tagIds = new ArrayList<>(); + + @Schema(description = "한줄평", example = "맛있고 좋아요.") + private String comment; + + @Size(max = 3) + @Schema(description = "첨부 사진") + private List images = new ArrayList<>(); +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/ModifyReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/ModifyReviewRes.java new file mode 100644 index 00000000..fdd545cb --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/ModifyReviewRes.java @@ -0,0 +1,14 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +@Schema(description = "리뷰 수정 응답 dto") +@Getter +public class ModifyReviewRes { + private Long reviewId; + + public ModifyReviewRes(Long id) { + this.reviewId = id; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java index 04b84a75..b626c2f2 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java +++ b/src/main/java/devkor/com/teamcback/domain/review/entity/Review.java @@ -3,8 +3,10 @@ import devkor.com.teamcback.domain.common.entity.BaseEntity; import devkor.com.teamcback.domain.place.entity.Place; import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; +import devkor.com.teamcback.domain.review.dto.request.ModifyReviewReq; import devkor.com.teamcback.domain.user.entity.User; import jakarta.persistence.*; +import jakarta.validation.Valid; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -54,4 +56,10 @@ public Review(CreateReviewReq createReviewReq, User user, Place place) { this.user = user; this.place = place; } + + public void modify(@Valid ModifyReviewReq modifyReviewReq) { + this.score = modifyReviewReq.getScore(); + this.isRevisit = modifyReviewReq.isRevisit(); + this.comment = modifyReviewReq.getComment(); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index efdd6ac8..cf77a6cb 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -7,10 +7,8 @@ import devkor.com.teamcback.domain.place.entity.PlaceType; import devkor.com.teamcback.domain.place.repository.PlaceRepository; import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; -import devkor.com.teamcback.domain.review.dto.response.CreateReviewRes; -import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; -import devkor.com.teamcback.domain.review.dto.response.SearchPlaceReviewRes; -import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; +import devkor.com.teamcback.domain.review.dto.request.ModifyReviewReq; +import devkor.com.teamcback.domain.review.dto.response.*; import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; import devkor.com.teamcback.domain.review.entity.Review; import devkor.com.teamcback.domain.review.entity.ReviewTag; @@ -26,6 +24,7 @@ import devkor.com.teamcback.global.exception.exception.GlobalException; import devkor.com.teamcback.global.response.ResultCode; import devkor.com.teamcback.infra.s3.FilePath; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -119,11 +118,6 @@ public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq c // 장소 검색 Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); - // 첨부파일 검사 (최대 3개) - if(createReviewReq.getImages().size() > 3) { - throw new GlobalException(ResultCode.EXCEEDED_MAXIMUM_FILE_COUNT); - } - // 리뷰 저장 Review savedReview = reviewRepository.save(new Review(createReviewReq, user, place)); @@ -160,6 +154,73 @@ public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq c return new CreateReviewRes(savedReview.getId()); } + /** + * 리뷰 수정 + */ + @Transactional + public ModifyReviewRes modifyReview(Long userId, Long reviewId, @Valid ModifyReviewReq modifyReviewReq) { + // 사용자 검색 + User user = userRepository.findById(userId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + + // 리뷰 검색 + Review review = reviewRepository.findById(reviewId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW)); + + // 권한 검사 (자신이 작성한 리뷰만 수정 가능) + if(!user.getUserId().equals(review.getUser().getUserId())) { + throw new GlobalException(ResultCode.UNAUTHORIZED); + } + + // 사진 삭제 + fileUtil.deleteFile(review.getFileUuid()); + + // 사진 저장 + fileUtil.upload(modifyReviewReq.getImages(), review.getFileUuid(), null, FilePath.REVIEW); + + // 리뷰 수정 + review.modify(modifyReviewReq); + + // 기존 태그 삭제 + List reviewTagMaps = review.getReviewTagMaps(); + review.setReviewTagMaps(null); + + for(ReviewTagMap reviewTagMap : reviewTagMaps) { + // 장소 태그 수정 + PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(review.getPlace(), reviewTagMap.getReviewTag()); + placeReviewTagMap.setNum(placeReviewTagMap.getNum() - 1); + + // 리뷰 태그 삭제 + reviewTagMapRepository.delete(reviewTagMap); + } + + // 태그 저장 + reviewTagMaps = new ArrayList<>(); + for(Long tagId : modifyReviewReq.getTagIds()) { + ReviewTag tag = reviewTagRepository.findById(tagId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW_TAG)); + + // 리뷰 태그 저장 + ReviewTagMap reviewTagMap = reviewTagMapRepository.save(new ReviewTagMap(review, tag)); + reviewTagMaps.add(reviewTagMap); + + // 장소 태그 저장 + PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(review.getPlace(), tag); + + if(placeReviewTagMap != null) { + placeReviewTagMap.setNum(placeReviewTagMap.getNum() + 1); + } + else { + placeReviewTagMapRepository.save(new PlaceReviewTagMap(review.getPlace(), tag)); + } + } + + // 리뷰에 태그 목록 저장 + review.setReviewTagMaps(reviewTagMaps); + + // 장소 평점 수정 + review.getPlace().setStarSum(review.getPlace().getStarSum() - review.getScore() + modifyReviewReq.getScore()); + + return new ModifyReviewRes(review.getId()); + } + /** * 장소별 리뷰 태그 조회 */ diff --git a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java index 1046f403..2e79b076 100644 --- a/src/main/java/devkor/com/teamcback/global/response/ResultCode.java +++ b/src/main/java/devkor/com/teamcback/global/response/ResultCode.java @@ -95,7 +95,8 @@ public enum ResultCode { NOT_FOUND_COURSE(HttpStatus.NOT_FOUND, 14000, "강의를 찾을 수 없습니다."), // 리뷰 15000번대 - NOT_FOUND_REVIEW_TAG(HttpStatus.NOT_FOUND, 15000, "리뷰 태그를 찾을 수 없습니다.") + NOT_FOUND_REVIEW_TAG(HttpStatus.NOT_FOUND, 15000, "리뷰 태그를 찾을 수 없습니다."), + NOT_FOUND_REVIEW(HttpStatus.NOT_FOUND, 15001, "리뷰를 찾을 수 없습니다.") ; private final HttpStatus status; From 198e3369a0a4e7af9dab238a17d33aa5212c9ea0 Mon Sep 17 00:00:00 2001 From: yejin Date: Sun, 25 Jan 2026 15:36:55 +0900 Subject: [PATCH 08/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 22 ++- .../review/dto/response/DeleteReviewRes.java | 9 + .../domain/review/service/ReviewService.java | 167 ++++++++++++------ 3 files changed, 139 insertions(+), 59 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/DeleteReviewRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index 8963cb9d..8422deb2 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -2,10 +2,7 @@ import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; import devkor.com.teamcback.domain.review.dto.request.ModifyReviewReq; -import devkor.com.teamcback.domain.review.dto.response.CreateReviewRes; -import devkor.com.teamcback.domain.review.dto.response.GetReviewPlaceDetailRes; -import devkor.com.teamcback.domain.review.dto.response.ModifyReviewRes; -import devkor.com.teamcback.domain.review.dto.response.SearchReviewImageRes; +import devkor.com.teamcback.domain.review.dto.response.*; import devkor.com.teamcback.domain.review.service.ReviewService; import devkor.com.teamcback.global.response.CommonResponse; import devkor.com.teamcback.global.security.UserDetailsImpl; @@ -84,7 +81,7 @@ public CommonResponse createReview( @ApiResponse(responseCode = "401", description = "권한이 없습니다.", content = @Content(schema = @Schema(implementation = CommonResponse.class))), }) - @PostMapping(value = "/{reviewId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PutMapping(value = "/{reviewId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public CommonResponse modifyReview( @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail, @Parameter(name = "reviewId", description = "리뷰 ID") @PathVariable Long reviewId, @@ -92,4 +89,19 @@ public CommonResponse modifyReview( return CommonResponse.success(reviewService.modifyReview(userDetail.getUser().getUserId(), reviewId, modifyReviewReq)); } + + // TODO: 리뷰 삭제 시 포인트 제거 + @Operation(summary = "리뷰 삭제", + description = "식당, 카페에 대한 리뷰를 삭제") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @DeleteMapping( "/{reviewId}") + public CommonResponse deleteReview( + @Parameter(description = "사용자정보", required = true) @AuthenticationPrincipal UserDetailsImpl userDetail, + @Parameter(name = "reviewId", description = "리뷰 ID") @PathVariable Long reviewId) { + return CommonResponse.success(reviewService.deleteReview(userDetail.getUser().getUserId(), reviewId)); + } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/DeleteReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/DeleteReviewRes.java new file mode 100644 index 00000000..cad485c2 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/DeleteReviewRes.java @@ -0,0 +1,9 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "리뷰 삭제 응답 dto") +@JsonIgnoreProperties +public class DeleteReviewRes { +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index cf77a6cb..2c1b69d4 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -41,9 +41,9 @@ public class ReviewService { private final ReviewTagRepository reviewTagRepository; private final ReviewTagMapRepository reviewTagMapRepository; private final PlaceReviewTagMapRepository placeReviewTagMapRepository; - private final FileUtil fileUtil; private final FileRepository fileRepository; private final UserRepository userRepository; + private final FileUtil fileUtil; /** * 리뷰 기능있는 장소 상세 조회 @@ -51,7 +51,7 @@ public class ReviewService { @Transactional(readOnly = true) public GetReviewPlaceDetailRes getReviewPlaceDetail(Long placeId) { // 장소 검색 - Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + Place place = findPlaceById(placeId); // 식당, 카페만 조회 가능하도록 제한 if(place.getType() != PlaceType.CAFETERIA && place.getType() != PlaceType.CAFE) { @@ -95,7 +95,7 @@ public GetReviewPlaceDetailRes getReviewPlaceDetail(Long placeId) { @Transactional(readOnly = true) public List getReviewPlaceDetailImages(Long placeId, Long lastFileId) { // 장소 검색 - Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + Place place = findPlaceById(placeId); // 식당, 카페만 조회 가능하도록 제한 if(place.getType() != PlaceType.CAFETERIA && place.getType() != PlaceType.CAFE) { @@ -113,10 +113,10 @@ public List getReviewPlaceDetailImages(Long placeId, Long @Transactional public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq createReviewReq) { // 사용자 검색 - User user = userRepository.findById(userId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + User user = findUserById(userId); // 장소 검색 - Place place = placeRepository.findById(placeId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + Place place = findPlaceById(placeId); // 리뷰 저장 Review savedReview = reviewRepository.save(new Review(createReviewReq, user, place)); @@ -125,27 +125,7 @@ public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq c fileUtil.upload(createReviewReq.getImages(), savedReview.getFileUuid(), null, FilePath.REVIEW); // 리뷰 태그 저장 - List reviewTagMaps = new ArrayList<>(); - for(Long tagId : createReviewReq.getTagIds()) { - ReviewTag tag = reviewTagRepository.findById(tagId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW_TAG)); - - // 리뷰 태그 저장 - ReviewTagMap reviewTagMap = reviewTagMapRepository.save(new ReviewTagMap(savedReview, tag)); - reviewTagMaps.add(reviewTagMap); - - // 장소 태그 저장 - PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(place, tag); - - if(placeReviewTagMap != null) { - placeReviewTagMap.setNum(placeReviewTagMap.getNum() + 1); - } - else { - placeReviewTagMapRepository.save(new PlaceReviewTagMap(place, tag)); - } - } - - // 리뷰에 태그 목록 저장 - savedReview.setReviewTagMaps(reviewTagMaps); + saveReviewTagMap(savedReview, createReviewReq.getTagIds()); // 장소 별점 추가 place.setStarNum(place.getStarNum() + 1); @@ -160,15 +140,13 @@ public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq c @Transactional public ModifyReviewRes modifyReview(Long userId, Long reviewId, @Valid ModifyReviewReq modifyReviewReq) { // 사용자 검색 - User user = userRepository.findById(userId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + User user = findUserById(userId); // 리뷰 검색 - Review review = reviewRepository.findById(reviewId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW)); + Review review = findReviewById(reviewId); // 권한 검사 (자신이 작성한 리뷰만 수정 가능) - if(!user.getUserId().equals(review.getUser().getUserId())) { - throw new GlobalException(ResultCode.UNAUTHORIZED); - } + validateUser(user, review); // 사진 삭제 fileUtil.deleteFile(review.getFileUuid()); @@ -180,21 +158,100 @@ public ModifyReviewRes modifyReview(Long userId, Long reviewId, @Valid ModifyRev review.modify(modifyReviewReq); // 기존 태그 삭제 - List reviewTagMaps = review.getReviewTagMaps(); - review.setReviewTagMaps(null); + deleteReviewTagMap(review); - for(ReviewTagMap reviewTagMap : reviewTagMaps) { - // 장소 태그 수정 - PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(review.getPlace(), reviewTagMap.getReviewTag()); - placeReviewTagMap.setNum(placeReviewTagMap.getNum() - 1); + // 태그 저장 + saveReviewTagMap(review, modifyReviewReq.getTagIds()); - // 리뷰 태그 삭제 - reviewTagMapRepository.delete(reviewTagMap); + // 장소 평점 수정 + review.getPlace().setStarSum(review.getPlace().getStarSum() - review.getScore() + modifyReviewReq.getScore()); + + return new ModifyReviewRes(review.getId()); + } + + /** + * 리뷰 삭제 + */ + @Transactional + public DeleteReviewRes deleteReview(Long userId, Long reviewId) { + // 사용자 검색 + User user = findUserById(userId); + + // 리뷰 검색 + Review review = findReviewById(reviewId); + + // 권한 검사 (자신이 작성한 리뷰만 삭제 가능) + validateUser(user, review); + + // 리뷰 태그 삭제 + deleteReviewTagMap(review); + + // 사진 삭제 + fileUtil.deleteFile(review.getFileUuid()); + + // 장소 평정 수정 + review.getPlace().setStarNum(review.getPlace().getStarNum() - 1); + review.getPlace().setStarSum(review.getPlace().getStarSum() - review.getScore()); + + // 리뷰 삭제 + reviewRepository.delete(review); + + return new DeleteReviewRes(); + } + + /** + * 장소 검색 + */ + private Place findPlaceById(Long id) { + return placeRepository.findById(id).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_PLACE)); + } + + /** + * 사용자 검색 + */ + private User findUserById(Long userId) { + return userRepository.findById(userId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_USER)); + } + + /** + * 리뷰 검색 + */ + private Review findReviewById(Long reviewId) { + return reviewRepository.findById(reviewId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW)); + } + + /** + * 장소별 리뷰 태그 조회 + */ + private List findPlaceReviewTagList(Place place) { + List reviewTagMaps = placeReviewTagMapRepository.findByPlaceOrderByNumDesc(place); + + List tagResList = new ArrayList<>(); + for(PlaceReviewTagMap reviewTagMap : reviewTagMaps) { + tagResList.add(new SearchPlaceReviewTagRes(reviewTagMap.getReviewTag().getId(), reviewTagMap.getReviewTag().getTag(), reviewTagMap.getNum())); + } + + return tagResList; + } + + /** + * 리뷰 권한 검사 + */ + private void validateUser(User user, Review review) { + // 권한 검사 (자신이 작성한 리뷰만 수정 가능) + if(!user.getUserId().equals(review.getUser().getUserId())) { + throw new GlobalException(ResultCode.UNAUTHORIZED); } + } + + /** + * 리뷰 태그 저장 + */ + private void saveReviewTagMap(Review review, List reviewTagIds) { // 태그 저장 - reviewTagMaps = new ArrayList<>(); - for(Long tagId : modifyReviewReq.getTagIds()) { + List reviewTagMaps = new ArrayList<>(); + for(Long tagId : reviewTagIds) { ReviewTag tag = reviewTagRepository.findById(tagId).orElseThrow(() -> new GlobalException(ResultCode.NOT_FOUND_REVIEW_TAG)); // 리뷰 태그 저장 @@ -214,24 +271,26 @@ public ModifyReviewRes modifyReview(Long userId, Long reviewId, @Valid ModifyRev // 리뷰에 태그 목록 저장 review.setReviewTagMaps(reviewTagMaps); - - // 장소 평점 수정 - review.getPlace().setStarSum(review.getPlace().getStarSum() - review.getScore() + modifyReviewReq.getScore()); - - return new ModifyReviewRes(review.getId()); } /** - * 장소별 리뷰 태그 조회 + * 기존 리뷰 태그 삭제 */ - private List findPlaceReviewTagList(Place place) { - List reviewTagMaps = placeReviewTagMapRepository.findByPlaceOrderByNumDesc(place); + private void deleteReviewTagMap(Review review) { + // 기존 태그 삭제 + List reviewTagMaps = review.getReviewTagMaps(); + review.setReviewTagMaps(null); - List tagResList = new ArrayList<>(); - for(PlaceReviewTagMap reviewTagMap : reviewTagMaps) { - tagResList.add(new SearchPlaceReviewTagRes(reviewTagMap.getReviewTag().getId(), reviewTagMap.getReviewTag().getTag(), reviewTagMap.getNum())); - } + for(ReviewTagMap reviewTagMap : reviewTagMaps) { + // 장소 태그 수정 + PlaceReviewTagMap placeReviewTagMap = placeReviewTagMapRepository.findByPlaceAndReviewTag(review.getPlace(), reviewTagMap.getReviewTag()); + placeReviewTagMap.setNum(placeReviewTagMap.getNum() - 1); + if(placeReviewTagMap.getNum() <= 0) { + placeReviewTagMapRepository.delete(placeReviewTagMap); + } - return tagResList; + // 리뷰 태그 삭제 + reviewTagMapRepository.delete(reviewTagMap); + } } } From 57b17776fc129e910c167bb10c1b14877ed97fa7 Mon Sep 17 00:00:00 2001 From: yejin Date: Sun, 25 Jan 2026 15:54:03 +0900 Subject: [PATCH 09/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 13 +++++ .../dto/response/GetReviewPlaceDetailRes.java | 1 - .../review/dto/response/GetReviewRes.java | 53 +++++++++++++++++++ .../review/dto/response/GetReviewTagRes.java | 18 +++++++ .../dto/response/SearchPlaceReviewRes.java | 4 +- .../domain/review/service/ReviewService.java | 20 +++++++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewRes.java create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index 8422deb2..dc3fd323 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -74,6 +74,19 @@ public CommonResponse createReview( return CommonResponse.success(reviewService.createReview(userDetail.getUser().getUserId(), placeId, createReviewReq)); } + @Operation(summary = "리뷰 조회", + description = "식당, 카페에 대한 리뷰 조회") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/{reviewId}") + public CommonResponse getReview( + @Parameter(name = "reviewId", description = "리뷰 ID") @PathVariable Long reviewId) { + return CommonResponse.success(reviewService.getReview(reviewId)); + } + @Operation(summary = "리뷰 수정", description = "식당, 카페에 대한 리뷰를 수정") @ApiResponses(value = { diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java index 94e48411..9cf1b561 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java @@ -6,7 +6,6 @@ import devkor.com.teamcback.domain.search.dto.response.SearchPlaceReviewTagRes; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; -import lombok.Setter; import java.util.List; diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewRes.java new file mode 100644 index 00000000..10918d5a --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewRes.java @@ -0,0 +1,53 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import devkor.com.teamcback.domain.review.entity.Review; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Locale; + +@Getter +public class GetReviewRes { + @Schema(description = "리뷰 id") + private Long reviewId; + @Schema(description = "리뷰 사용자 fineUuid") + private String fileUuid; + @Schema(description = "별점") + private double score; + @Schema(description = "재방문 여부") + private boolean isRevisit; + @Schema(description = "한줄평") + private String comment; + @Schema(description = "리뷰 장소 id") + private Long placeId; + @Schema(description = "리뷰 사용자 id") + private Long userId; + @Schema(description = "리뷰 작성일") + private String createdAt; + @Schema(description = "리뷰 태그 목록") + private List tagList; + @Schema(description = "리뷰 사진 목록") + private List reviewImageList; + + public GetReviewRes(Review review, List tagList, List reviewImageRes) { + this.userId = review.getUser() != null ? review.getUser().getUserId() : null; + this.reviewId = review.getId(); + this.placeId = review.getPlace().getId(); + this.score = review.getScore(); + this.fileUuid = review.getFileUuid(); + this.isRevisit = review.isRevisit(); + this.comment = review.getComment(); + + // 작성일 변환 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yy.MM.dd(E)", Locale.KOREAN); + this.createdAt = review.getCreatedAt() != null ? review.getCreatedAt().format(formatter): ""; + + // 리뷰 태그 + this.tagList = tagList; + + // 리뷰 사진 + this.reviewImageList = reviewImageRes; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java new file mode 100644 index 00000000..f6d15063 --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java @@ -0,0 +1,18 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import devkor.com.teamcback.domain.review.entity.ReviewTag; +import devkor.com.teamcback.domain.review.entity.TagType; +import lombok.Getter; + +@Getter +public class GetReviewTagRes { + private Long reviewTagId; + private String tag; + private TagType type; + + public GetReviewTagRes(ReviewTag reviewTag) { + this.reviewTagId = reviewTag.getId(); + this.tag = reviewTag.getTag(); + this.type = reviewTag.getType(); + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java index 2e4afcd4..b9f71187 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/SearchPlaceReviewRes.java @@ -11,10 +11,10 @@ @Getter public class SearchPlaceReviewRes { - @Schema(description = "후기 사용자 id") + @Schema(description = "리뷰 사용자 id") private Long userId; - @Schema(description = "후기 id") + @Schema(description = "리뷰 id") private Long reviewId; @Schema(description = "재방문 여부") diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index 2c1b69d4..49c4fc69 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -134,6 +134,26 @@ public CreateReviewRes createReview(Long userId, Long placeId, CreateReviewReq c return new CreateReviewRes(savedReview.getId()); } + /** + * 리뷰 조회 + */ + @Transactional(readOnly = true) + public GetReviewRes getReview(Long reviewId) { + // 리뷰 검색 + Review review = findReviewById(reviewId); + + // 리뷰 태그 조회 + List tagResList = new ArrayList<>(); + for(ReviewTagMap reviewTagMap : review.getReviewTagMaps()) { + tagResList.add(new GetReviewTagRes(reviewTagMap.getReviewTag())); + } + + // 리뷰 이미지 조회 + List imageResList = fileUtil.getFiles(review.getFileUuid()).stream().map(file -> new SearchReviewImageRes(file.getId(), file.getFileSavedName())).toList(); + + return new GetReviewRes(review, tagResList, imageResList); + } + /** * 리뷰 수정 */ From 2786959eaa5ae2b4d0992b852a9a300c7f3f7117 Mon Sep 17 00:00:00 2001 From: yejin Date: Sun, 25 Jan 2026 16:03:09 +0900 Subject: [PATCH 10/11] =?UTF-8?q?Feat:=20=EB=A6=AC=EB=B7=B0=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 15 +++++++++-- .../dto/response/GetReviewTagListRes.java | 15 +++++++++++ .../review/dto/response/GetReviewTagRes.java | 3 --- .../domain/review/service/ReviewService.java | 27 ++++++++++++++++--- 4 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagListRes.java diff --git a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java index dc3fd323..87f31e04 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java +++ b/src/main/java/devkor/com/teamcback/domain/review/controller/ReviewController.java @@ -28,7 +28,18 @@ public class ReviewController { private final ReviewService reviewService; - @Operation(summary = "리뷰가 있는 장소 상세 검색", + @Operation(summary = "리뷰 태그 종류 검색") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), + @ApiResponse(responseCode = "401", description = "권한이 없습니다.", + content = @Content(schema = @Schema(implementation = CommonResponse.class))), + }) + @GetMapping("/tags") + public CommonResponse getReviewTagList() { + return CommonResponse.success(reviewService.getReviewTagList()); + } + + @Operation(summary = "리뷰가 있는 장소의 리뷰 목록 포함 상세 검색", description = "식당, 카페") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), @@ -42,7 +53,7 @@ public CommonResponse getReviewPlaceDetail( return CommonResponse.success(reviewService.getReviewPlaceDetail(placeId)); } - @Operation(summary = "리뷰가 있는 장소 상세 검색 - 무한스크롤로 리뷰 사진 추가 조회", + @Operation(summary = "리뷰가 있는 장소의 리뷰 목록 포함 상세 검색 - 무한스크롤로 리뷰 사진 추가 조회", description = "식당, 카페") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "정상 처리 되었습니다."), diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagListRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagListRes.java new file mode 100644 index 00000000..048c086f --- /dev/null +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagListRes.java @@ -0,0 +1,15 @@ +package devkor.com.teamcback.domain.review.dto.response; + +import lombok.Getter; + +import java.util.List; +import java.util.Map; + +@Getter +public class GetReviewTagListRes { + private Map> reviewTags; + + public GetReviewTagListRes(Map> reviewTags) { + this.reviewTags = reviewTags; + } +} diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java index f6d15063..906d8a44 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewTagRes.java @@ -1,18 +1,15 @@ package devkor.com.teamcback.domain.review.dto.response; import devkor.com.teamcback.domain.review.entity.ReviewTag; -import devkor.com.teamcback.domain.review.entity.TagType; import lombok.Getter; @Getter public class GetReviewTagRes { private Long reviewTagId; private String tag; - private TagType type; public GetReviewTagRes(ReviewTag reviewTag) { this.reviewTagId = reviewTag.getId(); this.tag = reviewTag.getTag(); - this.type = reviewTag.getType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java index 49c4fc69..3a17e1d0 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java +++ b/src/main/java/devkor/com/teamcback/domain/review/service/ReviewService.java @@ -9,10 +9,7 @@ import devkor.com.teamcback.domain.review.dto.request.CreateReviewReq; import devkor.com.teamcback.domain.review.dto.request.ModifyReviewReq; import devkor.com.teamcback.domain.review.dto.response.*; -import devkor.com.teamcback.domain.review.entity.PlaceReviewTagMap; -import devkor.com.teamcback.domain.review.entity.Review; -import devkor.com.teamcback.domain.review.entity.ReviewTag; -import devkor.com.teamcback.domain.review.entity.ReviewTagMap; +import devkor.com.teamcback.domain.review.entity.*; import devkor.com.teamcback.domain.review.repository.PlaceReviewTagMapRepository; import devkor.com.teamcback.domain.review.repository.ReviewRepository; import devkor.com.teamcback.domain.review.repository.ReviewTagMapRepository; @@ -30,7 +27,9 @@ import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor @@ -45,6 +44,26 @@ public class ReviewService { private final UserRepository userRepository; private final FileUtil fileUtil; + /** + * 리뷰 태그 종류 검색 + */ + public GetReviewTagListRes getReviewTagList() { + Map> reviewTagMap = new HashMap<>(); + + // 태그 종류 + for(TagType tagType : TagType.values()) { + reviewTagMap.put(tagType.name(), new ArrayList<>()); + } + + // 태그 종류별 내용 + List reviewTagList = reviewTagRepository.findAll(); + for(ReviewTag reviewTag : reviewTagList) { + reviewTagMap.get(reviewTag.getType().name()).add(new GetReviewTagRes(reviewTag)); + } + + return new GetReviewTagListRes(reviewTagMap); + } + /** * 리뷰 기능있는 장소 상세 조회 */ From 95915e069cd3fd2c58e3efcd8d1b572fd9638043 Mon Sep 17 00:00:00 2001 From: yejin Date: Mon, 26 Jan 2026 14:43:43 +0900 Subject: [PATCH 11/11] =?UTF-8?q?Fix:=20=EC=9D=8C=EC=8B=9D=EC=A0=90=20?= =?UTF-8?q?=EC=A2=85=EB=A5=98=EB=A5=BC=20enum=EC=97=90=EC=84=9C=20String?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/place/entity/FoodType.java | 32 ------------------- .../teamcback/domain/place/entity/Place.java | 3 +- .../dto/response/GetReviewPlaceDetailRes.java | 4 +-- .../dto/response/SearchFacilityRes.java | 4 +-- .../search/dto/response/SearchPlaceRes.java | 4 +-- .../dto/response/SearchRoomDetailRes.java | 4 +-- 6 files changed, 9 insertions(+), 42 deletions(-) delete mode 100644 src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java b/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java deleted file mode 100644 index d7522b1c..00000000 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/FoodType.java +++ /dev/null @@ -1,32 +0,0 @@ -package devkor.com.teamcback.domain.place.entity; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum FoodType { - - KOREAN("한식"), - WESTERN("양식"), - JAPANESE("일식"), - CHINESE("중식"), - ASIAN("아시안"), - FUSION("퓨전"), - CAFE("카페"), - BAKERY("베이커리"), - SALAD("샐러드"), - PORRIDGE("죽"), - BBQ("고기요리"), - BUNSIK("분식"), - CHICKEN("치킨"), - PIZZA("피자"), - BURGER("햄버거"), - SEAFOOD("해산물"), - NOODLES("면요리"), - BISTRO("요리주점"), - BAR("바"); - - private final String type; - -} diff --git a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java index 96a8c84f..f54656f1 100644 --- a/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java +++ b/src/main/java/devkor/com/teamcback/domain/place/entity/Place.java @@ -74,8 +74,7 @@ public class Place extends BaseEntity { private String contact; // 연락처 등 @Column - @Enumerated(EnumType.STRING) - private FoodType foodType; + private String foodType; @ManyToOne @JoinColumn(name = "building_id") diff --git a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java index 9cf1b561..692c918f 100644 --- a/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java +++ b/src/main/java/devkor/com/teamcback/domain/review/dto/response/GetReviewPlaceDetailRes.java @@ -27,7 +27,7 @@ public class GetReviewPlaceDetailRes { @Schema(description = "별점 평균") private String starAverage; @Schema(description = "음식 카테고리", example = "한식") - private String foodTypeName; + private String foodType; @Schema(description = "장소 대표 사진 목록") private List placeImages; @@ -51,7 +51,7 @@ public GetReviewPlaceDetailRes(Place place, List placeImage this.isOperating = place.isOperating(); this.description = place.getDescription(); this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); - this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); + this.foodType = place.getFoodType(); this.placeImages = placeImageList; this.tagList = reviewTagList; this.reviewList = reviewList; diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java index 588cbe35..d5a539fd 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchFacilityRes.java @@ -49,7 +49,7 @@ public class SearchFacilityRes { @Schema(description = "편의시설 별점", example = "NaN 또는 3.6666666666666665") private String starAverage; @Schema(description = "식당인 경우 음식 카테고리", example = "한식") - private String foodTypeName; + private String foodType; @Setter @Schema(description = "식당인 경우 리뷰 태그 목록") private List tagList; @@ -73,6 +73,6 @@ public SearchFacilityRes(Place place, String imageUrl) { this.floor = place.getFloor().intValue(); this.description = place.getDescription(); this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); - this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); + this.foodType = place.getFoodType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java index eb7e1a1e..f2c851df 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchPlaceRes.java @@ -59,7 +59,7 @@ public class SearchPlaceRes { @Schema(description = "편의시설 별점", example = "NaN 또는 3.6666666666666665") private String starAverage; @Schema(description = "식당인 경우 음식 카테고리", example = "한식") - private String foodTypeName; + private String foodType; @Setter @Schema(description = "식당인 경우 리뷰 태그 목록") private List tagList; @@ -87,6 +87,6 @@ public SearchPlaceRes(Place place, String imageUrl) { this.placeType = place.getType(); this.description = place.getDescription(); this.starAverage = String.format("%.2f", ((double) place.getStarSum()) / place.getStarNum()); - this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); + this.foodType = place.getFoodType(); } } diff --git a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java index c15e1af8..8ec72be7 100644 --- a/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java +++ b/src/main/java/devkor/com/teamcback/domain/search/dto/response/SearchRoomDetailRes.java @@ -36,7 +36,7 @@ public class SearchRoomDetailRes { private String description; private String starAverage; private String nextPlaceTime; - private String foodTypeName; + private String foodType; @Setter private List tagList; @@ -68,6 +68,6 @@ else if(place.isOperating()) { // 운영 중이면 종료 시간 else { // 운영 종료인 경우 여는 시간 this.nextPlaceTime = place.getOperatingTime().substring(0, 5); } - this.foodTypeName = place.getFoodType() == null ? "" : place.getFoodType().getType(); + this.foodType = place.getFoodType(); } }