Skip to content

[Spring Core] 김준수 미션제출합니다. #78

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

Open
wants to merge 25 commits into
base: gogo1414
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
602e91e
쿠키를 이용한 로그인 구현
gogo1414 Jun 24, 2024
48d0d15
인증 정보 조회 구현
gogo1414 Jun 24, 2024
0d71c01
로그인 리팩터링을 통해 예약 생성 기능 변경
gogo1414 Jun 24, 2024
6c42437
3단계 HandlerInterceptor를 활용한 관리자 기능
gogo1414 Jun 24, 2024
ad519f4
코드 리뷰 반영
gogo1414 Jun 26, 2024
8b35084
secret key 감추기
gogo1414 Jun 26, 2024
5b15940
secret key 감추기2
gogo1414 Jun 26, 2024
8ca9829
인증 정보 안보이게 하기
gogo1414 Jun 26, 2024
1da7036
4단계 - JPA 전환 요구사항 분석
gogo1414 Jul 1, 2024
0774c67
요구사항 테스트 작성
gogo1414 Jul 1, 2024
eeeb47b
의존성 주입, 엔티티 매핑, 연관관계 매핑
gogo1414 Jul 1, 2024
7e59314
요구사항 분석
gogo1414 Jul 1, 2024
4a7b23c
dao -> repository 변경, 예약 목록 조회 일부 구현
gogo1414 Jul 1, 2024
9bd1774
5단계 완성
gogo1414 Jul 1, 2024
2d8065c
예약정보에 회원아이디 삽입
gogo1414 Jul 1, 2024
9f47682
예약 대기 생성
gogo1414 Jul 1, 2024
98e0144
예약 대기 생성 - 대기 번호
gogo1414 Jul 1, 2024
dd60a5e
예약 목록 조회 및 예약 대기 취소
gogo1414 Jul 1, 2024
7b02d1d
6단계 테스트 통과
gogo1414 Jul 1, 2024
a67c4b4
어노테이션 생성
gogo1414 Jul 5, 2024
cece4f1
7단계 요구사항 분석
gogo1414 Jul 12, 2024
ad71882
계층 분리 및 7단계 요구사항 해결
gogo1414 Jul 12, 2024
1bc5de1
8단계 요구사항 분석
gogo1414 Jul 12, 2024
068672c
Environment 분리 테스트 해결
gogo1414 Jul 12, 2024
4170a10
스키마 대신 데이터베이스 초기화 로직 클래스 구현
gogo1414 Jul 12, 2024
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
Binary file added .DS_Store
Binary file not shown.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/

### application.properties 안보이게 만들기 ###
application.properties

### STS ###
.apt_generated
.classpath
Expand Down
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Spring MVC 요구사항 분석
### 1단계 - 로그인
- 로그인 기능을 구현하세요.
- 로그인 후 Cookie를 이용하여 사용자의 정보를 조회하는 API를 구현하세요.
#### 로그인 기능
- 아래의 request와 response 요구사항에 따라 /login에 email, password 값을 body에 포함하세요.
- 응답에 Cookie에 "token"값으로 토큰이 포함되도록 하세요.
#### 인증 정보 조회
- 상단바 우측 로그인 상태를 표현해주기 위해 사용자의 정보를 조회하는 API를 구현하세요.
- Cookie를 이용하여 로그인 사용자의 정보확인하세요.

### 2단계 - 로그인 리팩터링
- 사용자의 정보를 조회하는 로직을 리팩터링 합니다.
- 예약 생성 API 및 기능을 리팩터링 합니다.
#### 로그인 리팩터링
- Cookie에 담긴 인증 정보를 이용해서 멤버 객체를 만드는 로직을 분리합니다.
- HandlerMethodArgumentResolver을 활용하면 회원정보를 객체를 컨트롤러 메서드에 주입할 수 있습니다.
#### 예약 생성 기능 변경
- 예약 생성 시 ReservationReqeust의 name이 없는 경우 Cookie에 담긴 정보를 활용하도록 리팩터링 합니다.
- ReservationReqeust에 name값이 있으면 name으로 Member를 찾고
- 없으며 로그인 정보를 활용해서 Member를 찾도록 수정합니다.

### 3단계 - 관리자 기능
- 어드민 페이지 진입은 admin권한이 있는 사람만 할 수 있도록 제한하세요.
- HandlerInterceptor를 활용하여 권한이 없는 경우 401코드를 응답하세요.

## Spring JPA 요구사항 분석
### 4단계 - JPA 전환
- JPA를 활용하여 데이터베이스에 접근하도록 수정하세요.

