From db45fad48c209d1c741550d0854b8b3e1524111c Mon Sep 17 00:00:00 2001 From: dwl21 Date: Fri, 11 Apr 2025 13:03:48 +0900 Subject: [PATCH 01/37] =?UTF-8?q?fix:=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index a0aabaf..7025e84 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,6 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test:3.4.4' testImplementation 'io.rest-assured:rest-assured:5.5.1' - testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.1' } tasks.named('test') { From 98f3c7a3198fc3446b49d5566119e6bce8ae0db3 Mon Sep 17 00:00:00 2001 From: kwonyj1022 Date: Fri, 11 Apr 2025 17:24:11 +0900 Subject: [PATCH 02/37] =?UTF-8?q?feat:=20=EC=9D=B8=ED=81=90=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=ED=8C=85=20=EC=97=94=EC=B4=88=EC=A1=B0=201=EB=8B=A8?= =?UTF-8?q?=EA=B3=84=20=ED=8E=98=EC=96=B4=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=98=EB=B0=8D=20=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yourssu/roomescape/JwtTokenProvider.java | 39 +++++++++++++++++++ .../roomescape/member/LoginCheckResponse.java | 13 +++++++ .../roomescape/member/LoginRequest.java | 16 ++++++++ .../roomescape/member/LoginResponse.java | 14 +++++++ .../roomescape/member/MemberController.java | 30 +++++++++++++- .../yourssu/roomescape/member/MemberDao.java | 13 +++++++ .../roomescape/member/MemberService.java | 18 ++++++++- src/main/resources/application.properties | 3 +- .../yourssu/roomescape/MissionStepTest.java | 10 +++++ 9 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/yourssu/roomescape/JwtTokenProvider.java create mode 100644 src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java create mode 100644 src/main/java/com/yourssu/roomescape/member/LoginRequest.java create mode 100644 src/main/java/com/yourssu/roomescape/member/LoginResponse.java diff --git a/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java b/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java new file mode 100644 index 0000000..b1c1e19 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java @@ -0,0 +1,39 @@ +package com.yourssu.roomescape; + +import com.yourssu.roomescape.member.LoginRequest; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenProvider { + @Value("${roomescape.auth.jwt.secret}") + private String secretKey; + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + + public String createToken(String payload) { + Claims claims = Jwts.claims().setSubject(payload).build(); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } + + public String getPayload(String token) { + return Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject(); + } +} diff --git a/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java b/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java new file mode 100644 index 0000000..c937eae --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java @@ -0,0 +1,13 @@ +package com.yourssu.roomescape.member; + +public class LoginCheckResponse { + private String name; + + public LoginCheckResponse(final String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/yourssu/roomescape/member/LoginRequest.java b/src/main/java/com/yourssu/roomescape/member/LoginRequest.java new file mode 100644 index 0000000..1e1d93c --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/LoginRequest.java @@ -0,0 +1,16 @@ +package com.yourssu.roomescape.member; + +public class LoginRequest { + + private String email; + private String password; + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } + +} diff --git a/src/main/java/com/yourssu/roomescape/member/LoginResponse.java b/src/main/java/com/yourssu/roomescape/member/LoginResponse.java new file mode 100644 index 0000000..5e0c3d3 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/LoginResponse.java @@ -0,0 +1,14 @@ +package com.yourssu.roomescape.member; + +public class LoginResponse { + + private String token; + + public LoginResponse(final String token) { + this.token = token; + } + + public String getToken() { + return token; + } +} diff --git a/src/main/java/com/yourssu/roomescape/member/MemberController.java b/src/main/java/com/yourssu/roomescape/member/MemberController.java index f90da55..fc70af2 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberController.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberController.java @@ -1,10 +1,13 @@ package com.yourssu.roomescape.member; import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; 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.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.net.URI; @@ -16,8 +19,33 @@ public class MemberController { public MemberController(MemberService memberService) { this.memberService = memberService; } + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletResponse httpServletResponse){ + LoginResponse loginResponse = memberService.login(loginRequest); // TODO: 여기 변수명과 반환값 이름에 대해서 생각해보기 - @PostMapping("/members") + Cookie cookie = new Cookie("token",loginResponse.getToken()); // TODO: constant 프로퍼티로 관리 + httpServletResponse.addCookie(cookie); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity loginCheck(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + + // TODO: 빈문자열 관련 예외처리 + String token = ""; + + // TODO: stream 사용해보기 + for (Cookie cookie : cookies) { + if(cookie.getName().equals("token")) { + token = cookie.getValue(); + } + } + String name = memberService.loginCheck(token); + return ResponseEntity.ok().body(new LoginCheckResponse(name)); + } + @PostMapping("/members") public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { MemberResponse member = memberService.createMember(memberRequest); return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); diff --git a/src/main/java/com/yourssu/roomescape/member/MemberDao.java b/src/main/java/com/yourssu/roomescape/member/MemberDao.java index 7923c15..f4b11e4 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberDao.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberDao.java @@ -52,4 +52,17 @@ public Member findByName(String name) { name ); } + + public Member findByEmail(String email) { + return jdbcTemplate.queryForObject( + "SELECT id, name, email, role FROM member WHERE email = ?", + (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ), + email + ); + } } diff --git a/src/main/java/com/yourssu/roomescape/member/MemberService.java b/src/main/java/com/yourssu/roomescape/member/MemberService.java index ff138c4..fc7492a 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberService.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberService.java @@ -1,17 +1,33 @@ package com.yourssu.roomescape.member; +import com.yourssu.roomescape.JwtTokenProvider; import org.springframework.stereotype.Service; @Service public class MemberService { private MemberDao memberDao; + private JwtTokenProvider jwtTokenProvider; - public MemberService(MemberDao memberDao) { + public MemberService(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) { this.memberDao = memberDao; + this.jwtTokenProvider = jwtTokenProvider; } public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } + + public LoginResponse login(LoginRequest loginRequest) { + Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); // TODO: 유저 찾기 여기 유저 없으면 예외 처리 어떻게 할지 고민해보기 + String token = jwtTokenProvider.createToken(member.getEmail()); + return new LoginResponse(token); + } + + public String loginCheck(String token) { + + String email = jwtTokenProvider.getPayload(token); + Member member = memberDao.findByEmail(email); + return member.getName(); + } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a0f33bb..26d9792 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -8,4 +8,5 @@ spring.datasource.url=jdbc:h2:mem:database #spring.jpa.ddl-auto=create-drop #spring.jpa.defer-datasource-initialization=true -#roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= \ No newline at end of file +roomescape.auth.jwt.secret= Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E= +security.jwt.token.expire-length=3600000 diff --git a/src/test/java/com/yourssu/roomescape/MissionStepTest.java b/src/test/java/com/yourssu/roomescape/MissionStepTest.java index 085d0d6..dee6d30 100644 --- a/src/test/java/com/yourssu/roomescape/MissionStepTest.java +++ b/src/test/java/com/yourssu/roomescape/MissionStepTest.java @@ -34,5 +34,15 @@ public class MissionStepTest { String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; assertThat(token).isNotBlank(); + + ExtractableResponse checkResponse = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .cookie("token", token) + .when().get("/login/check") + .then().log().all() + .statusCode(200) + .extract(); + + assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); } } From debe4954c383ae6272a8de4d4649028ecc40f7e3 Mon Sep 17 00:00:00 2001 From: rover0811 Date: Wed, 16 Apr 2025 16:45:38 +0900 Subject: [PATCH 03/37] [Feat] level2 login refactoring, reservation --- .../com/yourssu/roomescape/WebConfig.java | 23 ++++++++ .../roomescape/member/LoginMember.java | 31 ++++++++++ .../member/LoginMemberArgumentResolver.java | 56 +++++++++++++++++++ .../roomescape/member/MemberService.java | 9 ++- .../reservation/ReservationController.java | 13 +++-- .../reservation/ReservationService.java | 50 ++++++++++++++++- .../yourssu/roomescape/MissionStepTest.java | 52 +++++++++++++++++ 7 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/yourssu/roomescape/WebConfig.java create mode 100644 src/main/java/com/yourssu/roomescape/member/LoginMember.java create mode 100644 src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java diff --git a/src/main/java/com/yourssu/roomescape/WebConfig.java b/src/main/java/com/yourssu/roomescape/WebConfig.java new file mode 100644 index 0000000..4d5c3d2 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/WebConfig.java @@ -0,0 +1,23 @@ +package com.yourssu.roomescape; + +import com.yourssu.roomescape.member.LoginMemberArgumentResolver; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + } +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/LoginMember.java b/src/main/java/com/yourssu/roomescape/member/LoginMember.java new file mode 100644 index 0000000..13b1ee6 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/LoginMember.java @@ -0,0 +1,31 @@ +package com.yourssu.roomescape.member; + +public class LoginMember { + private Long id; + private String name; + private String email; + private String role; + + public LoginMember(Long id, String name, String email, String role) { + this.id = id; + this.name = name; + this.email = email; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public String getEmail() { + return email; + } + + public String getRole() { + return role; + } +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java b/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java new file mode 100644 index 0000000..6001ef5 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java @@ -0,0 +1,56 @@ +package com.yourssu.roomescape.member; + +import com.yourssu.roomescape.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +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; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + private final MemberService memberService; + private final JwtTokenProvider jwtTokenProvider; + + public LoginMemberArgumentResolver(MemberService memberService, JwtTokenProvider jwtTokenProvider) { + this.memberService = memberService; + this.jwtTokenProvider = jwtTokenProvider; + } + + @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 = (HttpServletRequest) webRequest.getNativeRequest(); + Cookie[] cookies = request.getCookies(); + + if (cookies == null) { + return null; + } + + String token = ""; + + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + token = cookie.getValue(); + break; + } + } + + if (token.isEmpty()) { + return null; + } + + String email = jwtTokenProvider.getPayload(token); + Member member = memberService.findByEmail(email); + + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/MemberService.java b/src/main/java/com/yourssu/roomescape/member/MemberService.java index fc7492a..872442f 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberService.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberService.java @@ -17,7 +17,6 @@ public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } - public LoginResponse login(LoginRequest loginRequest) { Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); // TODO: 유저 찾기 여기 유저 없으면 예외 처리 어떻게 할지 고민해보기 String token = jwtTokenProvider.createToken(member.getEmail()); @@ -30,4 +29,12 @@ public String loginCheck(String token) { Member member = memberDao.findByEmail(email); return member.getName(); } + public Member findByEmail(String email) { + return memberDao.findByEmail(email); + } + public Member findByName(String name) { + return memberDao.findByName(name); + } + + } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java index 77f108d..69d67d9 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java @@ -1,5 +1,6 @@ package com.yourssu.roomescape.reservation; +import com.yourssu.roomescape.member.LoginMember; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -21,14 +22,18 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, LoginMember loginMember) { + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + if (reservationRequest.getName() == null && loginMember == null) { + return ResponseEntity.badRequest().build(); + } + + // 비즈니스 로직을 서비스 계층에 위임 + ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java index 1dab4f6..4de5b29 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java @@ -1,5 +1,8 @@ package com.yourssu.roomescape.reservation; +import com.yourssu.roomescape.member.LoginMember; +import com.yourssu.roomescape.member.MemberDao; +import com.yourssu.roomescape.member.MemberService; import org.springframework.stereotype.Service; import java.util.List; @@ -7,17 +10,60 @@ @Service public class ReservationService { private ReservationDao reservationDao; + private MemberService memberService; - public ReservationService(ReservationDao reservationDao) { + public ReservationService(ReservationDao reservationDao, MemberService memberService) { this.reservationDao = reservationDao; + this.memberService = memberService; } public ReservationResponse save(ReservationRequest reservationRequest) { Reservation reservation = reservationDao.save(reservationRequest); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); } + public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { + // name이 있으면 그대로 사용하고, 없으면 로그인 멤버의 이름을 사용 + String reservationName = reservationRequest.getName() != null + ? reservationRequest.getName() + : loginMember.getName(); + + // 기존 ReservationRequest를 확장하여 이름을 설정 + ReservationRequest finalRequest = new ReservationRequest() { + @Override + public String getName() { + return reservationName; + } + + @Override + public String getDate() { + return reservationRequest.getDate(); + } + + @Override + public Long getTheme() { + return reservationRequest.getTheme(); + } + + @Override + public Long getTime() { + return reservationRequest.getTime(); + } + }; + + // 기존 save 메서드를 호출하여 예약 저장 + Reservation reservation = reservationDao.save(finalRequest); + + return new ReservationResponse( + reservation.getId(), + reservationName, + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue() + ); + } + + public void deleteById(Long id) { reservationDao.deleteById(id); } diff --git a/src/test/java/com/yourssu/roomescape/MissionStepTest.java b/src/test/java/com/yourssu/roomescape/MissionStepTest.java index dee6d30..546dfcd 100644 --- a/src/test/java/com/yourssu/roomescape/MissionStepTest.java +++ b/src/test/java/com/yourssu/roomescape/MissionStepTest.java @@ -1,5 +1,6 @@ package com.yourssu.roomescape; +import com.yourssu.roomescape.reservation.ReservationResponse; import io.restassured.RestAssured; import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; @@ -17,6 +18,23 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + String createToken(String email, String password) { + Map params = new HashMap<>(); + params.put("email", email); + params.put("password", password); + + ExtractableResponse response = RestAssured.given().log().all() + .contentType(ContentType.JSON) + .body(params) + .when().post("/login") + .then().log().all() + .statusCode(200) + .extract(); + + String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + return token; + } + @Test void 일단계() { Map params = new HashMap<>(); @@ -45,4 +63,38 @@ public class MissionStepTest { assertThat(checkResponse.body().jsonPath().getString("name")).isEqualTo("어드민"); } + @Test + void 이단계(){ + String token = createToken("admin@email.com", "password"); // 일단계에서 토큰을 추출하는 로직을 메서드로 따로 만들어서 활용하세요. + + Map params = new HashMap<>(); + params.put("date", "2024-03-01"); + params.put("time", "1"); + params.put("theme", "1"); + + ExtractableResponse response = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + + params.put("name", "브라운"); + + ExtractableResponse adminResponse = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); + + assertThat(adminResponse.statusCode()).isEqualTo(201); + assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); + + } } From 62d75e52adcfb8d46fdcc19fb32b716dd2f564e5 Mon Sep 17 00:00:00 2001 From: rover0811 Date: Wed, 16 Apr 2025 21:25:19 +0900 Subject: [PATCH 04/37] [Feat] level3 implements HandlerInterceptor, admin check --- .../com/yourssu/roomescape/WebConfig.java | 11 +++- .../member/AdminAuthInterceptor.java | 51 +++++++++++++++++++ .../yourssu/roomescape/MissionStepTest.java | 20 ++++++++ 3 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java diff --git a/src/main/java/com/yourssu/roomescape/WebConfig.java b/src/main/java/com/yourssu/roomescape/WebConfig.java index 4d5c3d2..4382d7d 100644 --- a/src/main/java/com/yourssu/roomescape/WebConfig.java +++ b/src/main/java/com/yourssu/roomescape/WebConfig.java @@ -1,8 +1,10 @@ package com.yourssu.roomescape; +import com.yourssu.roomescape.member.AdminAuthInterceptor; import com.yourssu.roomescape.member.LoginMemberArgumentResolver; 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 java.util.List; @@ -11,13 +13,20 @@ public class WebConfig implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AdminAuthInterceptor adminAuthInterceptor; - public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver) { + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, AdminAuthInterceptor adminAuthInterceptor) { this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.adminAuthInterceptor = adminAuthInterceptor; } @Override public void addArgumentResolvers(List resolvers) { resolvers.add(loginMemberArgumentResolver); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthInterceptor).addPathPatterns("/admin/**/"); + } } \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java b/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java new file mode 100644 index 0000000..f438a32 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java @@ -0,0 +1,51 @@ +package com.yourssu.roomescape.member; + +import com.yourssu.roomescape.JwtTokenProvider; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberService memberService; + + public AdminAuthInterceptor(JwtTokenProvider jwtTokenProvider, MemberService memberService) { + this.jwtTokenProvider = jwtTokenProvider; + this.memberService = memberService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + String token = ""; + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + token = cookie.getValue(); + break; + } + } + if (token.isEmpty()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + String email = jwtTokenProvider.getPayload(token); + Member member = memberService.findByEmail(email); + + if (member == null || !member.getRole().equals("ADMIN")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + return true; + + } +} \ No newline at end of file diff --git a/src/test/java/com/yourssu/roomescape/MissionStepTest.java b/src/test/java/com/yourssu/roomescape/MissionStepTest.java index 546dfcd..69a7926 100644 --- a/src/test/java/com/yourssu/roomescape/MissionStepTest.java +++ b/src/test/java/com/yourssu/roomescape/MissionStepTest.java @@ -97,4 +97,24 @@ String createToken(String email, String password) { assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); } + + @Test + void 삼단계() { + String brownToken = createToken("brown@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") + .then().log().all() + .statusCode(401); + + String adminToken = createToken("admin@email.com", "password"); + + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") + .then().log().all() + .statusCode(200); + } + } From 461209ec76b6e12ffa184af2e8ba432c7d68b667 Mon Sep 17 00:00:00 2001 From: rover0811 Date: Fri, 18 Apr 2025 11:33:22 +0900 Subject: [PATCH 05/37] =?UTF-8?q?[Fix]=20code=20review=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yourssu/roomescape/AppConstants.java | 20 +++++ .../yourssu/roomescape/JwtTokenProvider.java | 35 +++++--- .../com/yourssu/roomescape/WebConfig.java | 2 +- .../member/AdminAuthInterceptor.java | 32 +++++-- .../roomescape/member/LoginCheckResponse.java | 12 +-- .../member/LoginMemberArgumentResolver.java | 3 +- .../roomescape/member/MemberController.java | 10 +-- .../yourssu/roomescape/member/MemberDao.java | 78 ++++++++++------- .../roomescape/member/MemberService.java | 9 +- .../{LoginResponse.java => TokenDto.java} | 4 +- .../reservation/ReservationService.java | 86 ++++++++++++------- 11 files changed, 183 insertions(+), 108 deletions(-) create mode 100644 src/main/java/com/yourssu/roomescape/AppConstants.java rename src/main/java/com/yourssu/roomescape/member/{LoginResponse.java => TokenDto.java} (68%) diff --git a/src/main/java/com/yourssu/roomescape/AppConstants.java b/src/main/java/com/yourssu/roomescape/AppConstants.java new file mode 100644 index 0000000..44cc4d8 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/AppConstants.java @@ -0,0 +1,20 @@ +package com.yourssu.roomescape; + +/** + * Application-wide constants + */ +public class AppConstants { + // Cookie names + public static final String TOKEN_COOKIE_NAME = "token"; + + // Role names + public static final String ROLE_ADMIN = "ADMIN"; + + + // Other constants can be added here as needed + + // Private constructor to prevent instantiation + private AppConstants() { + throw new AssertionError("Constants class should not be instantiated"); + } +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java b/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java index b1c1e19..7dd7843 100644 --- a/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java +++ b/src/main/java/com/yourssu/roomescape/JwtTokenProvider.java @@ -3,6 +3,7 @@ import com.yourssu.roomescape.member.LoginRequest; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -22,18 +23,30 @@ public String createToken(String payload) { Date validity = new Date(now.getTime() + validityInMilliseconds); return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(validity) - .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) - .compact(); + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); } public String getPayload(String token) { - return Jwts.parser() - .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) - .build() - .parseClaimsJws(token) - .getBody().getSubject(); + try { + return Jwts.parser() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject(); + } catch (ExpiredJwtException e) { + throw new RuntimeException("Token has expired", e); + } catch (UnsupportedJwtException e) { + throw new RuntimeException("Unsupported JWT token", e); + } catch (MalformedJwtException e) { + throw new RuntimeException("Invalid JWT token", e); + } catch (SignatureException e) { + throw new RuntimeException("Invalid JWT signature", e); + } catch (IllegalArgumentException e) { + throw new RuntimeException("JWT claims string is empty", e); + } } -} +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/WebConfig.java b/src/main/java/com/yourssu/roomescape/WebConfig.java index 4382d7d..19b4392 100644 --- a/src/main/java/com/yourssu/roomescape/WebConfig.java +++ b/src/main/java/com/yourssu/roomescape/WebConfig.java @@ -27,6 +27,6 @@ public void addArgumentResolvers(List resolvers) @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(adminAuthInterceptor).addPathPatterns("/admin/**/"); + registry.addInterceptor(adminAuthInterceptor).addPathPatterns("/admin/**"); } } \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java b/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java index f438a32..aa739c1 100644 --- a/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java +++ b/src/main/java/com/yourssu/roomescape/member/AdminAuthInterceptor.java @@ -1,5 +1,6 @@ package com.yourssu.roomescape.member; +import com.yourssu.roomescape.AppConstants; import com.yourssu.roomescape.JwtTokenProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -24,28 +25,43 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons Cookie[] cookies = request.getCookies(); if (cookies == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Authentication required"); return false; } + String token = ""; for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { + if (cookie.getName() != null && AppConstants.TOKEN_COOKIE_NAME.equals(cookie.getName())) { token = cookie.getValue(); break; } } + if (token.isEmpty()) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Token is missing"); return false; } - String email = jwtTokenProvider.getPayload(token); - Member member = memberService.findByEmail(email); + try { + String email = jwtTokenProvider.getPayload(token); + Member member = memberService.findByEmail(email); + + if (member == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("User not found"); + return false; + } - if (member == null || !member.getRole().equals("ADMIN")) { + if (member.getRole() == null || !AppConstants.ROLE_ADMIN.equals(member.getRole())) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 상태 코드 사용 + response.getWriter().write("Admin privileges required"); + return false; + } + return true; + } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().write("Invalid token: " + e.getMessage()); return false; } - return true; - - } -} \ No newline at end of file + }} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java b/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java index c937eae..a8aa6f3 100644 --- a/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java +++ b/src/main/java/com/yourssu/roomescape/member/LoginCheckResponse.java @@ -1,13 +1,3 @@ package com.yourssu.roomescape.member; -public class LoginCheckResponse { - private String name; - - public LoginCheckResponse(final String name) { - this.name = name; - } - - public String getName() { - return name; - } -} +public record LoginCheckResponse(String name) {} diff --git a/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java b/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java index 6001ef5..0342c80 100644 --- a/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java +++ b/src/main/java/com/yourssu/roomescape/member/LoginMemberArgumentResolver.java @@ -1,5 +1,6 @@ package com.yourssu.roomescape.member; +import com.yourssu.roomescape.AppConstants; import com.yourssu.roomescape.JwtTokenProvider; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; @@ -38,7 +39,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m String token = ""; for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { + if (cookie.getName() != null && AppConstants.TOKEN_COOKIE_NAME.equals(cookie.getName())) { token = cookie.getValue(); break; } diff --git a/src/main/java/com/yourssu/roomescape/member/MemberController.java b/src/main/java/com/yourssu/roomescape/member/MemberController.java index fc70af2..911f8c4 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberController.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberController.java @@ -1,5 +1,6 @@ package com.yourssu.roomescape.member; +import com.yourssu.roomescape.AppConstants; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -7,7 +8,6 @@ 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.ResponseBody; import org.springframework.web.bind.annotation.RestController; import java.net.URI; @@ -21,9 +21,9 @@ public MemberController(MemberService memberService) { } @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletResponse httpServletResponse){ - LoginResponse loginResponse = memberService.login(loginRequest); // TODO: 여기 변수명과 반환값 이름에 대해서 생각해보기 + TokenDto tokenDTO = memberService.login(loginRequest); // TODO: 여기 변수명과 반환값 이름에 대해서 생각해보기 - Cookie cookie = new Cookie("token",loginResponse.getToken()); // TODO: constant 프로퍼티로 관리 + Cookie cookie = new Cookie(AppConstants.TOKEN_COOKIE_NAME, tokenDTO.getToken()); // TODO: constant 프로퍼티로 관리 httpServletResponse.addCookie(cookie); return ResponseEntity.ok().build(); @@ -38,7 +38,7 @@ public ResponseEntity loginCheck(HttpServletRequest request) { // TODO: stream 사용해보기 for (Cookie cookie : cookies) { - if(cookie.getName().equals("token")) { + if(cookie.getName().equals(AppConstants.TOKEN_COOKIE_NAME)) { token = cookie.getValue(); } } @@ -53,7 +53,7 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { - Cookie cookie = new Cookie("token", ""); + Cookie cookie = new Cookie(AppConstants.TOKEN_COOKIE_NAME, ""); cookie.setHttpOnly(true); cookie.setPath("/"); cookie.setMaxAge(0); diff --git a/src/main/java/com/yourssu/roomescape/member/MemberDao.java b/src/main/java/com/yourssu/roomescape/member/MemberDao.java index f4b11e4..4d0462a 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberDao.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberDao.java @@ -1,13 +1,24 @@ package com.yourssu.roomescape.member; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; @Repository public class MemberDao { - private JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; + + // Create a reusable RowMapper for Member objects + private static final RowMapper MEMBER_ROW_MAPPER = (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ); public MemberDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; @@ -28,41 +39,44 @@ public Member save(Member member) { } public Member findByEmailAndPassword(String email, String password) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - email, password - ); + try { + return jdbcTemplate.queryForObject( + "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", + MEMBER_ROW_MAPPER, + email, password + ); + } catch (EmptyResultDataAccessException e) { + return null; // No member found with the given email and password + } catch (IncorrectResultSizeDataAccessException e) { + throw new RuntimeException("Multiple members found with the same email and password", e); + } } public Member findByName(String name) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE name = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - name - ); + try { + return jdbcTemplate.queryForObject( + "SELECT id, name, email, role FROM member WHERE name = ?", + MEMBER_ROW_MAPPER, + name + ); + } catch (EmptyResultDataAccessException e) { + return null; // No member found with the given name + } catch (IncorrectResultSizeDataAccessException e) { + throw new RuntimeException("Multiple members found with the same name", e); + } } public Member findByEmail(String email) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - email - ); + try { + return jdbcTemplate.queryForObject( + "SELECT id, name, email, role FROM member WHERE email = ?", + MEMBER_ROW_MAPPER, + email + ); + } catch (EmptyResultDataAccessException e) { + return null; // No member found with the given email + } catch (IncorrectResultSizeDataAccessException e) { + throw new RuntimeException("Multiple members found with the same email", e); + } } -} +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/MemberService.java b/src/main/java/com/yourssu/roomescape/member/MemberService.java index 872442f..a97ed4a 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberService.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberService.java @@ -17,10 +17,11 @@ public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } - public LoginResponse login(LoginRequest loginRequest) { + + public TokenDto login(LoginRequest loginRequest) { Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); // TODO: 유저 찾기 여기 유저 없으면 예외 처리 어떻게 할지 고민해보기 String token = jwtTokenProvider.createToken(member.getEmail()); - return new LoginResponse(token); + return new TokenDto(token); } public String loginCheck(String token) { @@ -29,12 +30,12 @@ public String loginCheck(String token) { Member member = memberDao.findByEmail(email); return member.getName(); } + public Member findByEmail(String email) { return memberDao.findByEmail(email); } + public Member findByName(String name) { return memberDao.findByName(name); } - - } diff --git a/src/main/java/com/yourssu/roomescape/member/LoginResponse.java b/src/main/java/com/yourssu/roomescape/member/TokenDto.java similarity index 68% rename from src/main/java/com/yourssu/roomescape/member/LoginResponse.java rename to src/main/java/com/yourssu/roomescape/member/TokenDto.java index 5e0c3d3..c281284 100644 --- a/src/main/java/com/yourssu/roomescape/member/LoginResponse.java +++ b/src/main/java/com/yourssu/roomescape/member/TokenDto.java @@ -1,10 +1,10 @@ package com.yourssu.roomescape.member; -public class LoginResponse { +public class TokenDto { private String token; - public LoginResponse(final String token) { + public TokenDto(final String token) { this.token = token; } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java index 4de5b29..5e6ccfd 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java @@ -19,51 +19,37 @@ public ReservationService(ReservationDao reservationDao, MemberService memberSer public ReservationResponse save(ReservationRequest reservationRequest) { Reservation reservation = reservationDao.save(reservationRequest); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + return new ReservationResponse( + reservation.getId(), + reservationRequest.getName(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue() + ); } public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { - // name이 있으면 그대로 사용하고, 없으면 로그인 멤버의 이름을 사용 - String reservationName = reservationRequest.getName() != null - ? reservationRequest.getName() - : loginMember.getName(); - - // 기존 ReservationRequest를 확장하여 이름을 설정 - ReservationRequest finalRequest = new ReservationRequest() { - @Override - public String getName() { - return reservationName; - } - - @Override - public String getDate() { - return reservationRequest.getDate(); - } - - @Override - public Long getTheme() { - return reservationRequest.getTheme(); - } - - @Override - public Long getTime() { - return reservationRequest.getTime(); - } - }; - - // 기존 save 메서드를 호출하여 예약 저장 + // Create a new ReservationRequest with all the data we need + ReservationRequestDto finalRequest = new ReservationRequestDto( + reservationRequest.getName() != null ? reservationRequest.getName() : loginMember.getName(), + reservationRequest.getDate(), + reservationRequest.getTheme(), + reservationRequest.getTime() + ); + + // Use the DAO to save the reservation Reservation reservation = reservationDao.save(finalRequest); + // Return the response return new ReservationResponse( reservation.getId(), - reservationName, + finalRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue() ); } - public void deleteById(Long id) { reservationDao.deleteById(id); } @@ -73,4 +59,38 @@ public List findAll() { .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) .toList(); } -} + + private static class ReservationRequestDto extends ReservationRequest { + private final String name; + private final String date; + private final Long theme; + private final Long time; + + public ReservationRequestDto(String name, String date, Long theme, Long time) { + this.name = name; + this.date = date; + this.theme = theme; + this.time = time; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getDate() { + return date; + } + + @Override + public Long getTheme() { + return theme; + } + + @Override + public Long getTime() { + return time; + } + } +} \ No newline at end of file From 386079f8eed8c16e0cb041e195a2066225af11d8 Mon Sep 17 00:00:00 2001 From: rover0811 Date: Sun, 27 Apr 2025 19:57:48 +0900 Subject: [PATCH 06/37] [Migrate] Member data access from JdbcTemplate to JPA Replaced MemberDao with MemberRepository to leverage Spring Data JPA, simplifying data access and improving maintainability. Updated MemberService and Member entity to support JPA, including annotations for entity mapping and adjustments to business logic. --- .../com/yourssu/roomescape/member/Member.java | 20 ++++- .../yourssu/roomescape/member/MemberDao.java | 82 ------------------- .../roomescape/member/MemberRepository.java | 12 +++ .../roomescape/member/MemberService.java | 41 +++++++--- 4 files changed, 62 insertions(+), 93 deletions(-) delete mode 100644 src/main/java/com/yourssu/roomescape/member/MemberDao.java create mode 100644 src/main/java/com/yourssu/roomescape/member/MemberRepository.java diff --git a/src/main/java/com/yourssu/roomescape/member/Member.java b/src/main/java/com/yourssu/roomescape/member/Member.java index 6d655ef..f41d936 100644 --- a/src/main/java/com/yourssu/roomescape/member/Member.java +++ b/src/main/java/com/yourssu/roomescape/member/Member.java @@ -1,12 +1,30 @@ package com.yourssu.roomescape.member; +import jakarta.persistence.*; + +@Entity +@Table(name = "member") // 테이블 이름 명시적 지정 public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false) private String name; + + @Column(nullable = false, unique = true) private String email; + + @Column(nullable = false) private String password; + + @Column(nullable = false) private String role; + // 기본 생성자 - JPA에 필요 + public Member() { + } + public Member(Long id, String name, String email, String role) { this.id = id; this.name = name; @@ -40,4 +58,4 @@ public String getPassword() { public String getRole() { return role; } -} +} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/MemberDao.java b/src/main/java/com/yourssu/roomescape/member/MemberDao.java deleted file mode 100644 index 4d0462a..0000000 --- a/src/main/java/com/yourssu/roomescape/member/MemberDao.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.yourssu.roomescape.member; - -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.core.RowMapper; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -public class MemberDao { - private final JdbcTemplate jdbcTemplate; - - // Create a reusable RowMapper for Member objects - private static final RowMapper MEMBER_ROW_MAPPER = (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ); - - public MemberDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Member save(Member member) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO member(name, email, password, role) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, member.getName()); - ps.setString(2, member.getEmail()); - ps.setString(3, member.getPassword()); - ps.setString(4, member.getRole()); - return ps; - }, keyHolder); - - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), "USER"); - } - - public Member findByEmailAndPassword(String email, String password) { - try { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - MEMBER_ROW_MAPPER, - email, password - ); - } catch (EmptyResultDataAccessException e) { - return null; // No member found with the given email and password - } catch (IncorrectResultSizeDataAccessException e) { - throw new RuntimeException("Multiple members found with the same email and password", e); - } - } - - public Member findByName(String name) { - try { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE name = ?", - MEMBER_ROW_MAPPER, - name - ); - } catch (EmptyResultDataAccessException e) { - return null; // No member found with the given name - } catch (IncorrectResultSizeDataAccessException e) { - throw new RuntimeException("Multiple members found with the same name", e); - } - } - - public Member findByEmail(String email) { - try { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ?", - MEMBER_ROW_MAPPER, - email - ); - } catch (EmptyResultDataAccessException e) { - return null; // No member found with the given email - } catch (IncorrectResultSizeDataAccessException e) { - throw new RuntimeException("Multiple members found with the same email", e); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/member/MemberRepository.java b/src/main/java/com/yourssu/roomescape/member/MemberRepository.java new file mode 100644 index 0000000..a56e98c --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/MemberRepository.java @@ -0,0 +1,12 @@ +package com.yourssu.roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmail(String email); + Optional findByEmailAndPassword(String email, String password); + Optional findByName(String name); + +} diff --git a/src/main/java/com/yourssu/roomescape/member/MemberService.java b/src/main/java/com/yourssu/roomescape/member/MemberService.java index a97ed4a..4efe152 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberService.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberService.java @@ -1,41 +1,62 @@ package com.yourssu.roomescape.member; import com.yourssu.roomescape.JwtTokenProvider; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; @Service public class MemberService { - private MemberDao memberDao; + private MemberRepository memberRepository; private JwtTokenProvider jwtTokenProvider; - public MemberService(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) { - this.memberDao = memberDao; + public MemberService(JwtTokenProvider jwtTokenProvider, MemberRepository memberRepository) { this.jwtTokenProvider = jwtTokenProvider; + this.memberRepository = memberRepository; } + @Transactional public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); + Member member = memberRepository.save(new Member( + memberRequest.getName(), + memberRequest.getEmail(), + memberRequest.getPassword(), + "USER" + )); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } public TokenDto login(LoginRequest loginRequest) { - Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); // TODO: 유저 찾기 여기 유저 없으면 예외 처리 어떻게 할지 고민해보기 - String token = jwtTokenProvider.createToken(member.getEmail()); - return new TokenDto(token); + try { + // 1. 먼저 이메일로만 사용자를 찾고 + Member member = memberRepository.findByEmail(loginRequest.getEmail()) + .orElseThrow(() -> new RuntimeException("User not found")); + + // 2. 비밀번호 수동 검증 + if (!member.getPassword().equals(loginRequest.getPassword())) { + throw new RuntimeException("Invalid password"); + } + + // 3. 토큰 생성 및 반환 + String token = jwtTokenProvider.createToken(member.getEmail()); + return new TokenDto(token); + } catch (Exception e) { + e.printStackTrace(); // 디버깅용 + throw new RuntimeException("Invalid credentials", e); + } } public String loginCheck(String token) { String email = jwtTokenProvider.getPayload(token); - Member member = memberDao.findByEmail(email); + Member member = memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("Invalid credentials")); return member.getName(); } public Member findByEmail(String email) { - return memberDao.findByEmail(email); + return memberRepository.findByEmail(email).orElseThrow(() -> new RuntimeException("Invalid credentials")); } public Member findByName(String name) { - return memberDao.findByName(name); + return memberRepository.findByName(name).orElseThrow(() -> new RuntimeException("Invalid credentials")); } } From 2fac15fff7a5517cc0c33695f089aa444b99f186 Mon Sep 17 00:00:00 2001 From: rover0811 Date: Sun, 27 Apr 2025 19:58:39 +0900 Subject: [PATCH 07/37] [Migrate] Time management to use JPA Repository Replaced `TimeDao` with `TimeRepository` for better integration with Spring Data JPA. Updated related service methods and `Time` entity to use JPA annotations for ORM functionality. This simplifies database interaction and aligns with best practices. --- .../com/yourssu/roomescape/time/Time.java | 11 +++-- .../com/yourssu/roomescape/time/TimeDao.java | 41 ------------------- .../yourssu/roomescape/time/TimeService.java | 22 +++++----- 3 files changed, 19 insertions(+), 55 deletions(-) delete mode 100644 src/main/java/com/yourssu/roomescape/time/TimeDao.java diff --git a/src/main/java/com/yourssu/roomescape/time/Time.java b/src/main/java/com/yourssu/roomescape/time/Time.java index 4b298e2..89a9bb8 100644 --- a/src/main/java/com/yourssu/roomescape/time/Time.java +++ b/src/main/java/com/yourssu/roomescape/time/Time.java @@ -1,7 +1,14 @@ package com.yourssu.roomescape.time; +import jakarta.persistence.*; + +@Entity public class Time { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "time_value") private String value; public Time(Long id, String value) { @@ -13,9 +20,7 @@ public Time(String value) { this.value = value; } - public Time() { - - } + public Time() {} public Long getId() { return id; diff --git a/src/main/java/com/yourssu/roomescape/time/TimeDao.java b/src/main/java/com/yourssu/roomescape/time/TimeDao.java deleted file mode 100644 index 7d45644..0000000 --- a/src/main/java/com/yourssu/roomescape/time/TimeDao.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.yourssu.roomescape.time; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import java.sql.PreparedStatement; -import java.util.List; - -@Repository -public class TimeDao { - private final JdbcTemplate jdbcTemplate; - - public TimeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List