Skip to content

[Spring MVC(인증)] 권장순 미션 제출합니다. #489

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

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dca0905
feat(step1) : 1단계 - 홈 화면
jsoonworld May 27, 2025
96ce9ef
feat(step2) : 2단계 - 예약 조회
jsoonworld May 27, 2025
6629969
feat(step3) : 3단계 - 예약 추가 / 취소
jsoonworld May 27, 2025
5b5b097
feat(step4) : 4단계 - 예외 처리
jsoonworld May 29, 2025
c21871a
refactor(review) : 피드백 반영
jsoonworld Jun 2, 2025
a6663c6
feat(jdbc) : 5단계 - 데이터베이스 적용하기
jsoonworld Jun 5, 2025
0f3595f
feat(jdbc) : 6단계 - 데이터 조회하기
jsoonworld Jun 5, 2025
01f4d7e
feat(jdbc) : 7단계 - 데이터 추가/삭제하기
jsoonworld Jun 5, 2025
a585671
fix(JdbcTemplateReservationRepository) : SQL 저장 시 Date, Time 명시적 타입 적용
jsoonworld Jun 5, 2025
482a7ed
refactor(MVC) : 피드백 반영
jsoonworld Jun 7, 2025
c27aff1
refactor(MissionStepTest) : 테스트 메서드 이름을 역할 기반으로 변경하고 DisplayName 추가
jsoonworld Jun 7, 2025
f350c55
refactor(repository) : repository , service 계층 책임 개선
jsoonworld Jun 16, 2025
5a6428d
refactor(service): 서비스와 리포지토리 역할 분리 및 네이밍 개선
jsoonworld Jun 16, 2025
7454741
fix(db): 스키마 데이터 타입 최적화
jsoonworld Jun 16, 2025
2b14936
refactor : 시간 포맷 개선
jsoonworld Jun 16, 2025
c1342fc
fix(web): API 시간 포맷 수정 및 페이지 렌더링 오류 해결
jsoonworld Jun 16, 2025
3a65a6d
Merge branch 'spring-jdbc' into jsoonworld
jsoonworld Jun 17, 2025
912fc5d
merge(spring-jdbc) : merge pre step
jsoonworld Jun 17, 2025
0bbfde8
feat(step8) : 시간 관리 기능
jsoonworld Jun 17, 2025
66fd8cf
feat(step9) : 9단계 - 기존 코드 수정
jsoonworld Jun 18, 2025
4e4c075
feat(step10) : 10단계 - 계층화 리팩터링
jsoonworld Jun 20, 2025
c41d4a2
refactor(review) : 1차 피드백 반영
jsoonworld Jun 23, 2025
9ccb3b0
Merge branch 'jsoonworld' into jsoonworld
jsoonworld Jun 23, 2025
9d4f01a
refactor(review) : 1차 피드백 반영
jsoonworld Jun 23, 2025
005d297
chore(conflict) : 불필요한 파일 삭제
jsoonworld Jun 23, 2025
e70b782
refactor(review) : 2차 피드백 반영
jsoonworld Jun 24, 2025
eeca6e5
feat(auth) : 1단계 - 로그인
jsoonworld Jun 26, 2025
22e2cf4
feat(resolver) : 2단계 - 로그인 리팩터링
jsoonworld Jun 26, 2025
ede072d
feat(step3) : 3단계 - 관리자 기능
jsoonworld Jun 27, 2025
e0e7dfc
Merge branch 'jsoonworld' into feature/auth
jsoonworld Jun 28, 2025
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'

implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured:5.3.1'
testImplementation 'org.hamcrest:hamcrest:2.2'
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/roomescape/auth/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import roomescape.domain.Member;
import roomescape.domain.MemberRepository;
import roomescape.util.JwtUtil;

import java.util.Arrays;
import java.util.Optional;

public class AdminInterceptor implements HandlerInterceptor {

private final JwtUtil jwtUtil;
private final MemberRepository memberRepository;

public AdminInterceptor(JwtUtil jwtUtil, MemberRepository memberRepository) {
this.jwtUtil = jwtUtil;
this.memberRepository = memberRepository;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Optional<String> token = extractToken(request.getCookies());
if (token.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

Long memberId = jwtUtil.getMemberIdFromToken(token.get());
Optional<Member> member = memberRepository.findById(memberId);

if (member.isEmpty() || !"ADMIN".equals(member.get().getRole())) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}

return true;
}

private Optional<String> extractToken(Cookie[] cookies) {
if (cookies == null) {
return Optional.empty();
}
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst();
}
}
64 changes: 64 additions & 0 deletions src/main/java/roomescape/auth/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package roomescape.auth;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
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;
import roomescape.domain.Member;
import roomescape.domain.MemberRepository;
import roomescape.dto.LoginMember;
import roomescape.util.JwtUtil;

import java.util.Arrays;
import java.util.Optional;

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {

private final JwtUtil jwtUtil;
private final MemberRepository memberRepository;

public LoginMemberArgumentResolver(JwtUtil jwtUtil, MemberRepository memberRepository) {
this.jwtUtil = jwtUtil;
this.memberRepository = memberRepository;
}

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

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}

Optional<String> token = extractToken(request.getCookies());
if (token.isEmpty()) {
return null;
}

Long memberId = jwtUtil.getMemberIdFromToken(token.get());
Member member = memberRepository.findById(memberId).orElse(null);

if (member == null) {
return null;
}
return new LoginMember(member.getId(), member.getName(), member.getRole());
}

