Skip to content

Commit c1c685f

Browse files
authored
리뷰 관력 API 기능 구현 (#174)
1 parent ab264d9 commit c1c685f

File tree

10 files changed

+415
-53
lines changed

10 files changed

+415
-53
lines changed

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ dependencies {
7373

7474
tasks.withType<Test> {
7575
useJUnitPlatform()
76+
systemProperty("file.encoding", "UTF-8")
7677
}
7778

7879
// QueryDSL 설정 - 생성된 Q클래스들이 저장될 디렉토리 설정
Lines changed: 34 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,54 @@
11
package com.backend.domain.review.controller;
22

3-
import com.backend.domain.member.dto.MemberMyInfoResponseDto;
4-
import com.backend.domain.review.dto.ReviewEditResponseDto;
5-
import com.backend.domain.review.dto.ReviewResponseDto;
6-
import com.backend.domain.review.dto.ReviewWriteRequestDto;
7-
import com.backend.domain.review.dto.ReviewWriteResponseDto;
3+
import com.backend.domain.member.entity.Member;
4+
import com.backend.domain.member.repository.MemberRepository;
5+
import com.backend.domain.review.dto.ReviewRequest;
6+
import com.backend.domain.review.dto.ReviewResponse;
87
import com.backend.domain.review.service.ReviewService;
98
import com.backend.global.response.RsData;
10-
import io.swagger.v3.oas.annotations.Operation;
11-
import io.swagger.v3.oas.annotations.tags.Tag;
12-
import jakarta.validation.Valid;
9+
import com.backend.global.response.RsStatus;
1310
import lombok.RequiredArgsConstructor;
1411
import org.springframework.http.ResponseEntity;
15-
import org.springframework.security.core.Authentication;
16-
import org.springframework.stereotype.Controller;
12+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
13+
import org.springframework.security.core.userdetails.User;
1714
import org.springframework.web.bind.annotation.*;
1815

19-
import java.time.Instant;
20-
import java.time.LocalDateTime;
21-
import java.util.Date;
22-
23-
@Controller
24-
@RequestMapping("/api/v1")
16+
@RestController
17+
@RequestMapping("/api/v1/reviews")
2518
@RequiredArgsConstructor
26-
@Tag(name = "Member", description = "회원 관련 API")
2719
public class ApiV1ReviewController {
20+
2821
private final ReviewService reviewService;
22+
private final MemberRepository memberRepository;
2923

30-
@Operation(summary = "리뷰 작성 API", description = "상품 리뷰 작성")
31-
@PostMapping("/reviews/products")
32-
public ResponseEntity<RsData<ReviewWriteResponseDto>> reviewCreate(Authentication authentication, @Valid @RequestPart ReviewWriteRequestDto reviewWriteRequestDto) {
33-
ReviewWriteResponseDto reviewWriteResponseDto = new ReviewWriteResponseDto(1L, "유저1", "상품1", "좋은 거래였어요!", true);
34-
return ResponseEntity.ok(new RsData<>("200", "리뷰 작성이 완료되었습니다.", reviewWriteResponseDto));
24+
private Member getMember(User user) {
25+
return memberRepository.findByEmail(user.getUsername()).orElseThrow(() -> new RuntimeException("Member not found"));
3526
}
3627

37-
@Operation(summary = "리뷰 조회 API", description = "상품 리뷰 조회")
38-
@GetMapping("/reviews/products/{id}")
39-
public ResponseEntity<RsData<ReviewResponseDto>> reviewInfo(Authentication authentication, @PathVariable Long id) {
40-
ReviewResponseDto reviewResponseDto = new ReviewResponseDto(1L, "유저1", 1L,
41-
"좋은 거래였어요!", true, LocalDateTime.from(Instant.now()), LocalDateTime.from(Instant.now()));
42-
return ResponseEntity.ok(new RsData<>("200", "리뷰 조회가 완료되었습니다.", reviewResponseDto));
28+
@PostMapping
29+
public ResponseEntity<RsData<ReviewResponse>> createReview(@AuthenticationPrincipal User user, @RequestBody ReviewRequest request) {
30+
Member member = getMember(user);
31+
ReviewResponse response = reviewService.createReview(member.getId(), request);
32+
return ResponseEntity.ok(RsData.created("리뷰가 성공적으로 등록되었습니다.", response));
4333
}
4434

45-
@Operation(summary = "리뷰 삭제 API", description = "상품 리뷰 삭제")
46-
@DeleteMapping("/reviews/products")
47-
public ResponseEntity<RsData<ReviewWriteResponseDto>> reviewDelete(Authentication authentication, @Valid @RequestPart ReviewWriteRequestDto reviewWriteRequestDto) {
48-
return ResponseEntity.ok(new RsData<>("200", "리뷰 삭제가 완료되었습니다.", null));
35+
@GetMapping("/{reviewId}")
36+
public ResponseEntity<RsData<ReviewResponse>> getReview(@PathVariable Long reviewId) {
37+
ReviewResponse response = reviewService.getReview(reviewId);
38+
return ResponseEntity.ok(RsData.ok("리뷰를 성공적으로 조회했습니다.", response));
4939
}
5040

51-
@Operation(summary = "리뷰 수정 API", description = "상품 리뷰 수정")
52-
@PutMapping("/reviews/products")
53-
public ResponseEntity<RsData<ReviewEditResponseDto>> reviewModify(Authentication authentication, @Valid @RequestPart ReviewWriteRequestDto reviewWriteRequestDto) {
54-
ReviewEditResponseDto reviewEditResponseDto = new ReviewEditResponseDto(1L, "유저1", "상품1", "상품이 별로였어요!", false);
55-
return ResponseEntity.ok(new RsData<>("200", "리뷰 수정이 완료되었습니다.", reviewEditResponseDto));
41+
@PutMapping("/{reviewId}")
42+
public ResponseEntity<RsData<ReviewResponse>> updateReview(@AuthenticationPrincipal User user, @PathVariable Long reviewId, @RequestBody ReviewRequest request) {
43+
Member member = getMember(user);
44+
ReviewResponse response = reviewService.updateReview(member.getId(), reviewId, request);
45+
return ResponseEntity.ok(RsData.ok("리뷰가 성공적으로 수정되었습니다.", response));
5646
}
5747

58-
}
48+
@DeleteMapping("/{reviewId}")
49+
public ResponseEntity<RsData<Void>> deleteReview(@AuthenticationPrincipal User user, @PathVariable Long reviewId) {
50+
Member member = getMember(user);
51+
reviewService.deleteReview(member.getId(), reviewId);
52+
return ResponseEntity.ok(RsData.ok("리뷰가 성공적으로 삭제되었습니다."));
53+
}
54+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.backend.domain.review.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import jakarta.validation.constraints.NotNull;
5+
6+
public record ReviewRequest(
7+
@NotNull(message = "상품 ID는 필수입니다.")
8+
Long productId,
9+
10+
@NotBlank(message = "리뷰 내용은 필수입니다.")
11+
String comment,
12+
13+
@NotNull(message = "만족 여부는 필수입니다.")
14+
Boolean isSatisfied
15+
) {
16+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.backend.domain.review.dto;
2+
3+
import com.backend.domain.review.entity.Review;
4+
import java.time.LocalDateTime;
5+
6+
public record ReviewResponse(
7+
Long reviewId,
8+
String reviewerNickname,
9+
String comment,
10+
Boolean isSatisfied,
11+
LocalDateTime createdAt
12+
) {
13+
public static ReviewResponse from(Review review) {
14+
return new ReviewResponse(
15+
review.getId(),
16+
review.getReviewer().getNickname(),
17+
review.getComment(),
18+
review.getIsSatisfied(),
19+
review.getCreateDate()
20+
);
21+
}
22+
}

src/main/java/com/backend/domain/review/entity/Review.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
@Entity
1010
@Table(name = "reviews")
11-
@Getter @Setter
12-
@NoArgsConstructor
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
1313
@AllArgsConstructor
1414
@Builder
1515
public class Review extends BaseEntity {
@@ -21,10 +21,15 @@ public class Review extends BaseEntity {
2121
private Boolean isSatisfied;
2222

2323
@OneToOne(fetch = FetchType.LAZY)
24-
@JoinColumn(name = "product_id", insertable = false, updatable = false)
24+
@JoinColumn(name = "product_id")
2525
private Product product;
2626

2727
@ManyToOne(fetch = FetchType.LAZY)
28-
@JoinColumn(name = "reviewer_id", insertable = false, updatable = false)
28+
@JoinColumn(name = "reviewer_id")
2929
private Member reviewer;
30+
31+
public void update(String comment, Boolean isSatisfied) {
32+
this.comment = comment;
33+
this.isSatisfied = isSatisfied;
34+
}
3035
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.backend.domain.review.exception;
2+
3+
import com.backend.global.exception.ServiceException;
4+
import com.backend.global.response.RsStatus;
5+
6+
public class ReviewException extends ServiceException {
7+
public ReviewException(RsStatus rsStatus) {
8+
super(rsStatus.getResultCode(), rsStatus.getDefaultMessage());
9+
}
10+
11+
public static ReviewException reviewNotFound() {
12+
return new ReviewException(RsStatus.REVIEW_NOT_FOUND);
13+
}
14+
15+
public static ReviewException alreadyExists() {
16+
return new ReviewException(RsStatus.REVIEW_ALREADY_EXISTS);
17+
}
18+
19+
public static ReviewException accessDenied() {
20+
return new ReviewException(RsStatus.REVIEW_ACCESS_DENIED);
21+
}
22+
}

src/main/java/com/backend/domain/review/repository/ReviewRepository.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.backend.domain.review.entity.Review;
44
import org.springframework.data.jpa.repository.JpaRepository;
5-
import org.springframework.stereotype.Repository;
65

7-
@Repository
8-
public interface ReviewRepository extends JpaRepository<Review, Long> {
6+
import java.util.Optional;
97

10-
}
8+
public interface ReviewRepository extends JpaRepository<Review, Long> {
9+
Optional<Review> findByProductId(Long productId);
10+
}
Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
package com.backend.domain.review.service;
22

3-
4-
import com.backend.domain.review.dto.ReviewWriteRequestDto;
5-
import com.backend.domain.review.dto.ReviewWriteResponseDto;
6-
import com.backend.global.response.RsData;
3+
import com.backend.domain.member.entity.Member;
4+
import com.backend.domain.member.repository.MemberRepository;
5+
import com.backend.domain.product.entity.Product;
6+
import com.backend.domain.product.repository.ProductRepository;
7+
import com.backend.domain.review.dto.ReviewRequest;
8+
import com.backend.domain.review.dto.ReviewResponse;
9+
import com.backend.domain.review.entity.Review;
10+
import com.backend.domain.review.exception.ReviewException;
11+
import com.backend.domain.review.repository.ReviewRepository;
12+
import com.backend.global.response.RsStatus;
713
import lombok.RequiredArgsConstructor;
814
import org.springframework.stereotype.Service;
915
import org.springframework.transaction.annotation.Transactional;
@@ -13,9 +19,59 @@
1319
@Transactional
1420
public class ReviewService {
1521

16-
public RsData<ReviewWriteResponseDto> create(String name, ReviewWriteRequestDto reviewWriteRequestDto) {
22+
private final ReviewRepository reviewRepository;
23+
private final MemberRepository memberRepository;
24+
private final ProductRepository productRepository;
25+
26+
public ReviewResponse createReview(Long memberId, ReviewRequest request) {
27+
Member member = memberRepository.findById(memberId)
28+
.orElseThrow(() -> new ReviewException(RsStatus.MEMBER_NOT_FOUND));
29+
30+
Product product = productRepository.findById(request.productId())
31+
.orElseThrow(() -> new ReviewException(RsStatus.PRODUCT_NOT_FOUND));
32+
33+
reviewRepository.findByProductId(product.getId()).ifPresent(review -> {
34+
throw ReviewException.alreadyExists();
35+
});
36+
37+
Review review = Review.builder()
38+
.reviewer(member)
39+
.product(product)
40+
.comment(request.comment())
41+
.isSatisfied(request.isSatisfied())
42+
.build();
43+
44+
Review savedReview = reviewRepository.save(review);
45+
return ReviewResponse.from(savedReview);
46+
}
47+
48+
@Transactional(readOnly = true)
49+
public ReviewResponse getReview(Long reviewId) {
50+
Review review = reviewRepository.findById(reviewId)
51+
.orElseThrow(ReviewException::reviewNotFound);
52+
return ReviewResponse.from(review);
53+
}
54+
55+
public ReviewResponse updateReview(Long memberId, Long reviewId, ReviewRequest request) {
56+
Review review = reviewRepository.findById(reviewId)
57+
.orElseThrow(ReviewException::reviewNotFound);
58+
59+
if (!review.getReviewer().getId().equals(memberId)) {
60+
throw ReviewException.accessDenied();
61+
}
62+
63+
review.update(request.comment(), request.isSatisfied());
64+
return ReviewResponse.from(review);
65+
}
66+
67+
public void deleteReview(Long memberId, Long reviewId) {
68+
Review review = reviewRepository.findById(reviewId)
69+
.orElseThrow(ReviewException::reviewNotFound);
1770

71+
if (!review.getReviewer().getId().equals(memberId)) {
72+
throw ReviewException.accessDenied();
73+
}
1874

19-
return new RsData<>("200-1", "리뷰 작성이 완료되었습니다");
75+
reviewRepository.delete(review);
2076
}
21-
}
77+
}

src/main/java/com/backend/global/response/RsStatus.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ public enum RsStatus {
1515
FORBIDDEN(403, "권한 없음"),
1616
NOT_FOUND(404, "리소스를 찾을 수 없음"),
1717
CONFLICT(409, "중복된 요청"),
18+
19+
// Member
20+
MEMBER_NOT_FOUND(404, "사용자를 찾을 수 없음"),
21+
22+
// Product
23+
PRODUCT_NOT_FOUND(404, "상품을 찾을 수 없음"),
24+
25+
// Review
26+
REVIEW_NOT_FOUND(404, "리뷰를 찾을 수 없음"),
27+
REVIEW_ALREADY_EXISTS(409, "이미 리뷰를 작성한 상품입니다"),
28+
REVIEW_ACCESS_DENIED(403, "리뷰에 대한 권한이 없습니다"),
29+
1830

1931
// 서버 오류
2032
INTERNAL_SERVER_ERROR(500, "서버 내부 오류"),

0 commit comments

Comments
 (0)