Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public enum ExceptionMessage {
DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다."),
NOT_EXISTS_VOLUNTEER_APPLY("존재하지 않는 봉사 활동 지원입니다."),
REVIEW_ALREADY_EXISTS("이미 작성한 리뷰가 존재합니다."),
REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다.")
REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다."),
NOT_EXISTS_REVIEW("존재하지 않는 리뷰입니다."),
;

private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.somemore.review.controller;

import static org.springframework.data.domain.Sort.Direction.DESC;

import com.somemore.global.common.response.ApiResponse;
import com.somemore.recruitboard.domain.VolunteerType;
import com.somemore.review.dto.condition.ReviewSearchCondition;
import com.somemore.review.dto.response.ReviewResponseDto;
import com.somemore.review.dto.response.ReviewWithNicknameResponseDto;
import com.somemore.review.usecase.ReviewQueryUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = "Review Query API", description = "리뷰 조회 API")
@RequiredArgsConstructor
@RequestMapping("/api")
@RestController
public class ReviewQueryApiController {

private final ReviewQueryUseCase reviewQueryUseCase;

@Operation(summary = "리뷰 단건 조회", description = "리뷰 ID를 사용하여 단건 리뷰 조회")
@GetMapping("/review/{id}")
public ApiResponse<ReviewResponseDto> getById(@PathVariable Long id) {

return ApiResponse.ok(
200,
reviewQueryUseCase.getReviewById(id),
"리뷰 단건 조회 성공"
);
}

@Operation(summary = "기관별 리뷰 조회", description = "기관 ID를 사용하여 리뷰 조회")
@GetMapping("/reviews/center/{centerId}")
public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByCenterId(
@PathVariable UUID centerId,
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
@RequestParam(required = false) VolunteerType type
) {
ReviewSearchCondition condition = ReviewSearchCondition.builder()
.volunteerType(type)
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
reviewQueryUseCase.getReviewsByCenterId(centerId, condition),
"기관 리뷰 리스트 조회 성공"
);
}

