From dec8c131725df583ef1c76263758a03dbe1a9452 Mon Sep 17 00:00:00 2001 From: dd Date: Tue, 19 May 2020 17:04:29 +0900 Subject: [PATCH 01/30] =?UTF-8?q?feat:=20=EC=8B=A4=EC=8A=B51,=202=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 문서화 실습 구현 - 인증 실습 구현 --- build.gradle | 20 ++++++ src/docs/asciidoc/api-guide.adoc | 2 +- .../subway/config/ETagHeaderFilter.java | 2 +- .../web/member/LoginMemberController.java | 17 +++-- .../interceptor/BasicAuthInterceptor.java | 10 +-- .../interceptor/BearerAuthInterceptor.java | 6 +- .../wooteco/subway/AuthAcceptanceTest.java | 57 ++++++++++----- .../subway/doc/MemberDocumentation.java | 32 +++++---- .../subway/web/line/LineControllerTest.java | 47 ++++++------ .../web/member/MemberControllerTest.java | 72 ++++++++++++++++++- 10 files changed, 197 insertions(+), 68 deletions(-) diff --git a/build.gradle b/build.gradle index 35b189610..87db66be9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.springframework.boot' version '2.2.5.RELEASE' id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id "org.asciidoctor.convert" version "1.5.9.2" id 'java' } @@ -24,8 +25,27 @@ dependencies { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } runtimeOnly 'com.h2database:h2' + asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' +} + +ext { + snippetsDir = file('build/generated-snippets') } test { useJUnitPlatform() + outputs.dir snippetsDir +} + +asciidoctor { + inputs.dir snippetsDir + dependsOn test +} + +bootJar { + dependsOn asciidoctor + from ("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } } diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 89b6e97f8..de020315a 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -19,4 +19,4 @@ endif::[] [[resources-members-create]] === 회원 가입 -operation::members/create[snippets='http-request,http-response'] \ No newline at end of file +operation::members/create[snippets='http-request,http-response,request-fields,request-body,response-body'] \ No newline at end of file diff --git a/src/main/java/wooteco/subway/config/ETagHeaderFilter.java b/src/main/java/wooteco/subway/config/ETagHeaderFilter.java index b1738e542..b6ddc721b 100644 --- a/src/main/java/wooteco/subway/config/ETagHeaderFilter.java +++ b/src/main/java/wooteco/subway/config/ETagHeaderFilter.java @@ -9,7 +9,7 @@ public class ETagHeaderFilter { @Bean public FilterRegistrationBean shallowEtagHeaderFilter() { - FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); + FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter()); filterRegistrationBean.addUrlPatterns("/lines/detail"); filterRegistrationBean.setName("etagFilter"); return filterRegistrationBean; diff --git a/src/main/java/wooteco/subway/web/member/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/LoginMemberController.java index 9eb4f13a2..173624650 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMemberController.java +++ b/src/main/java/wooteco/subway/web/member/LoginMemberController.java @@ -1,16 +1,22 @@ package wooteco.subway.web.member; +import java.util.Map; + +import javax.servlet.http.HttpSession; + import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; import wooteco.subway.service.member.dto.LoginRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; -import javax.servlet.http.HttpSession; -import java.util.Map; - @RestController public class LoginMemberController { private MemberService memberService; @@ -26,7 +32,8 @@ public ResponseEntity login(@RequestBody LoginRequest param) { } @PostMapping("/login") - public ResponseEntity login(@RequestParam Map paramMap, HttpSession session) { + public ResponseEntity login(@RequestParam Map paramMap, + HttpSession session) { String email = paramMap.get("email"); String password = paramMap.get("password"); if (!memberService.loginWithForm(email, password)) { diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java index a5f3a481b..7e79a34a8 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java @@ -1,5 +1,7 @@ package wooteco.subway.web.member.interceptor; +import java.util.Base64; + import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import wooteco.subway.domain.member.Member; @@ -23,11 +25,11 @@ public BasicAuthInterceptor(AuthorizationExtractor authExtractor, MemberService @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // TODO: Authorization 헤더를 통해 Basic 값을 추출 (authExtractor.extract() 메서드 활용) - + String basic = authExtractor.extract(request, "Basic"); // TODO: 추출한 Basic 값을 Base64를 통해 email과 password 값 추출(Base64.getDecoder().decode() 메서드 활용) - - String email = ""; - String password = ""; + String[] decode = new String(Base64.getDecoder().decode(basic)).split(":"); + String email = decode[0]; + String password = decode[1]; Member member = memberService.findMemberByEmail(email); if (!member.checkPassword(password)) { diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java index bd3c1bb53..5a8e13541 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java @@ -23,11 +23,11 @@ public BearerAuthInterceptor(AuthorizationExtractor authExtractor, JwtTokenProvi public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // TODO: Authorization 헤더를 통해 Bearer 값을 추출 (authExtractor.extract() 메서드 활용) - + String bearer = authExtractor.extract(request, "bearer"); // TODO: 추출한 토큰값의 유효성 검사 (jwtTokenProvider.validateToken() 메서드 활용) - + jwtTokenProvider.validateToken(bearer); // TODO: 추출한 토큰값에서 email 정보 추출 (jwtTokenProvider.getSubject() 메서드 활용) - String email = ""; + String email = jwtTokenProvider.getSubject(bearer); request.setAttribute("loginMemberEmail", email); return true; diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index 9cf9ba43c..c5f11a3e1 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -1,17 +1,19 @@ package wooteco.subway; +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; + +import io.restassured.authentication.FormAuthConfig; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; -import java.util.HashMap; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - public class AuthAcceptanceTest extends AcceptanceTest { @DisplayName("Basic Auth") @Test @@ -51,17 +53,40 @@ void myInfoWithBearerAuth() { public MemberResponse myInfoWithBasicAuth(String email, String password) { // TODO: basic auth를 활용하여 /me/basic 요청하여 내 정보 조회 - return null; + return given().auth() + .preemptive() + .basic(email, password) + .when() + .get("/me/basic") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MemberResponse.class); } public MemberResponse myInfoWithSession(String email, String password) { // TODO: form auth를 활용하여 /me/session 요청하여 내 정보 조회 - return null; + return given().auth() + .form(email, password, new FormAuthConfig("/login", "email", "password")) + .when() + .get("/me/session") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MemberResponse.class); } public MemberResponse myInfoWithBearerAuth(TokenResponse tokenResponse) { // TODO: oauth2 auth(bearer)를 활용하여 /me/bearer 요청하여 내 정보 조회 - return null; + return given().auth() + .preemptive() + .oauth2(tokenResponse.getAccessToken()) + .when() + .get("/me/bearer") + .then() + .log().all() + .statusCode(HttpStatus.OK.value()) + .extract().as(MemberResponse.class); } public TokenResponse login(String email, String password) { @@ -70,15 +95,15 @@ public TokenResponse login(String email, String password) { params.put("password", password); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). when(). - post("/oauth/token"). + post("/oauth/token"). then(). - log().all(). - statusCode(HttpStatus.OK.value()). - extract().as(TokenResponse.class); + log().all(). + statusCode(HttpStatus.OK.value()). + extract().as(TokenResponse.class); } } diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index 0acbe7d4e..b7939f62a 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -1,19 +1,27 @@ package wooteco.subway.doc; +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; + +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; public class MemberDocumentation { -// public static RestDocumentationResultHandler createMember() { -// return document("members/create", -// requestFields( -// fieldWithPath("email").type(JsonFieldType.STRING).description("The user's email address"), -// fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), -// fieldWithPath("password").type(JsonFieldType.STRING).description("The user's password") -// ), -// responseHeaders( -// headerWithName("Location").description("The user's location who just created") -// ) -// ); -// } + public static RestDocumentationResultHandler createMember() { + return document("members/create", + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING) + .description("The user's email address"), + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("password").type(JsonFieldType.STRING) + .description("The user's password") + ), + responseHeaders( + headerWithName("Location").description("The user's location who just created") + ) + ); + } // // public static RestDocumentationResultHandler updateMember() { // return document("members/update", diff --git a/src/test/java/wooteco/subway/web/line/LineControllerTest.java b/src/test/java/wooteco/subway/web/line/LineControllerTest.java index db2e7b31a..cb388632a 100644 --- a/src/test/java/wooteco/subway/web/line/LineControllerTest.java +++ b/src/test/java/wooteco/subway/web/line/LineControllerTest.java @@ -1,32 +1,30 @@ package wooteco.subway.web.line; +import static org.mockito.BDDMockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.util.Arrays; +import java.util.List; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.context.annotation.Import; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import wooteco.subway.config.ETagHeaderFilter; -import wooteco.subway.web.LineController; + import wooteco.subway.domain.line.Line; import wooteco.subway.domain.station.Station; import wooteco.subway.service.line.LineService; import wooteco.subway.service.line.dto.LineDetailResponse; import wooteco.subway.service.line.dto.WholeSubwayResponse; -import java.util.Arrays; -import java.util.List; - -import static org.mockito.BDDMockito.given; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@WebMvcTest(controllers = LineController.class) -@Import(ETagHeaderFilter.class) +@SpringBootTest +@AutoConfigureMockMvc public class LineControllerTest { @MockBean private LineService lineService; @@ -37,24 +35,25 @@ public class LineControllerTest { @DisplayName("eTag를 활용한 HTTP 캐시 설정 검증") @Test void ETag() throws Exception { - WholeSubwayResponse response = WholeSubwayResponse.of(Arrays.asList(createMockResponse(), createMockResponse())); + WholeSubwayResponse response = WholeSubwayResponse.of( + Arrays.asList(createMockResponse(), createMockResponse())); given(lineService.findLinesWithStations()).willReturn(response); String uri = "/lines/detail"; MvcResult mvcResult = mockMvc.perform(get(uri)) - .andDo(print()) - .andExpect(status().isOk()) - .andExpect(header().exists("ETag")) - .andReturn(); + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(header().exists("ETag")) + .andReturn(); String eTag = mvcResult.getResponse().getHeader("ETag"); mockMvc.perform(get(uri).header("If-None-Match", eTag)) - .andDo(print()) - .andExpect(status().isNotModified()) - .andExpect(header().exists("ETag")) - .andReturn(); + .andDo(print()) + .andExpect(status().isNotModified()) + .andExpect(header().exists("ETag")) + .andReturn(); } private LineDetailResponse createMockResponse() { diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index ded0ed232..051e741b4 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -1,5 +1,73 @@ package wooteco.subway.web.member; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static wooteco.subway.AcceptanceTest.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +import wooteco.subway.doc.MemberDocumentation; +import wooteco.subway.domain.member.Member; +import wooteco.subway.service.member.MemberService; + +@ExtendWith(RestDocumentationExtension.class) +@SpringBootTest +@AutoConfigureMockMvc public class MemberControllerTest { - // TODO: 회원가입 API 테스트 -} + + @MockBean + protected MemberService memberService; + + @Autowired + protected MockMvc mockMvc; + + @BeforeEach + public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .addFilter(new ShallowEtagHeaderFilter()) + .apply(documentationConfiguration(restDocumentation)) + .build(); + } + + @Test + public void createMember() throws Exception { + Member member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + given(memberService.createMember(any())).willReturn(member); + + String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + + "\"name\":\"" + TEST_USER_NAME + "\"," + + "\"password\":\"" + TEST_USER_PASSWORD + "\"}"; + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(print()); + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(MemberDocumentation.createMember()); + } +} \ No newline at end of file From da9fb9456a4035b3d37cdd36594b9f3841d76639 Mon Sep 17 00:00:00 2001 From: dd Date: Wed, 20 May 2020 10:47:09 +0900 Subject: [PATCH 02/30] =?UTF-8?q?refactor:=20MemberResponse=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EC=83=9D=EC=84=B1=EC=9E=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/member/dto/MemberRequest.java | 3 +++ .../service/member/dto/MemberResponse.java | 3 +++ .../interceptor/BasicAuthInterceptor.java | 2 -- .../interceptor/BearerAuthInterceptor.java | 3 --- .../wooteco/subway/AuthAcceptanceTest.java | 3 --- .../subway/doc/MemberDocumentation.java | 24 +++++++++---------- 6 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java index 05aed3875..5477efa8b 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java @@ -7,6 +7,9 @@ public class MemberRequest { private String name; private String password; + public MemberRequest() { + } + public String getEmail() { return email; } diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java b/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java index ffad82ee2..e02f1cea4 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberResponse.java @@ -7,6 +7,9 @@ public class MemberResponse { private String email; private String name; + public MemberResponse() { + } + public MemberResponse(Long id, String email, String name) { this.id = id; this.email = email; diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java index 7e79a34a8..e725dab64 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java @@ -24,9 +24,7 @@ public BasicAuthInterceptor(AuthorizationExtractor authExtractor, MemberService @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // TODO: Authorization 헤더를 통해 Basic 값을 추출 (authExtractor.extract() 메서드 활용) String basic = authExtractor.extract(request, "Basic"); - // TODO: 추출한 Basic 값을 Base64를 통해 email과 password 값 추출(Base64.getDecoder().decode() 메서드 활용) String[] decode = new String(Base64.getDecoder().decode(basic)).split(":"); String email = decode[0]; String password = decode[1]; diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java index 5a8e13541..da5b41fdc 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java @@ -22,11 +22,8 @@ public BearerAuthInterceptor(AuthorizationExtractor authExtractor, JwtTokenProvi @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - // TODO: Authorization 헤더를 통해 Bearer 값을 추출 (authExtractor.extract() 메서드 활용) String bearer = authExtractor.extract(request, "bearer"); - // TODO: 추출한 토큰값의 유효성 검사 (jwtTokenProvider.validateToken() 메서드 활용) jwtTokenProvider.validateToken(bearer); - // TODO: 추출한 토큰값에서 email 정보 추출 (jwtTokenProvider.getSubject() 메서드 활용) String email = jwtTokenProvider.getSubject(bearer); request.setAttribute("loginMemberEmail", email); diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index c5f11a3e1..d26679516 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -52,7 +52,6 @@ void myInfoWithBearerAuth() { } public MemberResponse myInfoWithBasicAuth(String email, String password) { - // TODO: basic auth를 활용하여 /me/basic 요청하여 내 정보 조회 return given().auth() .preemptive() .basic(email, password) @@ -65,7 +64,6 @@ public MemberResponse myInfoWithBasicAuth(String email, String password) { } public MemberResponse myInfoWithSession(String email, String password) { - // TODO: form auth를 활용하여 /me/session 요청하여 내 정보 조회 return given().auth() .form(email, password, new FormAuthConfig("/login", "email", "password")) .when() @@ -77,7 +75,6 @@ public MemberResponse myInfoWithSession(String email, String password) { } public MemberResponse myInfoWithBearerAuth(TokenResponse tokenResponse) { - // TODO: oauth2 auth(bearer)를 활용하여 /me/bearer 요청하여 내 정보 조회 return given().auth() .preemptive() .oauth2(tokenResponse.getAccessToken()) diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index b7939f62a..aa8e4a7ab 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -22,16 +22,16 @@ public static RestDocumentationResultHandler createMember() { ) ); } -// -// public static RestDocumentationResultHandler updateMember() { -// return document("members/update", -// requestFields( -// fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), -// fieldWithPath("password").type(JsonFieldType.STRING).description("The user's password") -// ), -// requestHeaders( -// headerWithName("Authorization").description("The token for login which is Bearer Type") -// ) -// ); -// } + + public static RestDocumentationResultHandler updateMember() { + return document("members/update", + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("password").type(JsonFieldType.STRING).description("The user's password") + ), + requestHeaders( + headerWithName("Authorization").description("The token for login which is Bearer Type") + ) + ); + } } From 06c1b255addd8f4417cde6212dd97ebd4e23a0d9 Mon Sep 17 00:00:00 2001 From: dd Date: Wed, 20 May 2020 17:01:16 +0900 Subject: [PATCH 03/30] =?UTF-8?q?feat:=20MethodArgumentResolver=EC=97=90?= =?UTF-8?q?=EC=84=9C=20password=EC=99=80=20password-check=EA=B0=80=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=ED=95=98=EB=8A=94=20=EC=A7=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 81 +++++++++++++++++++ .../wooteco/subway/config/WebMvcConfig.java | 14 ++-- .../service/member/dto/MemberRawRequest.java | 40 +++++++++ .../service/member/dto/MemberRequest.java | 10 +++ .../subway/web/error/ErrorResponse.java | 17 ++++ .../subway/web/member/MemberController.java | 19 ++++- .../web/member/NotMatchPasswordException.java | 7 ++ .../subway/web/member/RegisterMember.java | 11 +++ .../RegisterMemberMethodArgumentResolver.java | 52 ++++++++++++ .../interceptor/SessionInterceptor.java | 21 ----- .../java/wooteco/subway/AcceptanceTest.java | 21 +++++ .../wooteco/subway/MemberAcceptanceTest.java | 20 +++++ 12 files changed, 281 insertions(+), 32 deletions(-) create mode 100644 README.md create mode 100644 src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java create mode 100644 src/main/java/wooteco/subway/web/error/ErrorResponse.java create mode 100644 src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java create mode 100644 src/main/java/wooteco/subway/web/member/RegisterMember.java create mode 100644 src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java delete mode 100644 src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java create mode 100644 src/test/java/wooteco/subway/MemberAcceptanceTest.java diff --git a/README.md b/README.md new file mode 100644 index 000000000..d65d8917c --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# 지하철 3단계 - 회원 관리, 즐겨찾기 + +## 요구 사항 + +- 회원 정보를 관리하는 기능 구현 +- 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 +- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가 +- side case에 대한 예외처리 +- 인수 테스트와 단위 테스트 작성 +- API 문서를 작성하고 문서화를 위한 테스트 작성 +- 페이지 연동 +- 즐겨찾기 기능을 추가(추가,삭제,조회) +- 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 +- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver) +- side case에 대한 예외처리 필수 +- 인수 테스트와 단위 테스트 작성 +- API 문서를 작성하고 문서화를 위한 테스트 작성 +- 페이지 연동 + +### 기능 목록 + +회원 정보 관리 + +- 회원 가입 +- 로그인 +- 로그인 후 회원정보 조회/수정/삭제 + +즐겨찾기 관리 + +- 즐겨찾기 추가 +- 즐겨찾기 목록조회 / 제거 + +### 예외 사항 + +- 회원 가입 시 공백이 존재하는 경우 +- 이메일 형식 검증 +- 패스워드 확인 일치 검증 +- 로그인이 안된 경우에 회원 정보에 접근하는 경우 +- 토큰이 만료된 경우 +- 로그인 정보가 올바르지 않은 경우 +- 로그인이 안된 경우 즐겨찾기를 추가하는 경우 +- 즐겨찾기 해제 기능## 요구 사항 + + - 회원 정보를 관리하는 기능 구현 + - 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 + - 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가 + - side case에 대한 예외처리 + - 인수 테스트와 단위 테스트 작성 + - API 문서를 작성하고 문서화를 위한 테스트 작성 + - 페이지 연동 + - 즐겨찾기 기능을 추가(추가,삭제,조회) + - 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 + - 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver) + - side case에 대한 예외처리 필수 + - 인수 테스트와 단위 테스트 작성 + - API 문서를 작성하고 문서화를 위한 테스트 작성 + - 페이지 연동 + + ### 기능 목록 + + 회원 정보 관리 + + - 회원 가입 + - 로그인 + - 로그인 후 회원정보 조회/수정/삭제 + + 즐겨찾기 관리 + + - 즐겨찾기 추가 + - 즐겨찾기 목록조회 / 제거 + + ### 예외 사항 + + - 회원 가입 시 공백이 존재하는 경우 + - 이메일 형식 검증 + - 패스워드 확인 일치 검증 + - 로그인이 안된 경우에 회원 정보에 접근하는 경우 + - 토큰이 만료된 경우 + - 로그인 정보가 올바르지 않은 경우 + - 로그인이 안된 경우 즐겨찾기를 추가하는 경우 + - 즐겨찾기 해제 기능 \ No newline at end of file diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 87217dc79..684357ffd 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -4,38 +4,38 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import wooteco.subway.web.member.LoginMemberMethodArgumentResolver; +import wooteco.subway.web.member.RegisterMemberMethodArgumentResolver; import wooteco.subway.web.member.interceptor.BasicAuthInterceptor; import wooteco.subway.web.member.interceptor.BearerAuthInterceptor; -import wooteco.subway.web.member.interceptor.SessionInterceptor; import java.util.List; @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final BasicAuthInterceptor basicAuthInterceptor; - private final SessionInterceptor sessionInterceptor; private final BearerAuthInterceptor bearerAuthInterceptor; private final LoginMemberMethodArgumentResolver loginMemberArgumentResolver; + private final RegisterMemberMethodArgumentResolver registerMemberMethodArgumentResolver; public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor, - SessionInterceptor sessionInterceptor, - BearerAuthInterceptor bearerAuthInterceptor, - LoginMemberMethodArgumentResolver loginMemberArgumentResolver) { + BearerAuthInterceptor bearerAuthInterceptor, + LoginMemberMethodArgumentResolver loginMemberArgumentResolver, + RegisterMemberMethodArgumentResolver registerMemberMethodArgumentResolver) { this.basicAuthInterceptor = basicAuthInterceptor; - this.sessionInterceptor = sessionInterceptor; this.bearerAuthInterceptor = bearerAuthInterceptor; this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.registerMemberMethodArgumentResolver = registerMemberMethodArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic"); - registry.addInterceptor(sessionInterceptor).addPathPatterns("/me/session"); registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer"); } @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(loginMemberArgumentResolver); + argumentResolvers.add(registerMemberMethodArgumentResolver); } } diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java new file mode 100644 index 000000000..db5118203 --- /dev/null +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java @@ -0,0 +1,40 @@ +package wooteco.subway.service.member.dto; + +import wooteco.subway.domain.member.Member; + +public class MemberRawRequest { + private String email; + private String name; + private String password; + private String passwordCheck; + + public MemberRawRequest() { + } + + public MemberRawRequest(String email, String name, String password, String passwordCheck) { + this.email = email; + this.name = name; + this.password = password; + this.passwordCheck = passwordCheck; + } + + public MemberRequest toMemberRequest() { + return new MemberRequest(email, name, password); + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + + public String getPasswordCheck() { + return passwordCheck; + } +} diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java index 5477efa8b..995b57d6f 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java @@ -1,8 +1,12 @@ package wooteco.subway.service.member.dto; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + import wooteco.subway.domain.member.Member; public class MemberRequest { + private String email; private String name; private String password; @@ -10,6 +14,12 @@ public class MemberRequest { public MemberRequest() { } + public MemberRequest(String email, String name, String password) { + this.email = email; + this.name = name; + this.password = password; + } + public String getEmail() { return email; } diff --git a/src/main/java/wooteco/subway/web/error/ErrorResponse.java b/src/main/java/wooteco/subway/web/error/ErrorResponse.java new file mode 100644 index 000000000..f0f76941d --- /dev/null +++ b/src/main/java/wooteco/subway/web/error/ErrorResponse.java @@ -0,0 +1,17 @@ +package wooteco.subway.web.error; + +public class ErrorResponse { + + private String message; + + public ErrorResponse() { + } + + public ErrorResponse(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/MemberController.java index 8a3046948..ef65a18f8 100644 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/MemberController.java @@ -1,12 +1,17 @@ package wooteco.subway.web.member; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.*; import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; import wooteco.subway.service.member.dto.MemberRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.UpdateMemberRequest; +import wooteco.subway.web.error.ErrorResponse; import java.net.URI; @@ -19,11 +24,11 @@ public MemberController(MemberService memberService) { } @PostMapping("/members") - public ResponseEntity createMember(@RequestBody MemberRequest view) { + public ResponseEntity createMember(@RegisterMember MemberRequest view) { Member member = memberService.createMember(view.toMember()); return ResponseEntity - .created(URI.create("/members/" + member.getId())) - .build(); + .created(URI.create("/members/" + member.getId())) + .build(); } @GetMapping("/members") @@ -33,7 +38,8 @@ public ResponseEntity getMemberByEmail(@RequestParam String emai } @PutMapping("/members/{id}") - public ResponseEntity updateMember(@PathVariable Long id, @RequestBody UpdateMemberRequest param) { + public ResponseEntity updateMember(@PathVariable Long id, + @RequestBody UpdateMemberRequest param) { memberService.updateMember(id, param); return ResponseEntity.ok().build(); } @@ -43,4 +49,9 @@ public ResponseEntity deleteMember(@PathVariable Long id) { memberService.deleteMember(id); return ResponseEntity.noContent().build(); } + + @ExceptionHandler(value = NotMatchPasswordException.class) + public ResponseEntity exceptionHandle(NotMatchPasswordException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage())); + } } diff --git a/src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java b/src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java new file mode 100644 index 000000000..16be14396 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.member; + +public class NotMatchPasswordException extends RuntimeException { + public NotMatchPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/member/RegisterMember.java b/src/main/java/wooteco/subway/web/member/RegisterMember.java new file mode 100644 index 000000000..e97236889 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/RegisterMember.java @@ -0,0 +1,11 @@ +package wooteco.subway.web.member; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface RegisterMember { +} diff --git a/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java b/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java new file mode 100644 index 000000000..f00b1ce43 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java @@ -0,0 +1,52 @@ +package wooteco.subway.web.member; + +import static org.springframework.web.context.request.RequestAttributes.*; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.Objects; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.apache.tomcat.util.json.JSONParser; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import com.fasterxml.jackson.databind.ObjectMapper; +import wooteco.subway.service.member.dto.MemberRawRequest; +import wooteco.subway.service.member.dto.MemberRequest; + +@Component +public class RegisterMemberMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RegisterMember.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); + BufferedReader reader = request.getReader(); + ObjectMapper objectMapper = new ObjectMapper(); + MemberRawRequest memberRawRequest = objectMapper.readValue(reader, MemberRawRequest.class); + + String password = memberRawRequest.getPassword(); + String passwordCheck = memberRawRequest.getPasswordCheck(); + + if (!Objects.equals(password, passwordCheck)) { + throw new NotMatchPasswordException("패스워드가 일치하지 않습니다."); + } + return memberRawRequest.toMemberRequest(); + } +} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java deleted file mode 100644 index 1c90fdbbd..000000000 --- a/src/main/java/wooteco/subway/web/member/interceptor/SessionInterceptor.java +++ /dev/null @@ -1,21 +0,0 @@ -package wooteco.subway.web.member.interceptor; - -import org.apache.logging.log4j.util.Strings; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@Component -public class SessionInterceptor implements HandlerInterceptor { - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String email = (String) request.getSession().getAttribute("loginMemberEmail"); - if (Strings.isNotBlank(email)) { - request.setAttribute("loginMemberEmail", email); - } - - return true; - } -} diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index ae0fb66d8..a3d758eee 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; +import javax.print.attribute.standard.Media; + @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql("/truncate.sql") public class AcceptanceTest { @@ -247,6 +249,25 @@ public void initStation() { addLineStation(lineResponse4.getId(), stationResponse1.getId(), stationResponse7.getId(), 40, 3); } + public String registerMember(String email, String name, String password, String passwordCheck) { + Map view = new HashMap<>(); + view.put("email", email); + view.put("name", name); + view.put("password", password); + view.put("passwordCheck", passwordCheck); + + return + given().body(view). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/members"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().header("Location"); + } + public String createMember(String email, String name, String password) { Map params = new HashMap<>(); params.put("email", email); diff --git a/src/test/java/wooteco/subway/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/MemberAcceptanceTest.java new file mode 100644 index 000000000..94f5f1894 --- /dev/null +++ b/src/test/java/wooteco/subway/MemberAcceptanceTest.java @@ -0,0 +1,20 @@ +package wooteco.subway; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import wooteco.subway.web.member.NotMatchPasswordException; + +public class MemberAcceptanceTest extends AcceptanceTest { + + // 회원 정보를 관리한다. + // 회원 가입을 한다. + + @Test + void memberAcceptanceTest() { + assertThat(registerMember("dd@naver.com", "디디", "1q2w3e4r", "1q2w3e4r")).isEqualTo("/members/1"); + assertThat(registerMember("fucct@naver.com", "둔덩", "qwerqwer", "qwerqwer")).isEqualTo("/members/2"); + + } +} From 470f7863d556265ba8b4594abe6c9a8b5223d918 Mon Sep 17 00:00:00 2001 From: dd Date: Wed, 20 May 2020 17:54:58 +0900 Subject: [PATCH 04/30] =?UTF-8?q?feat:=20interceptor=20=EC=99=80=20methodA?= =?UTF-8?q?rgumentResolver=20=EC=97=90=EC=84=9C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=98=88=EC=99=B8=20=EA=B4=80=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 아직 미완성.. --- .../subway/service/member/MemberService.java | 7 + .../service/member/dto/MemberRequest.java | 4 + .../web/member/DuplicateEmailException.java | 7 + .../subway/web/member/MemberController.java | 4 +- .../RegisterMemberMethodArgumentResolver.java | 11 + .../java/wooteco/subway/AcceptanceTest.java | 273 +++++++++--------- .../wooteco/subway/MemberAcceptanceTest.java | 10 +- 7 files changed, 181 insertions(+), 135 deletions(-) create mode 100644 src/main/java/wooteco/subway/web/member/DuplicateEmailException.java diff --git a/src/main/java/wooteco/subway/service/member/MemberService.java b/src/main/java/wooteco/subway/service/member/MemberService.java index 12f3c2a12..8789af09a 100644 --- a/src/main/java/wooteco/subway/service/member/MemberService.java +++ b/src/main/java/wooteco/subway/service/member/MemberService.java @@ -1,5 +1,7 @@ package wooteco.subway.service.member; +import java.util.Optional; + import org.springframework.stereotype.Service; import wooteco.subway.domain.member.Member; import wooteco.subway.domain.member.MemberRepository; @@ -48,4 +50,9 @@ public boolean loginWithForm(String email, String password) { Member member = findMemberByEmail(email); return member.checkPassword(password); } + + public boolean isExistEmail(String email) { + Optional byEmail = memberRepository.findByEmail(email); + return byEmail.isPresent(); + } } diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java index 995b57d6f..fdf798e9f 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java @@ -7,8 +7,12 @@ public class MemberRequest { + @Email + @NotBlank private String email; + @NotBlank private String name; + @NotBlank() private String password; public MemberRequest() { diff --git a/src/main/java/wooteco/subway/web/member/DuplicateEmailException.java b/src/main/java/wooteco/subway/web/member/DuplicateEmailException.java new file mode 100644 index 000000000..35d6b9f7a --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/DuplicateEmailException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.member; + +public class DuplicateEmailException extends RuntimeException { + public DuplicateEmailException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/MemberController.java index ef65a18f8..ff5c753fa 100644 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/MemberController.java @@ -24,7 +24,7 @@ public MemberController(MemberService memberService) { } @PostMapping("/members") - public ResponseEntity createMember(@RegisterMember MemberRequest view) { + public ResponseEntity createMember(@RegisterMember @Validated MemberRequest view) { Member member = memberService.createMember(view.toMember()); return ResponseEntity .created(URI.create("/members/" + member.getId())) @@ -52,6 +52,6 @@ public ResponseEntity deleteMember(@PathVariable Long id) { @ExceptionHandler(value = NotMatchPasswordException.class) public ResponseEntity exceptionHandle(NotMatchPasswordException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage())); + throw new RuntimeException(); } } diff --git a/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java b/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java index f00b1ce43..9ae477e89 100644 --- a/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java +++ b/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java @@ -22,11 +22,18 @@ import org.springframework.web.method.support.ModelAndViewContainer; import com.fasterxml.jackson.databind.ObjectMapper; +import wooteco.subway.service.member.MemberService; import wooteco.subway.service.member.dto.MemberRawRequest; import wooteco.subway.service.member.dto.MemberRequest; @Component public class RegisterMemberMethodArgumentResolver implements HandlerMethodArgumentResolver { + private final MemberService memberService; + + public RegisterMemberMethodArgumentResolver(MemberService memberService) { + this.memberService = memberService; + } + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RegisterMember.class); @@ -41,12 +48,16 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m ObjectMapper objectMapper = new ObjectMapper(); MemberRawRequest memberRawRequest = objectMapper.readValue(reader, MemberRawRequest.class); + String email = memberRawRequest.getEmail(); String password = memberRawRequest.getPassword(); String passwordCheck = memberRawRequest.getPasswordCheck(); if (!Objects.equals(password, passwordCheck)) { throw new NotMatchPasswordException("패스워드가 일치하지 않습니다."); } + if (memberService.isExistEmail(email)) { + throw new DuplicateEmailException("이미 존재하는 이메일입니다."); + } return memberRawRequest.toMemberRequest(); } } diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index a3d758eee..ca32f4b0a 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -1,27 +1,27 @@ package wooteco.subway; -import io.restassured.RestAssured; -import io.restassured.specification.RequestSpecification; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import org.junit.jupiter.api.BeforeEach; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.test.context.jdbc.Sql; + +import io.restassured.RestAssured; +import io.restassured.specification.RequestSpecification; import wooteco.subway.service.line.dto.LineDetailResponse; import wooteco.subway.service.line.dto.LineResponse; import wooteco.subway.service.line.dto.WholeSubwayResponse; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.path.dto.PathResponse; import wooteco.subway.service.station.dto.StationResponse; - -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.print.attribute.standard.Media; +import wooteco.subway.web.member.NotMatchPasswordException; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql("/truncate.sql") @@ -60,33 +60,33 @@ public StationResponse createStation(String name) { params.put("name", name); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/stations"). - then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().as(StationResponse.class); + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/stations"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().as(StationResponse.class); } public List getStations() { return - given().when(). - get("/stations"). - then(). - log().all(). - extract(). - jsonPath().getList(".", StationResponse.class); + given().when(). + get("/stations"). + then(). + log().all(). + extract(). + jsonPath().getList(".", StationResponse.class); } public void deleteStation(Long id) { given().when(). - delete("/stations/" + id). - then(). - log().all(); + delete("/stations/" + id). + then(). + log().all(); } public LineResponse createLine(String name) { @@ -97,25 +97,25 @@ public LineResponse createLine(String name) { params.put("intervalTime", "10"); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/lines"). - then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().as(LineResponse.class); + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/lines"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().as(LineResponse.class); } public LineDetailResponse getLine(Long id) { return - given().when(). - get("/lines/" + id). - then(). - log().all(). - extract().as(LineDetailResponse.class); + given().when(). + get("/lines/" + id). + then(). + log().all(). + extract().as(LineDetailResponse.class); } public void updateLine(Long id, LocalTime startTime, LocalTime endTime) { @@ -125,38 +125,39 @@ public void updateLine(Long id, LocalTime startTime, LocalTime endTime) { params.put("intervalTime", "10"); given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - put("/lines/" + id). - then(). - log().all(). - statusCode(HttpStatus.OK.value()); + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + put("/lines/" + id). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); } public List getLines() { return - given().when(). - get("/lines"). - then(). - log().all(). - extract(). - jsonPath().getList(".", LineResponse.class); + given().when(). + get("/lines"). + then(). + log().all(). + extract(). + jsonPath().getList(".", LineResponse.class); } public void deleteLine(Long id) { given().when(). - delete("/lines/" + id). - then(). - log().all(); + delete("/lines/" + id). + then(). + log().all(); } public void addLineStation(Long lineId, Long preStationId, Long stationId) { addLineStation(lineId, preStationId, stationId, 10, 10); } - public void addLineStation(Long lineId, Long preStationId, Long stationId, Integer distance, Integer duration) { + public void addLineStation(Long lineId, Long preStationId, Long stationId, Integer distance, + Integer duration) { Map params = new HashMap<>(); params.put("preStationId", preStationId == null ? "" : preStationId.toString()); params.put("stationId", stationId.toString()); @@ -164,48 +165,48 @@ public void addLineStation(Long lineId, Long preStationId, Long stationId, Integ params.put("duration", duration.toString()); given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/lines/" + lineId + "/stations"). - then(). - log().all(). - statusCode(HttpStatus.OK.value()); + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/lines/" + lineId + "/stations"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); } public void removeLineStation(Long lineId, Long stationId) { given(). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - delete("/lines/" + lineId + "/stations/" + stationId). - then(). - log().all(). - statusCode(HttpStatus.NO_CONTENT.value()); + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + delete("/lines/" + lineId + "/stations/" + stationId). + then(). + log().all(). + statusCode(HttpStatus.NO_CONTENT.value()); } public WholeSubwayResponse retrieveWholeSubway() { return - given(). - when(). - get("/lines/detail"). - then(). - log().all(). - extract().as(WholeSubwayResponse.class); + given(). + when(). + get("/lines/detail"). + then(). + log().all(). + extract().as(WholeSubwayResponse.class); } public PathResponse findPath(String source, String target, String type) { return - given(). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - get("/paths?source=" + source + "&target=" + target + "&type=" + type). - then(). - log().all(). - statusCode(HttpStatus.OK.value()). - extract().as(PathResponse.class); + given(). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + get("/paths?source=" + source + "&target=" + target + "&type=" + type). + then(). + log().all(). + statusCode(HttpStatus.OK.value()). + extract().as(PathResponse.class); } /** @@ -228,25 +229,32 @@ public void initStation() { // 2호선 LineResponse lineResponse1 = createLine("2호선"); addLineStation(lineResponse1.getId(), null, stationResponse1.getId(), 0, 0); - addLineStation(lineResponse1.getId(), stationResponse1.getId(), stationResponse2.getId(), 5, 10); - addLineStation(lineResponse1.getId(), stationResponse2.getId(), stationResponse3.getId(), 5, 10); + addLineStation(lineResponse1.getId(), stationResponse1.getId(), stationResponse2.getId(), 5, + 10); + addLineStation(lineResponse1.getId(), stationResponse2.getId(), stationResponse3.getId(), 5, + 10); // 분당선 LineResponse lineResponse2 = createLine("분당선"); addLineStation(lineResponse2.getId(), null, stationResponse3.getId(), 0, 0); - addLineStation(lineResponse2.getId(), stationResponse3.getId(), stationResponse4.getId(), 5, 10); - addLineStation(lineResponse2.getId(), stationResponse4.getId(), stationResponse5.getId(), 5, 10); + addLineStation(lineResponse2.getId(), stationResponse3.getId(), stationResponse4.getId(), 5, + 10); + addLineStation(lineResponse2.getId(), stationResponse4.getId(), stationResponse5.getId(), 5, + 10); // 3호선 LineResponse lineResponse3 = createLine("3호선"); addLineStation(lineResponse3.getId(), null, stationResponse5.getId(), 0, 0); - addLineStation(lineResponse3.getId(), stationResponse5.getId(), stationResponse6.getId(), 5, 10); - addLineStation(lineResponse3.getId(), stationResponse6.getId(), stationResponse7.getId(), 5, 10); + addLineStation(lineResponse3.getId(), stationResponse5.getId(), stationResponse6.getId(), 5, + 10); + addLineStation(lineResponse3.getId(), stationResponse6.getId(), stationResponse7.getId(), 5, + 10); // 신분당선 LineResponse lineResponse4 = createLine("신분당선"); addLineStation(lineResponse4.getId(), null, stationResponse1.getId(), 0, 0); - addLineStation(lineResponse4.getId(), stationResponse1.getId(), stationResponse7.getId(), 40, 3); + addLineStation(lineResponse4.getId(), stationResponse1.getId(), stationResponse7.getId(), + 40, 3); } public String registerMember(String email, String name, String password, String passwordCheck) { @@ -256,8 +264,8 @@ public String registerMember(String email, String name, String password, String view.put("password", password); view.put("passwordCheck", passwordCheck); - return - given().body(view). + try { + return given().body(view). contentType(MediaType.APPLICATION_JSON_VALUE). accept(MediaType.APPLICATION_JSON_VALUE). when(). @@ -266,6 +274,9 @@ public String registerMember(String email, String name, String password, String log().all(). statusCode(HttpStatus.CREATED.value()). extract().header("Location"); + } catch (Exception e) { + throw e; + } } public String createMember(String email, String name, String password) { @@ -275,28 +286,28 @@ public String createMember(String email, String name, String password) { params.put("password", password); return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/members"). - then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().header("Location"); + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/members"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().header("Location"); } public MemberResponse getMember(String email) { return - given(). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - get("/members?email=" + email). - then(). - log().all(). - statusCode(HttpStatus.OK.value()). - extract().as(MemberResponse.class); + given(). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + get("/members?email=" + email). + then(). + log().all(). + statusCode(HttpStatus.OK.value()). + extract().as(MemberResponse.class); } public void updateMember(MemberResponse memberResponse) { @@ -305,22 +316,22 @@ public void updateMember(MemberResponse memberResponse) { params.put("password", "NEW_" + TEST_USER_PASSWORD); given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - put("/members/" + memberResponse.getId()). - then(). - log().all(). - statusCode(HttpStatus.OK.value()); + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + put("/members/" + memberResponse.getId()). + then(). + log().all(). + statusCode(HttpStatus.OK.value()); } public void deleteMember(MemberResponse memberResponse) { given().when(). - delete("/members/" + memberResponse.getId()). - then(). - log().all(). - statusCode(HttpStatus.NO_CONTENT.value()); + delete("/members/" + memberResponse.getId()). + then(). + log().all(). + statusCode(HttpStatus.NO_CONTENT.value()); } } diff --git a/src/test/java/wooteco/subway/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/MemberAcceptanceTest.java index 94f5f1894..12e84a23e 100644 --- a/src/test/java/wooteco/subway/MemberAcceptanceTest.java +++ b/src/test/java/wooteco/subway/MemberAcceptanceTest.java @@ -13,8 +13,14 @@ public class MemberAcceptanceTest extends AcceptanceTest { @Test void memberAcceptanceTest() { - assertThat(registerMember("dd@naver.com", "디디", "1q2w3e4r", "1q2w3e4r")).isEqualTo("/members/1"); - assertThat(registerMember("fucct@naver.com", "둔덩", "qwerqwer", "qwerqwer")).isEqualTo("/members/2"); + assertThat(registerMember("dd@naver.com", "디디", "1q2w3e4r", "1q2w3e4r")).isEqualTo( + "/members/1"); + assertThat(registerMember("fucct@naver.com", "둔덩", "qwerqwer", "qwerqwer")).isEqualTo( + "/members/2"); + + assertThatThrownBy( + () -> registerMember("abc@naver.com", "abc", "abcd", "abc")).isInstanceOf( + RuntimeException.class).hasMessageMatching("패스워드가 일치하지 않습니다."); } } From d4d249b2a98b26f8bbafef616088748ab9fe3538 Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 11:20:08 +0900 Subject: [PATCH 05/30] =?UTF-8?q?feat:=20annotation=20=EA=B8=B0=EB=B0=98?= =?UTF-8?q?=20method=20argument=20validate=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 패스워드 일치 - 이메일 중복 --- .../wooteco/subway/config/WebMvcConfig.java | 12 ++-- .../subway/service/member/MemberService.java | 5 +- .../service/member/dto/MemberRawRequest.java | 12 +++- .../subway/web/member/DuplicateCheck.java | 20 ++++++ .../web/member/EmailMatchValidator.java | 24 +++++++ .../subway/web/member/MemberController.java | 31 +++++---- .../subway/web/member/PasswordMatch.java | 25 ++++++++ .../web/member/PasswordMatchValidator.java | 26 ++++++++ .../RegisterMemberMethodArgumentResolver.java | 63 ------------------- .../java/wooteco/subway/AcceptanceTest.java | 6 +- .../wooteco/subway/AuthAcceptanceTest.java | 6 +- .../wooteco/subway/MemberAcceptanceTest.java | 26 -------- .../member/MemberAcceptanceTest.java | 42 ++++++++++++- 13 files changed, 177 insertions(+), 121 deletions(-) create mode 100644 src/main/java/wooteco/subway/web/member/DuplicateCheck.java create mode 100644 src/main/java/wooteco/subway/web/member/EmailMatchValidator.java create mode 100644 src/main/java/wooteco/subway/web/member/PasswordMatch.java create mode 100644 src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java delete mode 100644 src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java delete mode 100644 src/test/java/wooteco/subway/MemberAcceptanceTest.java diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 684357ffd..e083e4b95 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -1,30 +1,27 @@ package wooteco.subway.config; +import java.util.List; + import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + import wooteco.subway.web.member.LoginMemberMethodArgumentResolver; -import wooteco.subway.web.member.RegisterMemberMethodArgumentResolver; import wooteco.subway.web.member.interceptor.BasicAuthInterceptor; import wooteco.subway.web.member.interceptor.BearerAuthInterceptor; -import java.util.List; - @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final BasicAuthInterceptor basicAuthInterceptor; private final BearerAuthInterceptor bearerAuthInterceptor; private final LoginMemberMethodArgumentResolver loginMemberArgumentResolver; - private final RegisterMemberMethodArgumentResolver registerMemberMethodArgumentResolver; public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor, BearerAuthInterceptor bearerAuthInterceptor, - LoginMemberMethodArgumentResolver loginMemberArgumentResolver, - RegisterMemberMethodArgumentResolver registerMemberMethodArgumentResolver) { + LoginMemberMethodArgumentResolver loginMemberArgumentResolver) { this.basicAuthInterceptor = basicAuthInterceptor; this.bearerAuthInterceptor = bearerAuthInterceptor; this.loginMemberArgumentResolver = loginMemberArgumentResolver; - this.registerMemberMethodArgumentResolver = registerMemberMethodArgumentResolver; } @Override @@ -36,6 +33,5 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List argumentResolvers) { argumentResolvers.add(loginMemberArgumentResolver); - argumentResolvers.add(registerMemberMethodArgumentResolver); } } diff --git a/src/main/java/wooteco/subway/service/member/MemberService.java b/src/main/java/wooteco/subway/service/member/MemberService.java index 8789af09a..57449cd04 100644 --- a/src/main/java/wooteco/subway/service/member/MemberService.java +++ b/src/main/java/wooteco/subway/service/member/MemberService.java @@ -3,6 +3,7 @@ import java.util.Optional; import org.springframework.stereotype.Service; + import wooteco.subway.domain.member.Member; import wooteco.subway.domain.member.MemberRepository; import wooteco.subway.infra.JwtTokenProvider; @@ -51,8 +52,8 @@ public boolean loginWithForm(String email, String password) { return member.checkPassword(password); } - public boolean isExistEmail(String email) { + public boolean isNotExistEmail(String email) { Optional byEmail = memberRepository.findByEmail(email); - return byEmail.isPresent(); + return !byEmail.isPresent(); } } diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java index db5118203..4428076d3 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java @@ -1,8 +1,16 @@ package wooteco.subway.service.member.dto; import wooteco.subway.domain.member.Member; +import wooteco.subway.web.member.DuplicateCheck; +import wooteco.subway.web.member.PasswordMatch; +@PasswordMatch( + field = "password", + fieldMatch = "passwordCheck" +) public class MemberRawRequest { + + @DuplicateCheck private String email; private String name; private String password; @@ -18,8 +26,8 @@ public MemberRawRequest(String email, String name, String password, String passw this.passwordCheck = passwordCheck; } - public MemberRequest toMemberRequest() { - return new MemberRequest(email, name, password); + public Member toMember() { + return new Member(email, name, password); } public String getEmail() { diff --git a/src/main/java/wooteco/subway/web/member/DuplicateCheck.java b/src/main/java/wooteco/subway/web/member/DuplicateCheck.java new file mode 100644 index 000000000..69d343d7d --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/DuplicateCheck.java @@ -0,0 +1,20 @@ +package wooteco.subway.web.member; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = EmailMatchValidator.class) +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface DuplicateCheck { + String message() default "이메일이 중복됩니다."; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/wooteco/subway/web/member/EmailMatchValidator.java b/src/main/java/wooteco/subway/web/member/EmailMatchValidator.java new file mode 100644 index 000000000..7aaffcbfd --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/EmailMatchValidator.java @@ -0,0 +1,24 @@ +package wooteco.subway.web.member; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.stereotype.Component; + +import wooteco.subway.service.member.MemberService; + +@Component +public class EmailMatchValidator implements ConstraintValidator { + private MemberService memberService; + + public EmailMatchValidator(MemberService memberService) { + this.memberService = memberService; + } + + public void initialize(DuplicateCheck constraint) { + } + + public boolean isValid(String email, ConstraintValidatorContext context) { + return memberService.isNotExistEmail(email); + } +} diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/MemberController.java index ff5c753fa..1c39b5b6a 100644 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/MemberController.java @@ -1,20 +1,27 @@ package wooteco.subway.web.member; -import org.springframework.http.HttpStatus; +import java.net.URI; + import org.springframework.http.ResponseEntity; -import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; -import wooteco.subway.service.member.dto.MemberRequest; +import wooteco.subway.service.member.dto.MemberRawRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.UpdateMemberRequest; import wooteco.subway.web.error.ErrorResponse; -import java.net.URI; - @RestController public class MemberController { private MemberService memberService; @@ -24,8 +31,8 @@ public MemberController(MemberService memberService) { } @PostMapping("/members") - public ResponseEntity createMember(@RegisterMember @Validated MemberRequest view) { - Member member = memberService.createMember(view.toMember()); + public ResponseEntity createMember(@Validated @RequestBody MemberRawRequest request) { + Member member = memberService.createMember(request.toMember()); return ResponseEntity .created(URI.create("/members/" + member.getId())) .build(); @@ -50,8 +57,8 @@ public ResponseEntity deleteMember(@PathVariable Long id) { return ResponseEntity.noContent().build(); } - @ExceptionHandler(value = NotMatchPasswordException.class) - public ResponseEntity exceptionHandle(NotMatchPasswordException e) { - throw new RuntimeException(); + @ExceptionHandler(value = MethodArgumentNotValidException.class) + public ResponseEntity exceptionHandle(MethodArgumentNotValidException e) { + return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); } } diff --git a/src/main/java/wooteco/subway/web/member/PasswordMatch.java b/src/main/java/wooteco/subway/web/member/PasswordMatch.java new file mode 100644 index 000000000..5d00e9f08 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/PasswordMatch.java @@ -0,0 +1,25 @@ +package wooteco.subway.web.member; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Constraint(validatedBy = PasswordMatchValidator.class) +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface PasswordMatch { + + String message() default "패스워드가 일치하지 않습니다."; + + String field(); + + String fieldMatch(); + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java b/src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java new file mode 100644 index 000000000..d95c2c522 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java @@ -0,0 +1,26 @@ +package wooteco.subway.web.member; + +import java.util.Objects; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +import org.springframework.beans.BeanWrapperImpl; + +public class PasswordMatchValidator implements ConstraintValidator { + private String field; + private String fieldMatch; + + + public void initialize(PasswordMatch constraint) { + this.field = constraint.field(); + this.fieldMatch = constraint.fieldMatch(); + } + + public boolean isValid(Object value, ConstraintValidatorContext context) { + Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field); + Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch); + + return Objects.equals(fieldValue, fieldMatchValue); + } +} diff --git a/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java b/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java deleted file mode 100644 index 9ae477e89..000000000 --- a/src/main/java/wooteco/subway/web/member/RegisterMemberMethodArgumentResolver.java +++ /dev/null @@ -1,63 +0,0 @@ -package wooteco.subway.web.member; - -import static org.springframework.web.context.request.RequestAttributes.*; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.Enumeration; -import java.util.Objects; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.tomcat.util.http.fileupload.IOUtils; -import org.apache.tomcat.util.json.JSONParser; -import org.springframework.core.MethodParameter; -import org.springframework.stereotype.Component; -import org.springframework.web.bind.support.WebDataBinderFactory; -import org.springframework.web.context.request.NativeWebRequest; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.method.support.ModelAndViewContainer; - -import com.fasterxml.jackson.databind.ObjectMapper; -import wooteco.subway.service.member.MemberService; -import wooteco.subway.service.member.dto.MemberRawRequest; -import wooteco.subway.service.member.dto.MemberRequest; - -@Component -public class RegisterMemberMethodArgumentResolver implements HandlerMethodArgumentResolver { - private final MemberService memberService; - - public RegisterMemberMethodArgumentResolver(MemberService memberService) { - this.memberService = memberService; - } - - @Override - public boolean supportsParameter(MethodParameter parameter) { - return parameter.hasParameterAnnotation(RegisterMember.class); - } - - @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, - NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { - - HttpServletRequest request = (HttpServletRequest)webRequest.getNativeRequest(); - BufferedReader reader = request.getReader(); - ObjectMapper objectMapper = new ObjectMapper(); - MemberRawRequest memberRawRequest = objectMapper.readValue(reader, MemberRawRequest.class); - - String email = memberRawRequest.getEmail(); - String password = memberRawRequest.getPassword(); - String passwordCheck = memberRawRequest.getPasswordCheck(); - - if (!Objects.equals(password, passwordCheck)) { - throw new NotMatchPasswordException("패스워드가 일치하지 않습니다."); - } - if (memberService.isExistEmail(email)) { - throw new DuplicateEmailException("이미 존재하는 이메일입니다."); - } - return memberRawRequest.toMemberRequest(); - } -} diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index ca32f4b0a..5d29cdb6e 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -21,7 +21,6 @@ import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.path.dto.PathResponse; import wooteco.subway.service.station.dto.StationResponse; -import wooteco.subway.web.member.NotMatchPasswordException; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @Sql("/truncate.sql") @@ -40,8 +39,10 @@ public class AcceptanceTest { public static final String LINE_NAME_SINBUNDANG = "신분당선"; public static final String TEST_USER_EMAIL = "brown@email.com"; + public static final String TEST_OTHER_USER_EMAIL = "pobi@email.com"; public static final String TEST_USER_NAME = "브라운"; public static final String TEST_USER_PASSWORD = "brown"; + public static final String TEST_OTHER_USER_PASSWORD = "pobi"; @LocalServerPort public int port; @@ -279,11 +280,12 @@ public String registerMember(String email, String name, String password, String } } - public String createMember(String email, String name, String password) { + public String createMember(String email, String name, String password, String passwordCheck) { Map params = new HashMap<>(); params.put("email", email); params.put("name", name); params.put("password", password); + params.put("passwordCheck", passwordCheck); return given(). diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index d26679516..1fb349fcd 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -18,7 +18,7 @@ public class AuthAcceptanceTest extends AcceptanceTest { @DisplayName("Basic Auth") @Test void myInfoWithBasicAuth() { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); MemberResponse memberResponse = myInfoWithBasicAuth(TEST_USER_EMAIL, TEST_USER_PASSWORD); @@ -30,7 +30,7 @@ void myInfoWithBasicAuth() { @DisplayName("Session") @Test void myInfoWithSession() { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); MemberResponse memberResponse = myInfoWithSession(TEST_USER_EMAIL, TEST_USER_PASSWORD); @@ -42,7 +42,7 @@ void myInfoWithSession() { @DisplayName("Bearer Auth") @Test void myInfoWithBearerAuth() { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); TokenResponse tokenResponse = login(TEST_USER_EMAIL, TEST_USER_PASSWORD); MemberResponse memberResponse = myInfoWithBearerAuth(tokenResponse); diff --git a/src/test/java/wooteco/subway/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/MemberAcceptanceTest.java deleted file mode 100644 index 12e84a23e..000000000 --- a/src/test/java/wooteco/subway/MemberAcceptanceTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package wooteco.subway; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; - -import wooteco.subway.web.member.NotMatchPasswordException; - -public class MemberAcceptanceTest extends AcceptanceTest { - - // 회원 정보를 관리한다. - // 회원 가입을 한다. - - @Test - void memberAcceptanceTest() { - assertThat(registerMember("dd@naver.com", "디디", "1q2w3e4r", "1q2w3e4r")).isEqualTo( - "/members/1"); - assertThat(registerMember("fucct@naver.com", "둔덩", "qwerqwer", "qwerqwer")).isEqualTo( - "/members/2"); - - assertThatThrownBy( - () -> registerMember("abc@naver.com", "abc", "abcd", "abc")).isInstanceOf( - RuntimeException.class).hasMessageMatching("패스워드가 일치하지 않습니다."); - - } -} diff --git a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java index 5c5d127fc..0a9b811e6 100644 --- a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java @@ -1,20 +1,35 @@ package wooteco.subway.acceptance.member; +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + import wooteco.subway.AcceptanceTest; import wooteco.subway.service.member.dto.MemberResponse; - -import static org.assertj.core.api.Assertions.assertThat; +import wooteco.subway.web.error.ErrorResponse; public class MemberAcceptanceTest extends AcceptanceTest { @DisplayName("회원 관리 기능") @Test void manageMember() { - String location = createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + String location = createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, + TEST_USER_PASSWORD); assertThat(location).isNotBlank(); + assertThat(createInvalidMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, + TEST_USER_PASSWORD)).isInstanceOf(ErrorResponse.class); + + assertThat(createInvalidMember(TEST_OTHER_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, + TEST_OTHER_USER_PASSWORD)).isInstanceOf(ErrorResponse.class); + + MemberResponse memberResponse = getMember(TEST_USER_EMAIL); assertThat(memberResponse.getId()).isNotNull(); assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); @@ -26,4 +41,25 @@ void manageMember() { deleteMember(memberResponse); } + + private ErrorResponse createInvalidMember(String email, String name, String password, + String passwordCheck) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("name", name); + params.put("password", password); + params.put("passwordCheck", passwordCheck); + + return + given(). + body(params). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + when(). + post("/members"). + then(). + log().all(). + statusCode(HttpStatus.BAD_REQUEST.value()). + extract().as(ErrorResponse.class); + } } From d274244d78064e07991c389a1a4654a365d29618 Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 14:18:39 +0900 Subject: [PATCH 06/30] =?UTF-8?q?feat:=20create=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=AC=B8=EC=84=9C=ED=99=94=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-guide.adoc | 10 ++- .../subway/web/member/MemberController.java | 12 ++- .../wooteco/subway/AuthAcceptanceTest.java | 24 ------ .../subway/doc/MemberDocumentation.java | 40 +++++++++- .../web/member/MemberControllerTest.java | 75 ++++++++++++++++++- 5 files changed, 133 insertions(+), 28 deletions(-) diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index de020315a..56d02c27c 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -19,4 +19,12 @@ endif::[] [[resources-members-create]] === 회원 가입 -operation::members/create[snippets='http-request,http-response,request-fields,request-body,response-body'] \ No newline at end of file +[[resources-members-duplicate-create]] +==== 회원 가입 이메일 중복 + +[[resources-members-not-match-password-create]] +==== 회원 가입 패스워드 불일치 + +operation::members/create[snippets='http-request,http-response,request-fields'] +operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] +operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] \ No newline at end of file diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/MemberController.java index 1c39b5b6a..895424fac 100644 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/MemberController.java @@ -1,8 +1,12 @@ package wooteco.subway.web.member; import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.DeleteMapping; @@ -59,6 +63,12 @@ public ResponseEntity deleteMember(@PathVariable Long id) { @ExceptionHandler(value = MethodArgumentNotValidException.class) public ResponseEntity exceptionHandle(MethodArgumentNotValidException e) { - return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage())); + BindingResult bindingResult = e.getBindingResult(); + List errorMessages = bindingResult.getAllErrors() + .stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.toList()); + + return ResponseEntity.badRequest().body(new ErrorResponse(errorMessages.get(0))); } } diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index 1fb349fcd..a9a13f37f 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -10,7 +10,6 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; -import io.restassured.authentication.FormAuthConfig; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; @@ -27,18 +26,6 @@ void myInfoWithBasicAuth() { assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); } - @DisplayName("Session") - @Test - void myInfoWithSession() { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); - - MemberResponse memberResponse = myInfoWithSession(TEST_USER_EMAIL, TEST_USER_PASSWORD); - - assertThat(memberResponse.getId()).isNotNull(); - assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); - assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); - } - @DisplayName("Bearer Auth") @Test void myInfoWithBearerAuth() { @@ -63,17 +50,6 @@ public MemberResponse myInfoWithBasicAuth(String email, String password) { .extract().as(MemberResponse.class); } - public MemberResponse myInfoWithSession(String email, String password) { - return given().auth() - .form(email, password, new FormAuthConfig("/login", "email", "password")) - .when() - .get("/me/session") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(MemberResponse.class); - } - public MemberResponse myInfoWithBearerAuth(TokenResponse tokenResponse) { return given().auth() .preemptive() diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index aa8e4a7ab..cb727a4ed 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -15,7 +15,9 @@ public static RestDocumentationResultHandler createMember() { .description("The user's email address"), fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), fieldWithPath("password").type(JsonFieldType.STRING) - .description("The user's password") + .description("The user's password"), + fieldWithPath("passwordCheck").type(JsonFieldType.STRING) + .description("The user's passwordCheck") ), responseHeaders( headerWithName("Location").description("The user's location who just created") @@ -23,6 +25,42 @@ public static RestDocumentationResultHandler createMember() { ); } + public static RestDocumentationResultHandler createDuplicateMember() { + return document("members/duplicate-create", + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING) + .description("The user's email address"), + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("password").type(JsonFieldType.STRING) + .description("The user's password"), + fieldWithPath("passwordCheck").type(JsonFieldType.STRING) + .description("The user's passwordCheck") + ), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING) + .description("The error message") + ) + ); + } + + public static RestDocumentationResultHandler createNotMatchPasswordMember() { + return document("members/not-match-password-create", + requestFields( + fieldWithPath("email").type(JsonFieldType.STRING) + .description("The user's email address"), + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("password").type(JsonFieldType.STRING) + .description("The user's password"), + fieldWithPath("passwordCheck").type(JsonFieldType.STRING) + .description("The user's passwordCheck") + ), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING) + .description("The error message") + ) + ); + } + public static RestDocumentationResultHandler updateMember() { return document("members/update", requestFields( diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index 051e741b4..b7ee01a0a 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -50,10 +50,12 @@ public void setUp(WebApplicationContext webApplicationContext, RestDocumentation public void createMember() throws Exception { Member member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); given(memberService.createMember(any())).willReturn(member); + given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + "\"name\":\"" + TEST_USER_NAME + "\"," + - "\"password\":\"" + TEST_USER_PASSWORD + "\"}"; + "\"password\":\"" + TEST_USER_PASSWORD + "\"," + + "\"passwordCheck\":\"" + TEST_USER_PASSWORD + "\"}"; this.mockMvc.perform(post("/members") .content(inputJson) @@ -70,4 +72,75 @@ public void createMember() throws Exception { .andDo(print()) .andDo(MemberDocumentation.createMember()); } + + @Test + void createDuplicateMember() throws Exception { + Member member1 = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + given(memberService.createMember(any())).willReturn(member1); + given(memberService.isNotExistEmail(any())).willReturn(true); + + String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + + "\"name\":\"" + TEST_USER_NAME + "\"," + + "\"password\":\"" + TEST_USER_PASSWORD + "\"," + + "\"passwordCheck\":\"" + TEST_USER_PASSWORD + "\"}"; + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(print()); + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(MemberDocumentation.createMember()); + + given(memberService.isNotExistEmail(any())).willReturn(false); + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andDo(MemberDocumentation.createDuplicateMember()); + } + + @Test + void createNotMatchPasswordMember() throws Exception { + Member member1 = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_OTHER_USER_PASSWORD); + given(memberService.createMember(any())).willReturn(member1); + given(memberService.isNotExistEmail(any())).willReturn(true); + + String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + + "\"name\":\"" + TEST_USER_NAME + "\"," + + "\"password\":\"" + TEST_USER_PASSWORD + "\"," + + "\"passwordCheck\":\"" + TEST_OTHER_USER_PASSWORD + "\"}"; + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()); + + this.mockMvc.perform(post("/members") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andDo(MemberDocumentation.createNotMatchPasswordMember()); + } } \ No newline at end of file From 41a0745857991c2bc73a7aeebce04073d88fa7e2 Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 14:50:10 +0900 Subject: [PATCH 07/30] =?UTF-8?q?feat:=20get=EC=97=90=20=EB=8C=80=ED=95=9C?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=99=80=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-guide.adoc | 6 +++- .../member/MemberAcceptanceTest.java | 7 ---- .../subway/doc/MemberDocumentation.java | 10 ++++++ .../service/member/MemberServiceTest.java | 34 ++++++++++++++----- .../web/member/MemberControllerTest.java | 34 ++++++++++++++++--- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 56d02c27c..7efb6bd7e 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -25,6 +25,10 @@ endif::[] [[resources-members-not-match-password-create]] ==== 회원 가입 패스워드 불일치 +[[resources-members-get]] +=== 정보 조회 + operation::members/create[snippets='http-request,http-response,request-fields'] operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] -operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] \ No newline at end of file +operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] +operation::members/get[snippets='http-request,http-response,request-fields'] \ No newline at end of file diff --git a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java index 0a9b811e6..293b1930c 100644 --- a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java @@ -23,13 +23,6 @@ void manageMember() { TEST_USER_PASSWORD); assertThat(location).isNotBlank(); - assertThat(createInvalidMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, - TEST_USER_PASSWORD)).isInstanceOf(ErrorResponse.class); - - assertThat(createInvalidMember(TEST_OTHER_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, - TEST_OTHER_USER_PASSWORD)).isInstanceOf(ErrorResponse.class); - - MemberResponse memberResponse = getMember(TEST_USER_EMAIL); assertThat(memberResponse.getId()).isNotNull(); assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index cb727a4ed..24ca337a5 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -72,4 +72,14 @@ public static RestDocumentationResultHandler updateMember() { ) ); } + + public static RestDocumentationResultHandler getMember() { + return document("members/get", + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("The user's id"), + fieldWithPath("email").type(JsonFieldType.STRING).description("The user's email"), + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name") + ) + ); + } } diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index 6166c637b..fdb38315b 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -1,30 +1,31 @@ package wooteco.subway.service.member; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import wooteco.subway.domain.member.Member; import wooteco.subway.domain.member.MemberRepository; import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.dto.LoginRequest; -import java.util.Optional; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) public class MemberServiceTest { + public static final String TEST_OTHER_USER_EMAIL = "pobi@email.com"; public static final String TEST_USER_EMAIL = "brown@email.com"; public static final String TEST_USER_NAME = "브라운"; public static final String TEST_USER_PASSWORD = "brown"; private MemberService memberService; - + private Member member; @Mock private MemberRepository memberRepository; @Mock @@ -33,6 +34,7 @@ public class MemberServiceTest { @BeforeEach void setUp() { this.memberService = new MemberService(memberRepository, jwtTokenProvider); + member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); } @Test @@ -44,6 +46,22 @@ void createMember() { verify(memberRepository).save(any()); } + @Test + void getMember() { + when(memberRepository.findByEmail(any())).thenReturn(Optional.of(member)); + + Member expect = memberService.findMemberByEmail(TEST_USER_EMAIL); + assertThat(expect).isEqualToComparingFieldByField(member); + } + + @Test + void getNotExistMember() { + when(memberRepository.findByEmail(TEST_USER_EMAIL)).thenReturn(Optional.of(member)); + + assertThatThrownBy(() -> memberService.findMemberByEmail(TEST_OTHER_USER_EMAIL)) + .isInstanceOf(RuntimeException.class); + } + @Test void createToken() { Member member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index b7ee01a0a..3ebda6fc2 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -8,6 +8,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static wooteco.subway.AcceptanceTest.*; +import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -38,17 +39,20 @@ public class MemberControllerTest { @Autowired protected MockMvc mockMvc; + private Member member; + @BeforeEach public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .addFilter(new ShallowEtagHeaderFilter()) .apply(documentationConfiguration(restDocumentation)) .build(); + + member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); } @Test public void createMember() throws Exception { - Member member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); given(memberService.createMember(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); @@ -75,8 +79,7 @@ public void createMember() throws Exception { @Test void createDuplicateMember() throws Exception { - Member member1 = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); - given(memberService.createMember(any())).willReturn(member1); + given(memberService.createMember(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + @@ -119,8 +122,7 @@ void createDuplicateMember() throws Exception { @Test void createNotMatchPasswordMember() throws Exception { - Member member1 = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_OTHER_USER_PASSWORD); - given(memberService.createMember(any())).willReturn(member1); + given(memberService.createMember(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + @@ -143,4 +145,26 @@ void createNotMatchPasswordMember() throws Exception { .andDo(print()) .andDo(MemberDocumentation.createNotMatchPasswordMember()); } + + @Test + void getMember() throws Exception { + given(memberService.findMemberByEmail(any())).willReturn(member); + + this.mockMvc.perform(get("/members") + .param("email", TEST_USER_EMAIL) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.email", Matchers.is(TEST_USER_EMAIL))) + .andExpect(jsonPath("$.name", Matchers.is(TEST_USER_NAME))) + .andDo(print()); + + this.mockMvc.perform(get("/members") + .param("email", TEST_USER_EMAIL) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.email", Matchers.is(TEST_USER_EMAIL))) + .andExpect(jsonPath("$.name", Matchers.is(TEST_USER_NAME))) + .andDo(print()) + .andDo(MemberDocumentation.getMember()); + } } \ No newline at end of file From e48e16cd1e0b0473c0a7f26827a3972b810d3e16 Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 16:01:22 +0900 Subject: [PATCH 08/30] =?UTF-8?q?feat:=20update=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=99=80=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-guide.adoc | 7 ++++- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../service/member/MemberServiceTest.java | 14 ++++++++- .../web/member/MemberControllerTest.java | 29 +++++++++++++++++++ 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 7efb6bd7e..73354a937 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -28,7 +28,12 @@ endif::[] [[resources-members-get]] === 정보 조회 +[[resources-members-update]] +=== 정보 수정 + + operation::members/create[snippets='http-request,http-response,request-fields'] operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] -operation::members/get[snippets='http-request,http-response,request-fields'] \ No newline at end of file +operation::members/get[snippets='http-request,http-response'] +operation::members/update[snippets='http-request,http-response,request-fields'] \ No newline at end of file diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index e083e4b95..99970b2cf 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -27,7 +27,7 @@ public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor, @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic"); - registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer"); + registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer").addPathPatterns("/members/*"); } @Override diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index fdb38315b..edf1e101d 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -16,6 +16,7 @@ import wooteco.subway.domain.member.MemberRepository; import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.dto.LoginRequest; +import wooteco.subway.service.member.dto.UpdateMemberRequest; @ExtendWith(MockitoExtension.class) public class MemberServiceTest { @@ -23,6 +24,7 @@ public class MemberServiceTest { public static final String TEST_USER_EMAIL = "brown@email.com"; public static final String TEST_USER_NAME = "브라운"; public static final String TEST_USER_PASSWORD = "brown"; + public static final long TEST_USER_ID = 1L; private MemberService memberService; private Member member; @@ -34,7 +36,7 @@ public class MemberServiceTest { @BeforeEach void setUp() { this.memberService = new MemberService(memberRepository, jwtTokenProvider); - member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + member = new Member(TEST_USER_ID, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); } @Test @@ -72,4 +74,14 @@ void createToken() { verify(jwtTokenProvider).createToken(anyString()); } + + @Test + void updateMember() { + when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member)); + when(memberRepository.save(any())).thenReturn(member); + memberService.updateMember(member.getId(), new UpdateMemberRequest( + "NEW_" + TEST_USER_NAME, "NEW_" + TEST_USER_PASSWORD)); + assertThat(member).extracting(Member::getName).isEqualTo("NEW_" + TEST_USER_NAME); + assertThat(member).extracting(Member::getPassword).isEqualTo("NEW_" + TEST_USER_PASSWORD); + } } diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index 3ebda6fc2..fe5b208d3 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -26,6 +26,7 @@ import wooteco.subway.doc.MemberDocumentation; import wooteco.subway.domain.member.Member; +import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.MemberService; @ExtendWith(RestDocumentationExtension.class) @@ -36,6 +37,9 @@ public class MemberControllerTest { @MockBean protected MemberService memberService; + @MockBean + protected JwtTokenProvider jwtTokenProvider; + @Autowired protected MockMvc mockMvc; @@ -167,4 +171,29 @@ void getMember() throws Exception { .andDo(print()) .andDo(MemberDocumentation.getMember()); } + + @Test + void updateMember() throws Exception { + + given(jwtTokenProvider.validateToken(any())).willReturn(true); + String inputJson = "{\"name\":\"" + TEST_USER_NAME + "\"," + + "\"password\":\"" + TEST_USER_PASSWORD + "\"}"; + + this.mockMvc.perform(put("/members/" + 1L) + .header("Authorization", "tmp") + .contentType(MediaType.APPLICATION_JSON) + .content(inputJson) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()); + + this.mockMvc.perform(put("/members/" + 1L) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "tmp") + .content(inputJson) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(print()) + .andDo(MemberDocumentation.updateMember()); + } } \ No newline at end of file From 0d08e62cec8ce2a4422ccded8806571d77ff707f Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 16:37:18 +0900 Subject: [PATCH 09/30] =?UTF-8?q?feat:=20delete=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4,=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=99=80=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/api-guide.adoc | 7 +++- .../member/GlobalMemberExceptionHandler.java | 16 +++++++++ .../web/member/NotFoundMemberException.java | 10 ++++++ .../java/wooteco/subway/AcceptanceTest.java | 31 ++++------------- .../member/MemberAcceptanceTest.java | 34 ++++++++----------- .../subway/doc/MemberDocumentation.java | 4 +++ .../service/member/MemberServiceTest.java | 8 ++++- .../web/member/MemberControllerTest.java | 12 +++++++ 8 files changed, 77 insertions(+), 45 deletions(-) create mode 100644 src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java create mode 100644 src/main/java/wooteco/subway/web/member/NotFoundMemberException.java diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 73354a937..ec00a4353 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -31,9 +31,14 @@ endif::[] [[resources-members-update]] === 정보 수정 +[[resources-members-delete]] +=== 정보 삭제 + + operation::members/create[snippets='http-request,http-response,request-fields'] operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] operation::members/get[snippets='http-request,http-response'] -operation::members/update[snippets='http-request,http-response,request-fields'] \ No newline at end of file +operation::members/update[snippets='http-request,http-response,request-fields'] +operation::members/delete[snippets='http-request,http-response] \ No newline at end of file diff --git a/src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java b/src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java new file mode 100644 index 000000000..0453824ea --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java @@ -0,0 +1,16 @@ +package wooteco.subway.web.member; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import wooteco.subway.web.error.ErrorResponse; + +@RestControllerAdvice +public class GlobalMemberExceptionHandler { + + @ExceptionHandler(value = RuntimeException.class) + public ResponseEntity temporalExceptionHandler(RuntimeException e) { + throw new NotFoundMemberException("멤버를 찾을 수 없습니다."); + } +} diff --git a/src/main/java/wooteco/subway/web/member/NotFoundMemberException.java b/src/main/java/wooteco/subway/web/member/NotFoundMemberException.java new file mode 100644 index 000000000..2dfb2de98 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/NotFoundMemberException.java @@ -0,0 +1,10 @@ +package wooteco.subway.web.member; + +public class NotFoundMemberException extends RuntimeException { + public NotFoundMemberException() { + } + + public NotFoundMemberException(String message) { + super(message); + } +} diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index 5d29cdb6e..d37ef7acc 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -19,6 +19,7 @@ import wooteco.subway.service.line.dto.LineResponse; import wooteco.subway.service.line.dto.WholeSubwayResponse; import wooteco.subway.service.member.dto.MemberResponse; +import wooteco.subway.service.member.dto.TokenResponse; import wooteco.subway.service.path.dto.PathResponse; import wooteco.subway.service.station.dto.StationResponse; @@ -258,28 +259,6 @@ public void initStation() { 40, 3); } - public String registerMember(String email, String name, String password, String passwordCheck) { - Map view = new HashMap<>(); - view.put("email", email); - view.put("name", name); - view.put("password", password); - view.put("passwordCheck", passwordCheck); - - try { - return given().body(view). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/members"). - then(). - log().all(). - statusCode(HttpStatus.CREATED.value()). - extract().header("Location"); - } catch (Exception e) { - throw e; - } - } - public String createMember(String email, String name, String password, String passwordCheck) { Map params = new HashMap<>(); params.put("email", email); @@ -312,12 +291,14 @@ public MemberResponse getMember(String email) { extract().as(MemberResponse.class); } - public void updateMember(MemberResponse memberResponse) { + public void updateMember(MemberResponse memberResponse, TokenResponse tokenResponse) { Map params = new HashMap<>(); params.put("name", "NEW_" + TEST_USER_NAME); params.put("password", "NEW_" + TEST_USER_PASSWORD); given(). + header("Authorization", + tokenResponse.getTokenType() + " " + tokenResponse.getAccessToken()). body(params). contentType(MediaType.APPLICATION_JSON_VALUE). accept(MediaType.APPLICATION_JSON_VALUE). @@ -328,8 +309,10 @@ public void updateMember(MemberResponse memberResponse) { statusCode(HttpStatus.OK.value()); } - public void deleteMember(MemberResponse memberResponse) { + public void deleteMember(MemberResponse memberResponse, TokenResponse tokenResponse) { given().when(). + header("Authorization", + tokenResponse.getTokenType() + " " + tokenResponse.getAccessToken()). delete("/members/" + memberResponse.getId()). then(). log().all(). diff --git a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java index 293b1930c..b184a992b 100644 --- a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java @@ -12,7 +12,7 @@ import wooteco.subway.AcceptanceTest; import wooteco.subway.service.member.dto.MemberResponse; -import wooteco.subway.web.error.ErrorResponse; +import wooteco.subway.service.member.dto.TokenResponse; public class MemberAcceptanceTest extends AcceptanceTest { @@ -28,31 +28,27 @@ void manageMember() { assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); - updateMember(memberResponse); + TokenResponse tokenResponse = getToken(TEST_USER_EMAIL, TEST_USER_PASSWORD); + updateMember(memberResponse, tokenResponse); MemberResponse updatedMember = getMember(TEST_USER_EMAIL); assertThat(updatedMember.getName()).isEqualTo("NEW_" + TEST_USER_NAME); - deleteMember(memberResponse); + deleteMember(memberResponse, tokenResponse); } - private ErrorResponse createInvalidMember(String email, String name, String password, - String passwordCheck) { + private TokenResponse getToken(String email, String password) { Map params = new HashMap<>(); params.put("email", email); - params.put("name", name); params.put("password", password); - params.put("passwordCheck", passwordCheck); - - return - given(). - body(params). - contentType(MediaType.APPLICATION_JSON_VALUE). - accept(MediaType.APPLICATION_JSON_VALUE). - when(). - post("/members"). - then(). - log().all(). - statusCode(HttpStatus.BAD_REQUEST.value()). - extract().as(ErrorResponse.class); + + return given() + .body(params) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when() + .post("/oauth/token") + .then() + .statusCode(HttpStatus.OK.value()) + .extract().as(TokenResponse.class); } } diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index 24ca337a5..b9ba4d683 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -82,4 +82,8 @@ public static RestDocumentationResultHandler getMember() { ) ); } + + public static RestDocumentationResultHandler deleteMember() { + return document("members/delete"); + } } diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index edf1e101d..45ee3903b 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -42,7 +42,6 @@ void setUp() { @Test void createMember() { Member member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); - memberService.createMember(member); verify(memberRepository).save(any()); @@ -84,4 +83,11 @@ void updateMember() { assertThat(member).extracting(Member::getName).isEqualTo("NEW_" + TEST_USER_NAME); assertThat(member).extracting(Member::getPassword).isEqualTo("NEW_" + TEST_USER_PASSWORD); } + + @Test + // TODO: 2020/05/21 어떻게 해야할지? + void deleteMember() { + // memberService.deleteMember(member.getId()); + // assertThatThrownBy(() -> memberService.findMemberByEmail(member.getEmail())).isInstanceOf(RuntimeException.class); + } } diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index fe5b208d3..668c0316c 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -196,4 +196,16 @@ void updateMember() throws Exception { .andDo(print()) .andDo(MemberDocumentation.updateMember()); } + + @Test + void deleteMember() throws Exception { + this.mockMvc.perform(delete("/members/" + 1L)) + .andExpect(status().isNoContent()) + .andDo(print()); + + this.mockMvc.perform(delete("/members/" + 1L)) + .andExpect(status().isNoContent()) + .andDo(print()) + .andDo(MemberDocumentation.deleteMember()); + } } \ No newline at end of file From 4d6dff82b37f4fe297e52f96d4fdb5a0aa5cce29 Mon Sep 17 00:00:00 2001 From: dd Date: Thu, 21 May 2020 17:34:29 +0900 Subject: [PATCH 10/30] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wooteco/subway/config/WebMvcConfig.java | 10 ++-- .../service/member/dto/MemberRawRequest.java | 11 ++++- .../interceptor/BasicAuthInterceptor.java | 40 ---------------- .../resources/static/service/api/index.js | 9 +++- .../resources/static/service/js/views/Join.js | 46 +++++++++++++++++++ .../resources/templates/service/join.html | 2 + 6 files changed, 70 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java create mode 100644 src/main/resources/static/service/js/views/Join.js diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 99970b2cf..91df70a9e 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -7,27 +7,25 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import wooteco.subway.web.member.LoginMemberMethodArgumentResolver; -import wooteco.subway.web.member.interceptor.BasicAuthInterceptor; import wooteco.subway.web.member.interceptor.BearerAuthInterceptor; @Configuration public class WebMvcConfig implements WebMvcConfigurer { - private final BasicAuthInterceptor basicAuthInterceptor; private final BearerAuthInterceptor bearerAuthInterceptor; private final LoginMemberMethodArgumentResolver loginMemberArgumentResolver; - public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor, + public WebMvcConfig( BearerAuthInterceptor bearerAuthInterceptor, LoginMemberMethodArgumentResolver loginMemberArgumentResolver) { - this.basicAuthInterceptor = basicAuthInterceptor; this.bearerAuthInterceptor = bearerAuthInterceptor; this.loginMemberArgumentResolver = loginMemberArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic"); - registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer").addPathPatterns("/members/*"); + registry.addInterceptor(bearerAuthInterceptor) + .addPathPatterns("/me/bearer") + .addPathPatterns("/members/*"); } @Override diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java index 4428076d3..d6fd1554b 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java @@ -1,5 +1,8 @@ package wooteco.subway.service.member.dto; +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; + import wooteco.subway.domain.member.Member; import wooteco.subway.web.member.DuplicateCheck; import wooteco.subway.web.member.PasswordMatch; @@ -10,10 +13,16 @@ ) public class MemberRawRequest { - @DuplicateCheck + @Email @DuplicateCheck @NotBlank(message = "이메일은 필수 입력 사항입니다.") private String email; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") private String name; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") private String password; + + @NotBlank(message = "이름은 필수 입력 사항입니다.") private String passwordCheck; public MemberRawRequest() { diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java deleted file mode 100644 index e725dab64..000000000 --- a/src/main/java/wooteco/subway/web/member/interceptor/BasicAuthInterceptor.java +++ /dev/null @@ -1,40 +0,0 @@ -package wooteco.subway.web.member.interceptor; - -import java.util.Base64; - -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; -import wooteco.subway.domain.member.Member; -import wooteco.subway.service.member.MemberService; -import wooteco.subway.web.member.AuthorizationExtractor; -import wooteco.subway.web.member.InvalidAuthenticationException; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@Component -public class BasicAuthInterceptor implements HandlerInterceptor { - private AuthorizationExtractor authExtractor; - private MemberService memberService; - - public BasicAuthInterceptor(AuthorizationExtractor authExtractor, MemberService memberService) { - this.authExtractor = authExtractor; - this.memberService = memberService; - } - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String basic = authExtractor.extract(request, "Basic"); - String[] decode = new String(Base64.getDecoder().decode(basic)).split(":"); - String email = decode[0]; - String password = decode[1]; - - Member member = memberService.findMemberByEmail(email); - if (!member.checkPassword(password)) { - throw new InvalidAuthenticationException("올바르지 않은 이메일과 비밀번호 입력"); - } - - request.setAttribute("loginMemberEmail", email); - return true; - } -} diff --git a/src/main/resources/static/service/api/index.js b/src/main/resources/static/service/api/index.js index 02a7f5731..0fd1492db 100644 --- a/src/main/resources/static/service/api/index.js +++ b/src/main/resources/static/service/api/index.js @@ -41,9 +41,16 @@ const api = (() => { } } + const member = { + join(joinForm) { + return request(`/members`, METHOD.POST(joinForm)); + } + } + return { line, - path + path, + member, } })() diff --git a/src/main/resources/static/service/js/views/Join.js b/src/main/resources/static/service/js/views/Join.js new file mode 100644 index 000000000..4869030a5 --- /dev/null +++ b/src/main/resources/static/service/js/views/Join.js @@ -0,0 +1,46 @@ +import { EVENT_TYPE } from '../../utils/constants.js' +import api from '../../api/index.js' + +function Join() { + const $email = document.querySelector('#email'); + const $name = document.querySelector('#name'); + const $password = document.querySelector('#password'); + const $passwordCheck = document.querySelector('#password-check'); + const $joinButton = document.querySelector('#join-button'); + + const onClickJoinButton = event => { + event.preventDefault(); + + const joinForm = { + email: $email.value, + name: $name.value, + password: $password.value, + passwordCheck: $passwordCheck.value, + }; + + api.member.join(joinForm) + .then(response => { + if (!response.ok) { + throw response; + } + window.location = "/login"; + }).catch(response => response.json()) + .then(errorResponse => { + if (errorResponse) { + console.log(errorResponse); + alert(errorResponse.message); + } + }); + } + + const initEventListener = () => { + $joinButton.addEventListener(EVENT_TYPE.CLICK, onClickJoinButton) + } + + this.init = () => { + initEventListener(); + } +} + +const join = new Join(); +join.init(); diff --git a/src/main/resources/templates/service/join.html b/src/main/resources/templates/service/join.html index 4d8f59512..2a481f1f8 100644 --- a/src/main/resources/templates/service/join.html +++ b/src/main/resources/templates/service/join.html @@ -59,6 +59,7 @@ + diff --git a/src/main/resources/templates/service/mypage.html b/src/main/resources/templates/service/mypage.html index fadc754cb..0e1eaa13b 100644 --- a/src/main/resources/templates/service/mypage.html +++ b/src/main/resources/templates/service/mypage.html @@ -21,7 +21,7 @@
From 9f4c16368711cf1625f4f27bc96216a1959504ec Mon Sep 17 00:00:00 2001 From: dd Date: Fri, 22 May 2020 22:04:20 +0900 Subject: [PATCH 13/30] =?UTF-8?q?feat:=20=ED=83=88=ED=87=B4=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/static/service/api/index.js | 3 ++ .../static/service/js/views/Login.js | 4 +-- .../static/service/js/views/MyPageEdit.js | 32 +++++++++++++++++-- .../templates/service/mypage-edit.html | 2 +- 4 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/main/resources/static/service/api/index.js b/src/main/resources/static/service/api/index.js index e483dace0..b64973c77 100644 --- a/src/main/resources/static/service/api/index.js +++ b/src/main/resources/static/service/api/index.js @@ -59,6 +59,9 @@ const api = (() => { }, update(id, updateForm){ return request(`/members/`+id, METHOD.PUT(updateForm)); + }, + delete(id){ + return request(`/members/`+id, METHOD.DELETE()); } } diff --git a/src/main/resources/static/service/js/views/Login.js b/src/main/resources/static/service/js/views/Login.js index 1d4e9ff10..c667bf7af 100644 --- a/src/main/resources/static/service/js/views/Login.js +++ b/src/main/resources/static/service/js/views/Login.js @@ -47,9 +47,7 @@ function Login() { }; - const deleteCookie = function () { - document.cookie = 'token=; expires=Thu, 01 Jan 1999 00:00:10 GMT;'; - } + this.init = () => { $loginButton.addEventListener(EVENT_TYPE.CLICK, onLogin); diff --git a/src/main/resources/static/service/js/views/MyPageEdit.js b/src/main/resources/static/service/js/views/MyPageEdit.js index 689471ed3..162f877e8 100644 --- a/src/main/resources/static/service/js/views/MyPageEdit.js +++ b/src/main/resources/static/service/js/views/MyPageEdit.js @@ -9,7 +9,7 @@ function MyPageEdit() { const $password = document.querySelector('#password'); const $passwordCheck = document.querySelector('#password-check'); const $updateButton = document.querySelector('#update-button'); - const $deleteButton = document.querySelector('#delete-button'); + const $signOutButton = document.querySelector('#sign-out-button'); const onClickUpdateButton = event => { event.preventDefault(); @@ -36,8 +36,32 @@ function MyPageEdit() { }); } + const onClickSignOutButton = event => { + event.preventDefault(); + + if (!confirm("정말로 회원 탈퇴를 하실겁니까? 😳")) { + window.location = "/my-page" + return; + } + api.member.delete($id.dataset.id, $oldPassword.value) + .then(response => { + if(!response.ok){ + throw response; + } + alert("실망이야..😢"); + deleteCookie(); + window.location = "/login" + }).catch(response => response.json()) + .then(error => { + if (error) { + alert(error.message); + } + }); + } + const initEventListener = () => { - $updateButton.addEventListener(EVENT_TYPE.CLICK, onClickUpdateButton) + $updateButton.addEventListener(EVENT_TYPE.CLICK, onClickUpdateButton); + $signOutButton.addEventListener(EVENT_TYPE.CLICK, onClickSignOutButton); } const getCookie = function () { @@ -45,6 +69,10 @@ function MyPageEdit() { return value ? value[2] : null; }; + const deleteCookie = function () { + document.cookie = 'token=; expires=Thu, 01 Jan 1999 00:00:10 GMT;'; + } + this.init = () => { if(getCookie()) { api.member.myPage().then(response => { diff --git a/src/main/resources/templates/service/mypage-edit.html b/src/main/resources/templates/service/mypage-edit.html index aa43372f4..f3f057a4b 100644 --- a/src/main/resources/templates/service/mypage-edit.html +++ b/src/main/resources/templates/service/mypage-edit.html @@ -80,7 +80,7 @@ > 저장 - From 6cf876bb33557b957e6dae0552f100557517983f Mon Sep 17 00:00:00 2001 From: dd Date: Mon, 25 May 2020 11:23:32 +0900 Subject: [PATCH 14/30] =?UTF-8?q?refactor,=20feat:=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=95=EC=83=81=ED=99=94=20=EB=B0=8F,=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전체 테스트 정상화 - 문서화 추가 양식 구현 (필수값 여부, 입력값 foramt) - 예외 사항 추가 --- build.gradle | 54 +++++++-------- src/docs/asciidoc/api-guide.adoc | 55 +++++++++++----- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../subway/service/member/MemberService.java | 20 +++--- .../service/member/dto/MemberRawRequest.java | 57 ---------------- .../service/member/dto/MemberRequest.java | 29 ++++++-- .../member/dto/UpdateMemberRequest.java | 35 ++-------- .../subway/web/member/RegisterMember.java | 11 ---- .../GlobalMemberExceptionHandler.java | 4 +- .../LoginMemberController.java | 3 +- .../{ => controller}/MemberController.java | 10 +-- .../DuplicateEmailException.java | 2 +- .../exception}/ErrorResponse.java | 2 +- .../InvalidAuthenticationException.java | 2 +- .../NotFoundMemberException.java | 4 +- .../NotMatchPasswordException.java | 2 +- .../member/{ => resolver}/LoginMember.java | 2 +- .../LoginMemberMethodArgumentResolver.java | 3 +- .../{ => validator}/DuplicateCheck.java | 2 +- .../{ => validator}/EmailMatchValidator.java | 2 +- .../member/{ => validator}/PasswordMatch.java | 2 +- .../PasswordMatchValidator.java | 2 +- .../static/service/js/views/Login.js | 10 +-- .../static/service/js/views/MyPage.js | 7 +- .../static/service/js/views/MyPageEdit.js | 19 ++---- .../static/service/utils/loginUtils.js | 15 +++++ .../java/wooteco/subway/AcceptanceTest.java | 3 +- .../wooteco/subway/AuthAcceptanceTest.java | 32 +-------- .../member/MemberAcceptanceTest.java | 57 ++++++++++------ .../wooteco/subway/doc/ApiDocumentUtils.java | 23 +++++++ .../subway/doc/DocumentFormatGenerator.java | 12 ++++ .../subway/doc/MemberDocumentation.java | 66 ++++++++++++++----- .../service/member/MemberServiceTest.java | 2 +- .../web/member/MemberControllerTest.java | 66 +++++-------------- .../restdocs/templates/request-fields.snippet | 15 +++++ 35 files changed, 311 insertions(+), 321 deletions(-) delete mode 100644 src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java delete mode 100644 src/main/java/wooteco/subway/web/member/RegisterMember.java rename src/main/java/wooteco/subway/web/member/{ => controller}/GlobalMemberExceptionHandler.java (82%) rename src/main/java/wooteco/subway/web/member/{ => controller}/LoginMemberController.java (92%) rename src/main/java/wooteco/subway/web/member/{ => controller}/MemberController.java (92%) rename src/main/java/wooteco/subway/web/member/{ => exception}/DuplicateEmailException.java (76%) rename src/main/java/wooteco/subway/web/{error => member/exception}/ErrorResponse.java (84%) rename src/main/java/wooteco/subway/web/member/{ => exception}/InvalidAuthenticationException.java (87%) rename src/main/java/wooteco/subway/web/member/{ => exception}/NotFoundMemberException.java (61%) rename src/main/java/wooteco/subway/web/member/{ => exception}/NotMatchPasswordException.java (77%) rename src/main/java/wooteco/subway/web/member/{ => resolver}/LoginMember.java (85%) rename src/main/java/wooteco/subway/web/member/{ => resolver}/LoginMemberMethodArgumentResolver.java (93%) rename src/main/java/wooteco/subway/web/member/{ => validator}/DuplicateCheck.java (92%) rename src/main/java/wooteco/subway/web/member/{ => validator}/EmailMatchValidator.java (93%) rename src/main/java/wooteco/subway/web/member/{ => validator}/PasswordMatch.java (93%) rename src/main/java/wooteco/subway/web/member/{ => validator}/PasswordMatchValidator.java (94%) create mode 100644 src/main/resources/static/service/utils/loginUtils.js create mode 100644 src/test/java/wooteco/subway/doc/ApiDocumentUtils.java create mode 100644 src/test/java/wooteco/subway/doc/DocumentFormatGenerator.java create mode 100644 src/test/resources/org/springframework/restdocs/templates/request-fields.snippet diff --git a/build.gradle b/build.gradle index 87db66be9..cf378221a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ plugins { - id 'org.springframework.boot' version '2.2.5.RELEASE' - id 'io.spring.dependency-management' version '1.0.9.RELEASE' - id "org.asciidoctor.convert" version "1.5.9.2" - id 'java' + id 'org.springframework.boot' version '2.2.5.RELEASE' + id 'io.spring.dependency-management' version '1.0.9.RELEASE' + id "org.asciidoctor.convert" version "1.5.9.2" + id 'java' } group = 'com.example' @@ -10,42 +10,42 @@ version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' - implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' - implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' - implementation 'org.jgrapht:jgrapht-core:1.0.1' - implementation 'io.jsonwebtoken:jjwt:0.9.1' - testImplementation 'io.rest-assured:rest-assured:3.3.0' - testImplementation('org.springframework.boot:spring-boot-starter-test') { - exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' - } - runtimeOnly 'com.h2database:h2' - asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' - testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' + implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1' + implementation 'pl.allegro.tech.boot:handlebars-spring-boot-starter:0.3.0' + implementation 'org.jgrapht:jgrapht-core:1.0.1' + implementation 'io.jsonwebtoken:jjwt:0.9.1' + testImplementation 'io.rest-assured:rest-assured:3.3.0' + testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' + } + runtimeOnly 'com.h2database:h2' + asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE' } ext { - snippetsDir = file('build/generated-snippets') + snippetsDir = file('build/generated-snippets') } test { - useJUnitPlatform() - outputs.dir snippetsDir + useJUnitPlatform() + outputs.dir snippetsDir } asciidoctor { - inputs.dir snippetsDir - dependsOn test + inputs.dir snippetsDir + dependsOn test } bootJar { - dependsOn asciidoctor - from ("${asciidoctor.outputDir}/html5") { - into 'static/docs' - } + dependsOn asciidoctor + from("${asciidoctor.outputDir}/html5") { + into 'static/docs' + } } diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index ec00a4353..2be8a4836 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -5,7 +5,7 @@ endif::[] :icons: font :source-highlighter: highlightjs :toc: left -:toclevels: 2 +:toclevels: 4 :sectlinks: :operation-http-request-title: Example Request :operation-http-response-title: Example Response @@ -19,26 +19,51 @@ endif::[] [[resources-members-create]] === 회원 가입 -[[resources-members-duplicate-create]] -==== 회원 가입 이메일 중복 +[[resources-members-create-success]] +==== 성공 -[[resources-members-not-match-password-create]] -==== 회원 가입 패스워드 불일치 +===== 올바른 가입 +operation::members/create[snippets='http-request,http-response,request-fields'] + + +[[resources-members-create-fail]] +==== 실패 + +[[resources-members-create-fail-duplicated-email]] +===== 이메일 중복 +operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] + + +[[resources-members-create-fail-not-match-password]] +===== 패스워드 불일치 +operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] + +[[resources-members-create]] +=== 회원 정보 조회 + +==== 성공 -[[resources-members-get]] -=== 정보 조회 +===== 로그인 후 조회 +operation::members/get[snippets='http-request,http-response,response-fields'] + +==== 실패 + +===== [[resources-members-update]] -=== 정보 수정 +=== 회원 정보 수정 + +==== 성공 + +===== 로그인 후 올바른 패스워드로 수정 +operation::members/update[snippets='http-request,http-response,request-fields'] [[resources-members-delete]] -=== 정보 삭제 +=== 회원 탈퇴 +==== 성공 + +===== 로그인 후 탈퇴 +operation::members/delete[snippets='http-request,http-response'] -operation::members/create[snippets='http-request,http-response,request-fields'] -operation::members/duplicate-create[snippets='http-request,http-response,request-fields'] -operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] -operation::members/get[snippets='http-request,http-response'] -operation::members/update[snippets='http-request,http-response,request-fields'] -operation::members/delete[snippets='http-request,http-response] \ No newline at end of file diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 5eb36cae5..2d09b0880 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -6,8 +6,8 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import wooteco.subway.web.member.LoginMemberMethodArgumentResolver; import wooteco.subway.web.member.interceptor.BearerAuthInterceptor; +import wooteco.subway.web.member.resolver.LoginMemberMethodArgumentResolver; @Configuration public class WebMvcConfig implements WebMvcConfigurer { diff --git a/src/main/java/wooteco/subway/service/member/MemberService.java b/src/main/java/wooteco/subway/service/member/MemberService.java index d46a1c3a7..0ca1c44c1 100644 --- a/src/main/java/wooteco/subway/service/member/MemberService.java +++ b/src/main/java/wooteco/subway/service/member/MemberService.java @@ -9,13 +9,13 @@ import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.dto.LoginRequest; import wooteco.subway.service.member.dto.UpdateMemberRequest; -import wooteco.subway.web.member.NotFoundMemberException; -import wooteco.subway.web.member.NotMatchPasswordException; +import wooteco.subway.web.member.exception.NotFoundMemberException; +import wooteco.subway.web.member.exception.NotMatchPasswordException; @Service public class MemberService { - private MemberRepository memberRepository; - private JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { this.memberRepository = memberRepository; @@ -27,11 +27,12 @@ public Member createMember(Member member) { } public void updateMember(Long id, UpdateMemberRequest param) { - Member member = memberRepository.findById(id).orElseThrow(RuntimeException::new); + Member member = memberRepository.findById(id) + .orElseThrow(() -> new NotFoundMemberException(param.getName())); if (!member.checkPassword(param.getOldPassword())) { - throw new NotMatchPasswordException("패스워드가 일치하지 않습니다"); + throw new NotMatchPasswordException("잘못된 패스워드 입니다."); } - member.update(param.getName(), param.getPassword()); + member.update(param.getName(), param.getNewPassword()); memberRepository.save(member); } @@ -43,14 +44,15 @@ public String createToken(LoginRequest param) { Member member = memberRepository.findByEmail(param.getEmail()) .orElseThrow(() -> new NotFoundMemberException(param.getEmail())); if (!member.checkPassword(param.getPassword())) { - throw new RuntimeException("잘못된 패스워드 입니다."); + throw new NotMatchPasswordException("잘못된 패스워드 입니다."); } return jwtTokenProvider.createToken(param.getEmail()); } public Member findMemberByEmail(String email) { - return memberRepository.findByEmail(email).orElseThrow(RuntimeException::new); + return memberRepository.findByEmail(email) + .orElseThrow(() -> new NotFoundMemberException(email)); } public boolean isNotExistEmail(String email) { diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java deleted file mode 100644 index d6fd1554b..000000000 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRawRequest.java +++ /dev/null @@ -1,57 +0,0 @@ -package wooteco.subway.service.member.dto; - -import javax.validation.constraints.Email; -import javax.validation.constraints.NotBlank; - -import wooteco.subway.domain.member.Member; -import wooteco.subway.web.member.DuplicateCheck; -import wooteco.subway.web.member.PasswordMatch; - -@PasswordMatch( - field = "password", - fieldMatch = "passwordCheck" -) -public class MemberRawRequest { - - @Email @DuplicateCheck @NotBlank(message = "이메일은 필수 입력 사항입니다.") - private String email; - - @NotBlank(message = "이름은 필수 입력 사항입니다.") - private String name; - - @NotBlank(message = "이름은 필수 입력 사항입니다.") - private String password; - - @NotBlank(message = "이름은 필수 입력 사항입니다.") - private String passwordCheck; - - public MemberRawRequest() { - } - - public MemberRawRequest(String email, String name, String password, String passwordCheck) { - this.email = email; - this.name = name; - this.password = password; - this.passwordCheck = passwordCheck; - } - - public Member toMember() { - return new Member(email, name, password); - } - - public String getEmail() { - return email; - } - - public String getName() { - return name; - } - - public String getPassword() { - return password; - } - - public String getPasswordCheck() { - return passwordCheck; - } -} diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java index fdf798e9f..6cb678086 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberRequest.java @@ -4,24 +4,39 @@ import javax.validation.constraints.NotBlank; import wooteco.subway.domain.member.Member; +import wooteco.subway.web.member.validator.DuplicateCheck; +import wooteco.subway.web.member.validator.PasswordMatch; +@PasswordMatch( + field = "password", + fieldMatch = "passwordCheck" +) public class MemberRequest { - @Email - @NotBlank + @Email @DuplicateCheck @NotBlank(message = "이메일은 필수 입력 사항입니다.") private String email; - @NotBlank + + @NotBlank(message = "이름은 필수 입력 사항입니다.") private String name; - @NotBlank() + + @NotBlank(message = "이름은 필수 입력 사항입니다.") private String password; + @NotBlank(message = "이름은 필수 입력 사항입니다.") + private String passwordCheck; + public MemberRequest() { } - public MemberRequest(String email, String name, String password) { + public MemberRequest(String email, String name, String password, String passwordCheck) { this.email = email; this.name = name; this.password = password; + this.passwordCheck = passwordCheck; + } + + public Member toMember() { + return new Member(email, name, password); } public String getEmail() { @@ -36,7 +51,7 @@ public String getPassword() { return password; } - public Member toMember() { - return new Member(email, name, password); + public String getPasswordCheck() { + return passwordCheck; } } diff --git a/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java b/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java index 26c782458..f020b6d76 100644 --- a/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java +++ b/src/main/java/wooteco/subway/service/member/dto/UpdateMemberRequest.java @@ -1,20 +1,9 @@ package wooteco.subway.service.member.dto; -import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; -import wooteco.subway.web.member.PasswordMatch; - -@PasswordMatch( - field = "password", - fieldMatch = "passwordCheck" -) public class UpdateMemberRequest { - @Email - @NotBlank - private String email; - @NotBlank private String name; @@ -22,40 +11,26 @@ public class UpdateMemberRequest { private String oldPassword; @NotBlank - private String password; - - @NotBlank - private String passwordCheck; + private String newPassword; public UpdateMemberRequest() { } - public UpdateMemberRequest(String email, String name, String oldPassword, String password, - String passwordCheck) { - this.email = email; + public UpdateMemberRequest(String name, String oldPassword, String newPassword) { this.name = name; this.oldPassword = oldPassword; - this.password = password; - this.passwordCheck = passwordCheck; + this.newPassword = newPassword; } public String getName() { return name; } - public String getEmail() { - return email; - } - public String getOldPassword() { return oldPassword; } - public String getPassword() { - return password; - } - - public String getPasswordCheck() { - return passwordCheck; + public String getNewPassword() { + return newPassword; } } diff --git a/src/main/java/wooteco/subway/web/member/RegisterMember.java b/src/main/java/wooteco/subway/web/member/RegisterMember.java deleted file mode 100644 index e97236889..000000000 --- a/src/main/java/wooteco/subway/web/member/RegisterMember.java +++ /dev/null @@ -1,11 +0,0 @@ -package wooteco.subway.web.member; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -public @interface RegisterMember { -} diff --git a/src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java b/src/main/java/wooteco/subway/web/member/controller/GlobalMemberExceptionHandler.java similarity index 82% rename from src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java rename to src/main/java/wooteco/subway/web/member/controller/GlobalMemberExceptionHandler.java index 9ade54b53..d1b9e8fb6 100644 --- a/src/main/java/wooteco/subway/web/member/GlobalMemberExceptionHandler.java +++ b/src/main/java/wooteco/subway/web/member/controller/GlobalMemberExceptionHandler.java @@ -1,10 +1,10 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import wooteco.subway.web.error.ErrorResponse; +import wooteco.subway.web.member.exception.ErrorResponse; @RestControllerAdvice public class GlobalMemberExceptionHandler { diff --git a/src/main/java/wooteco/subway/web/member/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java similarity index 92% rename from src/main/java/wooteco/subway/web/member/LoginMemberController.java rename to src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java index f1b864299..1d9e7f776 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.controller; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -11,6 +11,7 @@ import wooteco.subway.service.member.dto.LoginRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; +import wooteco.subway.web.member.resolver.LoginMember; @RestController public class LoginMemberController { diff --git a/src/main/java/wooteco/subway/web/member/MemberController.java b/src/main/java/wooteco/subway/web/member/controller/MemberController.java similarity index 92% rename from src/main/java/wooteco/subway/web/member/MemberController.java rename to src/main/java/wooteco/subway/web/member/controller/MemberController.java index cf037074b..88d4a8381 100644 --- a/src/main/java/wooteco/subway/web/member/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/MemberController.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.controller; import java.net.URI; import java.util.List; @@ -23,21 +23,21 @@ import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; -import wooteco.subway.service.member.dto.MemberRawRequest; +import wooteco.subway.service.member.dto.MemberRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.UpdateMemberRequest; -import wooteco.subway.web.error.ErrorResponse; +import wooteco.subway.web.member.exception.ErrorResponse; @RestController public class MemberController { - private MemberService memberService; + private final MemberService memberService; public MemberController(MemberService memberService) { this.memberService = memberService; } @PostMapping("/members") - public ResponseEntity createMember(@Validated @RequestBody MemberRawRequest request) { + public ResponseEntity createMember(@Validated @RequestBody MemberRequest request) { Member member = memberService.createMember(request.toMember()); return ResponseEntity .created(URI.create("/members/" + member.getId())) diff --git a/src/main/java/wooteco/subway/web/member/DuplicateEmailException.java b/src/main/java/wooteco/subway/web/member/exception/DuplicateEmailException.java similarity index 76% rename from src/main/java/wooteco/subway/web/member/DuplicateEmailException.java rename to src/main/java/wooteco/subway/web/member/exception/DuplicateEmailException.java index 35d6b9f7a..3c2d153ba 100644 --- a/src/main/java/wooteco/subway/web/member/DuplicateEmailException.java +++ b/src/main/java/wooteco/subway/web/member/exception/DuplicateEmailException.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.exception; public class DuplicateEmailException extends RuntimeException { public DuplicateEmailException(String message) { diff --git a/src/main/java/wooteco/subway/web/error/ErrorResponse.java b/src/main/java/wooteco/subway/web/member/exception/ErrorResponse.java similarity index 84% rename from src/main/java/wooteco/subway/web/error/ErrorResponse.java rename to src/main/java/wooteco/subway/web/member/exception/ErrorResponse.java index f0f76941d..01531c717 100644 --- a/src/main/java/wooteco/subway/web/error/ErrorResponse.java +++ b/src/main/java/wooteco/subway/web/member/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.error; +package wooteco.subway.web.member.exception; public class ErrorResponse { diff --git a/src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java b/src/main/java/wooteco/subway/web/member/exception/InvalidAuthenticationException.java similarity index 87% rename from src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java rename to src/main/java/wooteco/subway/web/member/exception/InvalidAuthenticationException.java index d0cabdf17..885668f1d 100644 --- a/src/main/java/wooteco/subway/web/member/InvalidAuthenticationException.java +++ b/src/main/java/wooteco/subway/web/member/exception/InvalidAuthenticationException.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; diff --git a/src/main/java/wooteco/subway/web/member/NotFoundMemberException.java b/src/main/java/wooteco/subway/web/member/exception/NotFoundMemberException.java similarity index 61% rename from src/main/java/wooteco/subway/web/member/NotFoundMemberException.java rename to src/main/java/wooteco/subway/web/member/exception/NotFoundMemberException.java index 0ba702f42..0dbec1894 100644 --- a/src/main/java/wooteco/subway/web/member/NotFoundMemberException.java +++ b/src/main/java/wooteco/subway/web/member/exception/NotFoundMemberException.java @@ -1,8 +1,8 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.exception; public class NotFoundMemberException extends RuntimeException { - public static final String ERROR_MESSAGE = " 를 찾을 수 없습니다."; + public static final String ERROR_MESSAGE = " 회원님을 찾을 수 없습니다."; public NotFoundMemberException() { } diff --git a/src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java b/src/main/java/wooteco/subway/web/member/exception/NotMatchPasswordException.java similarity index 77% rename from src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java rename to src/main/java/wooteco/subway/web/member/exception/NotMatchPasswordException.java index 16be14396..db18691f9 100644 --- a/src/main/java/wooteco/subway/web/member/NotMatchPasswordException.java +++ b/src/main/java/wooteco/subway/web/member/exception/NotMatchPasswordException.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.exception; public class NotMatchPasswordException extends RuntimeException { public NotMatchPasswordException(String message) { diff --git a/src/main/java/wooteco/subway/web/member/LoginMember.java b/src/main/java/wooteco/subway/web/member/resolver/LoginMember.java similarity index 85% rename from src/main/java/wooteco/subway/web/member/LoginMember.java rename to src/main/java/wooteco/subway/web/member/resolver/LoginMember.java index 8ddcafd88..a3566ea50 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMember.java +++ b/src/main/java/wooteco/subway/web/member/resolver/LoginMember.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.resolver; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java b/src/main/java/wooteco/subway/web/member/resolver/LoginMemberMethodArgumentResolver.java similarity index 93% rename from src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java rename to src/main/java/wooteco/subway/web/member/resolver/LoginMemberMethodArgumentResolver.java index e8fbd6492..f93e85f74 100644 --- a/src/main/java/wooteco/subway/web/member/LoginMemberMethodArgumentResolver.java +++ b/src/main/java/wooteco/subway/web/member/resolver/LoginMemberMethodArgumentResolver.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.resolver; import static org.springframework.web.context.request.RequestAttributes.*; @@ -12,6 +12,7 @@ import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; +import wooteco.subway.web.member.exception.InvalidAuthenticationException; @Component public class LoginMemberMethodArgumentResolver implements HandlerMethodArgumentResolver { diff --git a/src/main/java/wooteco/subway/web/member/DuplicateCheck.java b/src/main/java/wooteco/subway/web/member/validator/DuplicateCheck.java similarity index 92% rename from src/main/java/wooteco/subway/web/member/DuplicateCheck.java rename to src/main/java/wooteco/subway/web/member/validator/DuplicateCheck.java index 69d343d7d..44ee7c174 100644 --- a/src/main/java/wooteco/subway/web/member/DuplicateCheck.java +++ b/src/main/java/wooteco/subway/web/member/validator/DuplicateCheck.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.validator; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/wooteco/subway/web/member/EmailMatchValidator.java b/src/main/java/wooteco/subway/web/member/validator/EmailMatchValidator.java similarity index 93% rename from src/main/java/wooteco/subway/web/member/EmailMatchValidator.java rename to src/main/java/wooteco/subway/web/member/validator/EmailMatchValidator.java index 7aaffcbfd..845212efc 100644 --- a/src/main/java/wooteco/subway/web/member/EmailMatchValidator.java +++ b/src/main/java/wooteco/subway/web/member/validator/EmailMatchValidator.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.validator; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; diff --git a/src/main/java/wooteco/subway/web/member/PasswordMatch.java b/src/main/java/wooteco/subway/web/member/validator/PasswordMatch.java similarity index 93% rename from src/main/java/wooteco/subway/web/member/PasswordMatch.java rename to src/main/java/wooteco/subway/web/member/validator/PasswordMatch.java index 5d00e9f08..18105b05a 100644 --- a/src/main/java/wooteco/subway/web/member/PasswordMatch.java +++ b/src/main/java/wooteco/subway/web/member/validator/PasswordMatch.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.validator; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java b/src/main/java/wooteco/subway/web/member/validator/PasswordMatchValidator.java similarity index 94% rename from src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java rename to src/main/java/wooteco/subway/web/member/validator/PasswordMatchValidator.java index d95c2c522..7adf53626 100644 --- a/src/main/java/wooteco/subway/web/member/PasswordMatchValidator.java +++ b/src/main/java/wooteco/subway/web/member/validator/PasswordMatchValidator.java @@ -1,4 +1,4 @@ -package wooteco.subway.web.member; +package wooteco.subway.web.member.validator; import java.util.Objects; diff --git a/src/main/resources/static/service/js/views/Login.js b/src/main/resources/static/service/js/views/Login.js index c667bf7af..c58b38d35 100644 --- a/src/main/resources/static/service/js/views/Login.js +++ b/src/main/resources/static/service/js/views/Login.js @@ -1,5 +1,6 @@ import { ERROR_MESSAGE, EVENT_TYPE } from '../../utils/constants.js' import api from '../../api/index.js'; +import { setCookie } from '../../utils/loginUtils.js'; function Login() { const $loginButton = document.querySelector('#login-button'); @@ -40,15 +41,6 @@ function Login() { }); } - const setCookie = function (value) { - const date = new Date(); - date.setTime(date.getTime() + 5 * 60 * 1000); - document.cookie = "token=" + value + ';expires=' + date.toUTCString(); - }; - - - - this.init = () => { $loginButton.addEventListener(EVENT_TYPE.CLICK, onLogin); } diff --git a/src/main/resources/static/service/js/views/MyPage.js b/src/main/resources/static/service/js/views/MyPage.js index 2e6d532c3..25aa8154b 100644 --- a/src/main/resources/static/service/js/views/MyPage.js +++ b/src/main/resources/static/service/js/views/MyPage.js @@ -1,13 +1,8 @@ import api from '../../api/index.js'; +import { getCookie } from '../../utils/loginUtils.js'; function MyPage() { - - const getCookie = function () { - const value = document.cookie.match('(^|;) ?token=([^;]*)(;|$)'); - return value ? value[2] : null; - }; - this.init = () => { const $email = document.querySelector("#email"); const $name = document.querySelector("#name"); diff --git a/src/main/resources/static/service/js/views/MyPageEdit.js b/src/main/resources/static/service/js/views/MyPageEdit.js index 162f877e8..cec4a4167 100644 --- a/src/main/resources/static/service/js/views/MyPageEdit.js +++ b/src/main/resources/static/service/js/views/MyPageEdit.js @@ -1,5 +1,6 @@ import { EVENT_TYPE } from '../../utils/constants.js' import api from '../../api/index.js' +import { deleteCookie, getCookie } from '../../utils/loginUtils.js'; function MyPageEdit() { const $id = document.querySelector('#id'); @@ -15,13 +16,16 @@ function MyPageEdit() { event.preventDefault(); const updateForm = { - email: $email.value, name: $name.value, oldPassword: $oldPassword.value, - password: $password.value, - passwordCheck: $passwordCheck.value, + newPassword: $password.value, }; + if ($password.value !== $passwordCheck.value) { + alert("패스워드가 일치하지 않습니다. 😡"); + return; + } + api.member.update($id.dataset.id, updateForm) .then(response => { if (!response.ok) { @@ -64,15 +68,6 @@ function MyPageEdit() { $signOutButton.addEventListener(EVENT_TYPE.CLICK, onClickSignOutButton); } - const getCookie = function () { - const value = document.cookie.match('(^|;) ?token=([^;]*)(;|$)'); - return value ? value[2] : null; - }; - - const deleteCookie = function () { - document.cookie = 'token=; expires=Thu, 01 Jan 1999 00:00:10 GMT;'; - } - this.init = () => { if(getCookie()) { api.member.myPage().then(response => { diff --git a/src/main/resources/static/service/utils/loginUtils.js b/src/main/resources/static/service/utils/loginUtils.js new file mode 100644 index 000000000..16fb0a4c2 --- /dev/null +++ b/src/main/resources/static/service/utils/loginUtils.js @@ -0,0 +1,15 @@ +export const getCookie = function () { + const value = document.cookie.match('(^|;) ?token=([^;]*)(;|$)'); + return value ? value[2] : null; +}; + +export const deleteCookie = function () { + document.cookie = 'token=; expires=Thu, 01 Jan 1999 00:00:10 GMT;'; +} + +export const setCookie = function (value) { + const date = new Date(); + date.setTime(date.getTime() + 5 * 60 * 1000); + document.cookie = "token=" + value + ';expires=' + date.toUTCString(); +}; + diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index d37ef7acc..da37127f5 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -294,7 +294,8 @@ public MemberResponse getMember(String email) { public void updateMember(MemberResponse memberResponse, TokenResponse tokenResponse) { Map params = new HashMap<>(); params.put("name", "NEW_" + TEST_USER_NAME); - params.put("password", "NEW_" + TEST_USER_PASSWORD); + params.put("oldPassword", TEST_USER_PASSWORD); + params.put("newPassword", "NEW_" + TEST_USER_PASSWORD); given(). header("Authorization", diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index a9a13f37f..25e8de07b 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -14,18 +14,6 @@ import wooteco.subway.service.member.dto.TokenResponse; public class AuthAcceptanceTest extends AcceptanceTest { - @DisplayName("Basic Auth") - @Test - void myInfoWithBasicAuth() { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); - - MemberResponse memberResponse = myInfoWithBasicAuth(TEST_USER_EMAIL, TEST_USER_PASSWORD); - - assertThat(memberResponse.getId()).isNotNull(); - assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); - assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); - } - @DisplayName("Bearer Auth") @Test void myInfoWithBearerAuth() { @@ -38,24 +26,10 @@ void myInfoWithBearerAuth() { assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); } - public MemberResponse myInfoWithBasicAuth(String email, String password) { - return given().auth() - .preemptive() - .basic(email, password) - .when() - .get("/me/basic") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract().as(MemberResponse.class); - } - public MemberResponse myInfoWithBearerAuth(TokenResponse tokenResponse) { - return given().auth() - .preemptive() - .oauth2(tokenResponse.getAccessToken()) + return given().cookie("token", tokenResponse.getAccessToken()) .when() - .get("/me/bearer") + .get("/me") .then() .log().all() .statusCode(HttpStatus.OK.value()) @@ -73,7 +47,7 @@ public TokenResponse login(String email, String password) { contentType(MediaType.APPLICATION_JSON_VALUE). accept(MediaType.APPLICATION_JSON_VALUE). when(). - post("/oauth/token"). + post("/login"). then(). log().all(). statusCode(HttpStatus.OK.value()). diff --git a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java index b184a992b..c053c72e8 100644 --- a/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/member/MemberAcceptanceTest.java @@ -4,9 +4,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -17,26 +19,41 @@ public class MemberAcceptanceTest extends AcceptanceTest { @DisplayName("회원 관리 기능") - @Test - void manageMember() { - String location = createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, - TEST_USER_PASSWORD); - assertThat(location).isNotBlank(); - - MemberResponse memberResponse = getMember(TEST_USER_EMAIL); - assertThat(memberResponse.getId()).isNotNull(); - assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); - assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); - - TokenResponse tokenResponse = getToken(TEST_USER_EMAIL, TEST_USER_PASSWORD); - updateMember(memberResponse, tokenResponse); - MemberResponse updatedMember = getMember(TEST_USER_EMAIL); - assertThat(updatedMember.getName()).isEqualTo("NEW_" + TEST_USER_NAME); - - deleteMember(memberResponse, tokenResponse); + @TestFactory + Stream manageMember() { + return Stream.of( + DynamicTest.dynamicTest("Create member test", () -> { + String location = createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, + TEST_USER_PASSWORD); + assertThat(location).isNotBlank(); + } + ), + DynamicTest.dynamicTest("Login test", () -> { + TokenResponse token = login(TEST_USER_EMAIL, TEST_USER_PASSWORD); + assertThat(token).extracting(TokenResponse::getAccessToken).isNotNull(); + assertThat(token).extracting(TokenResponse::getTokenType).isEqualTo("bearer"); + }), + DynamicTest.dynamicTest("Get member test", () -> { + MemberResponse memberResponse = getMember(TEST_USER_EMAIL); + assertThat(memberResponse.getId()).isNotNull(); + assertThat(memberResponse.getEmail()).isEqualTo(TEST_USER_EMAIL); + assertThat(memberResponse.getName()).isEqualTo(TEST_USER_NAME); + }), + DynamicTest.dynamicTest("Update member test", () -> { + TokenResponse tokenResponse = login(TEST_USER_EMAIL, TEST_USER_PASSWORD); + MemberResponse memberResponse = getMember(TEST_USER_EMAIL); + updateMember(memberResponse, tokenResponse); + assertThat(getMember(TEST_USER_EMAIL)).extracting(MemberResponse::getName) + .isEqualTo("NEW_" + TEST_USER_NAME); + }), + DynamicTest.dynamicTest("Delete member test", () -> { + deleteMember(getMember(TEST_USER_EMAIL), + login(TEST_USER_EMAIL, "NEW_" + TEST_USER_PASSWORD)); + }) + ); } - private TokenResponse getToken(String email, String password) { + private TokenResponse login(String email, String password) { Map params = new HashMap<>(); params.put("email", email); params.put("password", password); @@ -46,7 +63,7 @@ private TokenResponse getToken(String email, String password) { .contentType(MediaType.APPLICATION_JSON_VALUE) .accept(MediaType.APPLICATION_JSON_VALUE) .when() - .post("/oauth/token") + .post("login") .then() .statusCode(HttpStatus.OK.value()) .extract().as(TokenResponse.class); diff --git a/src/test/java/wooteco/subway/doc/ApiDocumentUtils.java b/src/test/java/wooteco/subway/doc/ApiDocumentUtils.java new file mode 100644 index 000000000..0ef0eeb6a --- /dev/null +++ b/src/test/java/wooteco/subway/doc/ApiDocumentUtils.java @@ -0,0 +1,23 @@ +package wooteco.subway.doc; + + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; + +import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; +import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; + +public interface ApiDocumentUtils { + + static OperationRequestPreprocessor getDocumentRequest() { + return preprocessRequest( + modifyUris() + .scheme("https") + .host("docs.api.com") + .removePort(), + prettyPrint()); + } + + static OperationResponsePreprocessor getDocumentResponse() { + return preprocessResponse(prettyPrint()); + } +} diff --git a/src/test/java/wooteco/subway/doc/DocumentFormatGenerator.java b/src/test/java/wooteco/subway/doc/DocumentFormatGenerator.java new file mode 100644 index 000000000..2163bd2cf --- /dev/null +++ b/src/test/java/wooteco/subway/doc/DocumentFormatGenerator.java @@ -0,0 +1,12 @@ +package wooteco.subway.doc; + +import static org.springframework.restdocs.snippet.Attributes.*; + +import org.springframework.restdocs.snippet.Attributes; + +public interface DocumentFormatGenerator { + + static Attributes.Attribute getEmailFormat() { + return key("format").value("xxx@xxx.com"); + } +} diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index b9ba4d683..071ed2087 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -3,6 +3,8 @@ import static org.springframework.restdocs.headers.HeaderDocumentation.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static wooteco.subway.doc.ApiDocumentUtils.*; +import static wooteco.subway.doc.DocumentFormatGenerator.*; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.payload.JsonFieldType; @@ -10,9 +12,11 @@ public class MemberDocumentation { public static RestDocumentationResultHandler createMember() { return document("members/create", + getDocumentRequest(), + getDocumentResponse(), requestFields( fieldWithPath("email").type(JsonFieldType.STRING) - .description("The user's email address"), + .description("The user's email address").attributes(getEmailFormat()), fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), fieldWithPath("password").type(JsonFieldType.STRING) .description("The user's password"), @@ -27,9 +31,11 @@ public static RestDocumentationResultHandler createMember() { public static RestDocumentationResultHandler createDuplicateMember() { return document("members/duplicate-create", + getDocumentRequest(), + getDocumentResponse(), requestFields( fieldWithPath("email").type(JsonFieldType.STRING) - .description("The user's email address"), + .description("The user's email address").attributes(getEmailFormat()), fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), fieldWithPath("password").type(JsonFieldType.STRING) .description("The user's password"), @@ -38,16 +44,18 @@ public static RestDocumentationResultHandler createDuplicateMember() { ), responseFields( fieldWithPath("message").type(JsonFieldType.STRING) - .description("The error message") + .description("The error message") ) ); } public static RestDocumentationResultHandler createNotMatchPasswordMember() { return document("members/not-match-password-create", + getDocumentRequest(), + getDocumentResponse(), requestFields( fieldWithPath("email").type(JsonFieldType.STRING) - .description("The user's email address"), + .description("The user's email address").attributes(getEmailFormat()), fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), fieldWithPath("password").type(JsonFieldType.STRING) .description("The user's password"), @@ -61,29 +69,53 @@ public static RestDocumentationResultHandler createNotMatchPasswordMember() { ); } - public static RestDocumentationResultHandler updateMember() { - return document("members/update", - requestFields( - fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), - fieldWithPath("password").type(JsonFieldType.STRING).description("The user's password") - ), - requestHeaders( - headerWithName("Authorization").description("The token for login which is Bearer Type") - ) - ); - } + public static RestDocumentationResultHandler updateMember() { + return document("members/update", + getDocumentRequest(), + getDocumentResponse(), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("oldPassword").type(JsonFieldType.STRING) + .description("The user's old password"), + fieldWithPath("newPassword").type(JsonFieldType.STRING) + .description("The user's new password") + ), + requestHeaders( + headerWithName("Authorization").description( + "The token for login which is Bearer Type") + ) + ); + } public static RestDocumentationResultHandler getMember() { return document("members/get", + getDocumentRequest(), + getDocumentResponse(), responseFields( fieldWithPath("id").type(JsonFieldType.NUMBER).description("The user's id"), - fieldWithPath("email").type(JsonFieldType.STRING).description("The user's email"), + fieldWithPath("email").type(JsonFieldType.STRING) + .description("The user's email") + .attributes(getEmailFormat()), fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name") ) ); } + public static RestDocumentationResultHandler getNotExistMember() { + return document("members/not-exist-get", + getDocumentRequest(), + getDocumentResponse(), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING) + .description("The error message") + ) + ); + } + public static RestDocumentationResultHandler deleteMember() { - return document("members/delete"); + return document("members/delete", + getDocumentRequest(), + getDocumentResponse() + ); } } diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index 45ee3903b..5160f03fd 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -79,7 +79,7 @@ void updateMember() { when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member)); when(memberRepository.save(any())).thenReturn(member); memberService.updateMember(member.getId(), new UpdateMemberRequest( - "NEW_" + TEST_USER_NAME, "NEW_" + TEST_USER_PASSWORD)); + "NEW_" + TEST_USER_NAME, TEST_USER_PASSWORD, "NEW_" + TEST_USER_PASSWORD)); assertThat(member).extracting(Member::getName).isEqualTo("NEW_" + TEST_USER_NAME); assertThat(member).extracting(Member::getPassword).isEqualTo("NEW_" + TEST_USER_PASSWORD); } diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index 668c0316c..d70f84901 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -28,6 +28,7 @@ import wooteco.subway.domain.member.Member; import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.MemberService; +import wooteco.subway.web.member.exception.NotFoundMemberException; @ExtendWith(RestDocumentationExtension.class) @SpringBootTest @@ -46,7 +47,8 @@ public class MemberControllerTest { private Member member; @BeforeEach - public void setUp(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) { + public void setUp(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) .addFilter(new ShallowEtagHeaderFilter()) .apply(documentationConfiguration(restDocumentation)) @@ -65,13 +67,6 @@ public void createMember() throws Exception { "\"password\":\"" + TEST_USER_PASSWORD + "\"," + "\"passwordCheck\":\"" + TEST_USER_PASSWORD + "\"}"; - this.mockMvc.perform(post("/members") - .content(inputJson) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andDo(print()); - this.mockMvc.perform(post("/members") .content(inputJson) .accept(MediaType.APPLICATION_JSON) @@ -91,13 +86,6 @@ void createDuplicateMember() throws Exception { "\"password\":\"" + TEST_USER_PASSWORD + "\"," + "\"passwordCheck\":\"" + TEST_USER_PASSWORD + "\"}"; - this.mockMvc.perform(post("/members") - .content(inputJson) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isCreated()) - .andDo(print()); - this.mockMvc.perform(post("/members") .content(inputJson) .accept(MediaType.APPLICATION_JSON) @@ -108,13 +96,6 @@ void createDuplicateMember() throws Exception { given(memberService.isNotExistEmail(any())).willReturn(false); - this.mockMvc.perform(post("/members") - .content(inputJson) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - this.mockMvc.perform(post("/members") .content(inputJson) .accept(MediaType.APPLICATION_JSON) @@ -134,13 +115,6 @@ void createNotMatchPasswordMember() throws Exception { "\"password\":\"" + TEST_USER_PASSWORD + "\"," + "\"passwordCheck\":\"" + TEST_OTHER_USER_PASSWORD + "\"}"; - this.mockMvc.perform(post("/members") - .content(inputJson) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON)) - .andExpect(status().isBadRequest()) - .andDo(print()); - this.mockMvc.perform(post("/members") .content(inputJson) .accept(MediaType.APPLICATION_JSON) @@ -160,32 +134,28 @@ void getMember() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.email", Matchers.is(TEST_USER_EMAIL))) .andExpect(jsonPath("$.name", Matchers.is(TEST_USER_NAME))) - .andDo(print()); + .andDo(print()) + .andDo(MemberDocumentation.getMember()); + } + + @Test + void getNotExistMember() throws Exception { + given(memberService.findMemberByEmail(any())).willThrow(new NotFoundMemberException(any())); this.mockMvc.perform(get("/members") .param("email", TEST_USER_EMAIL) .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.email", Matchers.is(TEST_USER_EMAIL))) - .andExpect(jsonPath("$.name", Matchers.is(TEST_USER_NAME))) + .andExpect(status().isBadRequest()) .andDo(print()) - .andDo(MemberDocumentation.getMember()); + .andDo(MemberDocumentation.getNotExistMember()); } @Test void updateMember() throws Exception { - given(jwtTokenProvider.validateToken(any())).willReturn(true); String inputJson = "{\"name\":\"" + TEST_USER_NAME + "\"," + - "\"password\":\"" + TEST_USER_PASSWORD + "\"}"; - - this.mockMvc.perform(put("/members/" + 1L) - .header("Authorization", "tmp") - .contentType(MediaType.APPLICATION_JSON) - .content(inputJson) - .accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isOk()) - .andDo(print()); + "\"oldPassword\":\"" + TEST_USER_PASSWORD + "\"," + + "\"newPassword\":\"" + "NEW_" + TEST_USER_PASSWORD + "\"}"; this.mockMvc.perform(put("/members/" + 1L) .contentType(MediaType.APPLICATION_JSON) @@ -199,11 +169,9 @@ void updateMember() throws Exception { @Test void deleteMember() throws Exception { - this.mockMvc.perform(delete("/members/" + 1L)) - .andExpect(status().isNoContent()) - .andDo(print()); - - this.mockMvc.perform(delete("/members/" + 1L)) + given(jwtTokenProvider.validateToken(any())).willReturn(true); + this.mockMvc.perform(delete("/members/" + 1L) + .header("Authorization", "tmp")) .andExpect(status().isNoContent()) .andDo(print()) .andDo(MemberDocumentation.deleteMember()); diff --git a/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet new file mode 100644 index 000000000..eb994bc03 --- /dev/null +++ b/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet @@ -0,0 +1,15 @@ +===== Request Fields +|=== +|필드명|타입|필수값여부|양식|설명 + + +{{#fields}} +|{{#tableCellContent}}`+{{path}}+`{{/tableCellContent}} +|{{#tableCellContent}}`+{{type}}+`{{/tableCellContent}} +|{{#tableCellContent}}{{^optional}}true{{/optional}}{{/tableCellContent}} +|{{#tableCellContent}}{{#format}}{{.}}{{/format}}{{/tableCellContent}} +|{{#tableCellContent}}{{description}}{{/tableCellContent}} + +{{/fields}} + +|=== \ No newline at end of file From 4747fc661ee0d62b74b85964faa9827432a8df3e Mon Sep 17 00:00:00 2001 From: dd Date: Mon, 25 May 2020 15:21:54 +0900 Subject: [PATCH 15/30] =?UTF-8?q?feat:=20IsAuth=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=99=80=20=EB=AC=B8=EC=84=9C=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 --- src/docs/asciidoc/api-guide.adoc | 6 +++-- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../subway/infra/JwtTokenProvider.java | 19 +++++++++----- .../web/member/AuthorizationExtractor.java | 16 ++++++++---- .../controller/LoginMemberController.java | 3 +++ .../member/controller/MemberController.java | 5 ++++ .../exception/InvalidTokenException.java | 7 +++++ .../subway/web/member/interceptor/Auth.java | 5 ++++ .../interceptor/BearerAuthInterceptor.java | 26 +++++++++++++++---- .../subway/web/member/interceptor/IsAuth.java | 15 +++++++++++ .../java/wooteco/subway/AcceptanceTest.java | 15 +++++++---- .../wooteco/subway/AuthAcceptanceTest.java | 3 ++- .../subway/doc/MemberDocumentation.java | 4 --- .../web/member/MemberControllerTest.java | 14 +++++++--- 14 files changed, 107 insertions(+), 33 deletions(-) create mode 100644 src/main/java/wooteco/subway/web/member/exception/InvalidTokenException.java create mode 100644 src/main/java/wooteco/subway/web/member/interceptor/Auth.java create mode 100644 src/main/java/wooteco/subway/web/member/interceptor/IsAuth.java diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 2be8a4836..17cecd516 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -44,11 +44,13 @@ operation::members/not-match-password-create[snippets='http-request,http-respons ==== 성공 ===== 로그인 후 조회 -operation::members/get[snippets='http-request,http-response,response-fields'] +operation::members/get[snippets='http-request,http-response'] ==== 실패 -===== +===== 이메일이 존재하지 않음 +operation::members/not-exist-get[snippets='http-request,http-response'] + [[resources-members-update]] === 회원 정보 수정 diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 2d09b0880..93240b1a4 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -24,7 +24,7 @@ public WebMvcConfig( @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(bearerAuthInterceptor) - .addPathPatterns("/me"); + .addPathPatterns("/me", "/members", "/members/*"); } @Override diff --git a/src/main/java/wooteco/subway/infra/JwtTokenProvider.java b/src/main/java/wooteco/subway/infra/JwtTokenProvider.java index 59e039e71..7391dade8 100644 --- a/src/main/java/wooteco/subway/infra/JwtTokenProvider.java +++ b/src/main/java/wooteco/subway/infra/JwtTokenProvider.java @@ -1,11 +1,17 @@ package wooteco.subway.infra; -import io.jsonwebtoken.*; +import java.util.Base64; +import java.util.Date; + import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.util.Base64; -import java.util.Date; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import wooteco.subway.web.member.exception.InvalidAuthenticationException; @Component public class JwtTokenProvider { @@ -41,13 +47,12 @@ public boolean validateToken(String token) { Jws claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token); if (claims.getBody().getExpiration().before(new Date())) { - return false; + throw new InvalidAuthenticationException("토큰이 만료되었습니다. 다시 로그인 해주세요."); } - - return true; } catch (JwtException | IllegalArgumentException e) { - return false; + throw new InvalidAuthenticationException("로그인 후 이용해주세요."); } + return true; } } diff --git a/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java b/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java index 5faf0c1a0..adb44679c 100644 --- a/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java +++ b/src/main/java/wooteco/subway/web/member/AuthorizationExtractor.java @@ -7,14 +7,20 @@ import org.springframework.stereotype.Component; +import wooteco.subway.web.member.exception.InvalidTokenException; + @Component public class AuthorizationExtractor { public String extract(HttpServletRequest request) { - return Arrays.stream(request.getCookies()) - .filter(cookie -> cookie.getName().equals("token")) - .map(Cookie::getValue) - .findFirst() - .orElseThrow(() -> new RuntimeException("토큰을 찾을 수 없습니다.")); + try { + return Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("token")) + .map(Cookie::getValue) + .findFirst() + .orElseThrow(() -> new InvalidTokenException("토큰을 찾을 수 없습니다.")); + } catch (Exception e) { + throw new InvalidTokenException("토큰을 찾을 수 없습니다."); + } } } diff --git a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java index 1d9e7f776..98f4877ba 100644 --- a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java @@ -11,6 +11,8 @@ import wooteco.subway.service.member.dto.LoginRequest; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; +import wooteco.subway.web.member.interceptor.Auth; +import wooteco.subway.web.member.interceptor.IsAuth; import wooteco.subway.web.member.resolver.LoginMember; @RestController @@ -27,6 +29,7 @@ public ResponseEntity login(@RequestBody LoginRequest param) { return ResponseEntity.ok().body(new TokenResponse(token, "bearer")); } + @IsAuth(isAuth = Auth.AUTH) @GetMapping("/me") public ResponseEntity getMemberOfMineBasic(@LoginMember Member member) { return ResponseEntity.ok().body(MemberResponse.of(member)); diff --git a/src/main/java/wooteco/subway/web/member/controller/MemberController.java b/src/main/java/wooteco/subway/web/member/controller/MemberController.java index 88d4a8381..274d34d62 100644 --- a/src/main/java/wooteco/subway/web/member/controller/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/MemberController.java @@ -27,6 +27,8 @@ import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.UpdateMemberRequest; import wooteco.subway.web.member.exception.ErrorResponse; +import wooteco.subway.web.member.interceptor.Auth; +import wooteco.subway.web.member.interceptor.IsAuth; @RestController public class MemberController { @@ -44,12 +46,14 @@ public ResponseEntity createMember(@Validated @RequestBody MemberRequest r .build(); } + @IsAuth(isAuth = Auth.AUTH) @GetMapping("/members") public ResponseEntity getMemberByEmail(@RequestParam String email) { Member member = memberService.findMemberByEmail(email); return ResponseEntity.ok().body(MemberResponse.of(member)); } + @IsAuth(isAuth = Auth.AUTH) @PutMapping("/members/{id}") public ResponseEntity updateMember(@PathVariable Long id, @Valid @RequestBody UpdateMemberRequest param) { @@ -57,6 +61,7 @@ public ResponseEntity updateMember(@PathVariable Long id, return ResponseEntity.ok().build(); } + @IsAuth(isAuth = Auth.AUTH) @DeleteMapping("/members/{id}") public ResponseEntity deleteMember(@PathVariable Long id) { memberService.deleteMember(id); diff --git a/src/main/java/wooteco/subway/web/member/exception/InvalidTokenException.java b/src/main/java/wooteco/subway/web/member/exception/InvalidTokenException.java new file mode 100644 index 000000000..0a74df034 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/exception/InvalidTokenException.java @@ -0,0 +1,7 @@ +package wooteco.subway.web.member.exception; + +public class InvalidTokenException extends RuntimeException { + public InvalidTokenException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/Auth.java b/src/main/java/wooteco/subway/web/member/interceptor/Auth.java new file mode 100644 index 000000000..3c2487c82 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/interceptor/Auth.java @@ -0,0 +1,5 @@ +package wooteco.subway.web.member.interceptor; + +public enum Auth { + NONE, AUTH +} diff --git a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java index 3d5481868..d3a857b60 100644 --- a/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java +++ b/src/main/java/wooteco/subway/web/member/interceptor/BearerAuthInterceptor.java @@ -1,9 +1,14 @@ package wooteco.subway.web.member.interceptor; +import java.lang.annotation.Annotation; +import java.util.Optional; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; @@ -23,11 +28,17 @@ public BearerAuthInterceptor(AuthorizationExtractor authExtractor, JwtTokenProvi @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - String bearer = authExtractor.extract(request); - jwtTokenProvider.validateToken(bearer); - String email = jwtTokenProvider.getSubject(bearer); - - request.setAttribute("loginMemberEmail", email); + IsAuth annotation = getAnnotation((HandlerMethod)handler, IsAuth.class); + Auth auth = null; + if (!ObjectUtils.isEmpty(annotation)) { + auth = annotation.isAuth(); + if (auth == Auth.AUTH) { + String bearer = authExtractor.extract(request); + jwtTokenProvider.validateToken(bearer); + String email = jwtTokenProvider.getSubject(bearer); + request.setAttribute("loginMemberEmail", email); + } + } return true; } @@ -43,4 +54,9 @@ public void postHandle(HttpServletRequest request, public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } + + private A getAnnotation(HandlerMethod handlerMethod, Class annotationType) { + return Optional.ofNullable(handlerMethod.getMethodAnnotation(annotationType)) + .orElse(handlerMethod.getBeanType().getAnnotation(annotationType)); + } } diff --git a/src/main/java/wooteco/subway/web/member/interceptor/IsAuth.java b/src/main/java/wooteco/subway/web/member/interceptor/IsAuth.java new file mode 100644 index 000000000..a5f182329 --- /dev/null +++ b/src/main/java/wooteco/subway/web/member/interceptor/IsAuth.java @@ -0,0 +1,15 @@ +package wooteco.subway.web.member.interceptor; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface IsAuth { + + Auth isAuth() default Auth.NONE; + +} + diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index da37127f5..8abe06673 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -7,6 +7,7 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.http.HttpStatus; @@ -15,6 +16,7 @@ import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; +import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.line.dto.LineDetailResponse; import wooteco.subway.service.line.dto.LineResponse; import wooteco.subway.service.line.dto.WholeSubwayResponse; @@ -48,6 +50,9 @@ public class AcceptanceTest { @LocalServerPort public int port; + @Autowired + private JwtTokenProvider jwtTokenProvider; + @BeforeEach public void setUp() { RestAssured.port = port; @@ -280,11 +285,13 @@ public String createMember(String email, String name, String password, String pa } public MemberResponse getMember(String email) { + String token = jwtTokenProvider.createToken(email); return given(). accept(MediaType.APPLICATION_JSON_VALUE). + cookie("token", token). when(). - get("/members?email=" + email). + get("/members?email="+email). then(). log().all(). statusCode(HttpStatus.OK.value()). @@ -298,8 +305,7 @@ public void updateMember(MemberResponse memberResponse, TokenResponse tokenRespo params.put("newPassword", "NEW_" + TEST_USER_PASSWORD); given(). - header("Authorization", - tokenResponse.getTokenType() + " " + tokenResponse.getAccessToken()). + cookie("token", tokenResponse.getAccessToken()). body(params). contentType(MediaType.APPLICATION_JSON_VALUE). accept(MediaType.APPLICATION_JSON_VALUE). @@ -312,8 +318,7 @@ public void updateMember(MemberResponse memberResponse, TokenResponse tokenRespo public void deleteMember(MemberResponse memberResponse, TokenResponse tokenResponse) { given().when(). - header("Authorization", - tokenResponse.getTokenType() + " " + tokenResponse.getAccessToken()). + cookie("token", tokenResponse.getAccessToken()). delete("/members/" + memberResponse.getId()). then(). log().all(). diff --git a/src/test/java/wooteco/subway/AuthAcceptanceTest.java b/src/test/java/wooteco/subway/AuthAcceptanceTest.java index 25e8de07b..7e24fc8e9 100644 --- a/src/test/java/wooteco/subway/AuthAcceptanceTest.java +++ b/src/test/java/wooteco/subway/AuthAcceptanceTest.java @@ -27,7 +27,8 @@ void myInfoWithBearerAuth() { } public MemberResponse myInfoWithBearerAuth(TokenResponse tokenResponse) { - return given().cookie("token", tokenResponse.getAccessToken()) + return given() + .cookie("token", tokenResponse.getAccessToken()) .when() .get("/me") .then() diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index 071ed2087..019780eec 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -79,10 +79,6 @@ public static RestDocumentationResultHandler updateMember() { .description("The user's old password"), fieldWithPath("newPassword").type(JsonFieldType.STRING) .description("The user's new password") - ), - requestHeaders( - headerWithName("Authorization").description( - "The token for login which is Bearer Type") ) ); } diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index d70f84901..8f5463f1d 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -8,6 +8,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import static wooteco.subway.AcceptanceTest.*; +import javax.servlet.http.Cookie; + import org.hamcrest.Matchers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,6 +47,7 @@ public class MemberControllerTest { protected MockMvc mockMvc; private Member member; + private Cookie cookie; @BeforeEach public void setUp(WebApplicationContext webApplicationContext, @@ -55,6 +58,7 @@ public void setUp(WebApplicationContext webApplicationContext, .build(); member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + cookie = new Cookie("token", "dundung"); } @Test @@ -127,8 +131,10 @@ void createNotMatchPasswordMember() throws Exception { @Test void getMember() throws Exception { given(memberService.findMemberByEmail(any())).willReturn(member); + given(jwtTokenProvider.validateToken(any())).willReturn(true); this.mockMvc.perform(get("/members") + .cookie(cookie) .param("email", TEST_USER_EMAIL) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -140,9 +146,11 @@ void getMember() throws Exception { @Test void getNotExistMember() throws Exception { - given(memberService.findMemberByEmail(any())).willThrow(new NotFoundMemberException(any())); + given(memberService.findMemberByEmail(any())).willThrow(new NotFoundMemberException("이메일을 찾을 수 없습니다.")); + given(jwtTokenProvider.validateToken(any())).willReturn(true); this.mockMvc.perform(get("/members") + .cookie(cookie) .param("email", TEST_USER_EMAIL) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isBadRequest()) @@ -159,7 +167,7 @@ void updateMember() throws Exception { this.mockMvc.perform(put("/members/" + 1L) .contentType(MediaType.APPLICATION_JSON) - .header("Authorization", "tmp") + .cookie(cookie) .content(inputJson) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) @@ -171,7 +179,7 @@ void updateMember() throws Exception { void deleteMember() throws Exception { given(jwtTokenProvider.validateToken(any())).willReturn(true); this.mockMvc.perform(delete("/members/" + 1L) - .header("Authorization", "tmp")) + .cookie(cookie)) .andExpect(status().isNoContent()) .andDo(print()) .andDo(MemberDocumentation.deleteMember()); From 6f81c9cbdb00e26fd645e3d5640ab42d3967d219 Mon Sep 17 00:00:00 2001 From: dd Date: Mon, 25 May 2020 19:15:41 +0900 Subject: [PATCH 16/30] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰이 없는 경우 정보조회 테스트 추가 --- src/docs/asciidoc/api-guide.adoc | 7 ++++++- .../subway/doc/MemberDocumentation.java | 20 ++++++++++++++++++- .../web/member/MemberControllerTest.java | 17 ++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 17cecd516..653c9e487 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -48,7 +48,7 @@ operation::members/get[snippets='http-request,http-response'] ==== 실패 -===== 이메일이 존재하지 않음 +===== 토큰이 존재하지 않음 operation::members/not-exist-get[snippets='http-request,http-response'] @@ -60,6 +60,11 @@ operation::members/not-exist-get[snippets='http-request,http-response'] ===== 로그인 후 올바른 패스워드로 수정 operation::members/update[snippets='http-request,http-response,request-fields'] +==== 실패 + +===== 토큰이 존재하지 않음 +operation::member/not-exist-token-update[snippets='http-request, http-response, request-fields, response-fields'] + [[resources-members-delete]] === 회원 탈퇴 diff --git a/src/test/java/wooteco/subway/doc/MemberDocumentation.java b/src/test/java/wooteco/subway/doc/MemberDocumentation.java index 019780eec..2a421ad44 100644 --- a/src/test/java/wooteco/subway/doc/MemberDocumentation.java +++ b/src/test/java/wooteco/subway/doc/MemberDocumentation.java @@ -104,7 +104,7 @@ public static RestDocumentationResultHandler getNotExistMember() { responseFields( fieldWithPath("message").type(JsonFieldType.STRING) .description("The error message") - ) + ) ); } @@ -114,4 +114,22 @@ public static RestDocumentationResultHandler deleteMember() { getDocumentResponse() ); } + + public static RestDocumentationResultHandler notExistTokenUpdateMember() { + return document("members/not-exist-token-update", + getDocumentRequest(), + getDocumentResponse(), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("The user's name"), + fieldWithPath("oldPassword").type(JsonFieldType.STRING) + .description("The user's old password"), + fieldWithPath("newPassword").type(JsonFieldType.STRING) + .description("The user's new password") + ), + responseFields( + fieldWithPath("message").type(JsonFieldType.STRING) + .description("The error message") + ) + ); + } } diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index 8f5463f1d..96146a472 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -30,6 +30,7 @@ import wooteco.subway.domain.member.Member; import wooteco.subway.infra.JwtTokenProvider; import wooteco.subway.service.member.MemberService; +import wooteco.subway.web.member.exception.InvalidTokenException; import wooteco.subway.web.member.exception.NotFoundMemberException; @ExtendWith(RestDocumentationExtension.class) @@ -175,6 +176,22 @@ void updateMember() throws Exception { .andDo(MemberDocumentation.updateMember()); } + @Test + void notExistTokenUpdateMember() throws Exception { + given(jwtTokenProvider.validateToken(any())).willThrow(new InvalidTokenException("토큰이 존재하지 않습니다.")); + String inputJson = "{\"name\":\"" + TEST_USER_NAME + "\"," + + "\"oldPassword\":\"" + TEST_USER_PASSWORD + "\"," + + "\"newPassword\":\"" + "NEW_" + TEST_USER_PASSWORD + "\"}"; + + this.mockMvc.perform(put("/members/" + 1L) + .contentType(MediaType.APPLICATION_JSON) + .content(inputJson) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andDo(print()) + .andDo(MemberDocumentation.notExistTokenUpdateMember()); + } + @Test void deleteMember() throws Exception { given(jwtTokenProvider.validateToken(any())).willReturn(true); From bac251203f59621b3bd92ccd73b55ca62eab31a3 Mon Sep 17 00:00:00 2001 From: dd Date: Mon, 25 May 2020 21:59:48 +0900 Subject: [PATCH 17/30] =?UTF-8?q?feat:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 +--------- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../subway/domain/favorite/Favorite.java | 46 +++++++++++ .../domain/favorite/FavoriteRepository.java | 6 ++ .../wooteco/subway/domain/member/Member.java | 23 ++++++ .../service/favorite/FavoriteService.java | 24 ++++++ .../service/favorite/dto/FavoriteRequest.java | 34 ++++++++ .../subway/service/member/MemberService.java | 2 +- .../web/favorite/FavoriteController.java | 33 ++++++++ .../member/controller/MemberController.java | 2 +- src/main/resources/schema.sql | 9 +- .../favorite/FavoriteAcceptanceTest.java | 78 ++++++++++++++++++ .../subway/doc/FavoriteDocumentation.java | 26 ++++++ .../service/favorite/FavoriteServiceTest.java | 42 ++++++++++ .../service/member/MemberServiceTest.java | 2 +- .../web/favorite/FavoriteControllerTest.java | 82 +++++++++++++++++++ .../web/member/MemberControllerTest.java | 6 +- 17 files changed, 410 insertions(+), 48 deletions(-) create mode 100644 src/main/java/wooteco/subway/domain/favorite/Favorite.java create mode 100644 src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java create mode 100644 src/main/java/wooteco/subway/service/favorite/FavoriteService.java create mode 100644 src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java create mode 100644 src/main/java/wooteco/subway/web/favorite/FavoriteController.java create mode 100644 src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java create mode 100644 src/test/java/wooteco/subway/doc/FavoriteDocumentation.java create mode 100644 src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java create mode 100644 src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java diff --git a/README.md b/README.md index d65d8917c..b8defad88 100644 --- a/README.md +++ b/README.md @@ -39,43 +39,4 @@ - 토큰이 만료된 경우 - 로그인 정보가 올바르지 않은 경우 - 로그인이 안된 경우 즐겨찾기를 추가하는 경우 -- 즐겨찾기 해제 기능## 요구 사항 - - - 회원 정보를 관리하는 기능 구현 - - 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 - - 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가 - - side case에 대한 예외처리 - - 인수 테스트와 단위 테스트 작성 - - API 문서를 작성하고 문서화를 위한 테스트 작성 - - 페이지 연동 - - 즐겨찾기 기능을 추가(추가,삭제,조회) - - 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함 - - 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver) - - side case에 대한 예외처리 필수 - - 인수 테스트와 단위 테스트 작성 - - API 문서를 작성하고 문서화를 위한 테스트 작성 - - 페이지 연동 - - ### 기능 목록 - - 회원 정보 관리 - - - 회원 가입 - - 로그인 - - 로그인 후 회원정보 조회/수정/삭제 - - 즐겨찾기 관리 - - - 즐겨찾기 추가 - - 즐겨찾기 목록조회 / 제거 - - ### 예외 사항 - - - 회원 가입 시 공백이 존재하는 경우 - - 이메일 형식 검증 - - 패스워드 확인 일치 검증 - - 로그인이 안된 경우에 회원 정보에 접근하는 경우 - - 토큰이 만료된 경우 - - 로그인 정보가 올바르지 않은 경우 - - 로그인이 안된 경우 즐겨찾기를 추가하는 경우 - - 즐겨찾기 해제 기능 \ No newline at end of file +- 즐겨찾기 해제 기능 diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 93240b1a4..7dcfd2b09 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -24,7 +24,7 @@ public WebMvcConfig( @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(bearerAuthInterceptor) - .addPathPatterns("/me", "/members", "/members/*"); + .addPathPatterns("/me", "/members", "/members/*", "/favorites", "/favorites/*"); } @Override diff --git a/src/main/java/wooteco/subway/domain/favorite/Favorite.java b/src/main/java/wooteco/subway/domain/favorite/Favorite.java new file mode 100644 index 000000000..48933dc8c --- /dev/null +++ b/src/main/java/wooteco/subway/domain/favorite/Favorite.java @@ -0,0 +1,46 @@ +package wooteco.subway.domain.favorite; + +import java.util.Objects; + +import org.springframework.data.annotation.Id; + +public class Favorite { + @Id + private Long id; + private final Long sourceId; + private final Long targetId; + + public Favorite(Long id, Long sourceId, Long targetId) { + this.id = id; + this.sourceId = sourceId; + this.targetId = targetId; + } + + public Long getId() { + return id; + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + Favorite favorite = (Favorite)o; + return Objects.equals(sourceId, favorite.sourceId) && + Objects.equals(targetId, favorite.targetId); + } + + @Override + public int hashCode() { + return Objects.hash(sourceId, targetId); + } +} diff --git a/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java b/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java new file mode 100644 index 000000000..c351e9c8f --- /dev/null +++ b/src/main/java/wooteco/subway/domain/favorite/FavoriteRepository.java @@ -0,0 +1,6 @@ +package wooteco.subway.domain.favorite; + +import org.springframework.data.repository.CrudRepository; + +public interface FavoriteRepository extends CrudRepository { +} diff --git a/src/main/java/wooteco/subway/domain/member/Member.java b/src/main/java/wooteco/subway/domain/member/Member.java index a9adbb264..240f27777 100644 --- a/src/main/java/wooteco/subway/domain/member/Member.java +++ b/src/main/java/wooteco/subway/domain/member/Member.java @@ -1,14 +1,20 @@ package wooteco.subway.domain.member; +import java.util.LinkedHashSet; +import java.util.Set; + import org.apache.commons.lang3.StringUtils; import org.springframework.data.annotation.Id; +import wooteco.subway.domain.favorite.Favorite; + public class Member { @Id private Long id; private String email; private String name; private String password; + private Set favorites = new LinkedHashSet<>(); public Member() { } @@ -26,6 +32,15 @@ public Member(Long id, String email, String name, String password) { this.password = password; } + public Member(Long id, String email, String name, String password, + Set favorites) { + this.id = id; + this.email = email; + this.name = name; + this.password = password; + this.favorites = favorites; + } + public Long getId() { return id; } @@ -38,6 +53,10 @@ public String getName() { return name; } + public Set getFavorites() { + return favorites; + } + public String getPassword() { return password; } @@ -54,4 +73,8 @@ public void update(String name, String password) { public boolean checkPassword(String password) { return this.password.equals(password); } + + public void addFavorite(Favorite favorite) { + favorites.add(favorite); + } } diff --git a/src/main/java/wooteco/subway/service/favorite/FavoriteService.java b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java new file mode 100644 index 000000000..8834a25f2 --- /dev/null +++ b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java @@ -0,0 +1,24 @@ +package wooteco.subway.service.favorite; + +import org.springframework.stereotype.Service; + +import wooteco.subway.domain.favorite.Favorite; +import wooteco.subway.domain.member.Member; +import wooteco.subway.service.favorite.dto.FavoriteRequest; +import wooteco.subway.service.member.MemberService; + +@Service +public class FavoriteService { + + private final MemberService memberService; + + public FavoriteService(MemberService memberService) { + this.memberService = memberService; + } + + public Member addToMember(Member member, FavoriteRequest request) { + Favorite favorite = request.toFavorite(); + member.addFavorite(favorite); + return memberService.save(member); + } +} diff --git a/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java new file mode 100644 index 000000000..eed6415a3 --- /dev/null +++ b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java @@ -0,0 +1,34 @@ +package wooteco.subway.service.favorite.dto; + +import javax.validation.constraints.NotNull; + +import wooteco.subway.domain.favorite.Favorite; + +public class FavoriteRequest { + + @NotNull + private Long sourceId; + + @NotNull + private Long targetId; + + public FavoriteRequest() { + } + + public FavoriteRequest(Long sourceId, Long targetId) { + this.sourceId = sourceId; + this.targetId = targetId; + } + + public Favorite toFavorite() { + return new Favorite(null, sourceId, targetId); + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } +} diff --git a/src/main/java/wooteco/subway/service/member/MemberService.java b/src/main/java/wooteco/subway/service/member/MemberService.java index 0ca1c44c1..b73dbb8c9 100644 --- a/src/main/java/wooteco/subway/service/member/MemberService.java +++ b/src/main/java/wooteco/subway/service/member/MemberService.java @@ -22,7 +22,7 @@ public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtToke this.jwtTokenProvider = jwtTokenProvider; } - public Member createMember(Member member) { + public Member save(Member member) { return memberRepository.save(member); } diff --git a/src/main/java/wooteco/subway/web/favorite/FavoriteController.java b/src/main/java/wooteco/subway/web/favorite/FavoriteController.java new file mode 100644 index 000000000..2ff02827d --- /dev/null +++ b/src/main/java/wooteco/subway/web/favorite/FavoriteController.java @@ -0,0 +1,33 @@ +package wooteco.subway.web.favorite; + +import java.net.URI; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.service.favorite.FavoriteService; +import wooteco.subway.service.favorite.dto.FavoriteRequest; +import wooteco.subway.web.member.interceptor.Auth; +import wooteco.subway.web.member.interceptor.IsAuth; +import wooteco.subway.web.member.resolver.LoginMember; + +@RestController +public class FavoriteController { + private final FavoriteService favoriteService; + + public FavoriteController(FavoriteService favoriteService) { + this.favoriteService = favoriteService; + } + + @IsAuth(isAuth = Auth.AUTH) + @PostMapping("/favorites") + public ResponseEntity create(@LoginMember Member member, + @RequestBody FavoriteRequest favoriteRequest) { + Member persistMember = favoriteService.addToMember(member, favoriteRequest); + return ResponseEntity.created(URI.create("/members/" + persistMember.getId() + "/favorites")) + .build(); + } +} diff --git a/src/main/java/wooteco/subway/web/member/controller/MemberController.java b/src/main/java/wooteco/subway/web/member/controller/MemberController.java index 274d34d62..68fc2fd44 100644 --- a/src/main/java/wooteco/subway/web/member/controller/MemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/MemberController.java @@ -40,7 +40,7 @@ public MemberController(MemberService memberService) { @PostMapping("/members") public ResponseEntity createMember(@Validated @RequestBody MemberRequest request) { - Member member = memberService.createMember(request.toMember()); + Member member = memberService.save(request.toMember()); return ResponseEntity .created(URI.create("/members/" + member.getId())) .build(); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql index b74084067..dde7d0161 100644 --- a/src/main/resources/schema.sql +++ b/src/main/resources/schema.sql @@ -38,4 +38,11 @@ create table if not exists MEMBER primary key(id) ); --- // TODO 즐겨찾기 테이블 스키마 추가 +create table if not exists FAVORITE +( + id bigint auto_increment not null unique, + member bigint not null, + source_id bigint not null, + target_id bigint not null, + primary key(id) +); \ No newline at end of file diff --git a/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java new file mode 100644 index 000000000..e5971c1a1 --- /dev/null +++ b/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java @@ -0,0 +1,78 @@ +package wooteco.subway.acceptance.favorite; + +import static org.assertj.core.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import io.restassured.RestAssured; +import wooteco.subway.AcceptanceTest; +import wooteco.subway.domain.station.Station; +import wooteco.subway.infra.JwtTokenProvider; + +public class FavoriteAcceptanceTest extends AcceptanceTest { + + private Station station1; + private Station station2; + private Station station3; + private Station station4; + + @LocalServerPort + private int port; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + private String token; + + @Override + @BeforeEach + public void setUp() { + RestAssured.port = port; + token = jwtTokenProvider.createToken(TEST_USER_EMAIL); + station1 = new Station(1L, STATION_NAME_KANGNAM); + station2 = new Station(2L, STATION_NAME_DOGOK); + station3 = new Station(3L, STATION_NAME_SEOLLEUNG); + station4 = new Station(4L, STATION_NAME_YEOKSAM); + } + + @DisplayName("즐겨찾기 관리") + @TestFactory + public Stream manageFavorite() { + return Stream.of( + DynamicTest.dynamicTest("Create Favorite", () -> { + createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); + String location = createFavorite(1L, 2L); + assertThat(location).isNotNull(); + })); + } + + private String createFavorite(Long sourceId, Long targetId) { + Map params = new HashMap<>(); + params.put("sourceId", sourceId); + params.put("targetId", targetId); + + return given(). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + cookie("token", token). + body(params). + when(). + post("/favorites"). + then(). + log().all(). + statusCode(HttpStatus.CREATED.value()). + extract().header("Location"); + } + +} diff --git a/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java b/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java new file mode 100644 index 000000000..b55dff5a1 --- /dev/null +++ b/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java @@ -0,0 +1,26 @@ +package wooteco.subway.doc; + +import static org.springframework.restdocs.headers.HeaderDocumentation.*; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static wooteco.subway.doc.ApiDocumentUtils.*; + +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; + +public class FavoriteDocumentation { + public static RestDocumentationResultHandler addFavorite() { + return document("favorites/create", + getDocumentRequest(), + getDocumentResponse(), + requestFields( + fieldWithPath("sourceId").type(JsonFieldType.STRING) + .description("The Favorite source-id").attributes(), + fieldWithPath("targetId").type(JsonFieldType.STRING).description("The Favorite targetId") + ), + responseHeaders( + headerWithName("Location").description("The favorite path location which just created") + ) + ); + } +} diff --git a/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java b/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java new file mode 100644 index 000000000..06598e637 --- /dev/null +++ b/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java @@ -0,0 +1,42 @@ +package wooteco.subway.service.favorite; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.*; +import static wooteco.subway.service.member.MemberServiceTest.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import wooteco.subway.domain.member.Member; +import wooteco.subway.service.favorite.dto.FavoriteRequest; +import wooteco.subway.service.member.MemberService; + +@ExtendWith(SpringExtension.class) +public class FavoriteServiceTest { + + @MockBean + private MemberService memberService; + + private FavoriteService favoriteService; + private Member member; + private FavoriteRequest request; + + @BeforeEach + void setUp() { + favoriteService = new FavoriteService(memberService); + member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + request = new FavoriteRequest(1L, 2L); + } + + @Test + void addToMember() { + given(memberService.save(any())).willReturn(member); + + favoriteService.addToMember(member, request); + assertThat(member.getFavorites()).hasSize(1); + } +} diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index 5160f03fd..dcc30bc6e 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -42,7 +42,7 @@ void setUp() { @Test void createMember() { Member member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); - memberService.createMember(member); + memberService.save(member); verify(memberRepository).save(any()); } diff --git a/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java b/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java new file mode 100644 index 000000000..8693488cb --- /dev/null +++ b/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java @@ -0,0 +1,82 @@ +package wooteco.subway.web.favorite; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static wooteco.subway.AcceptanceTest.*; + +import javax.servlet.http.Cookie; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.filter.ShallowEtagHeaderFilter; + +import wooteco.subway.doc.FavoriteDocumentation; +import wooteco.subway.domain.member.Member; +import wooteco.subway.infra.JwtTokenProvider; +import wooteco.subway.service.favorite.FavoriteService; +import wooteco.subway.service.member.MemberService; + +@ExtendWith(RestDocumentationExtension.class) +@SpringBootTest +@AutoConfigureMockMvc +public class FavoriteControllerTest { + + @MockBean + private FavoriteService favoriteService; + @MockBean + private MemberService memberService; + + @MockBean + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private MockMvc mockMvc; + + private Member member; + private Cookie cookie; + + @BeforeEach + public void setUp(WebApplicationContext webApplicationContext, + RestDocumentationContextProvider restDocumentation) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext) + .addFilter(new ShallowEtagHeaderFilter()) + .apply(documentationConfiguration(restDocumentation)) + .build(); + + member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + cookie = new Cookie("token", "dundung"); + } + + @Test + public void addFavorite() throws Exception { + given(memberService.save(any())).willReturn(member); + given(favoriteService.addToMember(any(), any())).willReturn(member); + + String inputJson = "{\"sourceId\":\"" + 1L + "\"," + + "\"targetId\":\"" + 2L + "\"}"; + + this.mockMvc.perform(post("/favorites") + .cookie(cookie) + .content(inputJson) + .accept(MediaType.APPLICATION_JSON) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andDo(print()) + .andDo(FavoriteDocumentation.addFavorite()); + } +} diff --git a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java index 96146a472..7550cf1bc 100644 --- a/src/test/java/wooteco/subway/web/member/MemberControllerTest.java +++ b/src/test/java/wooteco/subway/web/member/MemberControllerTest.java @@ -64,7 +64,7 @@ public void setUp(WebApplicationContext webApplicationContext, @Test public void createMember() throws Exception { - given(memberService.createMember(any())).willReturn(member); + given(memberService.save(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + @@ -83,7 +83,7 @@ public void createMember() throws Exception { @Test void createDuplicateMember() throws Exception { - given(memberService.createMember(any())).willReturn(member); + given(memberService.save(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + @@ -112,7 +112,7 @@ void createDuplicateMember() throws Exception { @Test void createNotMatchPasswordMember() throws Exception { - given(memberService.createMember(any())).willReturn(member); + given(memberService.save(any())).willReturn(member); given(memberService.isNotExistEmail(any())).willReturn(true); String inputJson = "{\"email\":\"" + TEST_USER_EMAIL + "\"," + From 7d1af94903bb3e69a7a2e70201e624d62ebac7ee Mon Sep 17 00:00:00 2001 From: dd Date: Tue, 26 May 2020 11:03:29 +0900 Subject: [PATCH 18/30] =?UTF-8?q?feat:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../subway/domain/member/MemberTest.java | 38 +++++++++++++++++++ .../service/member/MemberServiceTest.java | 2 + 2 files changed, 40 insertions(+) create mode 100644 src/test/java/wooteco/subway/domain/member/MemberTest.java diff --git a/src/test/java/wooteco/subway/domain/member/MemberTest.java b/src/test/java/wooteco/subway/domain/member/MemberTest.java new file mode 100644 index 000000000..24a8d6f62 --- /dev/null +++ b/src/test/java/wooteco/subway/domain/member/MemberTest.java @@ -0,0 +1,38 @@ +package wooteco.subway.domain.member; + +import static org.assertj.core.api.Assertions.*; +import static wooteco.subway.service.member.MemberServiceTest.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import wooteco.subway.domain.favorite.Favorite; + +class MemberTest { + private Member member; + + @BeforeEach + void setUp() { + member = new Member(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); + } + + @Test + void update() { + member.update(TEST_OTHER_USER_NAME, TEST_OTHER_USER_PASSWORD); + assertThat(member.getName()).isEqualTo(TEST_OTHER_USER_NAME); + assertThat(member.getPassword()).isEqualTo(TEST_OTHER_USER_PASSWORD); + } + + @Test + void checkPassword() { + assertThat(member.checkPassword(TEST_USER_PASSWORD)).isTrue(); + assertThat(member.checkPassword(TEST_OTHER_USER_PASSWORD)).isFalse(); + } + + @Test + void addFavorite() { + Favorite favorite = new Favorite(1L, 1L, 2L); + member.addFavorite(favorite); + assertThat(member.getFavorites()).hasSize(1); + } +} \ No newline at end of file diff --git a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java index dcc30bc6e..a83d80ec3 100644 --- a/src/test/java/wooteco/subway/service/member/MemberServiceTest.java +++ b/src/test/java/wooteco/subway/service/member/MemberServiceTest.java @@ -23,7 +23,9 @@ public class MemberServiceTest { public static final String TEST_OTHER_USER_EMAIL = "pobi@email.com"; public static final String TEST_USER_EMAIL = "brown@email.com"; public static final String TEST_USER_NAME = "브라운"; + public static final String TEST_OTHER_USER_NAME = "포비"; public static final String TEST_USER_PASSWORD = "brown"; + public static final String TEST_OTHER_USER_PASSWORD = "pobi"; public static final long TEST_USER_ID = 1L; private MemberService memberService; From f9b8ca09694b472edd1056b1fbf54bbfe3696ed9 Mon Sep 17 00:00:00 2001 From: dd Date: Tue, 26 May 2020 15:10:16 +0900 Subject: [PATCH 19/30] =?UTF-8?q?feat:=20=EC=A6=90=EA=B2=A8=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 즐겨찾기 삭제 테스트 및 구현 --- src/docs/asciidoc/api-guide.adoc | 32 ++++++++++++- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../subway/domain/favorite/Favorite.java | 11 ++++- .../wooteco/subway/domain/member/Member.java | 7 +++ .../service/favorite/FavoriteService.java | 5 +++ .../member/dto/MemberDetailResponse.java | 45 +++++++++++++++++++ .../web/favorite/FavoriteController.java | 9 ++++ .../controller/LoginMemberController.java | 7 +++ .../java/wooteco/subway/AcceptanceTest.java | 19 +++++++- .../favorite/FavoriteAcceptanceTest.java | 45 ++++++++++++++++--- .../subway/doc/FavoriteDocumentation.java | 13 +++++- .../subway/domain/member/MemberTest.java | 8 ++++ .../service/favorite/FavoriteServiceTest.java | 21 +++++++-- .../web/favorite/FavoriteControllerTest.java | 14 ++++++ 14 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java diff --git a/src/docs/asciidoc/api-guide.adoc b/src/docs/asciidoc/api-guide.adoc index 653c9e487..06b713378 100644 --- a/src/docs/asciidoc/api-guide.adoc +++ b/src/docs/asciidoc/api-guide.adoc @@ -38,16 +38,20 @@ operation::members/duplicate-create[snippets='http-request,http-response,request ===== 패스워드 불일치 operation::members/not-match-password-create[snippets='http-request,http-response,request-fields'] -[[resources-members-create]] +[[resources-members-get]] === 회원 정보 조회 +[[resources-members-get-successful]] ==== 성공 +[[resources-members-get-successful-login]] ===== 로그인 후 조회 operation::members/get[snippets='http-request,http-response'] +[[resources-members-get-fail]] ==== 실패 +[[resources-members-get-fail-not-login]] ===== 토큰이 존재하지 않음 operation::members/not-exist-get[snippets='http-request,http-response'] @@ -55,22 +59,46 @@ operation::members/not-exist-get[snippets='http-request,http-response'] [[resources-members-update]] === 회원 정보 수정 +[[resources-members-update-successful]] ==== 성공 +[[resources-members-update-successful-login]] ===== 로그인 후 올바른 패스워드로 수정 operation::members/update[snippets='http-request,http-response,request-fields'] +[[resources-members-update-fail]] ==== 실패 +[[resources-members-update-fail-not-login]] ===== 토큰이 존재하지 않음 -operation::member/not-exist-token-update[snippets='http-request, http-response, request-fields, response-fields'] +operation::members/not-exist-token-update[snippets='http-request,http-response,request-fields,response-fields'] [[resources-members-delete]] === 회원 탈퇴 +[[resources-members-delete-successful]] ==== 성공 +[[resources-members-delete-successful-login]] ===== 로그인 후 탈퇴 operation::members/delete[snippets='http-request,http-response'] +[[resources-favorites]] +== 즐겨찾기 + +[[resources-favorites-create]] +=== 즐겨찾기 추가 + +[[resources-favorites-create-successful]] +===== 성공 +operation::favorites/create[snippets='http-request,http-response,request-fields'] + +[[resources-favorites-delete]] +=== 즐겨찾기 삭제 + +[[resources-favorites-delete-successful]] +===== 성공 +operation::favorites/delete[snippets='http-request,http-response'] + + diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index 7dcfd2b09..a3c4e7a81 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -24,7 +24,7 @@ public WebMvcConfig( @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(bearerAuthInterceptor) - .addPathPatterns("/me", "/members", "/members/*", "/favorites", "/favorites/*"); + .addPathPatterns("/me", "/me/detail", "/members", "/members/*", "/favorites", "/favorites/*"); } @Override diff --git a/src/main/java/wooteco/subway/domain/favorite/Favorite.java b/src/main/java/wooteco/subway/domain/favorite/Favorite.java index 48933dc8c..ae7db5a90 100644 --- a/src/main/java/wooteco/subway/domain/favorite/Favorite.java +++ b/src/main/java/wooteco/subway/domain/favorite/Favorite.java @@ -7,8 +7,11 @@ public class Favorite { @Id private Long id; - private final Long sourceId; - private final Long targetId; + private Long sourceId; + private Long targetId; + + public Favorite() { + } public Favorite(Long id, Long sourceId, Long targetId) { this.id = id; @@ -43,4 +46,8 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(sourceId, targetId); } + + public boolean isSameId(Long id) { + return Objects.equals(this.id, id); + } } diff --git a/src/main/java/wooteco/subway/domain/member/Member.java b/src/main/java/wooteco/subway/domain/member/Member.java index 240f27777..2be02c345 100644 --- a/src/main/java/wooteco/subway/domain/member/Member.java +++ b/src/main/java/wooteco/subway/domain/member/Member.java @@ -77,4 +77,11 @@ public boolean checkPassword(String password) { public void addFavorite(Favorite favorite) { favorites.add(favorite); } + + public void deleteFavorite(Long id) { + favorites.stream() + .filter(favorite -> favorite.isSameId(id)) + .findFirst() + .ifPresent(favorites::remove); + } } diff --git a/src/main/java/wooteco/subway/service/favorite/FavoriteService.java b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java index 8834a25f2..633e13b1c 100644 --- a/src/main/java/wooteco/subway/service/favorite/FavoriteService.java +++ b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java @@ -21,4 +21,9 @@ public Member addToMember(Member member, FavoriteRequest request) { member.addFavorite(favorite); return memberService.save(member); } + + public void deleteById(Member member, Long id) { + member.deleteFavorite(id); + memberService.save(member); + } } diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java b/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java new file mode 100644 index 000000000..17f47e412 --- /dev/null +++ b/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java @@ -0,0 +1,45 @@ +package wooteco.subway.service.member.dto; + +import java.util.Set; + +import wooteco.subway.domain.favorite.Favorite; +import wooteco.subway.domain.member.Member; + +public class MemberDetailResponse { + private Long id; + private String email; + private String name; + private Set favorites; + + public MemberDetailResponse() { + } + + public MemberDetailResponse(Long id, String email, String name, + Set favorites) { + this.id = id; + this.email = email; + this.name = name; + this.favorites = favorites; + } + + public static MemberDetailResponse of(Member member) { + return new MemberDetailResponse(member.getId(), member.getEmail(), member.getName(), + member.getFavorites()); + } + + public Long getId() { + return id; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public Set getFavorites() { + return favorites; + } +} diff --git a/src/main/java/wooteco/subway/web/favorite/FavoriteController.java b/src/main/java/wooteco/subway/web/favorite/FavoriteController.java index 2ff02827d..152815753 100644 --- a/src/main/java/wooteco/subway/web/favorite/FavoriteController.java +++ b/src/main/java/wooteco/subway/web/favorite/FavoriteController.java @@ -3,6 +3,8 @@ import java.net.URI; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @@ -30,4 +32,11 @@ public ResponseEntity create(@LoginMember Member member, return ResponseEntity.created(URI.create("/members/" + persistMember.getId() + "/favorites")) .build(); } + + @IsAuth(isAuth = Auth.AUTH) + @DeleteMapping("/favorites/{id}") + public ResponseEntity delete(@LoginMember Member member, @PathVariable Long id) { + favoriteService.deleteById(member, id); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java index 98f4877ba..ba7d999f5 100644 --- a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java @@ -9,6 +9,7 @@ import wooteco.subway.domain.member.Member; import wooteco.subway.service.member.MemberService; import wooteco.subway.service.member.dto.LoginRequest; +import wooteco.subway.service.member.dto.MemberDetailResponse; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; import wooteco.subway.web.member.interceptor.Auth; @@ -34,4 +35,10 @@ public ResponseEntity login(@RequestBody LoginRequest param) { public ResponseEntity getMemberOfMineBasic(@LoginMember Member member) { return ResponseEntity.ok().body(MemberResponse.of(member)); } + + @IsAuth(isAuth = Auth.AUTH) + @GetMapping("/me/detail") + public ResponseEntity getMemberDetailOfMineBasic(@LoginMember Member member) { + return ResponseEntity.ok().body(MemberDetailResponse.of(member)); + } } diff --git a/src/test/java/wooteco/subway/AcceptanceTest.java b/src/test/java/wooteco/subway/AcceptanceTest.java index 8abe06673..a879cff1f 100644 --- a/src/test/java/wooteco/subway/AcceptanceTest.java +++ b/src/test/java/wooteco/subway/AcceptanceTest.java @@ -20,6 +20,7 @@ import wooteco.subway.service.line.dto.LineDetailResponse; import wooteco.subway.service.line.dto.LineResponse; import wooteco.subway.service.line.dto.WholeSubwayResponse; +import wooteco.subway.service.member.dto.MemberDetailResponse; import wooteco.subway.service.member.dto.MemberResponse; import wooteco.subway.service.member.dto.TokenResponse; import wooteco.subway.service.path.dto.PathResponse; @@ -291,13 +292,29 @@ public MemberResponse getMember(String email) { accept(MediaType.APPLICATION_JSON_VALUE). cookie("token", token). when(). - get("/members?email="+email). + get("/me"). then(). log().all(). statusCode(HttpStatus.OK.value()). extract().as(MemberResponse.class); } + + public MemberDetailResponse getDetailMember(String email) { + String token = jwtTokenProvider.createToken(email); + return + given(). + contentType(MediaType.APPLICATION_JSON_VALUE). + accept(MediaType.APPLICATION_JSON_VALUE). + cookie("token", token). + when(). + get("/me/detail"). + then(). + log().all(). + statusCode(HttpStatus.OK.value()). + extract().as(MemberDetailResponse.class); + } + public void updateMember(MemberResponse memberResponse, TokenResponse tokenResponse) { Map params = new HashMap<>(); params.put("name", "NEW_" + TEST_USER_NAME); diff --git a/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java b/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java index e5971c1a1..b10b6c8f8 100644 --- a/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java +++ b/src/test/java/wooteco/subway/acceptance/favorite/FavoriteAcceptanceTest.java @@ -4,6 +4,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; @@ -17,8 +18,10 @@ import io.restassured.RestAssured; import wooteco.subway.AcceptanceTest; +import wooteco.subway.domain.favorite.Favorite; import wooteco.subway.domain.station.Station; import wooteco.subway.infra.JwtTokenProvider; +import wooteco.subway.service.member.dto.MemberDetailResponse; public class FavoriteAcceptanceTest extends AcceptanceTest { @@ -33,13 +36,10 @@ public class FavoriteAcceptanceTest extends AcceptanceTest { @Autowired private JwtTokenProvider jwtTokenProvider; - private String token; - @Override @BeforeEach public void setUp() { RestAssured.port = port; - token = jwtTokenProvider.createToken(TEST_USER_EMAIL); station1 = new Station(1L, STATION_NAME_KANGNAM); station2 = new Station(2L, STATION_NAME_DOGOK); station3 = new Station(3L, STATION_NAME_SEOLLEUNG); @@ -51,13 +51,44 @@ public void setUp() { public Stream manageFavorite() { return Stream.of( DynamicTest.dynamicTest("Create Favorite", () -> { - createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, TEST_USER_PASSWORD); - String location = createFavorite(1L, 2L); + createMember(TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD, + TEST_USER_PASSWORD); + String location = createFavorite(TEST_USER_EMAIL, 1L, 2L); + String location2 = createFavorite(TEST_USER_EMAIL, 1L, 4L); assertThat(location).isNotNull(); - })); + assertThat(location2).isNotNull(); + }), + DynamicTest.dynamicTest("Read Favorite", () -> { + MemberDetailResponse memberDetailResponse = getDetailMember(TEST_USER_EMAIL); + assertThat(memberDetailResponse.getFavorites()) + .usingRecursiveFieldByFieldElementComparator() + .containsExactly(new Favorite(1L, 1L, 2L), new Favorite(2L, 1L, 4L)); + }), + DynamicTest.dynamicTest("Delete Favorite", () -> { + deleteFavorite(TEST_USER_EMAIL, 1L); + MemberDetailResponse memberDetailResponse = getDetailMember(TEST_USER_EMAIL); + Set favorites = memberDetailResponse.getFavorites(); + assertThat(favorites).hasSize(1); + assertThat(favorites).usingRecursiveFieldByFieldElementComparator() + .containsExactly(new Favorite(2L, 1L, 4L)); + }) + ); + } + + private void deleteFavorite(String email, Long favoriteId) { + String token = jwtTokenProvider.createToken(email); + given() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .accept(MediaType.APPLICATION_JSON_VALUE) + .cookie("token", token) + .when() + .delete("/favorites/" + favoriteId) + .then() + .statusCode(HttpStatus.NO_CONTENT.value()); } - private String createFavorite(Long sourceId, Long targetId) { + private String createFavorite(String email, Long sourceId, Long targetId) { + String token = jwtTokenProvider.createToken(email); Map params = new HashMap<>(); params.put("sourceId", sourceId); params.put("targetId", targetId); diff --git a/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java b/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java index b55dff5a1..d9c3867bb 100644 --- a/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java +++ b/src/test/java/wooteco/subway/doc/FavoriteDocumentation.java @@ -16,11 +16,20 @@ public static RestDocumentationResultHandler addFavorite() { requestFields( fieldWithPath("sourceId").type(JsonFieldType.STRING) .description("The Favorite source-id").attributes(), - fieldWithPath("targetId").type(JsonFieldType.STRING).description("The Favorite targetId") + fieldWithPath("targetId").type(JsonFieldType.STRING) + .description("The Favorite targetId") ), responseHeaders( - headerWithName("Location").description("The favorite path location which just created") + headerWithName("Location").description( + "The favorite path location which just created") ) ); } + + public static RestDocumentationResultHandler deleteFavorite() { + return document("favorites/delete", + getDocumentRequest(), + getDocumentResponse() + ); + } } diff --git a/src/test/java/wooteco/subway/domain/member/MemberTest.java b/src/test/java/wooteco/subway/domain/member/MemberTest.java index 24a8d6f62..3fb614fef 100644 --- a/src/test/java/wooteco/subway/domain/member/MemberTest.java +++ b/src/test/java/wooteco/subway/domain/member/MemberTest.java @@ -35,4 +35,12 @@ void addFavorite() { member.addFavorite(favorite); assertThat(member.getFavorites()).hasSize(1); } + + @Test + void deleteFavorite() { + Favorite favorite = new Favorite(1L, 1L, 2L); + member.addFavorite(favorite); + member.deleteFavorite(favorite.getId()); + assertThat(member.getFavorites()).hasSize(0); + } } \ No newline at end of file diff --git a/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java b/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java index 06598e637..36f4fa665 100644 --- a/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java +++ b/src/test/java/wooteco/subway/service/favorite/FavoriteServiceTest.java @@ -23,20 +23,33 @@ public class FavoriteServiceTest { private FavoriteService favoriteService; private Member member; - private FavoriteRequest request; + private FavoriteRequest request1; + private FavoriteRequest request2; @BeforeEach void setUp() { favoriteService = new FavoriteService(memberService); member = new Member(1L, TEST_USER_EMAIL, TEST_USER_NAME, TEST_USER_PASSWORD); - request = new FavoriteRequest(1L, 2L); + request1 = new FavoriteRequest(1L, 2L); + request2 = new FavoriteRequest(1L, 4L); } @Test void addToMember() { given(memberService.save(any())).willReturn(member); - favoriteService.addToMember(member, request); - assertThat(member.getFavorites()).hasSize(1); + favoriteService.addToMember(member, request1); + favoriteService.addToMember(member, request2); + assertThat(member.getFavorites()).hasSize(2); + } + + @Test + void deleteById() { + given(memberService.save(any())).willReturn(member); + + favoriteService.addToMember(member, request1); + + favoriteService.deleteById(member, null); + assertThat(member.getFavorites()).hasSize(0); } } diff --git a/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java b/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java index 8693488cb..3065aaff0 100644 --- a/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java +++ b/src/test/java/wooteco/subway/web/favorite/FavoriteControllerTest.java @@ -38,6 +38,7 @@ public class FavoriteControllerTest { @MockBean private FavoriteService favoriteService; + @MockBean private MemberService memberService; @@ -79,4 +80,17 @@ public void addFavorite() throws Exception { .andDo(print()) .andDo(FavoriteDocumentation.addFavorite()); } + + @Test + void deleteFavorite() throws Exception { + given(memberService.save(member)).willReturn(member); + + this.mockMvc.perform(delete("/favorites/1") + .cookie(cookie) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNoContent()) + .andDo(print()) + .andDo(FavoriteDocumentation.deleteFavorite()); + } } From fa01d6aeed112c929c33646c7f14f8dd009e8368 Mon Sep 17 00:00:00 2001 From: dd Date: Tue, 26 May 2020 20:29:43 +0900 Subject: [PATCH 20/30] =?UTF-8?q?feat:=20=EC=A6=90=EA=B2=A8=20=EC=B0=BE?= =?UTF-8?q?=EA=B8=B0=20=ED=94=84=EB=A1=A0=ED=8A=B8=20=EC=97=B0=EB=8F=99=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wooteco/subway/config/WebMvcConfig.java | 2 +- .../subway/domain/favorite/Favorite.java | 4 ++ .../wooteco/subway/domain/member/Member.java | 4 ++ .../domain/station/StationRepository.java | 6 +- .../DuplicatedFavoriteException.java | 7 +++ .../exception/NotFoundStationException.java | 7 +++ .../service/favorite/FavoriteService.java | 40 ++++++++++++- .../favorite/dto/FavoriteDetailResponse.java | 38 ++++++++++++ .../service/favorite/dto/FavoriteRequest.java | 30 ++++------ .../favorite/dto/FavoriteResponse.java | 32 ++++++++++ .../member/dto/MemberDetailResponse.java | 7 ++- .../service/station/StationService.java | 22 +++++-- .../controller/LoginMemberController.java | 18 +++++- src/main/resources/data.sql | 31 +++++++++- .../resources/static/service/api/index.js | 16 +++++ .../static/service/js/views/Favorite.js | 53 +++++++++++++---- .../static/service/js/views/Search.js | 31 +++++++++- .../static/service/lib/snackbar/index.js | 10 ++++ .../static/service/utils/constants.js | 25 +++++--- .../static/service/utils/templates.js | 30 +++++++++- .../resources/templates/service/favorite.html | 59 ++++++++++++------- .../resources/templates/service/index.html | 3 + .../resources/templates/service/search.html | 25 ++++---- 23 files changed, 409 insertions(+), 91 deletions(-) create mode 100644 src/main/java/wooteco/subway/exception/DuplicatedFavoriteException.java create mode 100644 src/main/java/wooteco/subway/exception/NotFoundStationException.java create mode 100644 src/main/java/wooteco/subway/service/favorite/dto/FavoriteDetailResponse.java create mode 100644 src/main/java/wooteco/subway/service/favorite/dto/FavoriteResponse.java create mode 100644 src/main/resources/static/service/lib/snackbar/index.js diff --git a/src/main/java/wooteco/subway/config/WebMvcConfig.java b/src/main/java/wooteco/subway/config/WebMvcConfig.java index a3c4e7a81..37bd1d0e1 100644 --- a/src/main/java/wooteco/subway/config/WebMvcConfig.java +++ b/src/main/java/wooteco/subway/config/WebMvcConfig.java @@ -24,7 +24,7 @@ public WebMvcConfig( @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(bearerAuthInterceptor) - .addPathPatterns("/me", "/me/detail", "/members", "/members/*", "/favorites", "/favorites/*"); + .addPathPatterns("/me", "/me/detail","/me/favorites", "/members", "/members/*", "/favorites", "/favorites/*"); } @Override diff --git a/src/main/java/wooteco/subway/domain/favorite/Favorite.java b/src/main/java/wooteco/subway/domain/favorite/Favorite.java index ae7db5a90..d8f698e52 100644 --- a/src/main/java/wooteco/subway/domain/favorite/Favorite.java +++ b/src/main/java/wooteco/subway/domain/favorite/Favorite.java @@ -19,6 +19,10 @@ public Favorite(Long id, Long sourceId, Long targetId) { this.targetId = targetId; } + public static Favorite of(Long sourceId, Long targetId) { + return new Favorite(null, sourceId, targetId); + } + public Long getId() { return id; } diff --git a/src/main/java/wooteco/subway/domain/member/Member.java b/src/main/java/wooteco/subway/domain/member/Member.java index 2be02c345..2604cc02b 100644 --- a/src/main/java/wooteco/subway/domain/member/Member.java +++ b/src/main/java/wooteco/subway/domain/member/Member.java @@ -7,6 +7,7 @@ import org.springframework.data.annotation.Id; import wooteco.subway.domain.favorite.Favorite; +import wooteco.subway.exception.DuplicatedFavoriteException; public class Member { @Id @@ -75,6 +76,9 @@ public boolean checkPassword(String password) { } public void addFavorite(Favorite favorite) { + if (favorites.contains(favorite)) { + throw new DuplicatedFavoriteException("해당 경로는 이미 추가되어 있습니다."); + } favorites.add(favorite); } diff --git a/src/main/java/wooteco/subway/domain/station/StationRepository.java b/src/main/java/wooteco/subway/domain/station/StationRepository.java index 93c2d56cb..2a3d33fb8 100644 --- a/src/main/java/wooteco/subway/domain/station/StationRepository.java +++ b/src/main/java/wooteco/subway/domain/station/StationRepository.java @@ -1,12 +1,12 @@ package wooteco.subway.domain.station; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.query.Param; -import java.util.List; -import java.util.Optional; - public interface StationRepository extends CrudRepository { @Override List findAllById(Iterable ids); diff --git a/src/main/java/wooteco/subway/exception/DuplicatedFavoriteException.java b/src/main/java/wooteco/subway/exception/DuplicatedFavoriteException.java new file mode 100644 index 000000000..5c431f94e --- /dev/null +++ b/src/main/java/wooteco/subway/exception/DuplicatedFavoriteException.java @@ -0,0 +1,7 @@ +package wooteco.subway.exception; + +public class DuplicatedFavoriteException extends RuntimeException { + public DuplicatedFavoriteException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/exception/NotFoundStationException.java b/src/main/java/wooteco/subway/exception/NotFoundStationException.java new file mode 100644 index 000000000..12a573d51 --- /dev/null +++ b/src/main/java/wooteco/subway/exception/NotFoundStationException.java @@ -0,0 +1,7 @@ +package wooteco.subway.exception; + +public class NotFoundStationException extends RuntimeException { + public NotFoundStationException(String message) { + super(message); + } +} diff --git a/src/main/java/wooteco/subway/service/favorite/FavoriteService.java b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java index 633e13b1c..6c9b12daf 100644 --- a/src/main/java/wooteco/subway/service/favorite/FavoriteService.java +++ b/src/main/java/wooteco/subway/service/favorite/FavoriteService.java @@ -1,23 +1,35 @@ package wooteco.subway.service.favorite; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + import org.springframework.stereotype.Service; import wooteco.subway.domain.favorite.Favorite; import wooteco.subway.domain.member.Member; +import wooteco.subway.domain.station.Station; +import wooteco.subway.service.favorite.dto.FavoriteDetailResponse; import wooteco.subway.service.favorite.dto.FavoriteRequest; import wooteco.subway.service.member.MemberService; +import wooteco.subway.service.station.StationService; @Service public class FavoriteService { private final MemberService memberService; + private final StationService stationService; - public FavoriteService(MemberService memberService) { + public FavoriteService(MemberService memberService, + StationService stationService) { this.memberService = memberService; + this.stationService = stationService; } public Member addToMember(Member member, FavoriteRequest request) { - Favorite favorite = request.toFavorite(); + Favorite favorite = getFavorite(request); member.addFavorite(favorite); return memberService.save(member); } @@ -26,4 +38,28 @@ public void deleteById(Member member, Long id) { member.deleteFavorite(id); memberService.save(member); } + + private Favorite getFavorite(FavoriteRequest request) { + Long sourceId = stationService.findStationIdByName(request.getSourceName()); + Long targetId = stationService.findStationIdByName(request.getTargetName()); + + return Favorite.of(sourceId, targetId); + } + + public Set getAll(Member member) { + Set favorites = member.getFavorites(); + Set ids = new HashSet<>(); + for (Favorite favorite : favorites) { + ids.add(favorite.getSourceId()); + ids.add(favorite.getTargetId()); + } + List stations = stationService.findAllById(ids); + Map idToName = stations.stream() + .collect(Collectors.toMap(Station::getId, Station::getName)); + + return favorites.stream() + .map(favorite -> new FavoriteDetailResponse(favorite.getId(), favorite.getSourceId(), + favorite.getTargetId(), idToName.get(favorite.getSourceId()), idToName.get(favorite.getTargetId()))) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/wooteco/subway/service/favorite/dto/FavoriteDetailResponse.java b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteDetailResponse.java new file mode 100644 index 000000000..06152783b --- /dev/null +++ b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteDetailResponse.java @@ -0,0 +1,38 @@ +package wooteco.subway.service.favorite.dto; + +public class FavoriteDetailResponse { + private Long id; + private Long sourceId; + private Long targetId; + private String sourceName; + private String targetName; + + public FavoriteDetailResponse(Long id, Long sourceId, Long targetId, String sourceName, + String targetName) { + this.id = id; + this.sourceId = sourceId; + this.targetId = targetId; + this.sourceName = sourceName; + this.targetName = targetName; + } + + public Long getId() { + return id; + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } + + public String getSourceName() { + return sourceName; + } + + public String getTargetName() { + return targetName; + } +} diff --git a/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java index eed6415a3..3067a0ea9 100644 --- a/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java +++ b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteRequest.java @@ -1,34 +1,28 @@ package wooteco.subway.service.favorite.dto; -import javax.validation.constraints.NotNull; - -import wooteco.subway.domain.favorite.Favorite; +import javax.validation.constraints.NotBlank; public class FavoriteRequest { - @NotNull - private Long sourceId; + @NotBlank + private String sourceName; - @NotNull - private Long targetId; + @NotBlank + private String targetName; public FavoriteRequest() { } - public FavoriteRequest(Long sourceId, Long targetId) { - this.sourceId = sourceId; - this.targetId = targetId; - } - - public Favorite toFavorite() { - return new Favorite(null, sourceId, targetId); + public FavoriteRequest(String sourceName, String targetName) { + this.sourceName = sourceName; + this.targetName = targetName; } - public Long getSourceId() { - return sourceId; + public String getSourceName() { + return sourceName; } - public Long getTargetId() { - return targetId; + public String getTargetName() { + return targetName; } } diff --git a/src/main/java/wooteco/subway/service/favorite/dto/FavoriteResponse.java b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteResponse.java new file mode 100644 index 000000000..82206a98a --- /dev/null +++ b/src/main/java/wooteco/subway/service/favorite/dto/FavoriteResponse.java @@ -0,0 +1,32 @@ +package wooteco.subway.service.favorite.dto; + +import java.util.Set; +import java.util.stream.Collectors; + +import wooteco.subway.domain.favorite.Favorite; + +public class FavoriteResponse { + private Long sourceId; + private Long targetId; + + public FavoriteResponse(Long sourceId, Long targetId) { + this.sourceId = sourceId; + this.targetId = targetId; + } + + public static FavoriteResponse of(Favorite favorite) { + return new FavoriteResponse(favorite.getSourceId(), favorite.getTargetId()); + } + + public static Set setOf(Set favorites) { + return favorites.stream().map(FavoriteResponse::of).collect(Collectors.toSet()); + } + + public Long getSourceId() { + return sourceId; + } + + public Long getTargetId() { + return targetId; + } +} diff --git a/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java b/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java index 17f47e412..659358b57 100644 --- a/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java +++ b/src/main/java/wooteco/subway/service/member/dto/MemberDetailResponse.java @@ -4,12 +4,13 @@ import wooteco.subway.domain.favorite.Favorite; import wooteco.subway.domain.member.Member; +import wooteco.subway.service.favorite.dto.FavoriteResponse; public class MemberDetailResponse { private Long id; private String email; private String name; - private Set favorites; + private Set favorites; public MemberDetailResponse() { } @@ -19,7 +20,7 @@ public MemberDetailResponse(Long id, String email, String name, this.id = id; this.email = email; this.name = name; - this.favorites = favorites; + this.favorites = FavoriteResponse.setOf(favorites); } public static MemberDetailResponse of(Member member) { @@ -39,7 +40,7 @@ public String getName() { return name; } - public Set getFavorites() { + public Set getFavorites() { return favorites; } } diff --git a/src/main/java/wooteco/subway/service/station/StationService.java b/src/main/java/wooteco/subway/service/station/StationService.java index a2d6d83da..ba7d3f294 100644 --- a/src/main/java/wooteco/subway/service/station/StationService.java +++ b/src/main/java/wooteco/subway/service/station/StationService.java @@ -1,18 +1,22 @@ package wooteco.subway.service.station; +import java.util.Collection; +import java.util.List; + import org.springframework.stereotype.Service; -import wooteco.subway.service.line.LineStationService; + import wooteco.subway.domain.station.Station; import wooteco.subway.domain.station.StationRepository; - -import java.util.List; +import wooteco.subway.exception.NotFoundStationException; +import wooteco.subway.service.line.LineStationService; @Service public class StationService { private LineStationService lineStationService; private StationRepository stationRepository; - public StationService(LineStationService lineStationService, StationRepository stationRepository) { + public StationService(LineStationService lineStationService, + StationRepository stationRepository) { this.lineStationService = lineStationService; this.stationRepository = stationRepository; } @@ -29,4 +33,14 @@ public void deleteStationById(Long id) { lineStationService.deleteLineStationByStationId(id); stationRepository.deleteById(id); } + + public Long findStationIdByName(String name) { + return stationRepository.findByName(name) + .map(Station::getId) + .orElseThrow(() -> new NotFoundStationException(name + "역을 찾을 수 없습니다.")); + } + + public List findAllById(Collection ids) { + return stationRepository.findAllById(ids); + } } diff --git a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java index ba7d999f5..c94605736 100644 --- a/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java +++ b/src/main/java/wooteco/subway/web/member/controller/LoginMemberController.java @@ -1,5 +1,7 @@ package wooteco.subway.web.member.controller; +import java.util.Set; + import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -7,6 +9,8 @@ import org.springframework.web.bind.annotation.RestController; import wooteco.subway.domain.member.Member; +import wooteco.subway.service.favorite.FavoriteService; +import wooteco.subway.service.favorite.dto.FavoriteDetailResponse; import wooteco.subway.service.member.MemberService; import wooteco.subway.service.member.dto.LoginRequest; import wooteco.subway.service.member.dto.MemberDetailResponse; @@ -18,10 +22,13 @@ @RestController public class LoginMemberController { - private MemberService memberService; + private final MemberService memberService; + private final FavoriteService favoriteService; - public LoginMemberController(MemberService memberService) { + public LoginMemberController(MemberService memberService, + FavoriteService favoriteService) { this.memberService = memberService; + this.favoriteService = favoriteService; } @PostMapping("/login") @@ -41,4 +48,11 @@ public ResponseEntity getMemberOfMineBasic(@LoginMember Member m public ResponseEntity getMemberDetailOfMineBasic(@LoginMember Member member) { return ResponseEntity.ok().body(MemberDetailResponse.of(member)); } + + @IsAuth(isAuth = Auth.AUTH) + @GetMapping("/me/favorites") + public ResponseEntity> getMemberFavorites(@LoginMember Member member) { + Set responses = favoriteService.getAll(member); + return ResponseEntity.ok().body(responses); + } } diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql index d58d4ff50..b4c36e683 100644 --- a/src/main/resources/data.sql +++ b/src/main/resources/data.sql @@ -1,2 +1,31 @@ INSERT INTO member (email, name, password) -VALUES ('dd@email.com', 'dd', 123); \ No newline at end of file +VALUES ('dd@email.com', 'dd', 123); + +insert into station (name) +VALUES ('잠실'), + ('잠실새내'), + ('종합운동장'), + ('삼전'), + ('석촌고분'), + ('석촌'), + ('부산'), + ('대구'); + +insert into line (name, start_time, end_time, interval_time) +VALUES ('2호선', current_time, current_time, 3), + ('9호선', current_time, current_time, 3), + ('8호선', current_time, current_time, 3), + ('ktx', current_time, current_time, 3); + +insert into line_station (line, station_id, pre_station_id, distance, duration) +VALUES (1, 1, null, 0, 0), + (1, 2, 1, 10, 1), + (1, 3, 2, 10, 1), + (2, 3, null, 0, 0), + (2, 4, 3, 10, 1), + (2, 5, 4, 1, 10), + (2, 6, 5, 1, 10), + (3, 1, null, 0, 0), + (3, 6, 1, 1, 10), + (4, 7, null, 10, 10), + (4, 8, 7, 10, 10); \ No newline at end of file diff --git a/src/main/resources/static/service/api/index.js b/src/main/resources/static/service/api/index.js index b64973c77..31af89fef 100644 --- a/src/main/resources/static/service/api/index.js +++ b/src/main/resources/static/service/api/index.js @@ -65,10 +65,26 @@ const api = (() => { } } + const favorite = { + create(favoritePath) { + return request(`/favorites`, METHOD.POST(favoritePath)); + }, + get(id) { + return requestWithJsonData(`/favorites/${id}`); + }, + getAll() { + return requestWithJsonData(`/me/favorites`); + }, + delete(id) { + return request(`/favorites/${id}`, METHOD.DELETE()); + } + }; + return { line, path, member, + favorite, } })() diff --git a/src/main/resources/static/service/js/views/Favorite.js b/src/main/resources/static/service/js/views/Favorite.js index 8368bc827..6b8bbf5e5 100644 --- a/src/main/resources/static/service/js/views/Favorite.js +++ b/src/main/resources/static/service/js/views/Favorite.js @@ -1,18 +1,49 @@ -import { edgeItemTemplate } from '../../utils/templates.js' -import { defaultFavorites } from '../../utils/subwayMockData.js' +import { edgeItemTemplate } from "../../utils/templates.js"; +import api from "../../api/index.js"; +import { ERROR_MESSAGE, EVENT_TYPE, SUCCESS_MESSAGE } from "../../utils/constants.js"; function Favorite() { - const $favoriteList = document.querySelector('#favorite-list') + const $favoriteList = document.querySelector("#favorite-list"); - const loadFavoriteList = () => { - const template = defaultFavorites.map(edge => edgeItemTemplate(edge)).join('') - $favoriteList.insertAdjacentHTML('beforeend', template) - } + const initFavoriteList = async () => { + try { + const template = await api.favorite + .getAll() + .then(favorites => favorites.map(edge => edgeItemTemplate(edge)).join("")); + $favoriteList.innerHTML = template; + } + catch (e) { + alert(ERROR_MESSAGE.COMMON) + } + + }; + + const onDeleteHandler = async event => { + const $target = event.target; + const isDeleteButton = $target.classList.contains("mdi-delete"); + if (!isDeleteButton) { + return; + } + try { + const edgeId = $target.closest(".edge-item").dataset.edgeId; + await api.favorite.delete(edgeId); + await initFavoriteList(); + alert(SUCCESS_MESSAGE.COMMON); + } + catch (e) { + alert(ERROR_MESSAGE.COMMON); + } + }; + + const initEventListener = () => { + $favoriteList.addEventListener(EVENT_TYPE.CLICK, onDeleteHandler); + }; this.init = () => { - loadFavoriteList() - } + initFavoriteList(); + initEventListener(); + }; } -const favorite = new Favorite() -favorite.init() +const favorite = new Favorite(); +favorite.init(); diff --git a/src/main/resources/static/service/js/views/Search.js b/src/main/resources/static/service/js/views/Search.js index 45d42b747..dedc9cf94 100644 --- a/src/main/resources/static/service/js/views/Search.js +++ b/src/main/resources/static/service/js/views/Search.js @@ -1,7 +1,7 @@ -import { EVENT_TYPE } from '../../utils/constants.js' +import { ERROR_MESSAGE, EVENT_TYPE, PATH_TYPE } from '../../utils/constants.js' import api from '../../api/index.js' import { searchResultTemplate } from '../../utils/templates.js' -import { PATH_TYPE, ERROR_MESSAGE } from '../../utils/constants.js' +import { getCookie } from '../../utils/loginUtils.js'; function Search() { const $departureStationName = document.querySelector('#departure-station-name') @@ -49,7 +49,32 @@ function Search() { const onToggleFavorite = event => { event.preventDefault() - const isFavorite = $favoriteButton.classList.contains('mdi-star') + + if (!getCookie()) { + alert("로그인 먼저 해주세요."); + window.location="/login"; + return; + } + + const data = { + sourceName: $departureStationName.value, + targetName: $arrivalStationName.value + } + + api.favorite.create(data) + .then(response => { + if (!response.ok) { + throw response; + } + alert("등록되었습니다."); + }).catch(response => response.json()) + .then(error => { + if (error) { + alert(error.message); + return; + } + }); + const isFavorite = $favoriteButton.classList.contains('mdi-star'); const classList = $favoriteButton.classList if (isFavorite) { diff --git a/src/main/resources/static/service/lib/snackbar/index.js b/src/main/resources/static/service/lib/snackbar/index.js new file mode 100644 index 000000000..20c3070f7 --- /dev/null +++ b/src/main/resources/static/service/lib/snackbar/index.js @@ -0,0 +1,10 @@ +import Snackbar from "./snackbar.js"; + +export default function showSnackbar(message) { + Snackbar.show({ + text: message, + pos: "bottom-center", + showAction: false, + duration: 2000 + }); +} diff --git a/src/main/resources/static/service/utils/constants.js b/src/main/resources/static/service/utils/constants.js index ffae44e11..2d39fc49d 100644 --- a/src/main/resources/static/service/utils/constants.js +++ b/src/main/resources/static/service/utils/constants.js @@ -1,13 +1,22 @@ export const EVENT_TYPE = { - CLICK: 'click', - KEY_PRESS: 'keypress', -} + CLICK: "click", + KEY_PRESS: "keypress" +}; + +export const SUCCESS_MESSAGE = { + COMMON: "😁 성공적으로 변경되었습니다.", + SAVE: "😁 정보가 반영되었습니다..", + FAVORITE: "😁 즐겨찾기에 추가하였습니다." +}; export const ERROR_MESSAGE = { - LOGIN_FAIL: '😭 로그인이 실패했습니다. 다시 시도해주세요.' -} + COMMON: "🕵️‍♂️ 에러가 발생했습니다.", + PASSWORD_CHECK: "🕵️‍♂️ 패스워드가 일치하지 않습니다.", + LOGIN_FAIL: "😭 로그인이 실패했습니다. 다시 시도해주세요.", + JOIN_FAIL: "😭 회원가입이 실패했습니다. " +}; export const PATH_TYPE = { - DISTANCE: 'DISTANCE', - DURATION: 'DURATION' -} + DISTANCE: "DISTANCE", + DURATION: "DURATION" +}; \ No newline at end of file diff --git a/src/main/resources/static/service/utils/templates.js b/src/main/resources/static/service/utils/templates.js index 2e81c15bb..19a2c8276 100644 --- a/src/main/resources/static/service/utils/templates.js +++ b/src/main/resources/static/service/utils/templates.js @@ -58,12 +58,18 @@ export const navLoginTemplate = `` export const subwayLinesItemTemplate = line => { - const stations = line.stations ? line.stations.map(station => listItemTemplate(station)).join('') : null + const stations = line.stations ? line.stations.map(station => listItemTemplate(station)) + .join('') : null return `
${line.name}
@@ -76,7 +82,9 @@ export const subwayLinesItemTemplate = line => { export const searchResultTemplate = result => { const lastIndex = result.stations.length - 1 - const pathResultTemplate = result.stations.map((station, index) => pathStationTemplate(station.name, index, lastIndex)).join('') + const pathResultTemplate = result.stations.map((station, index) => pathStationTemplate(station.name, + index, + lastIndex)).join('') return `
@@ -117,3 +125,21 @@ export const initNavigation = () => { export const initLoginNavigation = () => { document.querySelector('body').insertAdjacentHTML('afterBegin', navLoginTemplate) } + +export const edgeItemTemplate = edge => { + return `
  • + + ${ + edge.sourceName ? edge.sourceName : "출발역" + } + + ${ + edge.targetName ? edge.targetName : "도착역" + } + +
  • `; +}; diff --git a/src/main/resources/templates/service/favorite.html b/src/main/resources/templates/service/favorite.html index d05f1f655..47a788e2f 100644 --- a/src/main/resources/templates/service/favorite.html +++ b/src/main/resources/templates/service/favorite.html @@ -1,26 +1,43 @@ - - - RunningMap - - - - - - - -
    -
    -
    -
    즐겨찾기
    -
    -
      -
      -
      + + + RunningMap + + + + + + + + + +
      +
      +
      +
      즐겨찾기
      +
      +
        - - - +
        +
        + + + + diff --git a/src/main/resources/templates/service/index.html b/src/main/resources/templates/service/index.html index bbf74b279..19cae15e1 100644 --- a/src/main/resources/templates/service/index.html +++ b/src/main/resources/templates/service/index.html @@ -37,6 +37,9 @@
      • 경로 검색
      • +
      • + 즐겨 찾기 +
      • diff --git a/src/main/resources/templates/service/search.html b/src/main/resources/templates/service/search.html index e0924309c..01702b2a3 100644 --- a/src/main/resources/templates/service/search.html +++ b/src/main/resources/templates/service/search.html @@ -8,6 +8,7 @@ +
        @@ -17,11 +18,11 @@
        @@ -30,11 +31,11 @@
        @@ -46,8 +47,8 @@