diff --git a/application/wypl-core/build.gradle b/application/wypl-core/build.gradle index 9c9fef0..a5baadb 100644 --- a/application/wypl-core/build.gradle +++ b/application/wypl-core/build.gradle @@ -9,8 +9,10 @@ java { dependencies { implementation project(':application:application-common') - implementation project(':common') implementation project(':domain:jpa-common') - implementation project(':domain:jpa-member-domain') + implementation project(':domain:mongo-common') + implementation project(':domain:jpamongo-review-domain') implementation project(':domain:jpa-calendar-domain') + implementation project(':domain:jpa-member-domain') + implementation project(':common') } \ No newline at end of file diff --git a/application/wypl-core/src/main/java/com/wypl/WyplCoreApplication.java b/application/wypl-core/src/main/java/com/wypl/WyplCoreApplication.java index 82bafa3..5cd2c9e 100644 --- a/application/wypl-core/src/main/java/com/wypl/WyplCoreApplication.java +++ b/application/wypl-core/src/main/java/com/wypl/WyplCoreApplication.java @@ -3,13 +3,14 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; @SpringBootApplication @EnableJpaRepositories(basePackages = {"com.wypl"}) +@EnableMongoRepositories(basePackages = {"com.wypl"}) public class WyplCoreApplication { - public static void main(String[] args) { - SpringApplication.run(WyplCoreApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(WyplCoreApplication.class, args); + } } diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/annotation/Authenticated.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/annotation/Authenticated.java new file mode 100644 index 0000000..3e4d12e --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/annotation/Authenticated.java @@ -0,0 +1,11 @@ +package com.wypl.wyplcore.auth.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface Authenticated { +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/domain/AuthMember.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/domain/AuthMember.java new file mode 100644 index 0000000..eb7124c --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/domain/AuthMember.java @@ -0,0 +1,7 @@ +package com.wypl.wyplcore.auth.domain; + +public record AuthMember( + long id +) { + +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/utils/AuthenticatedArgumentResolver.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/utils/AuthenticatedArgumentResolver.java new file mode 100644 index 0000000..76d0025 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/auth/utils/AuthenticatedArgumentResolver.java @@ -0,0 +1,32 @@ +package com.wypl.wyplcore.auth.utils; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import com.wypl.wyplcore.auth.annotation.Authenticated; +import com.wypl.wyplcore.auth.domain.AuthMember; + +@Component +public class AuthenticatedArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasParameterAnnotation = parameter.hasParameterAnnotation(Authenticated.class); + boolean assignableFrom = AuthMember.class.isAssignableFrom(parameter.getParameterType()); + return hasParameterAnnotation && assignableFrom; + } + + @Override + public AuthMember resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { + return null; + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/global/config/WebConfig.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/global/config/WebConfig.java new file mode 100644 index 0000000..d115f3b --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/global/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.wypl.wyplcore.global.config; + +import java.util.List; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.wypl.wyplcore.auth.utils.AuthenticatedArgumentResolver; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new AuthenticatedArgumentResolver()); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/controller/ReviewController.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/controller/ReviewController.java new file mode 100644 index 0000000..b5c172c --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/controller/ReviewController.java @@ -0,0 +1,98 @@ +package com.wypl.wyplcore.review.controller; + +import java.time.LocalDate; + +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.wypl.applicationcommon.WyplResponseEntity; +import com.wypl.wyplcore.auth.annotation.Authenticated; +import com.wypl.wyplcore.auth.domain.AuthMember; +import com.wypl.wyplcore.review.data.request.ReviewCreateRequest; +import com.wypl.wyplcore.review.data.request.ReviewType; +import com.wypl.wyplcore.review.data.request.ReviewUpdateRequest; +import com.wypl.wyplcore.review.data.response.ReviewDetailResponse; +import com.wypl.wyplcore.review.data.response.ReviewIdResponse; +import com.wypl.wyplcore.review.data.response.ReviewListResponse; +import com.wypl.wyplcore.review.service.ReviewModifyService; +import com.wypl.wyplcore.review.service.ReviewReadService; + +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/review") +public class ReviewController { + + private final ReviewModifyService reviewModifyService; + private final ReviewReadService reviewReadService; + + @PostMapping + public WyplResponseEntity createReview( + @Authenticated AuthMember authMember, + @RequestBody ReviewCreateRequest reviewCreateRequest + ) { + ReviewIdResponse response = reviewModifyService.createReview(authMember.id(), reviewCreateRequest); + return WyplResponseEntity.created(response, "리뷰 등록에 성공했습니다."); + } + + @PatchMapping("/{reviewId}") + public WyplResponseEntity updateReview( + @Authenticated AuthMember authMember, + @PathVariable int reviewId, + @RequestBody ReviewUpdateRequest reviewUpdateRequest + ) { + ReviewIdResponse response = reviewModifyService.updateReview(authMember.id(), reviewId, reviewUpdateRequest); + return WyplResponseEntity.ok(response, "리뷰 수정에 성공했습니다."); + } + + @DeleteMapping("/{reviewId}") + public WyplResponseEntity deleteReview( + @Authenticated AuthMember authMember, + @PathVariable("reviewId") int reviewId + ) { + ReviewIdResponse response = reviewModifyService.deleteReview(authMember.id(), reviewId); + return WyplResponseEntity.ok(response, "리뷰 삭제에 성공 했습니다."); + } + + @GetMapping("/detail/{reviewId}") + public WyplResponseEntity getDetailReview( + @Authenticated AuthMember authMember, + @PathVariable int reviewId + ) { + ReviewDetailResponse response = reviewReadService.getDetailReview(authMember.id(), reviewId); + return WyplResponseEntity.ok(response, "리뷰 상세 조회에 성공했습니다."); + } + + @GetMapping("/{type}") + public WyplResponseEntity getReviewsByMemberId( + @Authenticated AuthMember authMember, + @PathVariable("type") ReviewType reviewType, + @RequestParam(value = "lastReviewId", required = false) Long lastReviewId, + @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate, + @RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate + ) { + ReviewListResponse response = reviewReadService.getReviews(authMember.id(), lastReviewId, reviewType, startDate, + endDate); + return WyplResponseEntity.ok(response, "리뷰 목록 조회에 성공했습니다."); + } + + @GetMapping("/{type}/{scheduleId}") + public WyplResponseEntity getReviewsBySchedule( + @Authenticated AuthMember authMember, + @PathVariable("type") ReviewType reviewType, + @PathVariable("scheduleId") long scheduleId + ) { + ReviewListResponse response = reviewReadService.getReviewsByScheduleId(authMember.id(), scheduleId, + reviewType); + return WyplResponseEntity.ok(response, "일정 별 리뷰 조회에 성공했습니다."); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewCreateRequest.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewCreateRequest.java new file mode 100644 index 0000000..7bc689d --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewCreateRequest.java @@ -0,0 +1,22 @@ +package com.wypl.wyplcore.review.data.request; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +import lombok.Builder; + +@Builder +public record ReviewCreateRequest( + + String title, + + @JsonProperty("schedule_id") + int scheduleId, + + List> contents + +) { +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewType.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewType.java new file mode 100644 index 0000000..d9d4ee0 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewType.java @@ -0,0 +1,7 @@ +package com.wypl.wyplcore.review.data.request; + +public enum ReviewType { + NEWEST, + + OLDEST +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewUpdateRequest.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewUpdateRequest.java new file mode 100644 index 0000000..4698e2e --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/request/ReviewUpdateRequest.java @@ -0,0 +1,17 @@ +package com.wypl.wyplcore.review.data.request; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public record ReviewUpdateRequest( + String title, + + @JsonProperty("schedule_id") + int scheduleId, + + List> contents +) { +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewDetailResponse.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewDetailResponse.java new file mode 100644 index 0000000..e712a46 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewDetailResponse.java @@ -0,0 +1,35 @@ +package com.wypl.wyplcore.review.data.response; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.wypl.jpacalendardomain.schedule.domain.Schedule; +import com.wypl.jpamongoreviewdomain.review.domain.Review; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +import lombok.Builder; + +@Builder +public record ReviewDetailResponse( + @JsonProperty("review_id") + long reviewId, + + String title, + + // Todo : import 후 주석 해제 + // ScheduleResponse schedule, + + List> contents +) { + public static ReviewDetailResponse of(Review review, Schedule schedule, + List> reviewContents) { + return ReviewDetailResponse.builder() + .reviewId(review.getReviewId()) + .title(review.getTitle()) + // Todo : import 후 주석 해제 + // .schedule(ScheduleResponse.from(schedule)) + .contents(reviewContents) + .build(); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewIdResponse.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewIdResponse.java new file mode 100644 index 0000000..e1e81c8 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewIdResponse.java @@ -0,0 +1,12 @@ +package com.wypl.wyplcore.review.data.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ReviewIdResponse( + @JsonProperty("review_id") + long reviewId +) { + public static ReviewIdResponse from(long reviewId) { + return new ReviewIdResponse(reviewId); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewListResponse.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewListResponse.java new file mode 100644 index 0000000..d01879d --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewListResponse.java @@ -0,0 +1,17 @@ +package com.wypl.wyplcore.review.data.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record ReviewListResponse( + + @JsonProperty("review_count") + int reviewCount, + + List reviews +) { + public static ReviewListResponse from(List reviews) { + return new ReviewListResponse(reviews.size(), reviews); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewResponse.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewResponse.java new file mode 100644 index 0000000..7855c19 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/data/response/ReviewResponse.java @@ -0,0 +1,33 @@ +package com.wypl.wyplcore.review.data.response; + +import java.time.LocalDateTime; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.wypl.jpamongoreviewdomain.review.domain.Review; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +import lombok.Builder; + +@Builder +public record ReviewResponse( + @JsonProperty("review_id") + long reviewId, + + @JsonProperty("created_at") + LocalDateTime createdAt, + + String title, + + @JsonProperty("thumbnail_content") + Map thumbnailContent +) { + public static ReviewResponse from(Review review, Map thumbnailContent) { + return ReviewResponse.builder() + .createdAt(LocalDateTime.now()) + .reviewId(review.getReviewId()) + .title(review.getTitle()) + .thumbnailContent(thumbnailContent) + .build(); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewErrorCode.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewErrorCode.java new file mode 100644 index 0000000..4b28329 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewErrorCode.java @@ -0,0 +1,23 @@ +package com.wypl.wyplcore.review.exception; + +import com.wypl.common.exception.ServerErrorCode; + +import lombok.Getter; + +@Getter +public enum ReviewErrorCode implements ServerErrorCode { + EMPTY_CONTENTS(500, "REVIEW_001", "작성된 내용이 없습니다."), + INVALID_TITLE(500, "REVIEW_002", "제목의 길이가 올바르지 않습니다."), + NO_SUCH_REVIEW(500, "REVIEW_003", "존재하지 않는 회고입니다.") + ; + + private final int statusCode; + private final String errorCode; + private final String message; + + ReviewErrorCode(int statusCode, String errorCode, String message) { + this.statusCode = statusCode; + this.errorCode = errorCode; + this.message = message; + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewException.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewException.java new file mode 100644 index 0000000..f773fb2 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/exception/ReviewException.java @@ -0,0 +1,10 @@ +package com.wypl.wyplcore.review.exception; + +import com.wypl.common.exception.ServerErrorCode; +import com.wypl.common.exception.WyplException; + +public class ReviewException extends WyplException { + public ReviewException(ServerErrorCode serverErrorCode) { + super(serverErrorCode); + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewModifyService.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewModifyService.java new file mode 100644 index 0000000..b757d0f --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewModifyService.java @@ -0,0 +1,18 @@ +package com.wypl.wyplcore.review.service; + +import org.springframework.transaction.annotation.Transactional; + +import com.wypl.wyplcore.review.data.request.ReviewCreateRequest; +import com.wypl.wyplcore.review.data.request.ReviewUpdateRequest; +import com.wypl.wyplcore.review.data.response.ReviewIdResponse; + +public interface ReviewModifyService { + @Transactional + ReviewIdResponse createReview(long memberId, ReviewCreateRequest reviewCreateRequest); + + @Transactional + ReviewIdResponse updateReview(long memberId, long reviewId, ReviewUpdateRequest reviewUpdateRequest); + + @Transactional + ReviewIdResponse deleteReview(long memberId, long reviewId); +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewReadService.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewReadService.java new file mode 100644 index 0000000..477d278 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewReadService.java @@ -0,0 +1,16 @@ +package com.wypl.wyplcore.review.service; + +import java.time.LocalDate; + +import com.wypl.wyplcore.review.data.request.ReviewType; +import com.wypl.wyplcore.review.data.response.ReviewDetailResponse; +import com.wypl.wyplcore.review.data.response.ReviewListResponse; + +public interface ReviewReadService { + ReviewDetailResponse getDetailReview(long memberId, long reviewId); + + ReviewListResponse getReviews(long memberId, Long lastReviewId, ReviewType reviewType, LocalDate startDate, + LocalDate endDate); + + ReviewListResponse getReviewsByScheduleId(long memberId, long scheduleId, ReviewType reviewType); +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewServiceImpl.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewServiceImpl.java new file mode 100644 index 0000000..0972928 --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/service/ReviewServiceImpl.java @@ -0,0 +1,218 @@ +package com.wypl.wyplcore.review.service; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.wypl.jpacalendardomain.schedule.domain.Schedule; +import com.wypl.jpamemberdomain.member.Member; +import com.wypl.jpamongoreviewdomain.review.domain.Review; +import com.wypl.jpamongoreviewdomain.review.repository.ReviewRepository; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContents; +import com.wypl.jpamongoreviewdomain.reviewcontents.repository.ReviewContentsRepository; +import com.wypl.wyplcore.review.data.request.ReviewCreateRequest; +import com.wypl.wyplcore.review.data.request.ReviewType; +import com.wypl.wyplcore.review.data.request.ReviewUpdateRequest; +import com.wypl.wyplcore.review.data.response.ReviewDetailResponse; +import com.wypl.wyplcore.review.data.response.ReviewIdResponse; +import com.wypl.wyplcore.review.data.response.ReviewListResponse; +import com.wypl.wyplcore.review.data.response.ReviewResponse; +import com.wypl.wyplcore.review.exception.ReviewErrorCode; +import com.wypl.wyplcore.review.exception.ReviewException; +import com.wypl.wyplcore.review.utils.ReviewUtils; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewServiceImpl implements ReviewModifyService, ReviewReadService { + private final ReviewRepository reviewRepository; + + // private final ScheduleRepository scheduleRepository; + + private final ReviewContentsRepository reviewContentsRepository; + + @Override + @Transactional + public ReviewIdResponse createReview(long memberId, ReviewCreateRequest reviewCreateRequest) { + validateReviewContents(reviewCreateRequest.contents(), reviewCreateRequest.title()); + + //schedule, member 유효성 판단 + // MemberSchedule memberSchedule = memberScheduleService.getMemberScheduleByMemberAndSchedule(memberId, + // ScheduleServiceUtils.findById(scheduleRepository, reviewCreateRequest.scheduleId())); + // memberSchedule.writeReview(); + + Member findMember = null; // FIXME: MemberUtil + Schedule findSchedule = null; // FIXME: ScheduleUtil + + Review review = Review.of(reviewCreateRequest.title(), findMember, findSchedule); + Review savedReview = reviewRepository.save(review); + + reviewContentsRepository.save(ReviewContents.of(savedReview.getReviewId(), reviewCreateRequest.contents())); + return ReviewIdResponse.from(savedReview.getReviewId()); + } + + @Override + @Transactional + public ReviewIdResponse updateReview(long memberId, long reviewId, ReviewUpdateRequest reviewUpdateRequest) { + validateReviewContents(reviewUpdateRequest.contents(), reviewUpdateRequest.title()); + + Member member = null; // Todo : 조회 + + Review review = ReviewUtils.findByReviewIdAndMember(reviewRepository, reviewId, member); + review.updateTitle(reviewUpdateRequest.title()); + + ReviewContents reviewContents = reviewContentsRepository.findByReviewIdAndDeletedAtNull(review.getReviewId()); + reviewContents.updateContents(reviewUpdateRequest.contents()); + + reviewContentsRepository.save(reviewContents); + + return ReviewIdResponse.from(review.getReviewId()); + } + + @Override + @Transactional + public ReviewIdResponse deleteReview(long memberId, long reviewId) { + Member member = null; // Todo : 조회 + Review review = ReviewUtils.findByReviewIdAndMember(reviewRepository, reviewId, member); + + ReviewContents reviewContents = reviewContentsRepository.findByReviewIdAndDeletedAtNull(review.getReviewId()); + reviewContents.delete(); + + reviewContentsRepository.save(reviewContents); + + review.delete(); + + return ReviewIdResponse.from(review.getReviewId()); + } + + @Override + public ReviewDetailResponse getDetailReview(long memberId, long reviewId) { + Member member = null; // Todo : 조회 + Review review = ReviewUtils.findByReviewIdAndMember(reviewRepository, reviewId, member); + Schedule schedule = review.getSchedule(); + + // Todo : 저장할 때 null 검사 하는데, 가져올 때도 검사해야 할까??? + ReviewContents reviewContents = reviewContentsRepository.findByReviewIdAndDeletedAtNull(review.getReviewId()); + + return ReviewDetailResponse.of(review, schedule, reviewContents.getContents()); + } + + @Override + public ReviewListResponse getReviews(long memberId, Long lastReviewId, ReviewType reviewType, LocalDate startDate, + LocalDate endDate) { + + List reviews = switch (reviewType) { + case NEWEST -> { + if (startDate == null || endDate == null) { + if (lastReviewId == null) { + yield reviewRepository.getReviewsNewestAll(memberId); + } + yield reviewRepository.getReviewsNewestAllAfter(memberId, lastReviewId); + } + + LocalDateTime startDateTime = LocalDateTime.of(startDate, LocalTime.of(0, 0)); + LocalDateTime endDateTime = LocalDateTime.of(endDate, LocalTime.of(23, 59)); + + if (lastReviewId == null) { + yield reviewRepository.getReviewsNewest(memberId, startDateTime, endDateTime); + } + + yield reviewRepository.getReviewsNewestAfter(memberId, lastReviewId, startDateTime, endDateTime); + } + case OLDEST -> { + + if (startDate == null || endDate == null) { + if (lastReviewId == null) { + lastReviewId = 0L; + } + yield reviewRepository.getReviewsOldestAll(memberId, lastReviewId); + } + LocalDateTime startDateTime = LocalDateTime.of(startDate, LocalTime.of(0, 0)); + LocalDateTime endDateTime = LocalDateTime.of(endDate, LocalTime.of(23, 59)); + + if (lastReviewId == null) { + lastReviewId = 0L; + } + + yield reviewRepository.getReviewsOldest(memberId, lastReviewId, startDateTime, endDateTime); + } + }; + + return ReviewListResponse.from(reviews.stream().map( + review -> ReviewResponse.builder() + .title(review.getTitle()) + .createdAt(review.getCreatedAt()) + .reviewId(review.getReviewId()) + .thumbnailContent(getThumbnailContent( + reviewContentsRepository.findByReviewIdAndDeletedAtNull(review.getReviewId()) + .getContents())) + .build() + ).toList()); + } + + @Override + public ReviewListResponse getReviewsByScheduleId(long memberId, long scheduleId, ReviewType reviewType) { + // MemberSchedule memberSchedule = memberScheduleService.getMemberScheduleByMemberAndSchedule(memberId, + // ScheduleServiceUtils.findById(scheduleRepository, scheduleId)); + + Member member = null; // Todo : 조회 + Schedule schedule = null; // Todo : 조회 + + List reviews = switch (reviewType) { + case NEWEST -> { + yield reviewRepository.getReviewByMemberAndScheduleOrderByCreatedAtDesc(member, schedule); + } + case OLDEST -> { + yield reviewRepository.getReviewsByMemberAndScheduleOrderByCreatedAt(member, schedule); + } + }; + + List reviewResponses = new ArrayList<>(); + for (Review review : reviews) { + reviewResponses.add( + ReviewResponse.builder() + .reviewId(review.getReviewId()) + .title(review.getTitle()) + .createdAt(review.getCreatedAt()) + .thumbnailContent( + getThumbnailContent( + reviewContentsRepository.findByReviewIdAndDeletedAtNull( + review.getReviewId()).getContents()) + ) + .build() + ); + } + + return ReviewListResponse.from(reviewResponses); + } + + private Map getThumbnailContent(List> contents) { + for (Map content : contents) { + if (content.get("blockType").getBlockType().equals(BlockType.REVIEW_PICTURE)) { + return content; + } + } + + return contents.get(0); + } + + private void validateReviewContents(List> contents, String title) { + if (contents.isEmpty()) { + throw new ReviewException(ReviewErrorCode.EMPTY_CONTENTS); + } + + if (title == null || title.length() > 50 || title.length() == 0) { + throw new ReviewException(ReviewErrorCode.INVALID_TITLE); + } + } +} diff --git a/application/wypl-core/src/main/java/com/wypl/wyplcore/review/utils/ReviewUtils.java b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/utils/ReviewUtils.java new file mode 100644 index 0000000..0c1504c --- /dev/null +++ b/application/wypl-core/src/main/java/com/wypl/wyplcore/review/utils/ReviewUtils.java @@ -0,0 +1,27 @@ +package com.wypl.wyplcore.review.utils; + +import com.wypl.common.exception.CallConstructorException; +import com.wypl.jpamemberdomain.member.Member; +import com.wypl.jpamongoreviewdomain.review.domain.Review; +import com.wypl.jpamongoreviewdomain.review.repository.ReviewRepository; +import com.wypl.wyplcore.review.exception.ReviewErrorCode; +import com.wypl.wyplcore.review.exception.ReviewException; + +import lombok.Generated; + +public class ReviewUtils { + + @Generated + private ReviewUtils() { + throw new CallConstructorException(); + } + + public static Review findByReviewIdAndMember( + final ReviewRepository reviewRepository, + final long reviewId, + final Member member + ) { + return reviewRepository.findByReviewIdAndMember(reviewId, member) + .orElseThrow(() -> new ReviewException(ReviewErrorCode.NO_SUCH_REVIEW)); + } +} diff --git a/application/wypl-core/src/main/resources/application.yml b/application/wypl-core/src/main/resources/application.yml index 01132f8..dc9bdc2 100644 --- a/application/wypl-core/src/main/resources/application.yml +++ b/application/wypl-core/src/main/resources/application.yml @@ -17,3 +17,9 @@ spring: console: enabled: true path: /h2 + +de: + flapdoodle: + mongodb: + embedded: + version: 4.0.2 diff --git a/application/wypl-core/src/test/java/com/wypl/wyplcore/.gitkeep b/application/wypl-core/src/test/java/com/wypl/wyplcore/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/application/wypl-core/src/test/java/com/wypl/wyplcore/WyplCalendarApplicationTests.java b/application/wypl-core/src/test/java/com/wypl/wyplcore/WyplCalendarApplicationTests.java deleted file mode 100644 index 227c74f..0000000 --- a/application/wypl-core/src/test/java/com/wypl/wyplcore/WyplCalendarApplicationTests.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.wypl.wyplcore; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class WyplCalendarApplicationTests { - @Test - void contextLoads() { - } -} diff --git a/domain/jpa-member-domain/src/main/java/com/wypl/jpamemberdomain/member/Member.java b/domain/jpa-member-domain/src/main/java/com/wypl/jpamemberdomain/member/Member.java index 65bb7ae..6617f04 100644 --- a/domain/jpa-member-domain/src/main/java/com/wypl/jpamemberdomain/member/Member.java +++ b/domain/jpa-member-domain/src/main/java/com/wypl/jpamemberdomain/member/Member.java @@ -1,18 +1,29 @@ package com.wypl.jpamemberdomain.member; -import com.wypl.common.Color; -import jakarta.persistence.*; - import java.time.LocalDate; +import com.wypl.common.Color; +import com.wypl.jpacommon.JpaBaseEntity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; + +@Getter @Entity @Table(name = "member_tbl") -public class Member { +public class Member extends JpaBaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; + @Column(name = "member_id") + private Long memberId; @Column(name = "email", length = 50, unique = true, nullable = false) private String email; diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/domain/Review.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/domain/Review.java index 4210575..b1f2ffd 100644 --- a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/domain/Review.java +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/domain/Review.java @@ -2,7 +2,8 @@ import org.hibernate.annotations.SQLRestriction; -import com.wypl.jpacore.JpaBaseEntity; +import com.wypl.jpacalendardomain.schedule.domain.Schedule; +import com.wypl.jpacommon.JpaBaseEntity; import com.wypl.jpamemberdomain.member.Member; import jakarta.persistence.Column; @@ -38,5 +39,29 @@ public class Review extends JpaBaseEntity { @JoinColumn(name = "member_id") private Member member; - // TODO: 추후 schedule 와 `@ManyToOne` 관계가 필요 + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "schedule_id") + private Schedule schedule; + + public static Review of(String title, Member member, Schedule schedule) { + return Review.builder() + .title(title) + .member(member) + .schedule(schedule) + .build(); + } + + // public void validationOwnerByMemberId(long memberId) { + // if(isNotOwner(memberId)) { + // throw new ReviewException(ReviewErrorCode.NOT_PERMISSION_TO_REVIEW); + // } + // } + // + // private boolean isNotOwner(long memberId) { + // return getMember().getId() != memberId; + // } + + public void updateTitle(String title) { + this.title = title; + } } diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/repository/ReviewRepository.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/repository/ReviewRepository.java index 89bffe5..0cf0569 100644 --- a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/repository/ReviewRepository.java +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/review/repository/ReviewRepository.java @@ -1,8 +1,95 @@ package com.wypl.jpamongoreviewdomain.review.repository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import com.wypl.jpacalendardomain.schedule.domain.Schedule; +import com.wypl.jpamemberdomain.member.Member; import com.wypl.jpamongoreviewdomain.review.domain.Review; public interface ReviewRepository extends JpaRepository { + Optional findByReviewIdAndMember(long reviewId, Member member); + + //일정 별 조회 + //오래된순 + List getReviewsByMemberAndScheduleOrderByCreatedAt(Member member, Schedule schedule); + + //최신순 + List getReviewByMemberAndScheduleOrderByCreatedAtDesc(Member member, Schedule schedule); + + //리뷰 조회 + //1. 날짜 설정 안한 경우(오래된 순, 모든 리뷰, 무한 스크롤) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id and r.reviewId > :last_review_id " + + "order by r.reviewId asc " + + "limit 24") + List getReviewsOldestAll( + @Param("member_id") long memberId, + @Param("last_review_id") long lastReviewId + ); + + //2-1. 날짜 설정 안한 경우(최신순, 모든 리뷰, 무한스크롤, 첫번째) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id " + + "order by r.reviewId desc " + + "limit 24") + List getReviewsNewestAll( + @Param("member_id") long memberId + ); + + //2-2. 날짜 설정 안한 경우(최신순, 모든 리뷰, 무한스크롤, 첫번째 이후) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id and r.reviewId < :last_review_id " + + "order by r.reviewId desc " + + "limit 24") + List getReviewsNewestAllAfter( + @Param("member_id") long memberId, + @Param("last_review_id") long lastReviewId + ); + + //3. 날짜 설정한 경우(오래된 순, 무한 스크롤) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id and r.reviewId > :last_review_id and r.createdAt between :start_date and :end_date " + + "order by r.reviewId asc " + + "limit 24") + List getReviewsOldest( + @Param("member_id") long memberId, + @Param("last_review_id") long lastReviewId, + @Param("start_date") LocalDateTime startDate, + @Param("end_date") LocalDateTime endDate + ); + + //4-1. 날짜 설정한 경우(최신순 순, 무한 스크롤, 처음 이후) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id and r.reviewId < :last_review_id and r.createdAt between :start_date and :end_date " + + "order by r.reviewId desc " + + "limit 24") + List getReviewsNewestAfter( + @Param("member_id") long memberId, + @Param("last_review_id") long lastReviewId, + @Param("start_date") LocalDateTime startDate, + @Param("end_date") LocalDateTime endDate + ); + + //4-2. 날짜 설정한 경우(최신순 순, 무한 스크롤, 처음) + @Query("select r " + + "from Review r join fetch r.member m " + + "where m.memberId = :member_id and r.createdAt between :start_date and :end_date " + + "order by r.reviewId desc " + + "limit 24") + List getReviewsNewest( + @Param("member_id") long memberId, + @Param("start_date") LocalDateTime startDate, + @Param("end_date") LocalDateTime endDate + ); } diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/BlockType.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/BlockType.java new file mode 100644 index 0000000..c83a90e --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/BlockType.java @@ -0,0 +1,10 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain; + +public enum BlockType { + REVIEW_TEXT, + REVIEW_PICTURE, + REVIEW_EMOTION, + REVIEW_WEATHER, + REVIEW_KPT, + REVIEW_4F +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContent.java new file mode 100644 index 0000000..c466684 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContent.java @@ -0,0 +1,11 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain; + +import lombok.Getter; + +@Getter +public abstract class ReviewContent { + private BlockType blockType; + protected ReviewContent(BlockType blockType) { + this.blockType = blockType; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContents.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContents.java index d1fc5a3..ee54a96 100644 --- a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContents.java +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/ReviewContents.java @@ -5,7 +5,7 @@ import org.springframework.data.mongodb.core.mapping.Document; -import com.wypl.mongocore.MongoBaseEntity; +import com.wypl.mongocommon.MongoBaseEntity; import jakarta.persistence.Id; import lombok.AccessLevel; @@ -23,5 +23,14 @@ public class ReviewContents extends MongoBaseEntity { @Id private Long reviewId; - private List> contents; + // Todo : max size = 100 + private List> contents; + + public static ReviewContents of(Long reviewId, List> contents) { + return new ReviewContents(reviewId, contents); + } + + public void updateContents(List> contents) { + this.contents = contents; + } } diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/EmotionContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/EmotionContent.java new file mode 100644 index 0000000..f7cdc40 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/EmotionContent.java @@ -0,0 +1,16 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class EmotionContent extends ReviewContent { + private String emoji; + private String description; + + public EmotionContent(String emoji, String description) { + super(REVIEW_EMOTION); + this.emoji = emoji; + this.description = description; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/FourFContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/FourFContent.java new file mode 100644 index 0000000..2a73265 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/FourFContent.java @@ -0,0 +1,20 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class FourFContent extends ReviewContent { + private String facts; + private String feeling; + private String finding; + private String future; + + public FourFContent(String facts, String feeling, String finding, String future) { + super(REVIEW_4F); + this.facts = facts; + this.feeling = feeling; + this.finding = finding; + this.future = future; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/KPTContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/KPTContent.java new file mode 100644 index 0000000..60ad705 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/KPTContent.java @@ -0,0 +1,18 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class KPTContent extends ReviewContent { + private String keepStr; + private String problemStr; + private String tryStr; + + public KPTContent(String keepStr, String problemStr, String tryStr) { + super(REVIEW_KPT); + this.keepStr = keepStr; + this.problemStr = problemStr; + this.tryStr = tryStr; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/PictureContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/PictureContent.java new file mode 100644 index 0000000..52128b2 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/PictureContent.java @@ -0,0 +1,14 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class PictureContent extends ReviewContent { + private String path; + + public PictureContent(String path) { + super(REVIEW_PICTURE); + this.path = path; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/TextContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/TextContent.java new file mode 100644 index 0000000..538e58d --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/TextContent.java @@ -0,0 +1,13 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class TextContent extends ReviewContent { + private String text; + public TextContent(String text) { + super(REVIEW_TEXT); + this.text = text; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/WeatherContent.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/WeatherContent.java new file mode 100644 index 0000000..a35dc77 --- /dev/null +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/domain/contenttype/WeatherContent.java @@ -0,0 +1,16 @@ +package com.wypl.jpamongoreviewdomain.reviewcontents.domain.contenttype; + +import static com.wypl.jpamongoreviewdomain.reviewcontents.domain.BlockType.*; + +import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContent; + +public class WeatherContent extends ReviewContent { + private String weather; + private String description; + + public WeatherContent(String weather, String description) { + super(REVIEW_WEATHER); + this.weather = weather; + this.description = description; + } +} diff --git a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/repository/ReviewContentsRepository.java b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/repository/ReviewContentsRepository.java index 2ef7b09..79adc04 100644 --- a/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/repository/ReviewContentsRepository.java +++ b/domain/jpamongo-review-domain/src/main/java/com/wypl/jpamongoreviewdomain/reviewcontents/repository/ReviewContentsRepository.java @@ -1,8 +1,11 @@ package com.wypl.jpamongoreviewdomain.reviewcontents.repository; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; import com.wypl.jpamongoreviewdomain.reviewcontents.domain.ReviewContents; +@Repository public interface ReviewContentsRepository extends MongoRepository { + ReviewContents findByReviewIdAndDeletedAtNull(long reviewId); }