Skip to content

Commit e68c24d

Browse files
committed
feat(resolver) : 2단계 - 로그인 리팩터링
1 parent 3cd9533 commit e68c24d

File tree

9 files changed

+217
-39
lines changed

9 files changed

+217
-39
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package roomescape.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
5+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6+
import roomescape.util.LoginMemberArgumentResolver;
7+
import roomescape.domain.MemberRepository;
8+
import roomescape.util.JwtUtil;
9+
10+
import java.util.List;
11+
12+
@Configuration
13+
public class WebConfig implements WebMvcConfigurer {
14+
15+
private final JwtUtil jwtUtil;
16+
private final MemberRepository memberRepository;
17+
18+
public WebConfig(JwtUtil jwtUtil, MemberRepository memberRepository) {
19+
this.jwtUtil = jwtUtil;
20+
this.memberRepository = memberRepository;
21+
}
22+
23+
@Override
24+
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
25+
resolvers.add(new LoginMemberArgumentResolver(jwtUtil, memberRepository));
26+
}
27+
}

src/main/java/roomescape/controller/LoginController.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,22 @@
33
import jakarta.servlet.http.Cookie;
44
import jakarta.servlet.http.HttpServletResponse;
55
import org.springframework.http.ResponseEntity;
6-
import org.springframework.web.bind.annotation.CookieValue;
76
import org.springframework.web.bind.annotation.GetMapping;
87
import org.springframework.web.bind.annotation.PostMapping;
98
import org.springframework.web.bind.annotation.RequestBody;
109
import org.springframework.web.bind.annotation.RestController;
10+
import roomescape.dto.LoginMember;
1111
import roomescape.dto.LoginRequest;
1212
import roomescape.dto.MemberResponse;
1313
import roomescape.service.LoginService;
14-
import roomescape.util.JwtUtil;
1514

