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

🚀 3단계 - 즐겨찾기 기능 구현 #558

Open
wants to merge 25 commits into
base: hellonayeon
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6f87f59
Docs: 즐겨찾기 기능 인수 조건 도출
hellonayeon Aug 27, 2023
5867587
Chore: 테스트코드 외부 요청 컨트롤러 패키지 분리 (external 패키지 생성)
hellonayeon Aug 27, 2023
410cd62
Chore: 테스트용 properties 파일 이동 (/src/test)
hellonayeon Aug 27, 2023
7ee3362
Test: 즐겨찾기 기능 인수테스트 Fixture 생성
hellonayeon Aug 27, 2023
6e27bd4
Docs: 즐겨찾기 인수 조건 README.md 패키지 분리
hellonayeon Aug 27, 2023
59b729a
Docs: 인수 조건 오타 수정
hellonayeon Aug 31, 2023
1e135e7
Build: Validation 의존성 추가
hellonayeon Aug 31, 2023
2b4fdb5
Feature: Favorite Domain, Request/Response DTO 작성
hellonayeon Aug 31, 2023
99d2393
Feature: Server Exception Code, Handler 정의 (Bean Validation Handling)
hellonayeon Aug 31, 2023
888fa4f
Test: 즐겨찾기 인수 테스트 작성
hellonayeon Aug 31, 2023
37d70ce
Feat: 즐겨찾기 도메인 Entity, JoinColumn name 설정
hellonayeon Sep 2, 2023
b6eedcf
Test: 회원, 비회원 요청 Steps 분리, 목록 조회 검증 조건 수정
hellonayeon Sep 3, 2023
1e83088
Feat: TokenArgumentResolver Authorization null 검증 추가
hellonayeon Sep 3, 2023
c0b44d1
Feat: 즐겨찾기 API 구현
hellonayeon Sep 3, 2023
2ead078
Refactor: ArgumentResolver Header Authorization 검증 책임연쇄패턴 적용
hellonayeon Sep 3, 2023
6480e5e
Feat: 경로를 찾을 수 없는 경우 Exception 발생
hellonayeon Sep 3, 2023
f6bcc99
Feat: 즐겨찾기 생성 전 경로 조회 로직 추가
hellonayeon Sep 3, 2023
b3c7fb2
Docs: 비정상 경로를 즐겨찾기로 등록하는 경우의 인수 조건 도출
hellonayeon Sep 3, 2023
f086990
Test: 비정상 경로 즐겨찾기 등록 인수테스트 작성
hellonayeon Sep 3, 2023
0451bfa
Chore: 즐겨찾기 조회 -> 즐겨찾기 목록 조회 워딩 수정, 불필요한 즐겨찾기 Steps 제거
hellonayeon Sep 3, 2023
3b48f34
Fix: 인수 조건과 tc when 주석 통일 시키기
hellonayeon Sep 14, 2023
8a0c8e8
Fix: for-if 를 사용하여 검증하는 로직 제거
hellonayeon Sep 14, 2023
eda7832
Fix: 엔티티 기본 생성자 protected 로 선언
hellonayeon Sep 14, 2023
502ec5c
Fix: FavoriteService .save() 사용 메서드 @Transactional 적용
hellonayeon Sep 14, 2023
943ace5
Style: 트랜잭션 확인을 위한 임시 System.out.println 제거
hellonayeon Sep 17, 2023
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ dependencies {
implementation 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation:2.7.3'

runtimeOnly 'com.h2database:h2'
}

Expand Down
12 changes: 6 additions & 6 deletions src/main/java/nextstep/auth/config/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package nextstep.auth.config;

