From 6d2cefbce264852c1a8d0e7a4f312b3b2daab537 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:14:47 +0900 Subject: [PATCH 01/18] =?UTF-8?q?feat(volunteer-apply):=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../volunteerapply/domain/VolunteerApply.java | 37 ++++++++++++++++--- .../repository/VolunteerApplyRepository.java | 6 +-- .../VolunteerApplyRepositoryImpl.java | 15 ++++++-- 3 files changed, 45 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java b/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java index 64ceaf9c1..2f0a54615 100644 --- a/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java +++ b/src/main/java/com/somemore/volunteerapply/domain/VolunteerApply.java @@ -1,14 +1,22 @@ package com.somemore.volunteerapply.domain; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; + import com.somemore.global.common.BaseEntity; -import jakarta.persistence.*; +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 java.util.UUID; import lombok.AccessLevel; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.UUID; - @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @@ -33,12 +41,29 @@ public class VolunteerApply extends BaseEntity { private Boolean attended = false; @Builder - public VolunteerApply(UUID volunteerId, Long recruitBoardId, ApplyStatus status, Boolean attended) { + public VolunteerApply(UUID volunteerId, Long recruitBoardId, ApplyStatus status, + Boolean attended) { this.volunteerId = volunteerId; this.recruitBoardId = recruitBoardId; this.status = status; this.attended = attended; } -} -// TODO 상태 업데이트 메서드들을 만들고 빌더에서 status를 변경 불가하도록 + public void changeStatus(ApplyStatus status) { + if (isVolunteerActivityCompleted()) { + throw new IllegalStateException("이미 완료된 봉사활동에 대해서는 변경이 불가능합니다."); + } + this.status = status; + } + + public void changeAttended(Boolean attended) { + if (this.status != APPROVED) { + throw new IllegalStateException("승인되지 않은 봉사 지원은 참석 여부를 변경할 수 없습니다."); + } + this.attended = attended; + } + + public boolean isVolunteerActivityCompleted() { + return this.attended && this.status == APPROVED; + } +} diff --git a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java index 5f68b0c95..cd160a1fd 100644 --- a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java +++ b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepository.java @@ -1,12 +1,11 @@ package com.somemore.volunteerapply.repository; import com.somemore.volunteerapply.domain.VolunteerApply; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; - import java.util.List; import java.util.Optional; import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; public interface VolunteerApplyRepository { @@ -14,4 +13,5 @@ public interface VolunteerApplyRepository { Optional findById(Long id); List findVolunteerIdsByRecruitIds(List recruitIds); Page findAllByRecruitId(Long recruitId, Pageable pageable); + Optional findByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId); } diff --git a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java index 61112ce1b..3c0031234 100644 --- a/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java +++ b/src/main/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImpl.java @@ -5,6 +5,9 @@ import com.querydsl.jpa.impl.JPAQueryFactory; import com.somemore.volunteerapply.domain.QVolunteerApply; import com.somemore.volunteerapply.domain.VolunteerApply; +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.PageImpl; @@ -12,10 +15,6 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Repository; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - @RequiredArgsConstructor @Repository public class VolunteerApplyRepositoryImpl implements VolunteerApplyRepository { @@ -71,6 +70,14 @@ public Page findAllByRecruitId(Long recruitId, Pageable pageable ); } + @Override + public Optional findByRecruitIdAndVolunteerId(Long recruitId, + UUID volunteerId) { + BooleanExpression exp = volunteerApply.recruitBoardId.eq(recruitId) + .and(volunteerApply.volunteerId.eq(volunteerId)); + return findOne(exp); + } + private Long getCount(BooleanExpression exp) { return queryFactory .select(volunteerApply.count()) From e6f0414aa44e41dab17457a12e438c740dbc5fa3 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:15:11 +0900 Subject: [PATCH 02/18] =?UTF-8?q?feat(volunteer-apply):=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/VolunteerApplyTest.java | 104 ++++++++++++++++++ .../VolunteerApplyRepositoryImplTest.java | 47 ++++++-- 2 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java diff --git a/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java b/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java new file mode 100644 index 000000000..187068966 --- /dev/null +++ b/src/test/java/com/somemore/volunteerapply/domain/VolunteerApplyTest.java @@ -0,0 +1,104 @@ +package com.somemore.volunteerapply.domain; + +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; +import static com.somemore.volunteerapply.domain.ApplyStatus.REJECTED; +import static com.somemore.volunteerapply.domain.ApplyStatus.WAITING; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class VolunteerApplyTest { + + @DisplayName("지원 상태를 변경할 수 있다") + @Test + void changeStatus() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(WAITING) + .attended(false) + .build(); + + // when + apply.changeStatus(APPROVED); + + // then + assertThat(apply.getStatus()).isEqualTo(APPROVED); + } + + @Test + @DisplayName("지원 상태 변경 실패 - 이미 완료된 봉사활동") + void changeStatusWhenCompletedActivity() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(true) + .build(); + + // when + // then + assertThatThrownBy( + () -> apply.changeStatus(REJECTED) + ).isInstanceOf(IllegalStateException.class); + } + + @DisplayName("봉사 지원 상태를 변경") + @Test + void markAsAttended() { + // given + VolunteerApply volunteerApply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(false) + .build(); + + // when + volunteerApply.changeAttended(true); + + // then + assertThat(volunteerApply.getAttended()).isTrue(); + } + + @Test + @DisplayName("참석 처리 실패 - 승인되지 않은 상태") + void changeAttendedWhenNotApproved() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(WAITING) + .attended(false) + .build(); + + // when & then + assertThatThrownBy( + () -> apply.changeAttended(true) + ).isInstanceOf(IllegalStateException.class); + } + + @DisplayName("봉사 참석 여부 확인") + @Test + void isCompleted() { + // given + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(UUID.randomUUID()) + .recruitBoardId(1L) + .status(APPROVED) + .attended(true) + .build(); + + // when + boolean res = apply.isVolunteerActivityCompleted(); + + // then + assertThat(res).isTrue(); + } +} + diff --git a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java index 7792a3967..2ac0d9ac5 100644 --- a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java +++ b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java @@ -1,8 +1,13 @@ package com.somemore.volunteerapply.repository; +import static org.assertj.core.api.Assertions.assertThat; + import com.somemore.IntegrationTestSupport; -import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.domain.ApplyStatus; +import com.somemore.volunteerapply.domain.VolunteerApply; +import java.util.List; +import java.util.Optional; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -12,12 +17,6 @@ import org.springframework.data.domain.Sort; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - @Transactional class VolunteerApplyRepositoryImplTest extends IntegrationTestSupport { @@ -73,7 +72,8 @@ void saveAndFindById() { @Test void findVolunteerIdsByRecruitIds() { // When - List volunteerIds = volunteerApplyRepository.findVolunteerIdsByRecruitIds(List.of(1L, 2L)); + List volunteerIds = volunteerApplyRepository.findVolunteerIdsByRecruitIds( + List.of(1L, 2L)); // Then assertThat(volunteerIds).hasSize(20); @@ -87,8 +87,10 @@ void findAllByRecruitId() { PageRequest secondPage = PageRequest.of(1, 10, Sort.by(Sort.Order.asc("created_at"))); // When - Page firstPageResult = volunteerApplyRepository.findAllByRecruitId(1L, firstPage); - Page secondPageResult = volunteerApplyRepository.findAllByRecruitId(1L, secondPage); + Page firstPageResult = volunteerApplyRepository.findAllByRecruitId(1L, + firstPage); + Page secondPageResult = volunteerApplyRepository.findAllByRecruitId(1L, + secondPage); // Then assertThat(firstPageResult.getContent()).hasSize(10); @@ -101,4 +103,27 @@ void findAllByRecruitId() { assertThat(secondPageResult.hasNext()).isFalse(); assertThat(secondPageResult.hasPrevious()).isTrue(); } -} \ No newline at end of file + + @DisplayName("모집글 아이디와 봉사자 아이디로 봉사 지원을 조회할 수 있다.") + @Test + void findByRecruitIdAndVolunteerId() { + // given + Long recruitId = 1234L; + UUID volunteerId = UUID.randomUUID(); + + VolunteerApply newApply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(newApply); + + // when + Optional findApply = volunteerApplyRepository.findByRecruitIdAndVolunteerId( + recruitId, volunteerId); + + // then + assertThat(findApply).isPresent(); + } +} From 76540611eadc546f1ce035a93c22761184a8319d Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:15:55 +0900 Subject: [PATCH 03/18] =?UTF-8?q?feat(volunteer-apply):=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=9D=EC=84=B1=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/VolunteerApplyQueryService.java | 20 +++++++++++++++---- .../usecase/VolunteerApplyQueryUseCase.java | 3 ++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java index 100765e23..283864b86 100644 --- a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java +++ b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java @@ -1,15 +1,16 @@ package com.somemore.volunteerapply.service; +import com.somemore.global.exception.BadRequestException; +import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; import com.somemore.volunteerapply.usecase.VolunteerApplyQueryUseCase; +import java.util.List; +import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - @Slf4j @Service @RequiredArgsConstructor @@ -17,9 +18,20 @@ public class VolunteerApplyQueryService implements VolunteerApplyQueryUseCase { private final VolunteerApplyRepository volunteerApplyRepository; + @Override public List getVolunteerIdsByRecruitIds(List recruitIds) { - return volunteerApplyRepository.findVolunteerIdsByRecruitIds(recruitIds); } + + @Override + public VolunteerApply getByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId) { + return getVolunteerApplyBy(recruitId, volunteerId); + } + + private VolunteerApply getVolunteerApplyBy(Long recruitBoardId, UUID volunteerId) { + return volunteerApplyRepository.findByRecruitIdAndVolunteerId(recruitBoardId, + volunteerId).orElseThrow( + () -> new BadRequestException(NOT_EXISTS_VOLUNTEER_APPLY.getMessage())); + } } diff --git a/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java b/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java index e7ab746b2..80916166c 100644 --- a/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java +++ b/src/main/java/com/somemore/volunteerapply/usecase/VolunteerApplyQueryUseCase.java @@ -1,9 +1,10 @@ package com.somemore.volunteerapply.usecase; +import com.somemore.volunteerapply.domain.VolunteerApply; import java.util.List; import java.util.UUID; public interface VolunteerApplyQueryUseCase { - List getVolunteerIdsByRecruitIds(List recruitIds); + VolunteerApply getByRecruitIdAndVolunteerId(Long recruitId, UUID volunteerId); } From d8106ff8e05002d606c50b9262cb81ed5d6a4e7f Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:16:07 +0900 Subject: [PATCH 04/18] =?UTF-8?q?test(volunteer-apply):=20=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EC=83=9D=EC=84=B1=EC=97=90=20=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VolunteerApplyQueryServiceTest.java | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java index 96168a3d1..320e0bf29 100644 --- a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java +++ b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java @@ -1,19 +1,18 @@ package com.somemore.volunteerapply.service; +import static org.assertj.core.api.Assertions.assertThat; + import com.somemore.IntegrationTestSupport; import com.somemore.volunteerapply.domain.ApplyStatus; import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.List; +import java.util.UUID; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; - @Transactional class VolunteerApplyQueryServiceTest extends IntegrationTestSupport { @@ -39,7 +38,8 @@ void getVolunteerIdsByRecruitIds() { volunteerApplyRepository.save(apply2); // When - List volunteerIds = volunteerApplyQueryService.getVolunteerIdsByRecruitIds(List.of(recruitId1, recruitId2)); + List volunteerIds = volunteerApplyQueryService.getVolunteerIdsByRecruitIds( + List.of(recruitId1, recruitId2)); // Then assertThat(volunteerIds) @@ -55,4 +55,28 @@ private VolunteerApply createVolunteerApply(Long recruitId, UUID volunteerId) { .attended(false) .build(); } -} \ No newline at end of file + + @DisplayName("모집글 아이디와 봉사자 아이디로 조회할 수 있다") + @Test + void getByRecruitIdAndVolunteerId() { + // given + Long recruitId = 1234L; + UUID volunteerId = UUID.randomUUID(); + + VolunteerApply newApply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(newApply); + + // when + VolunteerApply apply = volunteerApplyQueryService.getByRecruitIdAndVolunteerId( + recruitId, volunteerId); + + // then + assertThat(apply.getRecruitBoardId()).isEqualTo(recruitId); + assertThat(apply.getVolunteerId()).isEqualTo(volunteerId); + } +} From 62ffd5cb0a53dc1c0d0552a4b32e066517eec61d Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:17:12 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/somemore/domains/Review.java | 27 ---------- .../com/somemore/review/domain/Review.java | 53 +++++++++++++++++++ .../repository/ReviewJpaRepository.java | 15 ++++++ .../review/repository/ReviewRepository.java | 13 +++++ .../repository/ReviewRepositoryImpl.java | 30 +++++++++++ 5 files changed, 111 insertions(+), 27 deletions(-) delete mode 100644 src/main/java/com/somemore/domains/Review.java create mode 100644 src/main/java/com/somemore/review/domain/Review.java create mode 100644 src/main/java/com/somemore/review/repository/ReviewJpaRepository.java create mode 100644 src/main/java/com/somemore/review/repository/ReviewRepository.java create mode 100644 src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java diff --git a/src/main/java/com/somemore/domains/Review.java b/src/main/java/com/somemore/domains/Review.java deleted file mode 100644 index ee4720bd6..000000000 --- a/src/main/java/com/somemore/domains/Review.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.somemore.domains; - -import jakarta.persistence.*; -import lombok.Getter; -import lombok.Setter; - -import java.util.UUID; - -@Getter -@Setter -@Entity -public class Review { - @Id - @GeneratedValue(strategy = GenerationType.UUID) - private UUID id; - - @Column(name = "center_id", nullable = false, length = 16) - private String centerId; - - @Lob - @Column(name = "content", nullable = false) - private String content; - - @Column(name = "img_url") - private String imgUrl; - -} \ No newline at end of file diff --git a/src/main/java/com/somemore/review/domain/Review.java b/src/main/java/com/somemore/review/domain/Review.java new file mode 100644 index 000000000..b4967407b --- /dev/null +++ b/src/main/java/com/somemore/review/domain/Review.java @@ -0,0 +1,53 @@ +package com.somemore.review.domain; + +import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PROTECTED; + +import com.somemore.global.common.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = PROTECTED) +@Entity +@Table(name = "review") +public class Review extends BaseEntity { + + @Id + @GeneratedValue(strategy = IDENTITY) + private Long id; + + @Column(name = "volunteer_apply_id", nullable = false) + private Long volunteerApplyId; + + @Column(name = "volunteer_id", nullable = false, length = 16) + private UUID volunteerId; + + @Column(name = "title", nullable = false) + private String title; + + @Lob + @Column(name = "content", nullable = false) + private String content; + + @Column(name = "img_url", nullable = false) + private String imgUrl; + + @Builder + public Review(Long volunteerApplyId, UUID volunteerId, String title, + String content, String imgUrl) { + this.volunteerApplyId = volunteerApplyId; + this.volunteerId = volunteerId; + this.title = title; + this.content = content; + this.imgUrl = imgUrl; + } +} diff --git a/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java b/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java new file mode 100644 index 000000000..694055295 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewJpaRepository.java @@ -0,0 +1,15 @@ +package com.somemore.review.repository; + +import com.somemore.review.domain.Review; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface ReviewJpaRepository extends JpaRepository { + + Optional findByIdAndDeletedFalse(Long id); + + @Query("SELECT COUNT(r) > 0 FROM Review r WHERE r.volunteerApplyId = :volunteerId AND r.deleted = false") + boolean existsByVolunteerApplyId(Long volunteerId); + +} diff --git a/src/main/java/com/somemore/review/repository/ReviewRepository.java b/src/main/java/com/somemore/review/repository/ReviewRepository.java new file mode 100644 index 000000000..08b817fd8 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewRepository.java @@ -0,0 +1,13 @@ +package com.somemore.review.repository; + +import com.somemore.review.domain.Review; +import java.util.Optional; + +public interface ReviewRepository { + + Review save(Review review); + + Optional findById(Long id); + + boolean existsByVolunteerApplyId(Long volunteerApplyId); +} diff --git a/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java b/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 000000000..856efe188 --- /dev/null +++ b/src/main/java/com/somemore/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,30 @@ +package com.somemore.review.repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.somemore.review.domain.Review; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@RequiredArgsConstructor +@Repository +public class ReviewRepositoryImpl implements ReviewRepository { + + private final ReviewJpaRepository reviewJpaRepository; + private final JPAQueryFactory queryFactory; + + @Override + public Review save(Review review) { + return reviewJpaRepository.save(review); + } + + @Override + public Optional findById(Long id) { + return reviewJpaRepository.findByIdAndDeletedFalse(id); + } + + @Override + public boolean existsByVolunteerApplyId(Long volunteerApplyId) { + return reviewJpaRepository.existsByVolunteerApplyId(volunteerApplyId); + } +} From 3ffa4b951318c9c012a368b8d860cc8e03c1ed62 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:17:39 +0900 Subject: [PATCH 06/18] =?UTF-8?q?test(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=A0=88=ED=8F=AC=EC=A7=80?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ReviewRepositoryImplTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java diff --git a/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java b/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java new file mode 100644 index 000000000..5a7697c8a --- /dev/null +++ b/src/test/java/com/somemore/review/repository/ReviewRepositoryImplTest.java @@ -0,0 +1,62 @@ +package com.somemore.review.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.somemore.IntegrationTestSupport; +import com.somemore.review.domain.Review; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class ReviewRepositoryImplTest extends IntegrationTestSupport { + + @Autowired + private ReviewRepositoryImpl reviewRepository; + + @DisplayName("리뷰 생성 및 조회") + @Test + void saveAndFind() { + // given + Review review = Review.builder() + .volunteerApplyId(1L) + .volunteerId(UUID.randomUUID()) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + reviewRepository.save(review); + + // when + Optional findReview = reviewRepository.findById(review.getId()); + + // then + assertThat(findReview).isPresent(); + assertThat(findReview.get().getId()).isEqualTo(review.getId()); + } + + @DisplayName("봉사 지원 아이디로 리뷰 존재 유무를 확인할 수 있다") + @Test + void existsByVolunteerApplyId() { + // given + Long volunteerApplyId = 1L; + Review review = Review.builder() + .volunteerApplyId(volunteerApplyId) + .volunteerId(UUID.randomUUID()) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + reviewRepository.save(review); + + // when + boolean result = reviewRepository.existsByVolunteerApplyId(volunteerApplyId); + + // then + assertThat(result).isTrue(); + } + +} From 282d5f203830ecc8887f79e0ce99c254f7f281dd Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:18:01 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/request/ReviewCreateRequestDto.java | 36 +++++++++++++ .../review/service/CreateReviewService.java | 52 +++++++++++++++++++ .../review/usecase/CreateReviewUseCase.java | 11 ++++ 3 files changed, 99 insertions(+) create mode 100644 src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java create mode 100644 src/main/java/com/somemore/review/service/CreateReviewService.java create mode 100644 src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java diff --git a/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java new file mode 100644 index 000000000..8a13cc331 --- /dev/null +++ b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java @@ -0,0 +1,36 @@ +package com.somemore.review.dto.request; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import com.somemore.review.domain.Review; +import com.somemore.volunteerapply.domain.VolunteerApply; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; +import lombok.Builder; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +@Builder +public record ReviewCreateRequestDto( + @Schema(description = "봉사 모집글 아이디", example = "1") + @NotNull(message = "봉사 모집글 아이디는 필수 값입니다.") + Long recruitBoardId, + @Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동") + @NotBlank(message = "리뷰 제목은 필수 값입니다.") + String title, + @Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요 더보기..
") + @NotBlank(message = "리뷰 내용은 필수 값입니다.") + String content +) { + + public Review toEntity(VolunteerApply apply, UUID volunteerId, String imgUrl) { + return Review.builder() + .volunteerApplyId(apply.getId()) + .volunteerId(volunteerId) + .title(title) + .content(content) + .imgUrl(imgUrl) + .build(); + } +} diff --git a/src/main/java/com/somemore/review/service/CreateReviewService.java b/src/main/java/com/somemore/review/service/CreateReviewService.java new file mode 100644 index 000000000..fa53b6255 --- /dev/null +++ b/src/main/java/com/somemore/review/service/CreateReviewService.java @@ -0,0 +1,52 @@ +package com.somemore.review.service; + +import static com.somemore.global.exception.ExceptionMessage.REVIEW_ALREADY_EXISTS; +import static com.somemore.global.exception.ExceptionMessage.REVIEW_RESTRICTED_TO_ATTENDED; + +import com.somemore.global.exception.BadRequestException; +import com.somemore.review.domain.Review; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.repository.ReviewRepository; +import com.somemore.review.usecase.CreateReviewUseCase; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.usecase.VolunteerApplyQueryUseCase; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Transactional +@Service +public class CreateReviewService implements CreateReviewUseCase { + + private final ReviewRepository reviewRepository; + private final VolunteerApplyQueryUseCase volunteerApplyQueryUseCase; + + @Override + public Long createReview(ReviewCreateRequestDto requestDto, UUID volunteerId, String imgUrl) { + VolunteerApply apply = getVolunteerApply(requestDto.recruitBoardId(), volunteerId); + validateReviewNotExist(apply); + validateActivityCompletion(apply); + + Review review = requestDto.toEntity(apply, volunteerId, imgUrl); + return reviewRepository.save(review).getId(); + } + + private VolunteerApply getVolunteerApply(Long recruitBoardId, UUID volunteerId) { + return volunteerApplyQueryUseCase.getByRecruitIdAndVolunteerId(recruitBoardId, volunteerId); + } + + private void validateReviewNotExist(VolunteerApply apply) { + if (reviewRepository.existsByVolunteerApplyId(apply.getId())) { + throw new BadRequestException(REVIEW_ALREADY_EXISTS.getMessage()); + } + } + + private void validateActivityCompletion(VolunteerApply apply) { + if (apply.isVolunteerActivityCompleted()) { + return; + } + throw new BadRequestException(REVIEW_RESTRICTED_TO_ATTENDED.getMessage()); + } +} diff --git a/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java b/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java new file mode 100644 index 000000000..ec2fb7f40 --- /dev/null +++ b/src/main/java/com/somemore/review/usecase/CreateReviewUseCase.java @@ -0,0 +1,11 @@ +package com.somemore.review.usecase; + +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import java.util.UUID; + +public interface CreateReviewUseCase { + + Long createReview(ReviewCreateRequestDto requestDto, UUID volunteerId, String imgUrl); + + +} From e64afd8f5eda34f8aa682956a79b5dfccb21693c Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:18:10 +0900 Subject: [PATCH 08/18] =?UTF-8?q?test(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CreateReviewServiceTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/test/java/com/somemore/review/service/CreateReviewServiceTest.java diff --git a/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java new file mode 100644 index 000000000..af86b7222 --- /dev/null +++ b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java @@ -0,0 +1,134 @@ +package com.somemore.review.service; + +import static com.somemore.global.exception.ExceptionMessage.REVIEW_ALREADY_EXISTS; +import static com.somemore.global.exception.ExceptionMessage.REVIEW_RESTRICTED_TO_ATTENDED; +import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import com.somemore.IntegrationTestSupport; +import com.somemore.global.exception.BadRequestException; +import com.somemore.global.exception.ExceptionMessage; +import com.somemore.review.domain.Review; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.repository.ReviewRepository; +import com.somemore.review.repository.ReviewRepositoryImpl; +import com.somemore.volunteerapply.domain.ApplyStatus; +import com.somemore.volunteerapply.domain.VolunteerApply; +import com.somemore.volunteerapply.repository.VolunteerApplyRepository; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class CreateReviewServiceTest extends IntegrationTestSupport { + + @Autowired + private CreateReviewService createReviewService; + + @Autowired + private ReviewRepository reviewRepository; + + @Autowired + private VolunteerApplyRepository volunteerApplyRepository; + + @DisplayName("리뷰 생성") + @Test + void createReview() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(true) + .build(); + volunteerApplyRepository.save(apply); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + Long reviewId = createReviewService.createReview(requestDto, volunteerId, ""); + + // then + Optional findReview = reviewRepository.findById(reviewId); + assertThat(findReview).isPresent(); + assertThat(findReview.get().getId()).isEqualTo(reviewId); + } + + @DisplayName("참석하지 않은 봉사 활동에 대해 리뷰를 생성하면 에러가 발생한다") + @Test + void createReviewWhenNotCompleted() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(apply); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + // then + assertThatThrownBy( + () -> createReviewService.createReview(requestDto, volunteerId, "") + ).isInstanceOf(BadRequestException.class) + .hasMessage(REVIEW_RESTRICTED_TO_ATTENDED.getMessage()); + } + + @DisplayName("이미 작성한 봉사 활동에 대해 리뷰를 생성하면 에러가 발생한다") + @Test + void createReviewWhenExistsReview() { + // given + UUID volunteerId = UUID.randomUUID(); + Long recruitId = 200L; + VolunteerApply apply = VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(200L) + .status(APPROVED) + .attended(false) + .build(); + volunteerApplyRepository.save(apply); + + Review review = Review.builder() + .volunteerApplyId(apply.getId()) + .volunteerId(volunteerId) + .title("리뷰 제목") + .content("리뷰 내용") + .imgUrl("") + .build(); + + reviewRepository.save(review); + + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(recruitId) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + // when + // then + assertThatThrownBy( + () -> createReviewService.createReview(requestDto, volunteerId, "") + ).isInstanceOf(BadRequestException.class) + .hasMessage(REVIEW_ALREADY_EXISTS.getMessage()); + } + +} From 5aa9c904213a2578610fdb96945509cf7012d35f Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:45:22 +0900 Subject: [PATCH 09/18] fix: conflict remove --- .../java/com/somemore/global/exception/ExceptionMessage.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/somemore/global/exception/ExceptionMessage.java b/src/main/java/com/somemore/global/exception/ExceptionMessage.java index 257147885..27fca6ff3 100644 --- a/src/main/java/com/somemore/global/exception/ExceptionMessage.java +++ b/src/main/java/com/somemore/global/exception/ExceptionMessage.java @@ -24,7 +24,10 @@ public enum ExceptionMessage { NOT_EXISTS_VOLUNTEER("존재하지 않는 봉사자입니다."), UNAUTHORIZED_VOLUNTEER_DETAIL("해당 봉사자의 상세 정보 조회 권한이 없습니다."), CANNOT_CANCEL_DELETED_INTEREST_CENTER("이미 삭제된 관심 기관은 취소할 수 없습니다."), - DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다.") + DUPLICATE_INTEREST_CENTER("이미 관심 표시한 기관입니다."), + NOT_EXISTS_VOLUNTEER_APPLY("존재하지 않는 봉사 활동 지원입니다."), + REVIEW_ALREADY_EXISTS("이미 작성한 리뷰가 존재합니다."), + REVIEW_RESTRICTED_TO_ATTENDED("리뷰는 참석한 봉사에 한해서만 작성할 수 있습니다.") ; private final String message; From 9846ca44f67b5e651f759b355cb577fbdba06fe7 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 13:51:50 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/somemore/review/service/CreateReviewServiceTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java index af86b7222..3b7ec75d6 100644 --- a/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java +++ b/src/test/java/com/somemore/review/service/CreateReviewServiceTest.java @@ -5,16 +5,12 @@ import static com.somemore.volunteerapply.domain.ApplyStatus.APPROVED; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.*; import com.somemore.IntegrationTestSupport; import com.somemore.global.exception.BadRequestException; -import com.somemore.global.exception.ExceptionMessage; import com.somemore.review.domain.Review; import com.somemore.review.dto.request.ReviewCreateRequestDto; import com.somemore.review.repository.ReviewRepository; -import com.somemore.review.repository.ReviewRepositoryImpl; -import com.somemore.volunteerapply.domain.ApplyStatus; import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; import java.util.Optional; From 47ac38ec977579a8af6273d69244c133f1026475 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 18:53:44 +0900 Subject: [PATCH 11/18] =?UTF-8?q?chore(review):=20import=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../volunteerapply/service/VolunteerApplyQueryService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java index 283864b86..6a832e5eb 100644 --- a/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java +++ b/src/main/java/com/somemore/volunteerapply/service/VolunteerApplyQueryService.java @@ -1,5 +1,7 @@ package com.somemore.volunteerapply.service; +import static com.somemore.global.exception.ExceptionMessage.NOT_EXISTS_VOLUNTEER_APPLY; + import com.somemore.global.exception.BadRequestException; import com.somemore.volunteerapply.domain.VolunteerApply; import com.somemore.volunteerapply.repository.VolunteerApplyRepository; From d89ae9705e06a4b62aa06e13e9fb30d95d6ff862 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 19:33:25 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewCommandApiController.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/java/com/somemore/review/controller/ReviewCommandApiController.java diff --git a/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java new file mode 100644 index 000000000..db37db055 --- /dev/null +++ b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java @@ -0,0 +1,51 @@ +package com.somemore.review.controller; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; + +import com.somemore.global.common.response.ApiResponse; +import com.somemore.imageupload.dto.ImageUploadRequestDto; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.usecase.CreateReviewUseCase; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@Tag(name = "Review Command API", description = "리뷰 생성 수정 삭제 API") +@RequiredArgsConstructor +@RequestMapping("/api") +@RestController +public class ReviewCommandApiController { + + private final CreateReviewUseCase createReviewUseCase; + private final ImageUploadUseCase imageUploadUseCase; + + @Secured("ROLE_VOLUNTEER") + @Operation(summary = "리뷰 등록", description = "리뷰를 등록합니다.") + @PostMapping(value = "/review", consumes = MULTIPART_FORM_DATA_VALUE) + public ApiResponse createReview( + @Valid @RequestPart("data") ReviewCreateRequestDto requestDto, + @RequestPart(value = "img_file", required = false) MultipartFile image) { + + String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); + return ApiResponse.ok( + 201, + createReviewUseCase.createReview(requestDto, getId(), imgUrl), + "리뷰 등록 성공" + ); + } + + private static UUID getId() { + return UUID.fromString(SecurityContextHolder.getContext().getAuthentication().getName()); + } + +} From 53fd38e235346b6d1cd25128e292027521a96fb3 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 19:33:34 +0900 Subject: [PATCH 13/18] =?UTF-8?q?test(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ReviewCommandApiControllerTest.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java diff --git a/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java b/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java new file mode 100644 index 000000000..2e7b1487c --- /dev/null +++ b/src/test/java/com/somemore/review/controller/ReviewCommandApiControllerTest.java @@ -0,0 +1,84 @@ +package com.somemore.review.controller; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.somemore.ControllerTestSupport; +import com.somemore.WithMockCustomUser; +import com.somemore.imageupload.usecase.ImageUploadUseCase; +import com.somemore.review.dto.request.ReviewCreateRequestDto; +import com.somemore.review.usecase.CreateReviewUseCase; +import java.util.UUID; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.web.servlet.MockMvc; + +class ReviewCommandApiControllerTest extends ControllerTestSupport { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @MockBean + private ImageUploadUseCase imageUploadUseCase; + + @MockBean + private CreateReviewUseCase createReviewUseCase; + + @DisplayName("리뷰 생성") + @Test + @WithMockCustomUser() + void createReview() throws Exception { + // given + ReviewCreateRequestDto requestDto = ReviewCreateRequestDto.builder() + .recruitBoardId(1L) + .title("리뷰 제목") + .content("리뷰 내용") + .build(); + + MockMultipartFile imageFile = new MockMultipartFile( + "img_file", + "test-image.jpg", + MediaType.IMAGE_JPEG_VALUE, + "test image content".getBytes() + ); + + MockMultipartFile requestData = new MockMultipartFile( + "data", + "", + MediaType.APPLICATION_JSON_VALUE, + objectMapper.writeValueAsBytes(requestDto) + ); + + String imgUrl = "https://example.com/image/test-image.jpg"; + Long reviewId = 1L; + + given(imageUploadUseCase.uploadImage(any())).willReturn(imgUrl); + given(createReviewUseCase.createReview(any(), any(UUID.class), + anyString())).willReturn(reviewId); + + // when + mockMvc.perform(multipart("/api/review") + .file(requestData) + .file(imageFile) + .contentType(MULTIPART_FORM_DATA) + .header("Authorization", "Bearer access-token")) + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(201)) + .andExpect(jsonPath("$.data").value(reviewId)) + .andExpect(jsonPath("$.message").value("리뷰 등록 성공")); + } +} From db50577d4b361462334b87090a60ae7bc206fab4 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 19:41:15 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat(exception-handler):=20400=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20title=20=EB=AC=B8=EA=B5=AC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/somemore/global/handler/GlobalExceptionHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java index d734b3d3d..6201d687d 100644 --- a/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/somemore/global/handler/GlobalExceptionHandler.java @@ -19,8 +19,8 @@ ProblemDetail handleBadRequestException(final BadRequestException e) { //status와 에러에 대한 자세한 설명 ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); - //아래와 같이 필드 확장 가능 - problemDetail.setTitle("무슨 에러입니다"); + // 아래와 같이 필드 확장 가능 + problemDetail.setTitle("잘못된 요청입니다"); return problemDetail; } From d2bfe3c466c0bc71fca06397186878f5c21a958f Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 19:41:33 +0900 Subject: [PATCH 15/18] =?UTF-8?q?test(review):=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VolunteerApplyRepositoryImplTest.java | 23 +++++++++---------- .../VolunteerApplyQueryServiceTest.java | 16 ++++++++----- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java index 2ac0d9ac5..5ee125c8f 100644 --- a/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java +++ b/src/test/java/com/somemore/volunteerapply/repository/VolunteerApplyRepositoryImplTest.java @@ -51,12 +51,7 @@ void setUp() { @Test void saveAndFindById() { // Given - VolunteerApply newApply = VolunteerApply.builder() - .volunteerId(UUID.randomUUID()) - .recruitBoardId(1L) - .status(ApplyStatus.APPROVED) - .attended(false) - .build(); + VolunteerApply newApply = createApply(UUID.randomUUID(), 1L); VolunteerApply savedApply = volunteerApplyRepository.save(newApply); // When @@ -111,12 +106,7 @@ void findByRecruitIdAndVolunteerId() { Long recruitId = 1234L; UUID volunteerId = UUID.randomUUID(); - VolunteerApply newApply = VolunteerApply.builder() - .volunteerId(volunteerId) - .recruitBoardId(recruitId) - .status(ApplyStatus.APPROVED) - .attended(false) - .build(); + VolunteerApply newApply = createApply(volunteerId, recruitId); volunteerApplyRepository.save(newApply); // when @@ -126,4 +116,13 @@ void findByRecruitIdAndVolunteerId() { // then assertThat(findApply).isPresent(); } + + private static VolunteerApply createApply(UUID volunteerId, Long recruitId) { + return VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + } } diff --git a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java index 320e0bf29..f5285a143 100644 --- a/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java +++ b/src/test/java/com/somemore/volunteerapply/service/VolunteerApplyQueryServiceTest.java @@ -63,12 +63,7 @@ void getByRecruitIdAndVolunteerId() { Long recruitId = 1234L; UUID volunteerId = UUID.randomUUID(); - VolunteerApply newApply = VolunteerApply.builder() - .volunteerId(volunteerId) - .recruitBoardId(recruitId) - .status(ApplyStatus.APPROVED) - .attended(false) - .build(); + VolunteerApply newApply = createApply(volunteerId, recruitId); volunteerApplyRepository.save(newApply); // when @@ -79,4 +74,13 @@ void getByRecruitIdAndVolunteerId() { assertThat(apply.getRecruitBoardId()).isEqualTo(recruitId); assertThat(apply.getVolunteerId()).isEqualTo(volunteerId); } + + private static VolunteerApply createApply(UUID volunteerId, Long recruitId) { + return VolunteerApply.builder() + .volunteerId(volunteerId) + .recruitBoardId(recruitId) + .status(ApplyStatus.APPROVED) + .attended(false) + .build(); + } } From fbb7ac6619898b30cfef37b6331e0b6c587e6d28 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 22:45:23 +0900 Subject: [PATCH 16/18] =?UTF-8?q?refactor(review):=20@AuthenticationPrinci?= =?UTF-8?q?pal=20=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20userId?= =?UTF-8?q?=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewCommandApiController.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java index db37db055..f316b73d2 100644 --- a/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java +++ b/src/main/java/com/somemore/review/controller/ReviewCommandApiController.java @@ -13,7 +13,7 @@ import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.security.access.annotation.Secured; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestPart; @@ -33,19 +33,20 @@ public class ReviewCommandApiController { @Operation(summary = "리뷰 등록", description = "리뷰를 등록합니다.") @PostMapping(value = "/review", consumes = MULTIPART_FORM_DATA_VALUE) public ApiResponse createReview( + @AuthenticationPrincipal String userId, @Valid @RequestPart("data") ReviewCreateRequestDto requestDto, @RequestPart(value = "img_file", required = false) MultipartFile image) { String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); return ApiResponse.ok( 201, - createReviewUseCase.createReview(requestDto, getId(), imgUrl), + createReviewUseCase.createReview(requestDto, getId(userId), imgUrl), "리뷰 등록 성공" ); } - private static UUID getId() { - return UUID.fromString(SecurityContextHolder.getContext().getAuthentication().getName()); + private static UUID getId(String id) { + return UUID.fromString(id); } } From b73407e487567199e5dc3e5d3a7c3a0348e7d8d6 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 22:46:41 +0900 Subject: [PATCH 17/18] =?UTF-8?q?hotfix(recruit-board):=20recruitBoard=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EC=8B=9C=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20optional=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RecruitBoardCommandApiController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java index 6d55d5ff2..d8274b6a0 100644 --- a/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java +++ b/src/main/java/com/somemore/recruitboard/controller/RecruitBoardCommandApiController.java @@ -68,7 +68,7 @@ public ApiResponse updateRecruitBoard( @AuthenticationPrincipal String userId, @PathVariable Long id, @Valid @RequestPart("data") RecruitBoardUpdateRequestDto requestDto, - @RequestPart("img_file") MultipartFile image + @RequestPart(value = "img_file", required = false) MultipartFile image ) { String imgUrl = imageUploadUseCase.uploadImage(new ImageUploadRequestDto(image)); updateRecruitBoardUseCase.updateRecruitBoard(requestDto, id, getCenterId(userId), imgUrl); From b2996f3d0dc242b2b968141f3a898f4121a41719 Mon Sep 17 00:00:00 2001 From: leebs0521 Date: Sun, 1 Dec 2024 22:48:49 +0900 Subject: [PATCH 18/18] =?UTF-8?q?refactor(review):=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?RequestDto=20=EC=98=88=EC=A0=9C=20=EB=AC=B8=EA=B5=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/somemore/review/dto/request/ReviewCreateRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java index 8a13cc331..026929be8 100644 --- a/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java +++ b/src/main/java/com/somemore/review/dto/request/ReviewCreateRequestDto.java @@ -19,7 +19,7 @@ public record ReviewCreateRequestDto( @Schema(description = "리뷰 제목", example = "내 인생 최고의 봉사 활동") @NotBlank(message = "리뷰 제목은 필수 값입니다.") String title, - @Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요 더보기..
") + @Schema(description = "리뷰 내용", example = "담당자님도 정말 친절하였고 정말 보람찬 봉사였어요
") @NotBlank(message = "리뷰 내용은 필수 값입니다.") String content ) {