1615
@RestController
1716
public class LoginController {
1817

1918
private final LoginService loginService;
20-
private final JwtUtil jwtUtil;
2119

22-
public LoginController(LoginService loginService, JwtUtil jwtUtil) {
20+
public LoginController(LoginService loginService) {
2321
this.loginService = loginService;
24-
this.jwtUtil = jwtUtil;
2522
}
2623

2724
@PostMapping("/login")
@@ -37,13 +34,10 @@ public ResponseEntity<Void> login(@RequestBody LoginRequest loginRequest, HttpSe
3734
}
3835

3936
@GetMapping("/login/check")
40-
public ResponseEntity<MemberResponse> checkLogin(@CookieValue(name = "token", required = false) String token) {
41-
if (token == null || token.isEmpty()) {
42-
throw new IllegalArgumentException("[ERROR] 토큰이 존재하지 않습니다.");
37+
public ResponseEntity<MemberResponse> checkLogin(LoginMember loginMember) {
38+
if (loginMember == null) {
39+
throw new IllegalArgumentException("[ERROR] 인증되지 않은 사용자입니다.");
4340
}
44-
45-
String name = jwtUtil.getNameFromToken(token);
46-
47-
return ResponseEntity.ok(new MemberResponse(name));
41+
return ResponseEntity.ok(new MemberResponse(loginMember.name()));
4842
}
4943
}

src/main/java/roomescape/controller/ReservationController.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.springframework.web.bind.annotation.PostMapping;
88
import org.springframework.web.bind.annotation.RequestBody;
99
import org.springframework.web.bind.annotation.RestController;
10+
import roomescape.dto.LoginMember;
1011
import roomescape.dto.ReservationRequest;
1112
import roomescape.dto.ReservationResponse;
1213
import roomescape.dto.TimeRequest;
@@ -32,8 +33,11 @@ public ResponseEntity<List<ReservationResponse>> getReservations() {
3233
}
3334

3435
@PostMapping("/reservations")
35-
public ResponseEntity<ReservationResponse> createReservation(@RequestBody ReservationRequest request) {
36-
ReservationResponse reservation = reservationService.createReservation(request);
36+
public ResponseEntity<ReservationResponse> createReservation(
37+
@RequestBody ReservationRequest request,
38+
LoginMember loginMember
39+
) {
40+
ReservationResponse reservation = reservationService.createReservation(request, loginMember);
3741
return ResponseEntity
3842
.created(URI.create("/reservations/" + reservation.id()))
3943
.body(reservation);

src/main/java/roomescape/domain/MemberRepository.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.jdbc.core.JdbcTemplate;
55
import org.springframework.jdbc.core.RowMapper;
66
import org.springframework.stereotype.Repository;
7+
78
import java.util.Optional;
89

910
@Repository
@@ -25,8 +26,16 @@ public MemberRepository(JdbcTemplate jdbcTemplate) {
2526
public Optional<Member> findByEmail(String email) {
2627
String sql = "SELECT id, email, password, name, role FROM member WHERE email = ?";
2728
try {
28-
Member member = jdbcTemplate.queryForObject(sql, memberRowMapper, email);
29-
return Optional.ofNullable(member);
29+
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, memberRowMapper, email));
30+
} catch (EmptyResultDataAccessException e) {
31+
return Optional.empty();
32+
}
33+
}
34+
35+
public Optional<Member> findById(Long id) {
36+
String sql = "SELECT id, email, password, name, role FROM member WHERE id = ?";
37+
try {
38+
return Optional.ofNullable(jdbcTemplate.queryForObject(sql, memberRowMapper, id));
3039
} catch (EmptyResultDataAccessException e) {
3140
return Optional.empty();
3241
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package roomescape.dto;
2+
3+
public record LoginMember(
4+
Long id,
5+
String name,
6+
String role
7+
) {
8+
}

src/main/java/roomescape/service/ReservationService.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import org.springframework.stereotype.Service;
44
import org.springframework.transaction.annotation.Transactional;
5+
import org.springframework.util.StringUtils;
56
import roomescape.domain.Reservation;
67
import roomescape.domain.ReservationRepository;
78
import roomescape.domain.Time;
89
import roomescape.domain.TimeRepository;
10+
import roomescape.dto.LoginMember;
911
import roomescape.dto.ReservationRequest;
1012
import roomescape.dto.ReservationResponse;
1113
import roomescape.dto.TimeRequest;
@@ -31,15 +33,22 @@ public ReservationService(ReservationRepository reservationRepository,
3133
}
3234

3335
@Transactional
34-
public ReservationResponse createReservation(ReservationRequest request) {
35-
String validatedName = request.getValidatedName();
36+
public ReservationResponse createReservation(ReservationRequest request, LoginMember loginMember) {
37+
String reservationName = request.name();
38+
if (!StringUtils.hasText(reservationName)) {
39+
if (loginMember == null) {
40+
throw new ReservationException("[ERROR] 예약자 이름을 입력해주세요 (비로그인 상태).");
41+
}
42+
reservationName = loginMember.name();
43+
}
44+
3645
LocalDate parsedDate = request.getParsedDate();
3746
Long timeId = request.timeId();
3847

3948
Time time = timeRepository.findById(timeId)
4049
.orElseThrow(() -> new ReservationException("[ERROR] 등록되지 않은 시간입니다."));
4150

42-
Reservation newReservation = Reservation.create(validatedName, parsedDate, time);
51+
Reservation newReservation = Reservation.create(reservationName, parsedDate, time);
4352

4453
if (reservationRepository.existsByDateAndTimeId(newReservation.getDate(), newReservation.getTime().getId())) {
4554
throw new ReservationException("[ERROR] 이미 예약된 시간입니다.");

src/main/java/roomescape/util/JwtUtil.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.jsonwebtoken.security.Keys;
66
import org.springframework.stereotype.Component;
77
import roomescape.domain.Member;
8+
89
import javax.crypto.SecretKey;
910
import java.nio.charset.StandardCharsets;
1011

@@ -26,8 +27,8 @@ public String createToken(Member member) {
2627
.compact();
2728
}
2829

29-
public String getNameFromToken(String token) {
30-
return parseClaims(token).get("name", String.class);
30+
public Long getMemberIdFromToken(String token) {
31+
return Long.valueOf(parseClaims(token).getSubject());
3132
}
3233

3334
private Claims parseClaims(String token) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package roomescape.util;
2+
3+
import jakarta.servlet.http.Cookie;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import org.springframework.core.MethodParameter;
6+
import org.springframework.web.bind.support.WebDataBinderFactory;
7+
import org.springframework.web.context.request.NativeWebRequest;
8+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
9+
import org.springframework.web.method.support.ModelAndViewContainer;
10+
import roomescape.domain.Member;
11+
import roomescape.domain.MemberRepository;
12+
import roomescape.dto.LoginMember;
13+
14+
import java.util.Arrays;
15+
import java.util.Optional;
16+
17+
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
18+
19+
private final JwtUtil jwtUtil;
20+
private final MemberRepository memberRepository;
21+
22+
public LoginMemberArgumentResolver(JwtUtil jwtUtil, MemberRepository memberRepository) {
23+
this.jwtUtil = jwtUtil;
24+
this.memberRepository = memberRepository;
25+
}
26+
27+
@Override
28+
public boolean supportsParameter(MethodParameter parameter) {
29+
return parameter.getParameterType().equals(LoginMember.class);
30+
}
31+
32+
@Override
33+
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
34+
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
35+
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
36+
if (request == null) {
37+
return null;
38+
}
39+
40+
Optional<String> token = extractToken(request.getCookies());
41+
if (token.isEmpty()) {
42+
return null;
43+
}
44+
45+
Long memberId = jwtUtil.getMemberIdFromToken(token.get());
46+
Member member = memberRepository.findById(memberId).orElse(null);
47+
48+
if (member == null) {
49+
return null;
50+
}
51+
return new LoginMember(member.getId(), member.getName(), member.getRole());
52+
}
53+
54+
private Optional<String> extractToken(Cookie[] cookies) {
55+
if (cookies == null) {
56+
return Optional.empty();
57+
}
58+
return Arrays.stream(cookies)
59+
.filter(cookie -> "token".equals(cookie.getName()))
60+
.map(Cookie::getValue)
61+
.findFirst();
62+
}
63+
}

src/test/java/roomescape/MissionStepTest.java

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,85 @@ public class MissionStepTest {
2929
private ReservationController reservationController;
3030

3131
@Test
32-
@DisplayName("로그인에 성공하고, 발급된 토큰으로 사용자 정보를 정상적으로 조회한다")
33-
void loginAndCheckUserStatus() {
34-
// given: 로그인에 필요한 정보를 준비
35-
Map<String, String> params = new HashMap<>();
36-
params.put("email", "admin@email.com");
37-
params.put("password", "password");
32+
@DisplayName("로그인한 사용자가 이름 없이 예약을 생성하면 자신의 이름으로 예약된다")
33+
void createReservationWithLoggedInUserName() {
34+
// given: 예약을 위한 시간 생성 및 로그인 토큰 발급
35+
Map<String, String> timeParams = new HashMap<>();
36+
timeParams.put("time", "11:00");
37+
RestAssured.given().log().all()
38+
.contentType(ContentType.JSON)
39+
.body(timeParams)
40+
.when().post("/times")
41+
.then().log().all()
42+
.statusCode(201);
43+
44+
String token = createToken("admin@email.com", "password");
45+
46+
// when: 로그인 상태에서 'name' 없이 예약을 요청
47+
Map<String, Object> paramsWithoutName = new HashMap<>();
48+
paramsWithoutName.put("date", "2025-08-05");
49+
paramsWithoutName.put("timeId", 1L);
3850

39-
// when: 로그인을 요청
4051
ExtractableResponse<Response> response = RestAssured.given().log().all()
52+
.body(paramsWithoutName)
53+
.cookie("token", token)
4154
.contentType(ContentType.JSON)
42-
.body(params)
43-
.when().post("/login")
55+
.when().post("/reservations")
56+
.then().log().all()
57+
.extract();
58+
59+
// then: 로그인한 사용자('어드민')의 이름으로 예약이 생성됨
60+
assertThat(response.statusCode()).isEqualTo(201);
61+
assertThat(response.jsonPath().getString("name")).isEqualTo("어드민");
62+
}
63+
64+
@Test
65+
@DisplayName("로그인한 사용자가 이름을 직접 입력하면 해당 이름으로 예약된다")
66+
void createReservationWithSpecifiedNameWhileLoggedIn() {
67+
// given: 예약을 위한 시간 생성 및 로그인 토큰 발급
68+
Map<String, String> timeParams = new HashMap<>();
69+
timeParams.put("time", "11:00");
70+
RestAssured.given().log().all()
71+
.contentType(ContentType.JSON)
72+
.body(timeParams)
73+
.when().post("/times")
74+
.then().log().all()
75+
.statusCode(201);
76+
77+
String token = createToken("admin@email.com", "password");
78+
79+
// when: 로그인 상태에서 'name'을 직접 입력하여 예약을 요청
80+
Map<String, Object> paramsWithName = new HashMap<>();
81+
paramsWithName.put("name", "브라운");
82+
paramsWithName.put("date", "2025-08-06");
83+
paramsWithName.put("timeId", 1L);
84+
85+
ExtractableResponse<Response> response = RestAssured.given().log().all()
86+
.body(paramsWithName)
87+
.cookie("token", token)
88+
.contentType(ContentType.JSON)
89+
.when().post("/reservations")
4490
.then().log().all()
45-
.statusCode(200)
4691
.extract();
4792

48-
// then: 응답 쿠키에서 토큰을 성공적으로 추출
49-
String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1];
93+
// then: 직접 입력한 이름('브라운')으로 예약이 생성됨
94+
assertThat(response.statusCode()).isEqualTo(201);
95+
assertThat(response.jsonPath().getString("name")).isEqualTo("브라운");
96+
}
97+
98+
@Test
99+
@DisplayName("로그인에 성공하고, 발급된 토큰으로 사용자 정보를 정상적으로 조회한다")
100+
void loginAndCheckUserStatus() {
101+
String token = createToken("admin@email.com", "password");
50102
assertThat(token).isNotBlank();
51103

52-
// when: 추출한 토큰으로 사용자 정보 조회를 요청
53-
ExtractableResponse<Response> checkResponse = RestAssured.given().log().all()
104+
RestAssured.given().log().all()
54105
.contentType(ContentType.JSON)
55106
.cookie("token", token)
56107
.when().get("/login/check")
57108
.then().log().all()
58109
.statusCode(200)
59-
.extract();
60-
61-
// then: 조회된 사용자 이름이 예상과 일치
62-
assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민");
110+
.body("name", is("어드민"));
63111
}
64112

65113
@Test
@@ -210,4 +258,19 @@ void controllerShouldNotDependOnJdbcTemplate() {
210258
}
211259
assertThat(isJdbcTemplateInjected).isFalse();
212260
}
261+
262+
private String createToken(String email, String password) {
263+
Map<String, String> loginParams = new HashMap<>();
264+
loginParams.put("email", email);
265+
loginParams.put("password", password);
266+
267+
return RestAssured.given().log().all()
268+
.contentType(ContentType.JSON)
269+
.body(loginParams)
270+
.when().post("/login")
271+
.then().log().all()
272+
.statusCode(200)
273+
.extract()
274+
.cookie("token");
275+
}
213276
}

0 commit comments

Comments
 (0)