#### gradle 의존성 추가
- build.gradle 파일을 이용하여 다음 의존성을 대체하세요.
- as is: `spring-boot-stater-jdbc` to be: `spring-boot-starter-data-jpa`

#### 엔티티 매핑
- 다른 클래스를 의존하지 않는 클래스 먼저 엔티티 설정을 하세요.
- ex) Theme나 Time 등

#### 연관관계 매핑
- 다른 클래스에 의존하는 클래스는 연관관계 매핑을 추가로 하세요.
- ex) Reservation은 Member나 Theme 등의 객체에 의존합니다.

### 5단계 - 내 예약 목록 조회
- 내 예약 목록을 조회하는 API를 구현하세요.
#### 내 예약 목록 기능
- request와 response 요구사항에 따라 기능을 구현하세요.

### 6단계 - 예약 대기 기능
- 예약 대기 요청 기능을 구현하세요.
- 예약 대기 취소 기능도 함께 구현하세요.
- 내 예약 목록 조회 시 예약 대기 목록도 함께 포함하세요.
- 중복 예약이 불가능 하도록 구현하세요.
#### 예약 대기 요청
#### 내 예약 목록에서 조회 & 예약 대기 취소

## Spring Core (배포) 요구사항 분석
### 7단계 - @Configuration
- JWT 관련 로직을 roomescape와 같은 계층의 auth 패키지의 클래스로 분리하세요.
- 불필요한 DB 접근을 최소화 하세요.

### 8단계 - Profile과 Resource
- schema.sql 대신 데이터베이스를 초기화 해주기 위해 실행하는 클래스를 만드세요.
- 스프링이 실행될 때 동작해야 합니다.
- token 생성에 필요한 비밀키 값을 외부 파일로 분리하세요.
#### 세부 요구사항
- Production용과 DataLoader와 Test용 TestDataLoader를 만드세요.
- DataLoader에서는 사용자 정보만 초기화
- TestDataLoader에서는 테스트에 필요한 사전 값 초기화
- Environemt 분리
- token 생성에 필요한 비밀키값을 application.properties 파일로 이동하세요.
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/roomescape/DataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package roomescape;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;

@Profile("default")
@Component
public class DataLoader implements CommandLineRunner {

private MemberRepository memberRepository;

public DataLoader(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

@Override
public void run(String... args) throws Exception {
Member admin = memberRepository.save(new Member("어드민", "[email protected]", "password", "ADMIN"));
Member testUser1 = memberRepository.save(new Member("브라운", "[email protected]", "password", "USER"));
}
}
9 changes: 9 additions & 0 deletions src/main/java/roomescape/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package roomescape;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories
@Configuration
public class JpaConfig {
}
1 change: 1 addition & 0 deletions src/main/java/roomescape/RoomescapeApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
public class RoomescapeApplication {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/roomescape/TestDataLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package roomescape;

import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.reservation.Reservation;
import roomescape.reservation.ReservationRepository;
import roomescape.theme.Theme;
import roomescape.theme.ThemeRepository;
import roomescape.time.Time;
import roomescape.time.TimeRepository;

@Profile("test")
@Component
public class TestDataLoader implements CommandLineRunner {

private TimeRepository timeRepository;
private ThemeRepository themeRepository;
private ReservationRepository reservationRepository;
private MemberRepository memberRepository;

public TestDataLoader(TimeRepository timeRepository,
ThemeRepository themeRepository,
ReservationRepository reservationRepository,
MemberRepository memberRepository) {
this.timeRepository = timeRepository;
this.themeRepository = themeRepository;
this.reservationRepository = reservationRepository;
this.memberRepository = memberRepository;
}

@Override
public void run(String... args) throws Exception {
Member admin = memberRepository.save(new Member("어드민", "[email protected]", "password", "ADMIN"));
Member testUser1 = memberRepository.save(new Member("브라운", "[email protected]", "password", "USER"));

final Theme theme1 = themeRepository.save(new Theme("테마1", "테마1입니다."));
final Theme theme2 = themeRepository.save(new Theme("테마2", "테마2입니다."));
final Theme theme3 = themeRepository.save(new Theme("테마3", "테마3입니다."));

final Time time1 = timeRepository.save(new Time("10:00"));
final Time time2 = timeRepository.save(new Time("12:00"));
final Time time3 = timeRepository.save(new Time("14:00"));
final Time time4 = timeRepository.save(new Time("16:00"));
final Time time5 = timeRepository.save(new Time("18:00"));
final Time time6 = timeRepository.save(new Time("20:00"));

reservationRepository.save(new Reservation("어드민", "2024-03-01", time1, theme1, admin));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time2, theme2, admin));
reservationRepository.save(new Reservation("어드민", "2024-03-01", time3, theme3, admin));
}
}
31 changes: 31 additions & 0 deletions src/main/java/roomescape/auth/AuthAdminInteceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.auth;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.member.Member;
import roomescape.member.MemberService;

