diff --git a/src/main/java/com/onebridge/ouch/controller/review/ReviewController.java b/src/main/java/com/onebridge/ouch/controller/review/ReviewController.java new file mode 100644 index 0000000..8e7457d --- /dev/null +++ b/src/main/java/com/onebridge/ouch/controller/review/ReviewController.java @@ -0,0 +1,97 @@ +package com.onebridge.ouch.controller.review; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.onebridge.ouch.apiPayload.ApiResponse; +import com.onebridge.ouch.dto.review.ReviewRequest; +import com.onebridge.ouch.dto.review.ReviewResponse; +import com.onebridge.ouch.security.authorization.UserId; +import com.onebridge.ouch.service.review.ReviewService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; + +@Tag(name = "리뷰 API", description = "리뷰 CRUD API") +@RestController +@RequestMapping("/reviews") +@RequiredArgsConstructor +public class ReviewController { + + private final ReviewService reviewService; + + @Operation(summary = "리뷰 작성 API", description = "리뷰 생성 API 입니다.") + @PostMapping + public ResponseEntity> createReview( + @RequestBody @Validated ReviewRequest request, + @UserId Long userId + ) { + reviewService.createReview(userId, request); + return ResponseEntity.ok(ApiResponse.successWithNoData()); + } + + @Operation(summary = "(테스트용)특정 리뷰 조회 API", description = "리뷰ID로 특정 리뷰를 조회하는 API입니다.") + @GetMapping("/{reviewId}") + public ResponseEntity getReview(@PathVariable("reviewId") Long reviewId) { + ReviewResponse response = reviewService.getReview(reviewId); + return ResponseEntity.ok(response); + } + + @Operation(summary = "(테스트용)모든 리뷰 조회 API", description = "모든 리뷰를 조회합니다.") + @GetMapping + public ResponseEntity> getAllReviews() { + List list = reviewService.getAllReviews(); + return ResponseEntity.ok(list); + } + + // 4) 특정 병원(ykiho) 리뷰 목록 조회 + @Operation(summary = "특정 병원 리뷰 조회 API", description = "병원 Id를 활용하여 특정 병원의 모든 리뷰를 조회합니다.") + @GetMapping("/hospitals/{ykiho}") + public ResponseEntity> getReviewsByHospital(@PathVariable("ykiho") String ykiho) { + List list = reviewService.getReviewsByHospital(ykiho); + return ResponseEntity.ok(list); + } + + @Operation(summary = "리뷰 수정 API", description = "리뷰 ID로 리뷰를 수정하는 API입니다.") + @PutMapping("/{reviewId}") + public ResponseEntity> updateReview( + @PathVariable("reviewId") Long reviewId, + @RequestBody @Validated ReviewRequest request, + @UserId Long userId + ) { + ReviewResponse updated = reviewService.updateReview(reviewId, userId, request); + return ResponseEntity.ok(ApiResponse.successWithNoData()); + } + + // 6) 리뷰 삭제 (JWT에서 추출한 userId로 작성자 검증) + @Operation(summary = "리뷰 삭제 API", description = "리뷰 ID로 리뷰를 삭제하는 API입니다.") + @DeleteMapping("/{reivewId}") + public ResponseEntity deleteReview( + @PathVariable("reivewId") Long reviewId, + @UserId Long userId + ) { + reviewService.deleteReview(reviewId, userId); + return ResponseEntity.noContent().build(); + } + + // 7) 내가 쓴 리뷰 목록 조회 + @Operation(summary = "내 리뷰 목록 조회 API", description = "내가 작성한 리뷰 목록을 조회하는 API입니다.") + @GetMapping("/me") + public ResponseEntity> getMyReviews( + @UserId Long userId + ) { + List list = reviewService.getMyReviews(userId); + return ResponseEntity.ok(list); + } +} \ No newline at end of file diff --git a/src/main/java/com/onebridge/ouch/domain/Review.java b/src/main/java/com/onebridge/ouch/domain/Review.java index c0ce733..cfac883 100644 --- a/src/main/java/com/onebridge/ouch/domain/Review.java +++ b/src/main/java/com/onebridge/ouch/domain/Review.java @@ -7,6 +7,7 @@ @Entity @Getter +@Setter @Builder @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor diff --git a/src/main/java/com/onebridge/ouch/dto/review/ReviewRequest.java b/src/main/java/com/onebridge/ouch/dto/review/ReviewRequest.java new file mode 100644 index 0000000..bd5f69f --- /dev/null +++ b/src/main/java/com/onebridge/ouch/dto/review/ReviewRequest.java @@ -0,0 +1,27 @@ +package com.onebridge.ouch.dto.review; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ReviewRequest { + + @NotBlank(message = "hospitalYkiho는 필수입니다.") + private String hospitalYkiho; //   String 타입으로 변경 + + private String contents; + + @NotNull(message = "score는 필수입니다.") + @Min(value = 1, message = "score는 최소 1점 이상이어야 합니다.") + @Max(value = 5, message = "score는 최대 5점 이하이어야 합니다.") + private Integer score; + + private String imageUrl; +} diff --git a/src/main/java/com/onebridge/ouch/dto/review/ReviewResponse.java b/src/main/java/com/onebridge/ouch/dto/review/ReviewResponse.java new file mode 100644 index 0000000..3280259 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/dto/review/ReviewResponse.java @@ -0,0 +1,20 @@ +package com.onebridge.ouch.dto.review; + +import lombok.*; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class ReviewResponse { + + private Long id; + private String userNickname; // userId 대신 닉네임 + private String hospitalYkiho; //   String 타입으로 변경 + private String contents; + private Integer score; + private String imageUrl; + private String createdAt; // BaseEntity에 만든 createdAt 필드(예: ISO 포맷 문자열) + private String updatedAt; // BaseEntity에 만든 modifiedAt 필드 +} diff --git a/src/main/java/com/onebridge/ouch/repository/review/ReviewRepository.java b/src/main/java/com/onebridge/ouch/repository/review/ReviewRepository.java new file mode 100644 index 0000000..f59c728 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/repository/review/ReviewRepository.java @@ -0,0 +1,16 @@ +package com.onebridge.ouch.repository.review; + +import java.util.List; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.onebridge.ouch.domain.Review; + +public interface ReviewRepository extends JpaRepository { + + // 특정 병원 리뷰 조회 + List findAllByHospital_Ykiho(String hospitalYkiho); + + // 특정 사용자(user.id) 기준으로 리뷰 조회 + List findAllByUser_Id(Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/onebridge/ouch/service/review/ReviewService.java b/src/main/java/com/onebridge/ouch/service/review/ReviewService.java new file mode 100644 index 0000000..c2c7401 --- /dev/null +++ b/src/main/java/com/onebridge/ouch/service/review/ReviewService.java @@ -0,0 +1,123 @@ +package com.onebridge.ouch.service.review; + +import com.onebridge.ouch.domain.Hospital; +import com.onebridge.ouch.domain.Review; +import com.onebridge.ouch.domain.User; +import com.onebridge.ouch.dto.review.ReviewRequest; +import com.onebridge.ouch.dto.review.ReviewResponse; +import com.onebridge.ouch.repository.hospital.HospitalRepository; +import com.onebridge.ouch.repository.review.ReviewRepository; +import com.onebridge.ouch.repository.user.UserRepository; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.persistence.EntityNotFoundException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ReviewService { + + private final ReviewRepository reviewRepository; + private final UserRepository userRepository; + private final HospitalRepository hospitalRepository; + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_LOCAL_DATE; + + @Transactional + public ReviewResponse createReview(Long userId, ReviewRequest request) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new EntityNotFoundException("해당 userId를 찾을 수 없습니다: " + userId)); + + Hospital hospital = hospitalRepository.findById(request.getHospitalYkiho()) + .orElseThrow(() -> new EntityNotFoundException("해당 hospitalYkiho를 찾을 수 없습니다: " + request.getHospitalYkiho())); + + Review review = Review.builder() + .user(user) + .hospital(hospital) + .contents(request.getContents()) + .score(request.getScore()) + .imageUrl(request.getImageUrl()) + .build(); + + Review saved = reviewRepository.save(review); + return toDto(saved); + } + + @Transactional(readOnly = true) + public ReviewResponse getReview(Long reviewId) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new EntityNotFoundException("해당 reviewId를 찾을 수 없습니다: " + reviewId)); + return toDto(review); + } + + @Transactional(readOnly = true) + public List getAllReviews() { + return reviewRepository.findAll().stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getReviewsByHospital(String hospitalYkiho) { + return reviewRepository.findAllByHospital_Ykiho(hospitalYkiho).stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + @Transactional + public ReviewResponse updateReview(Long reviewId, Long userId, ReviewRequest request) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new EntityNotFoundException("해당 reviewId를 찾을 수 없습니다: " + reviewId)); + + // 작성자만 수정할 수 있도록 간단 검증 + if (!review.getUser().getId().equals(userId)) { + throw new IllegalArgumentException("작성자만 수정할 수 있습니다."); + } + + review.setContents(request.getContents()); + review.setScore(request.getScore()); + review.setImageUrl(request.getImageUrl()); + + Review updated = reviewRepository.save(review); + return toDto(updated); + } + + @Transactional + public void deleteReview(Long reviewId, Long userId) { + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new EntityNotFoundException("해당 reviewId를 찾을 수 없습니다: " + reviewId)); + + // 작성자만 삭제할 수 있도록 간단 검증 + if (!review.getUser().getId().equals(userId)) { + throw new IllegalArgumentException("작성자만 삭제할 수 있습니다."); + } + + reviewRepository.delete(review); + } + + @Transactional(readOnly = true) + public List getMyReviews(Long userId) { + List reviews = reviewRepository.findAllByUser_Id(userId); + return reviews.stream() + .map(this::toDto) + .collect(Collectors.toList()); + } + + private ReviewResponse toDto(Review review) { + return ReviewResponse.builder() + .id(review.getId()) + .userNickname(review.getUser().getNickname()) + .hospitalYkiho(review.getHospital().getYkiho()) + .contents(review.getContents()) + .score(review.getScore()) + .imageUrl(review.getImageUrl()) + .createdAt(review.getCreatedAt().format(dateFormatter)) + .updatedAt(review.getUpdatedAt().format(dateFormatter)) + .build(); + } +} \ No newline at end of file