Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[라빈] API 테스트/문서자동화 미션 제출합니다 #9

Merged
merged 31 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4ce99ce
feat: 로그인 프로세스 실습 구현
giantim May 19, 2020
7522f8f
feat: 프론트 병합
giantim May 20, 2020
661639d
docs: 1단계 요구사항 및 기능 목록 작성
giantim May 20, 2020
37fc95d
feat: 로그인 인수 테스트 작성
giantim May 20, 2020
a839826
feat: Request 검증 로직 추가
giantim May 20, 2020
7083e60
feat: 중복 email 검사 및 에러 메시지 출력 기능 구현
giantim May 20, 2020
5dca144
feat: 로그인 한 회원의 정보 조회 기능 구현
giantim May 21, 2020
3d5e6f3
feat: 로그인 되어있는 회원의 정보 수정 기능 구현
giantim May 21, 2020
74ec47a
feat: 회원 탈퇴 기능 구현
giantim May 21, 2020
95b39d3
feat: 로그아웃 기능 구현
giantim May 21, 2020
ceb95b6
refactor: 예외 사항에 대한 커스텀 익셉션 생성
giantim May 21, 2020
6443efb
feat: 로그인 상태에서 로그인 / 회원가입 링크 노출 안하게 변경
giantim May 21, 2020
07a9e84
feat: 회원 정보 업데이트 api 문서화
giantim May 21, 2020
22dfdf2
refactor: gradle 오류 수정
giantim May 21, 2020
eae5df8
feat: 로그인 api 문서화 구현
giantim May 21, 2020
736e257
refactor: 사용하지 않는 인증방식 메서드 및 클래스 삭제
giantim May 22, 2020
461f9e0
test: LoginMemberAcceptanceTest, 수정 삭제에 대한 인수 테스트 작성
kouz95 May 22, 2020
84e7533
chore: front 병합
kouz95 May 22, 2020
7630144
docs: 2단계 기능 목록 작성
kouz95 May 22, 2020
8e00f2d
test: 즐겨찾기 추가 인수 테스트 작성
kouz95 May 22, 2020
f81becb
test: 즐겨찾기 조회, 삭제 인수 테스트 작성
kouz95 May 22, 2020
fb49ac0
test: FavoriteControllerTest 작성
kouz95 May 22, 2020
dcc429e
refactor: Transactional 추가, createMember() email 중복 검사 추가
kouz95 May 25, 2020
d51f574
feat: FavoriteService create 구현 및 테스트 작성
giantim May 25, 2020
05bc60a
feat: 중복된 Favorite를 생성시 예외 처리 구현
giantim May 25, 2020
af4e3f2
feat: 즐겨찾기 삭제 기능 구현
giantim May 25, 2020
c0c3ba5
feat: FavoriteService.findAll() 구현
kouz95 May 25, 2020
a9ae838
feat: 즐겨찾기 스키마 작성 및 즐겨찾기 인터셉터 등록, 테스트 수정
kouz95 May 25, 2020
fe18718
feat: 프론트 연결
giantim May 26, 2020
0803372
feat: 즐겨찾기 api 문서화
kouz95 May 26, 2020
12593b7
refactor: 리뷰 반영
giantim May 31, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# 1단계 - 회원관리 기능

## 요구 사항
- 회원 정보를 관리하는 기능 구현
- 자신의 정보만 수정 가능하도록 해야하며 로그인이 선행되어야 함
- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가
- side case에 대한 예외처리
- 인수 테스트와 단위 테스트 작성
- API 문서를 작성하고 문서화를 위한 테스트 작성
- 페이지 연동

## 기능 목록
1. 회원 가입
- [x] password / password 확인의 값이 같지 않다면 api 를 호출하지 않는다
- [x] email, name, password 를 담은 request 를 보낸다
- [x] 각 값에 대한 유효성을 서버에서 검사한다
- [x] 중복된 email 이 입력되었는지 검사한다
- [x] 정보가 유효하다면 회원을 생성한다
- [x] 실행 결과를 alert 를 이용해서 출력한다

2. 로그인
- [x] email / password 의 값을 담아서 request 를 보낸다
- [x] 각 값에 대한 유효성을 검사한다
- [x] 유효한 회원이라면 토큰을 발행해서 로그인 상태를 유지한다

3. 로그인 후 회원정보 조회 / 수정 / 삭제
- 조회
- [x] 로그인이 되어있지 않다면 로그인 화면으로 연결한다.
- [x] 나의 정보를 조회 할때, 토큰을 검사해야 한다.
- [x] 이메일과 이름을 표시한다.

- 수정(jwt 있어야 가능하다)
- [x] email 은 수정할 수 없다
- [x] name / password 만 수정 가능하다

