Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 키워드 도메인 구체화, 정책 구현 #40

Merged
merged 16 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.projectlombok:lombok'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Expand Down
30 changes: 29 additions & 1 deletion backend/src/main/java/reviewme/keyword/domain/Keyword.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.Objects;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -23,7 +24,34 @@ public class Keyword {
@Column(name = "detail", nullable = false)
private String detail;

public Keyword(String detail) {
Keyword(Long id, String detail) {
this.id = id;
this.detail = detail;
}

public Keyword(String detail) {
this(null, detail);
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Keyword keyword)) {
return false;
}
if (id == null) {
return Objects.equals(detail, keyword.detail);
}
return Objects.equals(id, keyword.id);
}

@Override
public int hashCode() {
if (id == null) {
return Objects.hash(detail);
}
return Objects.hash(id);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

equals, hashCode에서 엔티티가 영속화 되지 않으면 id가 null 일텐데 괜찮을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detail로 비교합니다! 최고~!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package reviewme.keyword.domain;

import java.util.Collections;
import java.util.List;
import reviewme.keyword.domain.exception.DuplicateKeywordException;
import reviewme.keyword.domain.exception.KeywordLimitExceedException;

public class SelectedKeywords {

private static final int MAX_KEYWORD_COUNT = 5;

private final List<Keyword> keywords;

public SelectedKeywords(List<Keyword> selectedKeywords) {
if (selectedKeywords.size() > MAX_KEYWORD_COUNT) {
throw new KeywordLimitExceedException(MAX_KEYWORD_COUNT);
}
if (hasDuplicateKeywords(selectedKeywords)) {
throw new DuplicateKeywordException();
}
Comment on lines +15 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직은 메서드 분리를 하지 않는 것이 더 나을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검증은 한 줄로 이루어져야 읽기 편하다고 생각해서 분리해 두었습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 제가 얘기한 건 validateSize, validateDuplicate 등으로 메서드 분리를 하지 않은 이유가 검증 로직이 많지 않아서인지의 궁금증이었습니다!

Copy link
Contributor Author

@donghoony donghoony Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 아직 읽기 불편하지 않았습니다 🔥

this.keywords = selectedKeywords;
}

private boolean hasDuplicateKeywords(List<Keyword> selectedKeywords) {
long distinctKeywordCount = selectedKeywords.stream()
.distinct()
.count();
return selectedKeywords.size() != distinctKeywordCount;
}

public List<Keyword> getKeywords() {
return Collections.unmodifiableList(keywords);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋네요👍

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package reviewme.keyword.domain.exception;

import reviewme.global.exception.BadRequestException;

public class DuplicateKeywordException extends BadRequestException {

public DuplicateKeywordException() {
super("키워드는 중복되지 않게 선택해 주세요.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package reviewme.keyword.domain.exception;

import reviewme.global.exception.BadRequestException;

public class KeywordLimitExceedException extends BadRequestException {

public KeywordLimitExceedException(int maxSize) {
super("키워드는 최대 %d개 선택할 수 있습니다.".formatted(maxSize));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
public interface ReviewKeywordRepository extends JpaRepository<ReviewKeyword, Long> {

List<ReviewKeyword> findByReview(Review review);

void deleteAllByReview(Review review);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package reviewme.review.service;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reviewme.keyword.domain.Keyword;
import reviewme.keyword.domain.SelectedKeywords;
import reviewme.review.domain.Review;
import reviewme.review.domain.ReviewKeyword;
import reviewme.review.repository.ReviewKeywordRepository;

@Service
@RequiredArgsConstructor
public class ReviewKeywordService {

private final ReviewKeywordRepository reviewKeywordRepository;

@Transactional
public List<ReviewKeyword> attachSelectedKeywordsOnReview(Review review, List<Keyword> selectedKeywords) {
reviewKeywordRepository.deleteAllByReview(review);
SelectedKeywords keywords = new SelectedKeywords(selectedKeywords);
List<ReviewKeyword> reviewKeywords = keywords.getKeywords()
.stream()
.map(keyword -> new ReviewKeyword(review, keyword))
.toList();
return reviewKeywordRepository.saveAll(reviewKeywords);
}
}
20 changes: 20 additions & 0 deletions backend/src/test/java/reviewme/fixture/KeywordFixture.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package reviewme.fixture;

import reviewme.keyword.domain.Keyword;

public enum KeywordFixture {

추진력이_좋아요,
회의를_이끌어요,
의견을_잘_조율해요,
꼼꼼하게_기록해요,
;

public Keyword create() {
return new Keyword(replaceUnderscore());
}

private String replaceUnderscore() {
return name().replace("_", " ");
}
}
Comment on lines +5 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enum 으로 만든 이유가 따로 있나요?
public static List 로 미리 만들어둔 것을 가져다 쓴다면 그때그때 생성하지 않아도 될 것 같아서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

다른 Fixture와의 통일성이 한몫합니다. 추가적인 필드가 들어오면 대응하기에도 훨씬 수월하다고 생각했어요.

Copy link
Contributor

@nayonsoso nayonsoso Jul 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

익숙하진 않지만, 정적인 Fixture 와 동적인 Fixture 를 모두 커버할 수 있어 좋은 아이디어라 생각합니다!
그런데 진짜로 익숙하진 않음!
다른 사람들의 의견도 들어보고 싶습니다~

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package reviewme.fixture;

import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import reviewme.member.domain.Member;
import reviewme.member.domain.ReviewerGroup;

@RequiredArgsConstructor
public enum ReviewerGroupFixture {

리뷰_그룹("리뷰 그룹", "그룹 설명", LocalDateTime.of(2024, 1, 1, 12, 0)),
;

private final String groupName;
private final String description;
private final LocalDateTime createdAt;

public ReviewerGroup create(Member reviewee) {
return new ReviewerGroup(reviewee, groupName, description, createdAt);
}
Comment on lines +18 to +20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 이렇게 하면 리뷰이를 그 때 그 때 넣어줄 수 있겠군요!! 얻어갑니다😋

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package reviewme.keyword.domain;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static reviewme.fixture.KeywordFixture.꼼꼼하게_기록해요;

import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import reviewme.keyword.domain.exception.DuplicateKeywordException;
import reviewme.keyword.domain.exception.KeywordLimitExceedException;

class SelectedKeywordsTest {

@Test
void 키워드는_최대_5개만_선택할_수_있다() {
// given
List<Keyword> keywords = Stream.of("1", "2", "3", "4", "5")
.map(Keyword::new)
.toList();

// when, then
assertDoesNotThrow(() -> new SelectedKeywords(keywords));
}

@Test
void 키워드는_5개를_초과해서_선택할_수_없다() {
// given
List<Keyword> keywords = Stream.of("1", "2", "3", "4", "5", "6")
.map(Keyword::new)
.toList();

// when, then
assertThatThrownBy(() -> new SelectedKeywords(keywords))
.isInstanceOf(KeywordLimitExceedException.class);
}

@Test
void 키워드는_중복으로_선택할_수_없다() {
// given
List<Keyword> keywords = List.of(
꼼꼼하게_기록해요.create(),
꼼꼼하게_기록해요.create()
);

// when, then
assertThatThrownBy(() -> new SelectedKeywords(keywords))
.isInstanceOf(DuplicateKeywordException.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package reviewme.review.repository;

import static org.assertj.core.api.Assertions.assertThat;
import static reviewme.fixture.KeywordFixture.꼼꼼하게_기록해요;
import static reviewme.fixture.KeywordFixture.의견을_잘_조율해요;
import static reviewme.fixture.KeywordFixture.회의를_이끌어요;
import static reviewme.fixture.ReviewerGroupFixture.리뷰_그룹;

import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import reviewme.fixture.KeywordFixture;
import reviewme.keyword.repository.KeywordRepository;
import reviewme.member.domain.Member;
import reviewme.member.domain.ReviewerGroup;
import reviewme.member.repository.MemberRepository;
import reviewme.member.repository.ReviewerGroupRepository;
import reviewme.review.domain.Review;
import reviewme.review.domain.ReviewKeyword;

@DataJpaTest
class ReviewKeywordRepositoryTest {

@Autowired
private ReviewKeywordRepository reviewKeywordRepository;

@Autowired
private KeywordRepository keywordRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private ReviewerGroupRepository reviewerGroupRepository;

@Autowired
private ReviewRepository reviewRepository;

@Test
void 리뷰에_해당하는_키워드를_모두_삭제한다() {
// given
Member sancho = memberRepository.save(new Member("산초"));
Member kirby = memberRepository.save(new Member("커비"));
ReviewerGroup group = reviewerGroupRepository.save(리뷰_그룹.create(sancho));
Review review = reviewRepository.save(new Review(kirby, group));
Comment on lines +42 to +47
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수에 '기존에 키워드가 있다면 수정하는' 기능도 있는 만큼, 그 기능도 테스트할 필요가 있어 보여요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service단에 추가할게요~

List<ReviewKeyword> reviewKeywords = Stream.of(꼼꼼하게_기록해요, 회의를_이끌어요, 의견을_잘_조율해요)
.map(KeywordFixture::create)
.map(keywordRepository::save)
.map(keyword -> new ReviewKeyword(review, keyword))
.toList();
reviewKeywordRepository.saveAll(reviewKeywords);

// when
reviewKeywordRepository.deleteAllByReview(review);

// then
List<ReviewKeyword> actual = reviewKeywordRepository.findByReview(review);
assertThat(actual).isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package reviewme.review.service;

import static org.assertj.core.api.Assertions.assertThat;
import static reviewme.fixture.KeywordFixture.꼼꼼하게_기록해요;
import static reviewme.fixture.KeywordFixture.의견을_잘_조율해요;
import static reviewme.fixture.KeywordFixture.회의를_이끌어요;
import static reviewme.fixture.ReviewerGroupFixture.리뷰_그룹;

import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import reviewme.fixture.KeywordFixture;
import reviewme.keyword.domain.Keyword;
import reviewme.keyword.repository.KeywordRepository;
import reviewme.member.domain.Member;
import reviewme.member.domain.ReviewerGroup;
import reviewme.member.repository.MemberRepository;
import reviewme.member.repository.ReviewerGroupRepository;
import reviewme.review.domain.Review;
import reviewme.review.domain.ReviewKeyword;
import reviewme.review.repository.ReviewKeywordRepository;
import reviewme.review.repository.ReviewRepository;
import reviewme.support.ServiceTest;

@ServiceTest
class ReviewKeywordServiceTest {

@Autowired
private ReviewKeywordService reviewKeywordService;

@Autowired
private ReviewKeywordRepository reviewKeywordRepository;

@Autowired
private KeywordRepository keywordRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private ReviewerGroupRepository reviewerGroupRepository;

@Autowired
private ReviewRepository reviewRepository;

@Test
void 리뷰에_키워드를_추가한다() {
// given
Member sancho = memberRepository.save(new Member("산초"));
Member kirby = memberRepository.save(new Member("커비"));
ReviewerGroup group = reviewerGroupRepository.save(리뷰_그룹.create(sancho));
Review review = reviewRepository.save(new Review(kirby, group));

List<Keyword> keywords = Stream.of(꼼꼼하게_기록해요, 회의를_이끌어요, 의견을_잘_조율해요)
.map(KeywordFixture::create)
.map(keywordRepository::save)
.toList();
List<Keyword> selectedKeywords = List.of(keywords.get(0), keywords.get(1));

// when
reviewKeywordService.attachSelectedKeywordsOnReview(review, selectedKeywords);

// then
List<ReviewKeyword> actual = reviewKeywordRepository.findByReview(review);
assertThat(actual).hasSize(2);
}

@Test
void 키워드가_이미_존재하는_경우_키워드_등록_시_모두_대체된다() {
// given
Member sancho = memberRepository.save(new Member("산초"));
Member kirby = memberRepository.save(new Member("커비"));
ReviewerGroup group = reviewerGroupRepository.save(리뷰_그룹.create(sancho));
Review review = reviewRepository.save(new Review(kirby, group));

List<Keyword> keywords = Stream.of(꼼꼼하게_기록해요, 회의를_이끌어요, 의견을_잘_조율해요)
.map(KeywordFixture::create)
.map(keywordRepository::save)
.toList();
reviewKeywordRepository.save(new ReviewKeyword(review, keywords.get(0)));
List<Keyword> selectedKeywords = List.of(keywords.get(1), keywords.get(2));

// when
reviewKeywordService.attachSelectedKeywordsOnReview(review, selectedKeywords);

// then
List<ReviewKeyword> actual = reviewKeywordRepository.findByReview(review);
assertThat(actual).extracting(ReviewKeyword::getKeyword)
.containsExactlyInAnyOrderElementsOf(selectedKeywords);
}
}