@Operation(summary = "봉사자 리뷰 조회", description = "봉사자 ID를 사용하여 리뷰 조회")
@GetMapping("/reviews/volunteer/{volunteerId}")
public ApiResponse<Page<ReviewWithNicknameResponseDto>> getReviewsByVolunteerId(
@PathVariable UUID volunteerId,
@PageableDefault(sort = "created_at", direction = DESC) Pageable pageable,
@RequestParam(required = false) VolunteerType type
) {
ReviewSearchCondition condition = ReviewSearchCondition.builder()
.volunteerType(type)
.pageable(pageable)
.build();

return ApiResponse.ok(
200,
reviewQueryUseCase.getReviewsByVolunteerId(volunteerId, condition),
"유저 리뷰 리스트 조회 성공"
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.somemore.review.dto.condition;

import com.somemore.recruitboard.domain.VolunteerType;
import lombok.Builder;
import org.springframework.data.domain.Pageable;

@Builder
public record ReviewSearchCondition(
VolunteerType volunteerType,
Pageable pageable
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.somemore.review.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.review.domain.Review;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Builder;

@Builder
@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "리뷰 응답 DTO")
public record ReviewResponseDto(
@Schema(description = "리뷰 ID", example = "123")
Long id,
@Schema(description = "봉사자(작성자) ID", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")
UUID volunteerId,
@Schema(description = "리뷰 제목", example = "제 인생 최고의 봉사활동")
String title,
@Schema(description = "리뷰 내용", example = "정말 유익했습니다. 더보기..")
String content,
@Schema(description = "이미지 링크", example = "https://image.domain.com/links")
String imgUrl,
@Schema(description = "작성 일자", example = "2024-12-01T09:00:00")
LocalDateTime createdAt,
@Schema(description = "수정 일자", example = "2024-12-01T09:00:00")
LocalDateTime updateAt
) {

public static ReviewResponseDto from(Review review) {
return ReviewResponseDto.builder()
.id(review.getId())
.volunteerId(review.getVolunteerId())
.title(review.getTitle())
.content(review.getContent())
.imgUrl(review.getImgUrl())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.somemore.review.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.somemore.review.domain.Review;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.Builder;

@Builder
@JsonNaming(SnakeCaseStrategy.class)
@Schema(description = "작성자 닉네임이 포함된 리뷰 응답 DTO")
public record ReviewWithNicknameResponseDto(
@Schema(description = "리뷰 ID", example = "123")
Long id,
@Schema(description = "봉사자(작성자) ID", example = "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d")
UUID volunteerId,
@Schema(description = "작성자 닉네임", example = "volunteer123")
String volunteerNickname,
@Schema(description = "리뷰 제목", example = "제 인생 최고의 봉사활동")
String title,
@Schema(description = "리뷰 내용", example = "정말 유익했습니다. 더보기..")
String content,
@Schema(description = "이미지 링크", example = "https://image.domain.com/links")
String imgUrl,
@Schema(description = "작성 일자", example = "2024-12-01T09:00:00")
LocalDateTime createdAt,
@Schema(description = "수정 일자", example = "2024-12-01T09:00:00")
LocalDateTime updateAt
) {

public static ReviewWithNicknameResponseDto from(Review review, String volunteerNickname) {
return ReviewWithNicknameResponseDto.builder()
.id(review.getId())
.volunteerId(review.getVolunteerId())
.volunteerNickname(volunteerNickname)
.title(review.getTitle())
.content(review.getContent())
.imgUrl(review.getImgUrl())
.build();
}

}
10 changes: 10 additions & 0 deletions src/main/java/com/somemore/review/repository/ReviewRepository.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package com.somemore.review.repository;

import com.somemore.review.domain.Review;
import com.somemore.review.dto.condition.ReviewSearchCondition;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.domain.Page;

public interface ReviewRepository {

Review save(Review review);

List<Review> saveAll(List<Review> reviews);

Optional<Review> findById(Long id);

boolean existsByVolunteerApplyId(Long volunteerApplyId);

Page<Review> findAllByVolunteerIdAndSearch(UUID volunteerId, ReviewSearchCondition condition);

Page<Review> findAllByCenterIdAndSearch(UUID centerId, ReviewSearchCondition condition);
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
package com.somemore.review.repository;

import com.querydsl.core.types.OrderSpecifier;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import com.somemore.recruitboard.domain.QRecruitBoard;
import com.somemore.recruitboard.domain.VolunteerType;
import com.somemore.review.domain.QReview;
import com.somemore.review.domain.Review;
import com.somemore.review.dto.condition.ReviewSearchCondition;
import com.somemore.volunteerapply.domain.QVolunteerApply;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;

@RequiredArgsConstructor
Expand All @@ -13,11 +27,20 @@ public class ReviewRepositoryImpl implements ReviewRepository {
private final ReviewJpaRepository reviewJpaRepository;
private final JPAQueryFactory queryFactory;

private final static QReview review = QReview.review;
private final static QVolunteerApply volunteerApply = QVolunteerApply.volunteerApply;
private final static QRecruitBoard recruitBoard = QRecruitBoard.recruitBoard;

@Override
public Review save(Review review) {
return reviewJpaRepository.save(review);
}

@Override
public List<Review> saveAll(List<Review> reviews) {
return reviewJpaRepository.saveAll(reviews);
}

@Override
public Optional<Review> findById(Long id) {
return reviewJpaRepository.findByIdAndDeletedFalse(id);
Expand All @@ -27,4 +50,77 @@ public Optional<Review> findById(Long id) {
public boolean existsByVolunteerApplyId(Long volunteerApplyId) {
return reviewJpaRepository.existsByVolunteerApplyId(volunteerApplyId);
}

@Override
public Page<Review> findAllByVolunteerIdAndSearch(UUID volunteerId,
ReviewSearchCondition condition) {

BooleanExpression predicate = isNotDeleted()
.and(review.volunteerId.eq(volunteerId))
.and(eqVolunteerType(condition.volunteerType()));

return getReviews(condition, predicate);

}

@Override
public Page<Review> findAllByCenterIdAndSearch(UUID centerId, ReviewSearchCondition condition) {

BooleanExpression predicate = isNotDeleted()
.and(recruitBoard.centerId.eq(centerId))
.and(eqVolunteerType(condition.volunteerType()));

return getReviews(condition, predicate);
}

@NotNull
private Page<Review> getReviews(ReviewSearchCondition condition, BooleanExpression predicate) {
List<Review> content = queryFactory.select(review)
.from(review)
.join(volunteerApply).on(review.volunteerApplyId.eq(volunteerApply.id))
.join(recruitBoard).on(recruitBoard.id.eq(volunteerApply.recruitBoardId))
.where(predicate)
.offset(condition.pageable().getOffset())
.limit(condition.pageable().getPageSize())
.orderBy(toOrderSpecifiers(condition.pageable().getSort()))
.fetch();

JPAQuery<Long> countQuery = queryFactory
.select(review.count())
.join(volunteerApply).on(review.volunteerApplyId.eq(volunteerApply.id))
.join(recruitBoard).on(recruitBoard.id.eq(volunteerApply.recruitBoardId))
.from(review)
.where(predicate);

return PageableExecutionUtils.getPage(content, condition.pageable(), countQuery::fetchOne);
}

private BooleanExpression isNotDeleted() {
return review.deleted.isFalse();
}

private BooleanExpression eqVolunteerType(VolunteerType type) {
return type != null
? recruitBoard.recruitmentInfo.volunteerType.eq(type) : null;
}

private OrderSpecifier<?>[] toOrderSpecifiers(Sort sort) {
return sort.stream()
.map(order -> {
String property = order.getProperty();

if ("created_at".equals(property)) {
return order.isAscending()
? review.createdAt.asc()
: review.createdAt.desc();
} else if ("updated_at".equals(property)) {
return order.isAscending()
? review.updatedAt.asc()
: review.updatedAt.desc();
} else {
throw new IllegalStateException("Invalid sort property: " + property);
}
})
.toArray(OrderSpecifier[]::new);
}
}
Loading