- 탈퇴
- [x] jwt 토큰을 복호화한 email 로 member 를 찾는다
- [x] 해당 member 를 삭제하고 jwt 값을 비운다

4. 로그아웃
- [x] 클라이언트의 localStorage 의 jwt 값 비우고 메인 화면으로 이동

5. 뷰 수정
- [x] 로그인 되어있으면 로그인 -> 로그아웃 / 회원가입 노출 안함

# 2단계 - 즐겨찾기 기능



## 요구사항

- 즐겨찾기 기능을 추가(추가,삭제,조회)
- 자신의 정보만 수정 가능하도록 해야하며 **로그인이 선행**되어야 함
- 토큰의 유효성 검사와 본인 여부를 판단하는 로직 추가(interceptor, argument resolver)
- side case에 대한 예외처리 필수
- 인수 테스트와 단위 테스트 작성
- API 문서를 작성하고 문서화를 위한 테스트 작성
- 페이지 연동



## 기능 목록

### 1. 즐겨찾기 추가

- [x] 로그인이 선행된 상태에서 즐겨찾기를 추가할 수 있다
- [x] 즐겨찾기 버튼을 누를 때 토큰을 검사한다
- [x] 즐겨찾기 버튼을 누르면 해당 유저의 즐겨찾기 목록에 추가한다.

### 2. 즐겨찾기 목록조회 / 제거

- [x] 즐겨찾기 페이지에 접속하면, 해당 유저의 즐겨찾기 목록을 출력한다.
- [x] 즐겨 찾기 페이지는 로그인후 접속 가능하다.
- [x] 즐겨찾기를 삭제하면, 해당 유저의 즐겨 찾기 목록에서 제거한다.
21 changes: 20 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
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'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
Expand All @@ -19,13 +19,32 @@ dependencies {
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'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:2.0.4.RELEASE'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc:2.0.4.RELEASE'
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'
}

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'
}
}
36 changes: 34 additions & 2 deletions src/docs/asciidoc/api-guide.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ endif::[]
:sectlinks:
:operation-http-request-title: Example Request
:operation-http-response-title: Example Response

[[resources]]
= Resources

Expand All @@ -19,4 +18,37 @@ endif::[]
[[resources-members-create]]
=== 회원 가입

operation::members/create[snippets='http-request,http-response']
operation::members/create[snippets='http-request,http-response,request-fields']

[[resources-oauth-token-bearer]]
=== 로그인

operation::oauth/token/bearer[snippets='http-request,request-fields,http-response,response-fields']

[[resources-me-bearer-update]]
=== 회원 정보 수정

operation::me/bearer/update[snippets='http-request,request-headers,request-body,request-fields']

[[resources-me-bearer-delete]]
=== 회원 탈퇴

operation::me/bearer/delete[snippets='http-request,request-headers']

[[resources-favorites]]
== Favorite

[[resources-favorites-create]]
=== 즐겨찾기 생성

operation::favorites/create[snippets='http-request,request-headers,request-fields,http-response,response-headers']

[[resources-favorites-show]]
=== 즐겨찾기 조회

operation::favorites/show[snippets='http-request,request-headers,http-response,response-fields']

[[resources-favorites-delete]]
=== 즐겨찾기 삭제

operation::favorites/delete[snippets='http-request,request-headers,request-fields']
18 changes: 5 additions & 13 deletions src/main/java/wooteco/subway/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,29 @@
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.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;

public WebMvcConfig(BasicAuthInterceptor basicAuthInterceptor,
SessionInterceptor sessionInterceptor,
BearerAuthInterceptor bearerAuthInterceptor,
public WebMvcConfig(BearerAuthInterceptor bearerAuthInterceptor,
LoginMemberMethodArgumentResolver loginMemberArgumentResolver) {
this.basicAuthInterceptor = basicAuthInterceptor;
this.sessionInterceptor = sessionInterceptor;
this.bearerAuthInterceptor = bearerAuthInterceptor;
this.loginMemberArgumentResolver = loginMemberArgumentResolver;
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthInterceptor).addPathPatterns("/me/basic");
registry.addInterceptor(sessionInterceptor).addPathPatterns("/me/session");
registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/me/bearer");
registry.addInterceptor(bearerAuthInterceptor).addPathPatterns("/favorites");
}

@Override
Expand Down
51 changes: 51 additions & 0 deletions src/main/java/wooteco/subway/domain/favorite/Favorite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package wooteco.subway.domain.favorite;

import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import wooteco.subway.service.favorite.dto.FavoriteRequest;