private Optional<String> extractToken(Cookie[] cookies) {
if (cookies == null) {
return Optional.empty();
}
return Arrays.stream(cookies)
.filter(cookie -> "token".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst();
}
}
35 changes: 35 additions & 0 deletions src/main/java/roomescape/config/WebConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package roomescape.config;

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;
import roomescape.domain.MemberRepository;
import roomescape.auth.AdminInterceptor;
import roomescape.util.JwtUtil;
import roomescape.auth.LoginMemberArgumentResolver;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {

private final JwtUtil jwtUtil;
private final MemberRepository memberRepository;

public WebConfig(JwtUtil jwtUtil, MemberRepository memberRepository) {
this.jwtUtil = jwtUtil;
this.memberRepository = memberRepository;
}

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver(jwtUtil, memberRepository));
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AdminInterceptor(jwtUtil, memberRepository))
.addPathPatterns("/admin/**");
}
}
43 changes: 43 additions & 0 deletions src/main/java/roomescape/controller/LoginController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package roomescape.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
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.RestController;
import roomescape.dto.LoginMember;
import roomescape.dto.LoginRequest;
import roomescape.dto.MemberResponse;
import roomescape.service.LoginService;

@RestController
public class LoginController {

private final LoginService loginService;

public LoginController(LoginService loginService) {
this.loginService = loginService;
}

@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
String token = loginService.login(loginRequest.email(), loginRequest.password());

Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);

return ResponseEntity.ok().build();
}

@GetMapping("/login/check")
public ResponseEntity<MemberResponse> checkLogin(LoginMember loginMember) {
if (loginMember == null) {
throw new IllegalArgumentException("[ERROR] 인증되지 않은 사용자입니다.");
}
return ResponseEntity.ok(new MemberResponse(loginMember.name()));
}
}
10 changes: 10 additions & 0 deletions src/main/java/roomescape/controller/PageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@ public String reservationPage() {
public String timePage() {
return "time";
}

@GetMapping("/login")
public String loginPage() {
return "login";
}

@GetMapping("/admin")
public String adminPage() {
return "admin";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import roomescape.dto.LoginMember;
import roomescape.dto.ReservationRequest;
import roomescape.dto.ReservationResponse;
import roomescape.dto.TimeRequest;
Expand All @@ -32,8 +33,11 @@ public ResponseEntity<List<ReservationResponse>> getReservations() {
}

@PostMapping("/reservations")
public ResponseEntity<ReservationResponse> createReservation(@RequestBody ReservationRequest request) {
ReservationResponse reservation = reservationService.createReservation(request);
public ResponseEntity<ReservationResponse> createReservation(
@RequestBody ReservationRequest request,
LoginMember loginMember
) {
ReservationResponse reservation = reservationService.createReservation(request, loginMember);
return ResponseEntity
.created(URI.create("/reservations/" + reservation.id()))
.body(reservation);
Expand Down
31 changes: 31 additions & 0 deletions src/main/java/roomescape/domain/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.domain;

import java.util.Objects;

public class Member {
private final Long id;
private final String email;
private final String password;
private final String name;
private final String role;

public Member(Long id, String email, String password, String name, String role) {
this.id = id;
this.email = email;
this.password = password;
this.name = name;
this.role = role;
}

public void checkPassword(String passwordToCompare) {
if (!Objects.equals(this.password, passwordToCompare)) {
throw new IllegalArgumentException("[ERROR] 비밀번호가 일치하지 않습니다.");
}
}

public Long getId() { return id; }
public String getEmail() { return email; }
public String getPassword() { return password; }
public String getName() { return name; }
public String getRole() { return role; }
}
43 changes: 43 additions & 0 deletions src/main/java/roomescape/domain/MemberRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package roomescape.domain;

import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class MemberRepository {
private final JdbcTemplate jdbcTemplate;

public MemberRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

private final RowMapper<Member> memberRowMapper = (rs, rowNum) -> new Member(
rs.getLong("id"),
rs.getString("email"),
rs.getString("password"),
rs.getString("name"),
rs.getString("role")
);

public Optional<Member> findByEmail(String email) {
String sql = "SELECT id, email, password, name, role FROM member WHERE email = ?";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, memberRowMapper, email));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}

public Optional<Member> findById(Long id) {
String sql = "SELECT id, email, password, name, role FROM member WHERE id = ?";
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, memberRowMapper, id));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
8 changes: 8 additions & 0 deletions src/main/java/roomescape/dto/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package roomescape.dto;

public record LoginMember(
Long id,
String name,
String role
) {
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.dto;

public record LoginRequest(String email, String password) {
}
4 changes: 4 additions & 0 deletions src/main/java/roomescape/dto/MemberResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package roomescape.dto;

public record MemberResponse(String name) {
}
26 changes: 26 additions & 0 deletions src/main/java/roomescape/service/LoginService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package roomescape.service;

import org.springframework.stereotype.Service;
import roomescape.domain.Member;
import roomescape.domain.MemberRepository;
import roomescape.util.JwtUtil;

@Service
public class LoginService {
private final MemberRepository memberRepository;
private final JwtUtil jwtUtil;

public LoginService(MemberRepository memberRepository, JwtUtil jwtUtil) {
this.memberRepository = memberRepository;
this.jwtUtil = jwtUtil;
}

public String login(String email, String password) {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new IllegalArgumentException("[ERROR] 아이디 또는 비밀번호가 일치하지 않습니다."));

member.checkPassword(password);

return jwtUtil.createToken(member);
}
}
Loading