@Component
public class AuthAdminInteceptor implements HandlerInterceptor {
private final MemberService memberService;
private final JwtUtils jwtUtils;

public AuthAdminInteceptor(MemberService memberService, JwtUtils jwtUtils) {
this.memberService = memberService;
this.jwtUtils = jwtUtils;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("--------- : " + request.getCookies());
Long memberId = jwtUtils.getPayload(request.getCookies());
Member member = memberService.findMemberById(memberId);
if (member == null || !member.getRole().equals("ADMIN")) {
response.setStatus(401);
return false;
}
return true;
}
}
13 changes: 13 additions & 0 deletions src/main/java/roomescape/auth/AuthConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package roomescape.auth;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AuthConfig {

@Bean
public JwtUtils jwtUtils() {
return new JwtUtils();
}
}
50 changes: 50 additions & 0 deletions src/main/java/roomescape/auth/AuthMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package roomescape.auth;

import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
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 roomescape.member.LoginMember;
import roomescape.member.Member;
import roomescape.member.MemberService;

@Component
public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private MemberService memberService;
@Autowired
private JwtUtils jwtUtils;

private final String INVALID_MEMBERID = "회원 아이디를 찾을 수 없습니다.";
private final String INVALID_MEMBER = "유효하지 않은 회원 정보입니다.";

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(AuthSession.class)
&& parameter.getParameterType().equals(LoginMember.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();

Long memberId = jwtUtils.getPayload(httpServletRequest.getCookies());
if(memberId == null) {
throw new IllegalArgumentException(INVALID_MEMBERID);
}

Member member = memberService.findMemberById(memberId);
if(member == null) {
throw new IllegalArgumentException(INVALID_MEMBER);
}

return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole());
}
}
40 changes: 40 additions & 0 deletions src/main/java/roomescape/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import org.springframework.stereotype.Service;
import roomescape.member.Member;
import roomescape.member.MemberRepository;
import roomescape.auth.jwt.TokenRequest;
import roomescape.auth.jwt.TokenResponse;

@Service
public class AuthService {
private final String INVALID_MEMBER_MSG = "존재하지 않는 email 또는 password 입니다.";

private final JwtUtils jwtUtils;
private final MemberRepository memberRepository;

public AuthService(JwtUtils jwtUtils, MemberRepository memberRepository) {
this.jwtUtils = jwtUtils;
this.memberRepository = memberRepository;
}

public Member checkInvalidLogin(String principal, String credentials) {
Member member = memberRepository.findByEmailAndPassword(principal, credentials);
if(member == null) {
throw new AuthorizationException(INVALID_MEMBER_MSG);
}

return member;
}

public Long findMemberIdByToken(Cookie[] cookies) {
return jwtUtils.getPayload(cookies);
}

public TokenResponse createToken(TokenRequest tokenRequest) {
Member member = checkInvalidLogin(tokenRequest.getEmail(), tokenRequest.getPassword());
String accessToken = jwtUtils.createToken(member);
return new TokenResponse(accessToken);
}
}
11 changes: 11 additions & 0 deletions src/main/java/roomescape/auth/AuthSession.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package roomescape.auth;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface AuthSession {
}
28 changes: 28 additions & 0 deletions src/main/java/roomescape/auth/AuthWebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package roomescape.auth;

import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AuthWebConfig implements WebMvcConfigurer {
private final AuthMemberArgumentResolver authMemberArgumentResolver;
private final AuthAdminInteceptor authAdminInteceptor;

public AuthWebConfig(AuthMemberArgumentResolver authMemberArgumentResolver, AuthAdminInteceptor authAdminInteceptor) {
this.authMemberArgumentResolver = authMemberArgumentResolver;
this.authAdminInteceptor = authAdminInteceptor;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(authMemberArgumentResolver);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authAdminInteceptor).addPathPatterns("/admin");
}
}
14 changes: 14 additions & 0 deletions src/main/java/roomescape/auth/AuthorizationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package roomescape.auth;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.UNAUTHORIZED)
public class AuthorizationException extends RuntimeException {
public AuthorizationException() {
}

public AuthorizationException(String message) {
super(message);
}
}
Loading