import lombok.RequiredArgsConstructor;
import nextstep.auth.principal.AuthenticationPrincipalArgumentResolver;
import nextstep.auth.token.JwtTokenProvider;
import nextstep.auth.validation.AuthorizationValidatorGroup;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class AuthConfig implements WebMvcConfigurer {
private JwtTokenProvider jwtTokenProvider;

public AuthConfig(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
private final JwtTokenProvider jwtTokenProvider;
private final AuthorizationValidatorGroup authorizationValidatorGroup;

@Override
public void addArgumentResolvers(List argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver(jwtTokenProvider));
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver(jwtTokenProvider, authorizationValidatorGroup));
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package nextstep.auth.principal;

import nextstep.auth.exception.AuthenticationException;
import lombok.RequiredArgsConstructor;
import nextstep.auth.token.JwtTokenProvider;
import nextstep.auth.validation.AuthorizationValidatorGroup;
import org.springframework.core.MethodParameter;
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;

@RequiredArgsConstructor
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
private JwtTokenProvider jwtTokenProvider;

public AuthenticationPrincipalArgumentResolver(JwtTokenProvider jwtTokenProvider) {
this.jwtTokenProvider = jwtTokenProvider;
}
private final JwtTokenProvider jwtTokenProvider;
private final AuthorizationValidatorGroup authorizationValidatorGroup;

@Override
public boolean supportsParameter(MethodParameter parameter) {
Expand All @@ -23,15 +22,9 @@ public boolean supportsParameter(MethodParameter parameter) {
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
String authorization = webRequest.getHeader("Authorization");
if (!"bearer".equalsIgnoreCase(authorization.split(" ")[0])) {
throw new AuthenticationException();
}
String token = authorization.split(" ")[1];

if (!jwtTokenProvider.validateToken(token)) {
throw new AuthenticationException();
}
authorizationValidatorGroup.execute(authorization);

String token = authorization.split(" ")[1];
String username = jwtTokenProvider.getPrincipal(token);
String role = jwtTokenProvider.getRoles(token);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nextstep.auth.validation;

import nextstep.auth.exception.AuthenticationException;
import org.springframework.stereotype.Component;

@Component
public class AuthorizationBearerStringValidator extends AuthorizationValidator {

@Override
protected void validate(String authorization) {
String authPrefix = authorization.split(" ")[0];

if (!"bearer".equalsIgnoreCase(authPrefix)) {
throw new AuthenticationException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nextstep.auth.validation;

import nextstep.auth.exception.AuthenticationException;
import org.springframework.stereotype.Component;

@Component
public class AuthorizationNullValidator extends AuthorizationValidator {

@Override
protected void validate(String authorization) {
if (authorization == null) {
throw new AuthenticationException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package nextstep.auth.validation;

import lombok.RequiredArgsConstructor;
import nextstep.auth.exception.AuthenticationException;
import nextstep.auth.token.JwtTokenProvider;
import org.springframework.stereotype.Component;

@RequiredArgsConstructor
@Component
public class AuthorizationTokenValidator extends AuthorizationValidator {

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void validate(String authorization) {
String token = authorization.split(" ")[1];

if (!jwtTokenProvider.validateToken(token)) {
throw new AuthenticationException();
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/nextstep/auth/validation/AuthorizationValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package nextstep.auth.validation;

public abstract class AuthorizationValidator {

private AuthorizationValidator nextValidator = null;

public AuthorizationValidator setNext(AuthorizationValidator validator) {
this.nextValidator = validator;
return validator;
}

protected abstract void validate(String authorization);

public void execute(String authorization) {
validate(authorization);

if (nextValidator != null)
nextValidator.execute(authorization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package nextstep.auth.validation;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AuthorizationValidatorGroup {

private final AuthorizationNullValidator authorizationNullValidator;
private final AuthorizationBearerStringValidator authorizationBearerStringValidator;
private final AuthorizationTokenValidator authorizationTokenValidator;

private void chaining() {
authorizationNullValidator
.setNext(authorizationBearerStringValidator)
.setNext(authorizationTokenValidator);
}

public void execute(String authorization) {
chaining();
authorizationNullValidator.execute(authorization);
}
}
2 changes: 2 additions & 0 deletions src/main/java/nextstep/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ public Member(String email, String password, Integer age, String role) {
this.role = role;
}



public Long getId() {
return id;
}
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/nextstep/subway/applicaion/FavoriteService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package nextstep.subway.applicaion;

import java.util.List;
import lombok.RequiredArgsConstructor;
import nextstep.subway.applicaion.dto.FavoriteRequest;
import nextstep.subway.domain.Favorite;
import nextstep.subway.domain.FavoriteRepository;
import nextstep.subway.domain.Station;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class FavoriteService {

private final FavoriteRepository favoriteRepository;
private final StationService stationService;
private final PathService pathService;


public List<Favorite> getFavorites(Long memberId) {
return favoriteRepository.findAllByMemberId(memberId);
}

public Favorite getFavorite(Long id) {
return favoriteRepository.findById(id)
.orElseThrow(IllegalArgumentException::new);
}

@Transactional
public Favorite createFavorite(Long memberId, FavoriteRequest request) {
pathService.findPath(request.getSource(), request.getTarget());

Station source = stationService.findById(request.getSource());
Station target = stationService.findById(request.getTarget());
return favoriteRepository.save(new Favorite(source, target, memberId));
}

public void deleteFavorite(Long id) {
favoriteRepository.deleteById(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Service
@Transactional(readOnly = true)
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/nextstep/subway/applicaion/dto/FavoriteRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package nextstep.subway.applicaion.dto;

import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public class FavoriteRequest {

@NotNull(message = "출발역은 반드시 입력해야합니다.")
private Long source;

@NotNull(message = "도착역은 반드시 입력해야합니다.")
private Long target;

}
27 changes: 27 additions & 0 deletions src/main/java/nextstep/subway/applicaion/dto/FavoriteResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package nextstep.subway.applicaion.dto;

import java.util.List;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.Getter;
import nextstep.subway.domain.Favorite;

@Getter
@AllArgsConstructor
public class FavoriteResponse {
private Long id;
private StationResponse source;
private StationResponse target;

public static FavoriteResponse of(Favorite favorite) {
return new FavoriteResponse(favorite.getId(),
StationResponse.of(favorite.getSource()),
StationResponse.of(favorite.getTarget()));
}

public static List<FavoriteResponse> listOf(List<Favorite> favorites) {
return favorites.stream()
.map(FavoriteResponse::of)
.collect(Collectors.toList());
}
}
40 changes: 40 additions & 0 deletions src/main/java/nextstep/subway/domain/Favorite.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package nextstep.subway.domain;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import lombok.Getter;

@Entity
@Getter
public class Favorite {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "source_id")
private Station source;

@OneToOne(cascade = CascadeType.PERSIST)
@JoinColumn(name = "target_id")
private Station target;

@JoinColumn(name = "member_id")
private Long memberId;

protected Favorite() {

}

public Favorite(Station source, Station target, Long memberId) {
this.source = source;
this.target = target;
this.memberId = memberId;
}
}
10 changes: 10 additions & 0 deletions src/main/java/nextstep/subway/domain/FavoriteRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package nextstep.subway.domain;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FavoriteRepository extends JpaRepository<Favorite, Long> {

List<Favorite> findAllByMemberId(Long id);

}
2 changes: 1 addition & 1 deletion src/main/java/nextstep/subway/domain/Line.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class Line {
@Embedded
private Sections sections = new Sections();

public Line() {
protected Line() {
}

public Line(String name, String color) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextstep/subway/domain/Section.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class Section extends DefaultWeightedEdge {

private int distance;

public Section() {
protected Section() {

}

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextstep/subway/domain/Sections.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public class Sections {
@OneToMany(mappedBy = "line", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
private List<Section> sections = new ArrayList<>();

public Sections() {
protected Sections() {
}

public Sections(List<Section> sections) {
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/nextstep/subway/domain/Station.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class Station {
private Long id;
private String name;

public Station() {
protected Station() {
}

public Station(String name) {
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/nextstep/subway/domain/SubwayMap.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package nextstep.subway.domain;

import java.util.Optional;
import org.jgrapht.GraphPath;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.SimpleDirectedWeightedGraph;
Expand Down Expand Up @@ -45,7 +46,8 @@ public Path findPath(Station source, Station target) {

// 다익스트라 최단 경로 찾기
DijkstraShortestPath<Station, SectionEdge> dijkstraShortestPath = new DijkstraShortestPath<>(graph);
GraphPath<Station, SectionEdge> result = dijkstraShortestPath.getPath(source, target);
GraphPath<Station, SectionEdge> result = Optional.ofNullable(dijkstraShortestPath.getPath(source, target))
.orElseThrow(IllegalArgumentException::new);

List<Section> sections = result.getEdgeList().stream()
.map(it -> it.getSection())
Expand Down
Loading