Skip to content

Commit

Permalink
feat: 로드맵 키워드 구현 (#1098)
Browse files Browse the repository at this point in the history
* feat: Keyword의 정적 팩터리 메서드 생성 및 order 예약어 문제로 인한 컬럼명 수정

* feat: Keyword의 생성 어플리케이션 비즈니스 로직 구현

* feat: Keyword의 부모와 자식 키워드에 대한 전체 조회 도메인 기능 구현

* fix: Keyword의 parent 값도 Fetch로 가져오도록 수정

* feat: Keyword와 자식 Keyword들을 조회하는 어플리케이션 비즈니스 로직 구현

* feat: Keyword의 단일 조회에 대한 어플리케이션 비즈니스 로직 구현

* feat: Keyword의 수정에 대한 어플리케이션 비즈니스 로직 구현

* feat: Keyword의 삭제에 대한 어플리케이션 비즈니스 로직 구현

* feat: Session에 속하는 Keyword 목록을 가져오는 도메인 기능 구현

* feat: 하나의 Session에 속하는 최상위 Keyword 목록을 조회하는 어플리케이션 비즈니스 로직 구현

* refactor: ordinal -> seq로 이름 변경

* feat: Keyword를 생성하는 API 구현

* refactor: Keyword 엔티티와 대응되도록 컬럼 위치 변경

* feat: Keyword를 단건 조회하는 API 구현

* feat: Keyword를 단건 수정하는 API 구현

* feat: Keyword를 삭제하는 API 구현

* feat: 세선별 최상위 Keyword 목록 조회 API 구현

* feat: 최상위 Keyword에 속한 모든 자식 Keyword 목록 조회 API 구현

* test: 키워드 생성에 대한 인수테스트 작성

* test: 키워드 단일 조회에 대한 인수테스트 작성

* test: 키워드 수정에 대한 인수테스트 작성

* fix: children에 대해서 Left Fetch Join으로 수정

* test: 키워드 삭제에 대한 인수테스트 작성

* test: 세션별 최상위 키워드 목록 조회 인수테스트 작성

* test: 최상위 키워드의 모든 자식 키워드 조회 인수테스트 작성

* refactor: Fixture 이름 변경

* fix: documentTest의 URI 작성 오류 수정

* refactor: 최상위 키워드의 모든 자식 키워드 조회 개선

* fix: 잘못 작성된 @when@given으로 수정

* fix: 누락된 URL 추가

* refactor: 키워드 ID로 검색했을 때, 키워드가 없는 경우 예외가 발생하도록 변경

* feat: 잘못 사용되고 있는 검증 제거

* refactor: Builder 의존 제거

* feat: update에도 순서에 대한 검증 추가

* refactor: 코드 컨벤션 적용

* refactor: 도메인 생성 책임을 DTO로 변경

* refactor: 불필요한 주석 제거

* refactor: 패키지 구조 변경

* docs: javadoc를 통한 설명 추가

* refactor: 메서드 호출에 대한 validate 검증을 호출 시점으로 변경

* feat: AfterEach 상황에 동작하는 DataCleaner 추가

* refactor: 계층 구조로 테스트 코드 리팩토링 및 NewDataCleaner로 변경

* feat: equals & hashCode 재정의

* refactor: 기본 path 변경
  • Loading branch information
wishoon authored and her0807 committed Nov 21, 2022
1 parent a6c663f commit 8619abf
Show file tree
Hide file tree
Showing 24 changed files with 998 additions and 224 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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 키워드 목록이 조회된다
Original file line number Diff line number Diff line change
@@ -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
))
)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading

0 comments on commit 8619abf

Please sign in to comment.