Skip to content

Commit

Permalink
Merge pull request #9 from giantim/feature/favorite
Browse files Browse the repository at this point in the history
[라빈] API 테스트/문서자동화 미션 제출합니다
  • Loading branch information
jihan805 authored Jun 2, 2020
2 parents 63a6117 + 12593b7 commit 3983563
Show file tree
Hide file tree
Showing 73 changed files with 2,590 additions and 465 deletions.
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

0 comments on commit 3983563

Please sign in to comment.