public class Favorite {
@Id
private Long id;
private Long memberId;
private Long departureId;
private Long arrivalId;

public Favorite(Long memberId, Long departureId, Long arrivalId) {
this(null, memberId, departureId, arrivalId);
}

@PersistenceConstructor
public Favorite(Long id, Long memberId, Long departureId, Long arrivalId) {
this.id = id;
this.memberId = memberId;
this.departureId = departureId;
this.arrivalId = arrivalId;
}

public static Favorite of(Long memberId, FavoriteRequest favoriteRequest) {
return new Favorite(memberId, favoriteRequest.getDepartureId(), favoriteRequest.getArrivalId());
}

public boolean isDuplicate(Favorite favorite) {
return this.memberId.equals(favorite.memberId)
&& this.departureId.equals(favorite.departureId)
&& this.arrivalId.equals(favorite.arrivalId);
}

public Long getId() {
return id;
}

public Long getMemberId() {
return memberId;
}

public Long getDepartureId() {
return departureId;
}

public Long getArrivalId() {
return arrivalId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wooteco.subway.domain.favorite;

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 FavoriteRepository extends CrudRepository<Favorite, Long> {
@Query("SELECT * FROM favorite WHERE member_id = :memberId")
List<Favorite> findAllByMemberId(@Param("memberId") Long memberId);

@Query("SELECT * FROM favorite " +
"WHERE member_id = :memberId AND departure_id = :departureId AND arrival_id = :arrivalId")
Optional<Favorite> findByMemberIdAndDepartureIdAndArrivalId(@Param("memberId") Long memberId,
@Param("departureId") Long departureId,
@Param("arrivalId") Long arrivalId);
}
3 changes: 3 additions & 0 deletions src/main/java/wooteco/subway/domain/member/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.annotation.Id;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;

public class Member {
@Id
private Long id;
Expand Down
66 changes: 66 additions & 0 deletions src/main/java/wooteco/subway/service/favorite/FavoriteService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package wooteco.subway.service.favorite;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import wooteco.subway.domain.favorite.Favorite;
import wooteco.subway.domain.favorite.FavoriteRepository;
import wooteco.subway.domain.member.MemberRepository;
import wooteco.subway.domain.station.StationRepository;
import wooteco.subway.service.favorite.dto.FavoriteRequest;
import wooteco.subway.service.favorite.dto.FavoriteResponse;
import wooteco.subway.service.favorite.exception.DuplicateFavoriteException;
import wooteco.subway.service.favorite.exception.NoExistFavoriteException;
import wooteco.subway.service.member.exception.InvalidMemberIdException;
import wooteco.subway.service.station.exception.InvalidStationNameException;

import java.util.List;

@Service
public class FavoriteService {
private final MemberRepository memberRepository;
private final StationRepository stationRepository;
private final FavoriteRepository favoriteRepository;

public FavoriteService(MemberRepository memberRepository, StationRepository stationRepository,
FavoriteRepository favoriteRepository) {
this.memberRepository = memberRepository;
this.stationRepository = stationRepository;
this.favoriteRepository = favoriteRepository;
}

@Transactional
public Long create(Long memberId, FavoriteRequest favoriteRequest) {
memberRepository.findById(memberId).orElseThrow(InvalidMemberIdException::new);
stationRepository.findById(favoriteRequest.getDepartureId()).orElseThrow(InvalidStationNameException::new);
stationRepository.findById(favoriteRequest.getArrivalId()).orElseThrow(InvalidStationNameException::new);

Favorite favorite = Favorite.of(memberId, favoriteRequest);

if (isDuplicateFavorite(memberId, favorite)) {
throw new DuplicateFavoriteException();
}

return favoriteRepository.save(favorite).getId();
}

private boolean isDuplicateFavorite(Long memberId, Favorite favorite) {
return favoriteRepository.findAllByMemberId(memberId)
.stream()
.anyMatch(f -> f.isDuplicate(favorite));
}

@Transactional
public void delete(Long memberId, FavoriteRequest favoriteRequest) {
Favorite favorite = favoriteRepository.findByMemberIdAndDepartureIdAndArrivalId(memberId,
favoriteRequest.getDepartureId(), favoriteRequest.getArrivalId())
.orElseThrow(NoExistFavoriteException::new);

favoriteRepository.delete(favorite);
}

@Transactional(readOnly = true)
public List<FavoriteResponse> findAll(Long memberId) {
List<Favorite> favorites = favoriteRepository.findAllByMemberId(memberId);
return FavoriteResponse.listOf(favorites);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package wooteco.subway.service.favorite.dto;

import javax.validation.constraints.NotNull;

public class FavoriteRequest {
@NotNull
private Long departureId;
@NotNull
private Long arrivalId;

private FavoriteRequest() {
}

public FavoriteRequest(Long departureId, Long arrivalId) {
this.departureId = departureId;
this.arrivalId = arrivalId;
}

public Long getDepartureId() {
return departureId;
}

public Long getArrivalId() {
return arrivalId;
}
}
Loading