diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/AcceptanceContext.java b/backend/src/acceptanceTest/java/wooteco/prolog/AcceptanceContext.java index 50669cd91..78fb1f3f4 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/AcceptanceContext.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/AcceptanceContext.java @@ -44,6 +44,20 @@ public void invokeHttpPost(String path, Object data) { response.then().log().all(); } + public void invokeHttpPut(final String path, Object data) { + request = RestAssured + .given().log().all() + .body(data).contentType(ContentType.JSON); + response = request.put(path); + response.then().log().all(); + } + + public void invokeHttpDelete(final String path, Object... pathParams) { + request = RestAssured.given().log().all(); + response = request.when().delete(path, pathParams); + response.then().log().all(); + } + public void invokeHttpGetWithToken(String path, Object... pathParams) { request = RestAssured.given().log().all() .auth().oauth2(accessToken); diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/KeywordAcceptanceFixture.java b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/KeywordAcceptanceFixture.java new file mode 100644 index 000000000..af91cbc3b --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/fixtures/KeywordAcceptanceFixture.java @@ -0,0 +1,28 @@ +package wooteco.prolog.fixtures; + +import wooteco.prolog.roadmap.application.dto.KeywordCreateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordUpdateRequest; + +public enum KeywordAcceptanceFixture { + + KEYWORD_REQUEST("키워드에 대한 설명입니다.") + ; + + private final String description; + + KeywordAcceptanceFixture(final String description) { + this.description = description; + } + + public KeywordCreateRequest getSaveParent(final String keywordName, final int seq, final int importance) { + return new KeywordCreateRequest(keywordName, this.description, seq, importance, null); + } + + public KeywordCreateRequest getSaveChild(final String keywordName, final int seq, final int importance, final Long parentKeywordId) { + return new KeywordCreateRequest(keywordName, this.description, seq, importance, parentKeywordId); + } + + public KeywordUpdateRequest getUpdateParent(final String keywordName, final int seq, final int importance) { + return new KeywordUpdateRequest(keywordName, this.description, seq, importance, null); + } +} diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java new file mode 100644 index 000000000..19c1e04d0 --- /dev/null +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/KeywordStepDefinitions.java @@ -0,0 +1,109 @@ +package wooteco.prolog.steps; + +import static org.assertj.core.api.Assertions.assertThat; +import static wooteco.prolog.fixtures.KeywordAcceptanceFixture.KEYWORD_REQUEST; + +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.springframework.http.HttpStatus; +import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.session.application.dto.SessionRequest; + +public class KeywordStepDefinitions extends AcceptanceSteps { + + /** + * MissionStepDefinitions 에 세션을 만드는 내용이 있지만 세션 1, 세션 2로 만들고 있어서 새롭게 생성하였음 + * 추후 논의 후 MissionDefinitions 의 세션을 제거 + */ + @Given("{string} 세션을 생성하고 - {int}번 세션") + public void 세션을_생성하고(String sessionName, int number) { + context.invokeHttpPost("/sessions", new SessionRequest(sessionName)); + assertThat(context.response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + } + + @Given("{int}번 세션에 {string}라는 키워드를 순서 {int}, 중요도 {int}로 작성하고") + @When("{int}번 세션에 {string}라는 키워드를 순서 {int}, 중요도 {int}로 작성하면") + public void 키워드를_작성하고(int sessionId, String keywordName, int seq, int importance) { + context.invokeHttpPost( + "/sessions/" + sessionId + "/keywords", + KEYWORD_REQUEST.getSaveParent(keywordName, seq, importance)); + } + + @Given("{int}번 세션에 {string}라는 키워드를 순서 {int}, 중요도 {int}, 부모 키워드 {long}로 작성하고") + public void 키워드를_부모_키워드와_함께_작성하고(int sessionId, String keywordName, int seq, int importance, long parentId) { + context.invokeHttpPost( + "/sessions/" + sessionId + "/keywords", + KEYWORD_REQUEST.getSaveChild(keywordName, seq, importance, parentId)); + } + + @When("{int}번 세션과 {int}번 키워드를 조회하면") + public void 키워드를_조회하면(int sessionId, int keywordId) { + context.invokeHttpGet( + "/sessions/" + sessionId + "/keywords/" + keywordId + ); + } + + @When("{int}번 세션과 {int}번 키워드를 키워드 {string}, 순서 {int}, 중요도 {int}로 수정하면") + public void 키워드를_수정하면(int sessionId, int keywordId, String keywordName, int seq, int importance) { + context.invokeHttpPut( + "/sessions/" + sessionId + "/keywords/" + keywordId, + KEYWORD_REQUEST.getUpdateParent(keywordName, seq, importance)); + } + + @When("{int}번 세션에 대한 {int}번 키워드를 삭제하면") + public void 키워드를_삭제하면(int sessionId, int keywordId) { + context.invokeHttpDelete( + "/sessions/" + sessionId + "/keywords/" + keywordId + ); + } + + @When("{int}번 세션에 대해서 최상위 키워드들을 조회하면") + public void 세션에_대해서_키워드들을_조회하면(int sessionId) { + context.invokeHttpGet( + "/sessions/" + sessionId + "/keywords" + ); + } + + @When("{int}번 세션에 대한 {int}번 키워드와 자식 키워드를 함께 조회하면") + public void 키워드와_자식_키워드를_함께_조회하면(int sessionId, int keywordId) { + context.invokeHttpGet( + "/sessions/" + sessionId + "/keywords/" + keywordId + "/children" + ); + } + + @Then("키워드가 생성된다") + public void 키워드가_생성된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.CREATED.value()); + } + + @Then("키워드가 조회된다") + public void 키워드가_조회된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.OK.value()); + } + + @Then("키워드가 수정된다") + public void 키워드가_수정된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Then("키워드가 삭제된다") + public void 키워드가_삭제된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Then("키워드 목록이 조회된다") + public void 키워드_목록이_조회된다() { + int statusCode = context.response.statusCode(); + + assertThat(statusCode).isEqualTo(HttpStatus.OK.value()); + } +} diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/keyword.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/keyword.feature new file mode 100644 index 000000000..06f654ad5 --- /dev/null +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/keyword.feature @@ -0,0 +1,39 @@ +@api +Feature: 로드맵 키워드 관련 기능 + + Background: 사전 작업 + Given "2022 백엔드 레벨1" 세션을 생성하고 - 1번 세션 + And "2022 프론트엔드 레벨1" 세션을 생성하고 - 2번 세션 + + Scenario: 키워드 생성하기 + When 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하면 + Then 키워드가 생성된다 + + Scenario: 키워드 단일 조회하기 + Given 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하고 + When 1번 세션과 1번 키워드를 조회하면 + Then 키워드가 조회된다 + + Scenario: 키워드 수정하기 + Given 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하고 + When 1번 세션과 1번 키워드를 키워드 "스프링", 순서 1, 중요도 2로 수정하면 + Then 키워드가 수정된다 + + Scenario: 키워드 삭제하기 + Given 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하고 + When 1번 세션에 대한 1번 키워드를 삭제하면 + Then 키워드가 삭제된다 + + Scenario: 세션별 최상위 키워드 목록 조회하기 + Given 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하고 + And 1번 세션에 "스프링"라는 키워드를 순서 1, 중요도 1로 작성하고 + When 1번 세션에 대해서 최상위 키워드들을 조회하면 + Then 키워드 목록이 조회된다 + + Scenario: 최상위 키워드의 모든 자식 키워드를 조회하기 + Given 1번 세션에 "자바"라는 키워드를 순서 1, 중요도 1로 작성하고 + And 1번 세션에 "List"라는 키워드를 순서 1, 중요도 1, 부모 키워드 1로 작성하고 + And 1번 세션에 "List.of()"라는 키워드를 순서 1, 중요도 1, 부모 키워드 2로 작성하고 + And 1번 세션에 "Set"라는 키워드를 순서 1, 중요도 1, 부모 키워드 1로 작성하고 + When 1번 세션에 대한 1번 키워드와 자식 키워드를 함께 조회하면 + Then 키워드 목록이 조회된다 diff --git a/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java new file mode 100644 index 000000000..d8d715429 --- /dev/null +++ b/backend/src/documentation/java/wooteco/prolog/docu/KeywordDocumentation.java @@ -0,0 +1,156 @@ +package wooteco.prolog.docu; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; + +import java.util.Arrays; +import java.util.HashSet; +import org.elasticsearch.common.collect.List; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import wooteco.prolog.NewDocumentation; +import wooteco.prolog.roadmap.application.KeywordService; +import wooteco.prolog.roadmap.application.dto.KeywordCreateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordResponse; +import wooteco.prolog.roadmap.application.dto.KeywordUpdateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordsResponse; +import wooteco.prolog.roadmap.ui.KeywordController; + +@WebMvcTest(controllers = KeywordController.class) +public class KeywordDocumentation extends NewDocumentation { + + @MockBean + private KeywordService keywordService; + + @Test + void 키워드_생성() { + given(keywordService.createKeyword(any(), any())).willReturn(1L); + + given + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(KEYWORD_CREATE_REQUEST) + .when().post("/sessions/1/keywords") + .then().log().all().apply(document("keywords/create")) + .statusCode(HttpStatus.CREATED.value()); + } + + @Test + void 키워드_단일_조회() { + given(keywordService.findKeyword(any(), any())).willReturn(KEYWORD_SINGLE_RESPONSE); + + given + .when().get("/sessions/1/keywords/1") + .then().log().all().apply(document("keywords/find")) + .statusCode(HttpStatus.OK.value()); + } + + @Test + void 키워드_단일_수정() { + doNothing().when(keywordService).updateKeyword(any(), any(), any()); + + given + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(KEYWORD_UPDATE_REQUEST) + .when().put("/sessions/1/keywords/1") + .then().log().all().apply(document("keywords/update")) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void 키워드_단일_삭제() { + doNothing().when(keywordService).deleteKeyword(any(), any()); + + given + .when().delete("/sessions/1/keywords/1") + .then().log().all().apply(document("keywords/delete")) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void 세션별_키워드_목록_조회() { + given(keywordService.findSessionIncludeRootKeywords(any())).willReturn(KEYWORD_SESSION_INCLUDE_MULTI_RESPONSE); + + given + .when().get("/sessions/1/keywords") + .then().log().all().apply(document("keywords/find-childAll")) + .statusCode(HttpStatus.OK.value()); + } + + @Test + void 최상위_키워드의_모든_자식_키워드들의_목록_조회() { + given(keywordService.findKeywordWithAllChild(any(), any())).willReturn(KEYWORD_WITH_ALL_CHILD_MULTI_RESPONSE); + + given + .when().get("/sessions/1/keywords/1/children") + .then().log().all().apply(document("keywords/find-with-childAll")) + .statusCode(HttpStatus.OK.value()); + } + + private static final KeywordCreateRequest KEYWORD_CREATE_REQUEST = new KeywordCreateRequest( + "자바", + "자바에 대한 설명을 작성했습니다.", + 1, + 1, + null + ); + + private static final KeywordResponse KEYWORD_SINGLE_RESPONSE = new KeywordResponse( + 1L, + "자바", + "자바에 대한 설명을 작성했습니다.", + 1, + 1, + null, + null + ); + + private static final KeywordUpdateRequest KEYWORD_UPDATE_REQUEST = new KeywordUpdateRequest( + "자바", + "자바에 대한 설명을 작성했습니다.", + 1, + 1, + null + ); + + private static final KeywordsResponse KEYWORD_SESSION_INCLUDE_MULTI_RESPONSE = new KeywordsResponse( + List.of( + KEYWORD_SINGLE_RESPONSE, + KEYWORD_SINGLE_RESPONSE + ) + ); + + private static final KeywordResponse KEYWORD_WITH_ALL_CHILD_MULTI_RESPONSE = new KeywordResponse( + 1L, + "자바", + "자바에 대한 설명을 작성했습니다.", + 1, + 1, + null, + new HashSet<>( + Arrays.asList( + new KeywordResponse( + 2L, + "List", + "자바의 자료구조인 List에 대한 설명을 작성했습니다.", + 1, + 1, + 1L, + null + ), + new KeywordResponse( + 1L, + "Set", + "자바의 자료구조인 Set에 대한 설명을 작성했습니다.", + 2, + 1, + 1L, + null + )) + ) + ); +} diff --git a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java index f694e351b..aa99b0159 100644 --- a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java +++ b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java @@ -5,6 +5,9 @@ import lombok.Getter; import wooteco.prolog.levellogs.exception.InvalidLevelLogAuthorException; import wooteco.prolog.levellogs.exception.LevelLogNotFoundException; +import wooteco.prolog.roadmap.exception.KeywordAndKeywordParentSameException; +import wooteco.prolog.roadmap.exception.KeywordNotFoundException; +import wooteco.prolog.roadmap.exception.KeywordSeqException; import wooteco.prolog.login.excetpion.GithubApiFailException; import wooteco.prolog.login.excetpion.GithubConnectionException; import wooteco.prolog.login.excetpion.RoleNameNotFoundException; @@ -21,6 +24,7 @@ import wooteco.prolog.report.exception.ReportTitleLengthException; import wooteco.prolog.report.exception.ReportUpdateException; import wooteco.prolog.report.exception.UnRelatedAbilityExistenceException; +import wooteco.prolog.session.exception.SessionNotFoundException; import wooteco.prolog.studylog.exception.CommentDeleteException; import wooteco.prolog.studylog.exception.CommentNotFoundException; import wooteco.prolog.studylog.exception.DuplicateReportTitleException; diff --git a/backend/src/main/java/wooteco/prolog/roadmap/Keyword.java b/backend/src/main/java/wooteco/prolog/roadmap/Keyword.java deleted file mode 100644 index aa44dd8e2..000000000 --- a/backend/src/main/java/wooteco/prolog/roadmap/Keyword.java +++ /dev/null @@ -1,83 +0,0 @@ -package wooteco.prolog.roadmap; - -import java.util.ArrayList; -import java.util.List; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.ManyToOne; -import javax.persistence.OneToMany; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.hibernate.annotations.BatchSize; -import wooteco.prolog.roadmap.exception.KeywordOrderException; - -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Entity -public class Keyword { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String name; - - private String description; - - @Column(nullable = false) - private int order; - - @Column(nullable = false) - private int importance; - - @Column(name = "session_id", nullable = false) - private Long sessionId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "parent_id") - private Keyword parent; - - @BatchSize(size = 1000) - @OneToMany(mappedBy = "keyword", cascade = CascadeType.ALL, orphanRemoval = true) - private List children = new ArrayList<>(); - - public Keyword(final Long id, - final String name, - final String description, - final int order, - final int importance, - final Long sessionId, - final Keyword parent) { - validateOrder(order); - this.id = id; - this.name = name; - this.description = description; - this.order = order; - this.importance = importance; - this.sessionId = sessionId; - this.parent = parent; - } - - private void validateOrder(final int order) { - if (order <= 0) { - throw new KeywordOrderException(); - } - } - - public Keyword(final String name, - final String description, - final int order, - final int importance, - final Long sessionId, - final Keyword parent) { - this(null, name, description, order, importance, sessionId, parent); - } -} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java b/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java new file mode 100644 index 000000000..2460e8825 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/KeywordService.java @@ -0,0 +1,107 @@ +package wooteco.prolog.roadmap.application; + +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.application.dto.KeywordCreateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordResponse; +import wooteco.prolog.roadmap.application.dto.KeywordUpdateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordsResponse; +import wooteco.prolog.roadmap.exception.KeywordNotFoundException; +import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.prolog.session.exception.SessionNotFoundException; + +@Transactional +@Service +public class KeywordService { + + private final SessionRepository sessionRepository; + private final KeywordRepository keywordRepository; + + public KeywordService(final SessionRepository sessionRepository, final KeywordRepository keywordRepository) { + this.sessionRepository = sessionRepository; + this.keywordRepository = keywordRepository; + } + + /** + * 최상위 키워드를 만드는 경우, 키워드의 부모 값에 null을 넣어줌 + */ + public Long createKeyword(final Long sessionId, final KeywordCreateRequest request) { + existSession(sessionId); + Keyword keywordParent = findKeywordParentOrNull(request.getParentKeywordId()); + + Keyword keyword = request.toEntity(sessionId, keywordParent); + keywordRepository.save(keyword); + + return keyword.getId(); + } + + @Transactional(readOnly = true) + public KeywordResponse findKeyword(final Long sessionId, final Long keywordId) { + existSession(sessionId); + Keyword keyword = keywordRepository.findById(keywordId) + .orElseThrow(KeywordNotFoundException::new); + + return KeywordResponse.createResponse(keyword); + } + + @Transactional(readOnly = true) + public KeywordResponse findKeywordWithAllChild(final Long sessionId, final Long keywordId) { + existSession(sessionId); + existKeyword(keywordId); + + Keyword keyword = keywordRepository.findFetchById(keywordId); + + return KeywordResponse.createWithAllChildResponse(keyword); + } + + @Transactional(readOnly = true) + public KeywordsResponse findSessionIncludeRootKeywords(final Long sessionId) { + existSession(sessionId); + + List keywords = keywordRepository.findBySessionId(sessionId); + + return KeywordsResponse.createResponse(keywords); + } + + public void updateKeyword(final Long sessionId, final Long keywordId, final KeywordUpdateRequest request) { + existSession(sessionId); + Keyword keyword = keywordRepository.findById(keywordId) + .orElseThrow(KeywordNotFoundException::new); + Keyword keywordParent = findKeywordParentOrNull(request.getParentKeywordId()); + + keyword.update( + request.getName(), request.getDescription(), request.getOrder(), request.getImportance(), keywordParent); + } + + public void deleteKeyword(final Long sessionId, final Long keywordId) { + existSession(sessionId); + Keyword keyword = keywordRepository.findFetchById(keywordId); + + keywordRepository.delete(keyword); + } + + private void existSession(final Long sessionId) { + boolean exists = sessionRepository.existsById(sessionId); + if (!exists) { + throw new SessionNotFoundException(); + } + } + + private void existKeyword(final Long keywordId) { + boolean exists = keywordRepository.existsById(keywordId); + if (!exists) { + throw new KeywordNotFoundException(); + } + } + + private Keyword findKeywordParentOrNull(final Long keywordId) { + if (keywordId == null) { + return null; + } + return keywordRepository.findById(keywordId) + .orElseThrow(KeywordNotFoundException::new); + } +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordCreateRequest.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordCreateRequest.java index a93c84379..954b58a1b 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordCreateRequest.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordCreateRequest.java @@ -3,6 +3,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import wooteco.prolog.roadmap.domain.Keyword; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -22,4 +23,11 @@ public KeywordCreateRequest(final String name, final String description, final i this.importance = importance; this.parentKeywordId = parentKeywordId; } + + public Keyword toEntity(final Long sessionId, final Keyword keywordParent) { + return Keyword.createKeyword( + this.getName(), this.getDescription(), this.getOrder(), + this.getImportance(), sessionId, keywordParent + ); + } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java index d7f996227..a74a5b3f1 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordResponse.java @@ -5,6 +5,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import wooteco.prolog.roadmap.domain.Keyword; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -29,4 +30,34 @@ public KeywordResponse(final Long keywordId, final String name, final String des this.parentKeywordId = parentKeywordId; this.childrenKeywords = childrenKeywords; } + + public static KeywordResponse createResponse(final Keyword keyword) { + return new KeywordResponse( + keyword.getId(), + keyword.getName(), + keyword.getDescription(), + keyword.getSeq(), + keyword.getImportance(), + keyword.getParentIdOrNull(), + null); + } + + public static KeywordResponse createWithAllChildResponse(final Keyword keyword) { + return new KeywordResponse( + keyword.getId(), + keyword.getName(), + keyword.getDescription(), + keyword.getSeq(), + keyword.getImportance(), + keyword.getParentIdOrNull(), + createKeywordChild(keyword.getChildren())); + } + + private static Set createKeywordChild(final Set children) { + Set keywords = new HashSet<>(); + for (Keyword keyword : children) { + keywords.add(createWithAllChildResponse(keyword)); + } + return keywords; + } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java index 50c298c48..aee3cd48b 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/application/dto/KeywordsResponse.java @@ -5,6 +5,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import wooteco.prolog.roadmap.domain.Keyword; @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @@ -15,4 +16,11 @@ public class KeywordsResponse { public KeywordsResponse(final List data) { this.data = data; } + + public static KeywordsResponse createResponse(final List keywords) { + List keywordsResponse = keywords.stream() + .map(KeywordResponse::createResponse) + .collect(Collectors.toList()); + return new KeywordsResponse(keywordsResponse); + } } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java new file mode 100644 index 000000000..ca0530420 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/Keyword.java @@ -0,0 +1,122 @@ +package wooteco.prolog.roadmap.domain; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.BatchSize; +import wooteco.prolog.roadmap.exception.KeywordAndKeywordParentSameException; +import wooteco.prolog.roadmap.exception.KeywordSeqException; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +public class Keyword { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String name; + + private String description; + + @Column(nullable = false) + private int seq; + + @Column(nullable = false) + private int importance; + + @Column(name = "session_id", nullable = false) + private Long sessionId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Keyword parent; + + @BatchSize(size = 1000) + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private Set children = new HashSet<>(); + + public Keyword(final Long id, final String name, final String description, final int seq, final int importance, + final Long sessionId, final Keyword parent, final Set children) { + validateSeq(seq); + this.id = id; + this.name = name; + this.description = description; + this.seq = seq; + this.importance = importance; + this.sessionId = sessionId; + this.parent = parent; + this.children = children; + } + + public static Keyword createKeyword(final String name, + final String description, + final int seq, + final int importance, + final Long sessionId, + final Keyword parent) { + return new Keyword(null, name, description, seq, importance, sessionId, parent, null); + } + + public void update(final String name, final String description, final int seq, + final int importance, final Keyword keywordParent) { + validateSeq(seq); + validateKeywordParent(keywordParent); + this.name = name; + this.description = description; + this.seq = seq; + this.importance = importance; + this.parent = keywordParent; + } + + private void validateSeq(final int seq) { + if (seq <= 0) { + throw new KeywordSeqException(); + } + } + + private void validateKeywordParent(final Keyword parentKeyword) { + if (this.parent != null && this.id.equals(parentKeyword.getId())) { + throw new KeywordAndKeywordParentSameException(); + } + } + + public Long getParentIdOrNull() { + if (parent == null) { + return null; + } + return parent.getId(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Keyword)) { + return false; + } + Keyword keyword = (Keyword) o; + return Objects.equals(id, keyword.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/Quiz.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/Quiz.java similarity index 96% rename from backend/src/main/java/wooteco/prolog/roadmap/Quiz.java rename to backend/src/main/java/wooteco/prolog/roadmap/domain/Quiz.java index 2d9b1feca..ba7131307 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/Quiz.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/Quiz.java @@ -1,4 +1,4 @@ -package wooteco.prolog.roadmap; +package wooteco.prolog.roadmap.domain; import javax.persistence.Column; import javax.persistence.Entity; diff --git a/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java new file mode 100644 index 000000000..a764732fc --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/KeywordRepository.java @@ -0,0 +1,18 @@ +package wooteco.prolog.roadmap.domain.repository; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import wooteco.prolog.roadmap.domain.Keyword; + +public interface KeywordRepository extends JpaRepository { + + @Query("SELECT k FROM Keyword k " + + "LEFT JOIN FETCH k.children c " + + "LEFT JOIN FETCH k.parent p " + + "LEFT JOIN FETCH c.children lc WHERE k.id = :keywordId ORDER BY k.seq") + Keyword findFetchById(@Param("keywordId") Long keywordId); + + List findBySessionId(Long sessionId); +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/repository/QuizRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java similarity index 58% rename from backend/src/main/java/wooteco/prolog/roadmap/repository/QuizRepository.java rename to backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java index 4aa04b45f..b80c6aa96 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/repository/QuizRepository.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/domain/repository/QuizRepository.java @@ -1,7 +1,7 @@ -package wooteco.prolog.roadmap.repository; +package wooteco.prolog.roadmap.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; -import wooteco.prolog.roadmap.Keyword; +import wooteco.prolog.roadmap.domain.Keyword; public interface QuizRepository extends JpaRepository { diff --git a/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordAndKeywordParentSameException.java b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordAndKeywordParentSameException.java new file mode 100644 index 000000000..96f7a761d --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordAndKeywordParentSameException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.roadmap.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class KeywordAndKeywordParentSameException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordNotFoundException.java b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordNotFoundException.java new file mode 100644 index 000000000..597eca4d2 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordNotFoundException.java @@ -0,0 +1,7 @@ +package wooteco.prolog.roadmap.exception; + +import wooteco.prolog.common.exception.BadRequestException; + +public class KeywordNotFoundException extends BadRequestException { + +} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordOrderException.java b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordSeqException.java similarity index 62% rename from backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordOrderException.java rename to backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordSeqException.java index 9ce2d51fa..a9278d650 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordOrderException.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/exception/KeywordSeqException.java @@ -2,6 +2,6 @@ import wooteco.prolog.common.exception.BadRequestException; -public class KeywordOrderException extends BadRequestException { +public class KeywordSeqException extends BadRequestException { } diff --git a/backend/src/main/java/wooteco/prolog/roadmap/repository/KeywordRepository.java b/backend/src/main/java/wooteco/prolog/roadmap/repository/KeywordRepository.java deleted file mode 100644 index 4f164c98f..000000000 --- a/backend/src/main/java/wooteco/prolog/roadmap/repository/KeywordRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package wooteco.prolog.roadmap.repository; - -import org.springframework.data.jpa.repository.JpaRepository; -import wooteco.prolog.roadmap.Keyword; - -public interface KeywordRepository extends JpaRepository { - -} diff --git a/backend/src/main/java/wooteco/prolog/roadmap/ui/KeywordController.java b/backend/src/main/java/wooteco/prolog/roadmap/ui/KeywordController.java index 5502e17b6..682db3f5c 100644 --- a/backend/src/main/java/wooteco/prolog/roadmap/ui/KeywordController.java +++ b/backend/src/main/java/wooteco/prolog/roadmap/ui/KeywordController.java @@ -1,9 +1,6 @@ package wooteco.prolog.roadmap.ui; import java.net.URI; -import java.util.Arrays; -import java.util.HashSet; -import org.elasticsearch.common.collect.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -13,161 +10,61 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import wooteco.prolog.roadmap.application.KeywordService; import wooteco.prolog.roadmap.application.dto.KeywordCreateRequest; import wooteco.prolog.roadmap.application.dto.KeywordResponse; import wooteco.prolog.roadmap.application.dto.KeywordUpdateRequest; import wooteco.prolog.roadmap.application.dto.KeywordsResponse; @RestController -@RequestMapping("/sessions") +@RequestMapping("/sessions/{sessionId}/keywords") public class KeywordController { - @PostMapping("/{sessionId}/keywords") + private final KeywordService keywordService; + + public KeywordController(final KeywordService keywordService) { + this.keywordService = keywordService; + } + + @PostMapping public ResponseEntity createKeyword(@PathVariable Long sessionId, @RequestBody KeywordCreateRequest createRequest) { - return ResponseEntity.created(URI.create("/sessions/" + sessionId + "/keywords/" + 1L)).build(); + Long keywordId = keywordService.createKeyword(sessionId, createRequest); + return ResponseEntity.created(URI.create("/sessions/" + sessionId + "/keywords/" + keywordId)).build(); } - @GetMapping("/{sessionId}/keywords/{keywordId}") + @GetMapping("/{keywordId}") public ResponseEntity findKeyword(@PathVariable Long sessionId, @PathVariable Long keywordId) { - return ResponseEntity.ok(keywordResponse()); + KeywordResponse response = keywordService.findKeyword(sessionId, keywordId); + return ResponseEntity.ok(response); } - private KeywordResponse keywordResponse() { - return new KeywordResponse( - 2L, - "SRP", - "단일 책임 원칙입니다.", - 1, - 4, - 1L, - null - ); - } - - @PutMapping("/{sessionId}/keywords/{keywordId}") + @PutMapping("/{keywordId}") public ResponseEntity updateKeyword(@PathVariable Long sessionId, @PathVariable Long keywordId, @RequestBody KeywordUpdateRequest updateRequest) { + keywordService.updateKeyword(sessionId, keywordId, updateRequest); return ResponseEntity.noContent().build(); } - @DeleteMapping("/{sessionId}/keywords/{keywordId}") + @DeleteMapping("/{keywordId}") public ResponseEntity deleteKeyword(@PathVariable Long sessionId, @PathVariable Long keywordId) { + keywordService.deleteKeyword(sessionId, keywordId); return ResponseEntity.noContent().build(); } - @GetMapping("/{sessionId}/keywords") + @GetMapping public ResponseEntity findSessionIncludeRootKeywords(@PathVariable Long sessionId) { - return ResponseEntity.ok(keywordsResponse()); - } - - private KeywordsResponse keywordsResponse() { - return new KeywordsResponse(List.of( - new KeywordResponse( - 1L, - "자바", - "자바입니다.", - 1, - 5, - null, - null - ), - new KeywordResponse( - 2L, - "스프링", - "스프링입니다.", - 1, - 4, - null, - null - ) - )); + KeywordsResponse response = keywordService.findSessionIncludeRootKeywords(sessionId); + return ResponseEntity.ok(response); } - - @GetMapping("/{sessionId}/keywords/{keywordId}/children") - public ResponseEntity find(@PathVariable Long sessionId, + + @GetMapping("/children") + public ResponseEntity find(@PathVariable Long sessionId, @PathVariable Long keywordId) { - return ResponseEntity.ok(keywordResponseJava()); - } - - private KeywordsResponse keywordResponseJava() { - return new KeywordsResponse(Arrays.asList( - new KeywordResponse( - 1L, - "자바", - "자바입니다.", - 1, - 5, - null, - new HashSet<>(Arrays.asList( - new KeywordResponse( - 2L, - "List", - "자바의 자료구조인 List입니다.", - 1, - 3, - 1L, - keywordResponseList() - ), - new KeywordResponse( - 3L, - "Set", - "자바의 자료구조인 Set입니다.", - 2, - 3, - 1L, - keywordResponseSet() - ) - )))) - ); - } - - private HashSet keywordResponseList() { - return new HashSet<>(Arrays.asList( - new KeywordResponse( - 4L, - "ArrayList", - "자바 List의 구현체 ArrayList입니다.", - 1, - 2, - 2L, - null - ), - new KeywordResponse( - 5L, - "LinkedList", - "자바 List의 구현체 ArrayList입니다.", - 2, - 2, - 2L, - null - ) - )); - } - - private HashSet keywordResponseSet() { - return new HashSet<>(Arrays.asList( - new KeywordResponse( - 6L, - "HashSet", - "자바 Set의 구현체 HashSet입니다.", - 1, - 1, - 3L, - null - ), - new KeywordResponse( - 7L, - "TreeSet", - "자바 Set의 구현체 TreeSet입니다.", - 2, - 1, - 3L, - null - ) - )); + KeywordResponse response = keywordService.findKeywordWithAllChild(sessionId, keywordId); + return ResponseEntity.ok(response); } } diff --git a/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java b/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java new file mode 100644 index 000000000..c6e195d0f --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/roadmap/application/KeywordServiceTest.java @@ -0,0 +1,198 @@ +package wooteco.prolog.roadmap.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.Optional; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.TestConstructor; +import wooteco.prolog.roadmap.application.dto.KeywordUpdateRequest; +import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.application.dto.KeywordCreateRequest; +import wooteco.prolog.roadmap.application.dto.KeywordResponse; +import wooteco.prolog.roadmap.application.dto.KeywordsResponse; +import wooteco.prolog.roadmap.exception.KeywordAndKeywordParentSameException; +import wooteco.prolog.roadmap.exception.KeywordNotFoundException; +import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.prolog.session.domain.Session; +import wooteco.prolog.session.domain.repository.SessionRepository; +import wooteco.support.utils.NewIntegrationTest; + +@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) +@NewIntegrationTest +class KeywordServiceTest { + + private final KeywordService keywordService; + private final SessionRepository sessionRepository; + private final KeywordRepository keywordRepository; + private final EntityManager em; + + public KeywordServiceTest(final KeywordService keywordService, + final SessionRepository sessionRepository, + final KeywordRepository keywordRepository, + final EntityManager em) { + this.keywordService = keywordService; + this.sessionRepository = sessionRepository; + this.keywordRepository = keywordRepository; + this.em = em; + } + + @Nested + class 키워드_생성_요청_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + KeywordCreateRequest createRequest = new KeywordCreateRequest("자바", "자바에 대한 설명", 1, 1, null); + + @Nested + class 부모_값에_null이_포함된_키워드를_생성하면 { + Long extract = keywordService.createKeyword(session.getId(), createRequest); + + @Test + void 최상위_키워드를_생성한다() { + assertThat(extract).isNotNull(); + } + } + + @Nested + class 부모_값에_다른_키워드_값이_포함되면 { + Keyword keyword = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session.getId(), null)); + + KeywordCreateRequest createRequest = new KeywordCreateRequest("List", "List에 대한 설명", 1, 1, keyword.getId()); + Long extract = keywordService.createKeyword(session.getId(), createRequest); + + @Test + void 자식_키워드를_생성한다() { + assertThat(extract).isNotNull(); + } + } + } + + @Nested + class 단일_키워드_조회_요청_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + Keyword keyword = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session.getId(), null)); + + @Nested + class 세션_ID와_키워드_ID를_통해 { + KeywordResponse extract = keywordService.findKeyword(session.getId(), keyword.getId()); + + @Test + void 단일_키워드를_조회할_수_있다() { + assertThat(extract.getKeywordId()).isEqualTo(1); + } + } + + @Test + void 존재하지_않는_키워드로_조회하면_예외가_발생한다() { + assertThatThrownBy(() -> keywordService.findKeyword(createAndSaveSession(new Session("2022 백엔드 레벨 1")).getId(), 999L)) + .isInstanceOf(KeywordNotFoundException.class); + } + } + + @Nested + class 단일_키워드_연관_조회_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + Keyword parent = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session.getId(), null)); + + Keyword child_one = createKeywordChildren(Keyword.createKeyword("List", "List에 대한 설명", 1, 1, session.getId(), parent)); + Keyword child_two = createKeywordChildren(Keyword.createKeyword("Set", "Set에 대한 설명", 2, 1, session.getId(), parent)); + + Keyword child_one_child = createKeywordChildren(Keyword.createKeyword("List.of()", "List.of()에 대한 설명", 1, 1, session.getId(), child_one)); + Keyword child_two_child = createKeywordChildren(Keyword.createKeyword("Set.of()", "Set.of()에 대한 설명", 1, 1, session.getId(), child_two)); + + @Nested + class 세션_ID와_키워드_ID를_통해 { + KeywordResponse extract = keywordService.findKeywordWithAllChild(session.getId(), parent.getId()); + + @Test + void 조회한_키워드의_자식_키워드들까지_함께_조회할_수_있다() { + assertAll( + () -> assertThat(extract.getChildrenKeywords()).hasSize(2), + () -> assertThat(extract.getChildrenKeywords().iterator().next() + .getChildrenKeywords()).hasSize(1) + ); + } + } + } + + @Nested + class 세션_연관_키워드_조회_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + Keyword keyword_one = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session.getId(), null)); + Keyword keyword_two = createKeywordParent(Keyword.createKeyword("스프링", "스프링에 대한 설명", 2, 1, session.getId(), null)); + + @Nested + class 세션_ID를_통해서 { + KeywordsResponse extract = keywordService.findSessionIncludeRootKeywords(session.getId()); + + @Test + void 최상위_키워드들을_조회할_수_있다() { + assertThat(extract.getData()).hasSize(2); + } + } + } + + @Nested + class 키워드_수정_요청_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + Keyword parent = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, session.getId(), null)); + + @Test + void 내용을_수정할_수_있다() { + KeywordUpdateRequest keywordUpdateRequest = new KeywordUpdateRequest("자바스크립트", "자바스크립트에 대한 설명", 1, 2, null); + keywordService.updateKeyword(session.getId(), parent.getId(), keywordUpdateRequest); + keywordRepository.flush(); + em.clear(); + + Keyword extract = keywordRepository.findById(parent.getId()).get(); + assertThat(extract.getName()).isEqualTo("자바스크립트"); + } + + @Nested + class 수정한_키워드의_ID가_부모의_ID와_같은경우 { + Keyword child = createKeywordChildren(Keyword.createKeyword("List", "List에 대한 설명", 1, 1, 1L, parent)); + KeywordUpdateRequest request = new KeywordUpdateRequest("자바스크립트", "자바스크립트에 대한 설명", 1, 2, child.getId()); + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> keywordService.updateKeyword(session.getId(), child.getId(), request)) + .isInstanceOf(KeywordAndKeywordParentSameException.class); + } + } + } + + @Nested + class 키워드_삭제_요청_시 { + Session session = createAndSaveSession(new Session("2022 백엔드 레벨 1")); + Keyword parent = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, 1L, null)); + Keyword child = createKeywordChildren(Keyword.createKeyword("List", "List에 대한 설명", 1, 1, 1L, parent)); + + @Test + void 키워드를_삭제할_수_있다() { + keywordService.deleteKeyword(session.getId(), parent.getId()); + keywordRepository.flush(); + em.clear(); + + Optional extractParent = keywordRepository.findById(parent.getId()); + Optional extractChild = keywordRepository.findById(child.getId()); + assertAll( + () -> assertThat(extractParent).isNotPresent(), + () -> assertThat(extractChild).isNotPresent() + ); + } + } + + private Keyword createKeywordParent(final Keyword keyword) { + return keywordRepository.save(keyword); + } + + private Keyword createKeywordChildren(final Keyword keyword) { + return keywordRepository.save(keyword); + } + + private Session createAndSaveSession(final Session session) { + return sessionRepository.save(session); + } +} \ No newline at end of file diff --git a/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java b/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java new file mode 100644 index 000000000..acf48064f --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/roadmap/repository/KeywordRepositoryTest.java @@ -0,0 +1,76 @@ +package wooteco.prolog.roadmap.repository; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import javax.persistence.EntityManager; +import org.junit.jupiter.api.Test; +import org.springframework.test.context.TestConstructor; +import wooteco.prolog.roadmap.domain.Keyword; +import wooteco.prolog.roadmap.domain.repository.KeywordRepository; +import wooteco.support.utils.RepositoryTest; + +@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL) +@RepositoryTest +class KeywordRepositoryTest { + + private final KeywordRepository keywordRepository; + private final EntityManager em; + + public KeywordRepositoryTest(final KeywordRepository keywordRepository, final EntityManager em) { + this.keywordRepository = keywordRepository; + this.em = em; + } + + @Test + void 부모_키워드와_1뎁스까지의_자식_키워드를_함께_조회할_수_있다() { + // given + Keyword keywordParent = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, 1L, null)); + createKeywordChildren(Keyword.createKeyword("List", "List에 대한 설명", 1, 1, 1L, keywordParent)); + createKeywordChildren(Keyword.createKeyword("Set", "Set에 대한 설명", 2, 1, 1L, keywordParent)); + Long keywordParentId = keywordParent.getId(); + em.clear(); + + // when + Keyword extract = keywordRepository.findFetchById(keywordParentId); + + // then + assertAll( + () -> assertThat(extract.getId()).isNotNull(), + () -> assertThat(extract.getChildren()).hasSize(2)); + } + + @Test + void 부모_키워드와_2뎁스까지의_자식_키워드를_함께_조회할_수_있다() { + // given + Keyword parent = createKeywordParent(Keyword.createKeyword("자바", "자바에 대한 설명", 1, 1, 1L, null)); + + Keyword children1 = createKeywordChildren(Keyword.createKeyword("List", "List에 대한 설명", 1, 1, 1L, parent)); + Keyword children2 = createKeywordChildren(Keyword.createKeyword("Set", "Set에 대한 설명", 2, 1, 1L, parent)); + + createKeywordChildren(Keyword.createKeyword("List.of()", "List.of()에 대한 설명", 1, 1, 1L, children1)); + createKeywordChildren(Keyword.createKeyword("Set.of()", "Set.of()에 대한 설명", 1, 1, 1L, children2)); + + Long keywordParentId = parent.getId(); + em.clear(); + + // when + Keyword extract = keywordRepository.findFetchById(keywordParentId); + + // then + assertAll( + () -> assertThat(extract.getId()).isNotNull(), + () -> assertThat(extract.getChildren()).hasSize(2), + () -> assertThat(extract.getChildren().iterator().next() + .getChildren()).hasSize(1) + ); + } + + private Keyword createKeywordParent(final Keyword keyword) { + return keywordRepository.save(keyword); + } + + private Keyword createKeywordChildren(final Keyword keyword) { + return keywordRepository.save(keyword); + } +} \ No newline at end of file diff --git a/backend/src/test/java/wooteco/support/utils/DataCleanerExtension.java b/backend/src/test/java/wooteco/support/utils/DataCleanerExtension.java new file mode 100644 index 000000000..59f59ebd0 --- /dev/null +++ b/backend/src/test/java/wooteco/support/utils/DataCleanerExtension.java @@ -0,0 +1,20 @@ +package wooteco.support.utils; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import wooteco.prolog.common.DataInitializer; + +public class DataCleanerExtension implements AfterEachCallback { + + @Override + public void afterEach(final ExtensionContext context) { + DataInitializer dataCleaner = getDataCleaner(context); + dataCleaner.execute(); + } + + private DataInitializer getDataCleaner(final ExtensionContext extensionContext) { + return (DataInitializer) SpringExtension.getApplicationContext(extensionContext) + .getBean("dataInitializer"); + } +} diff --git a/backend/src/test/java/wooteco/support/utils/NewIntegrationTest.java b/backend/src/test/java/wooteco/support/utils/NewIntegrationTest.java new file mode 100644 index 000000000..b5bffde91 --- /dev/null +++ b/backend/src/test/java/wooteco/support/utils/NewIntegrationTest.java @@ -0,0 +1,16 @@ +package wooteco.support.utils; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.context.SpringBootTest; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@SpringBootTest +@ExtendWith(DataCleanerExtension.class) +public @interface NewIntegrationTest { + +}