Skip to content

Commit aea0954

Browse files
authored
Merge pull request #340 from SOLPLY/#333-feat-place-review
[FEAT] 장소 리뷰 api
2 parents 61977d4 + c4bfafc commit aea0954

File tree

16 files changed

+429
-8
lines changed

16 files changed

+429
-8
lines changed

src/main/java/org/sopt/solply_server/domain/place/repository/PlaceRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,12 @@ public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceReposi
6565
and p.active = true
6666
""")
6767
Optional<Place> findActiveByIdWithTownAndCheckpoints(@Param("placeId") Long placeId);
68+
69+
@Query("""
70+
select p
71+
from Place p
72+
where p.id = :placeId
73+
and p.active = true
74+
""")
75+
Optional<Place> findActiveById(@Param("placeId") Long placeId);
6876
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.sopt.solply_server.domain.review.controller;
2+
3+
import jakarta.validation.Valid;
4+
import lombok.RequiredArgsConstructor;
5+
import org.sopt.solply_server.domain.review.dto.request.CreatePlaceReviewRequest;
6+
import org.sopt.solply_server.domain.review.dto.response.CreatePlaceReviewResponse;
7+
import org.sopt.solply_server.domain.review.service.PlaceReviewService;
8+
import org.sopt.solply_server.global.annotation.CurrentUserId;
9+
import org.sopt.solply_server.global.dto.CustomApiResponse;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
@RequestMapping("/api/places/reviews")
16+
public class PlaceReviewController {
17+
18+
private final PlaceReviewService placeReviewService;
19+
20+
@PostMapping
21+
public ResponseEntity<CustomApiResponse<CreatePlaceReviewResponse>> createRecord(
22+
@CurrentUserId Long userId,
23+
@Valid @RequestBody CreatePlaceReviewRequest request
24+
) {
25+
CreatePlaceReviewResponse response = placeReviewService.createReview(userId, request);
26+
return CustomApiResponse.success("혼놀 기록 작성이 완료되었습니다.", response);
27+
}
28+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package org.sopt.solply_server.domain.review.dto.request;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
import jakarta.validation.constraints.PastOrPresent;
6+
import jakarta.validation.constraints.Size;
7+
import java.time.LocalDate;
8+
import java.util.List;
9+
import org.sopt.solply_server.domain.review.entity.VisitTime;
10+
11+
public record CreatePlaceReviewRequest(
12+
13+
@NotNull(message = "장소 ID는 필수입니다.")
14+
Long placeId,
15+
16+
@NotNull(message = "방문 날짜는 필수입니다.")
17+
@PastOrPresent(message = "방문 날짜는 오늘 또는 이전 날짜만 선택할 수 있습니다.")
18+
LocalDate visitedAt,
19+
20+
@NotNull(message = "방문 시간대는 필수입니다.")
21+
VisitTime visitTimeSlot,
22+
23+
@NotBlank(message = "오늘의 기록은 필수입니다.")
24+
@Size(min = 10, max = 500, message = "오늘의 기록은 10자 이상 500자 이하여야 합니다.")
25+
String content,
26+
27+
List<String> imageKeys
28+
) {
29+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.sopt.solply_server.domain.review.dto.response;
2+
3+
import org.sopt.solply_server.domain.review.entity.PlaceReview;
4+
5+
public record CreatePlaceReviewResponse(
6+
Long reviewId
7+
) {
8+
public static CreatePlaceReviewResponse from(PlaceReview placeReview) {
9+
return new CreatePlaceReviewResponse(placeReview.getId());
10+
}
11+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package org.sopt.solply_server.domain.review.entity;
2+
3+
import jakarta.persistence.*;
4+
import java.time.LocalDate;
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
import lombok.AccessLevel;
8+
import lombok.Builder;
9+
import lombok.Getter;
10+
import lombok.NoArgsConstructor;
11+
import org.sopt.solply_server.domain.place.entity.Place;
12+
import org.sopt.solply_server.domain.user.entity.User;
13+
import org.sopt.solply_server.global.entity.BaseTimeEntity;
14+
15+
@Entity
16+
@Table(
17+
name = "place_reviews",
18+
indexes = {
19+
@Index(name = "idx_place_reviews_place_id_created_at", columnList = "place_id, created_at DESC"),
20+
@Index(name = "idx_place_reviews_user_id_created_at", columnList = "user_id, created_at DESC")
21+
}
22+
)
23+
@Getter
24+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
25+
public class PlaceReview extends BaseTimeEntity {
26+
27+
@Id
28+
@GeneratedValue(strategy = GenerationType.IDENTITY)
29+
private Long id;
30+
31+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
32+
@JoinColumn(name = "user_id", nullable = false)
33+
private User user;
34+
35+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
36+
@JoinColumn(name = "place_id", nullable = false)
37+
private Place place;
38+
39+
@Column(name = "visited_at", nullable = false)
40+
private LocalDate visitedAt;
41+
42+
@Enumerated(EnumType.STRING)
43+
@Column(name = "visit_time_slot", nullable = false, length = 20)
44+
private VisitTime visitTimeSlot;
45+
46+
@Column(name = "content", nullable = false, length = 500)
47+
private String content;
48+
49+
@OneToMany(mappedBy = "placeReview", cascade = CascadeType.ALL, orphanRemoval = true)
50+
private List<PlaceReviewImage> placeReviewImages = new ArrayList<>();
51+
52+
@Builder
53+
private PlaceReview(
54+
User user,
55+
Place place,
56+
LocalDate visitedAt,
57+
VisitTime visitTimeSlot,
58+
String content
59+
) {
60+
this.user = user;
61+
this.place = place;
62+
this.visitedAt = visitedAt;
63+
this.visitTimeSlot = visitTimeSlot;
64+
this.content = content;
65+
}
66+
67+
public static PlaceReview create(
68+
User user,
69+
Place place,
70+
LocalDate visitedAt,
71+
VisitTime visitTimeSlot,
72+
String content
73+
) {
74+
return PlaceReview.builder()
75+
.user(user)
76+
.place(place)
77+
.visitedAt(visitedAt)
78+
.visitTimeSlot(visitTimeSlot)
79+
.content(content)
80+
.build();
81+
}
82+
83+
public void addImage(PlaceReviewImage placeReviewImage) {
84+
this.placeReviewImages.add(placeReviewImage);
85+
}
86+
87+
public void replaceImages(List<String> imageUrls) {
88+
this.placeReviewImages.clear();
89+
90+
for (String imageUrl : imageUrls) {
91+
this.placeReviewImages.add(PlaceReviewImage.create(this, imageUrl));
92+
}
93+
}
94+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.sopt.solply_server.domain.review.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AccessLevel;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
@Entity
10+
@Table(name = "place_review_images")
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
public class PlaceReviewImage {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@ManyToOne(fetch = FetchType.LAZY, optional = false)
20+
@JoinColumn(name = "place_review_id", nullable = false)
21+
private PlaceReview placeReview;
22+
23+
@Column(name = "image_url", nullable = false, length = 500)
24+
private String imageUrl;
25+
26+
@Builder
27+
private PlaceReviewImage(PlaceReview placeReview, String imageUrl) {
28+
this.placeReview = placeReview;
29+
this.imageUrl = imageUrl;
30+
}
31+
32+
public static PlaceReviewImage create(PlaceReview placeReview, String imageUrl) {
33+
return PlaceReviewImage.builder()
34+
.placeReview(placeReview)
35+
.imageUrl(imageUrl)
36+
.build();
37+
}
38+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.sopt.solply_server.domain.review.entity;
2+
3+
public enum VisitTime {
4+
MORNING,
5+
AFTERNOON,
6+
EVENING
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.sopt.solply_server.domain.review.repository;
2+
3+
import org.sopt.solply_server.domain.review.entity.PlaceReviewImage;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface PlaceReviewImageRepository extends JpaRepository<PlaceReviewImage, Long> {
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.sopt.solply_server.domain.review.repository;
2+
3+
import org.sopt.solply_server.domain.review.entity.PlaceReview;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface PlaceReviewRepository extends JpaRepository<PlaceReview, Long> {
7+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.sopt.solply_server.domain.review.service;
2+
3+
import java.util.List;
4+
import lombok.RequiredArgsConstructor;
5+
import org.sopt.solply_server.domain.review.entity.PlaceReview;
6+
import org.sopt.solply_server.domain.review.repository.PlaceReviewRepository;
7+
import org.sopt.solply_server.global.exception.EntityNotFoundException;
8+
import org.sopt.solply_server.global.exception.ErrorCode;
9+
import org.sopt.solply_server.global.listener.ImageFieldUpdater;
10+
import org.sopt.solply_server.global.util.s3.TargetDir;
11+
import org.springframework.stereotype.Component;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
@Component
15+
@RequiredArgsConstructor
16+
public class PlaceReviewImageFieldUpdater implements ImageFieldUpdater {
17+
18+
private final PlaceReviewRepository placeReviewRepository;
19+
20+
@Override
21+
public TargetDir supportedDir() {
22+
return TargetDir.RECORD;
23+
}
24+
25+
@Override
26+
@Transactional
27+
public void replaceImages(long targetId, List<String> destKeys) {
28+
PlaceReview placeReview = placeReviewRepository.findById(targetId)
29+
.orElseThrow(() -> new EntityNotFoundException(ErrorCode.PLACE_REVIEW_NOT_FOUND));
30+
31+
placeReview.replaceImages(destKeys);
32+
}
33+
}

0 commit comments

Comments
 (0)