diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..b1568bd6a Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index c2065bc26..c988fb160 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,9 @@ build/ !**/src/main/**/build/ !**/src/test/**/build/ +### application.properties 안보이게 만들기 ### +application.properties + ### STS ### .apt_generated .classpath diff --git a/README.md b/README.md new file mode 100644 index 000000000..b305a46d3 --- /dev/null +++ b/README.md @@ -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 파일로 이동하세요. \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8d52aebc6..95d1a0a43 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/src/main/java/roomescape/DataLoader.java b/src/main/java/roomescape/DataLoader.java new file mode 100644 index 000000000..a5aac53e6 --- /dev/null +++ b/src/main/java/roomescape/DataLoader.java @@ -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("어드민", "admin@email.com", "password", "ADMIN")); + Member testUser1 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + } +} diff --git a/src/main/java/roomescape/JpaConfig.java b/src/main/java/roomescape/JpaConfig.java new file mode 100644 index 000000000..3df72f7d1 --- /dev/null +++ b/src/main/java/roomescape/JpaConfig.java @@ -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 { +} diff --git a/src/main/java/roomescape/RoomescapeApplication.java b/src/main/java/roomescape/RoomescapeApplication.java index 2ca0f743f..50ec20a1e 100644 --- a/src/main/java/roomescape/RoomescapeApplication.java +++ b/src/main/java/roomescape/RoomescapeApplication.java @@ -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 { diff --git a/src/main/java/roomescape/TestDataLoader.java b/src/main/java/roomescape/TestDataLoader.java new file mode 100644 index 000000000..1341a057d --- /dev/null +++ b/src/main/java/roomescape/TestDataLoader.java @@ -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("어드민", "admin@email.com", "password", "ADMIN")); + Member testUser1 = memberRepository.save(new Member("브라운", "brown@email.com", "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)); + } +} diff --git a/src/main/java/roomescape/auth/AuthAdminInteceptor.java b/src/main/java/roomescape/auth/AuthAdminInteceptor.java new file mode 100644 index 000000000..2e356619d --- /dev/null +++ b/src/main/java/roomescape/auth/AuthAdminInteceptor.java @@ -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; + } +} diff --git a/src/main/java/roomescape/auth/AuthConfig.java b/src/main/java/roomescape/auth/AuthConfig.java new file mode 100644 index 000000000..d7f3c6986 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthConfig.java @@ -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(); + } +} diff --git a/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java b/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java new file mode 100644 index 000000000..79435f363 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java @@ -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()); + } +} diff --git a/src/main/java/roomescape/auth/AuthService.java b/src/main/java/roomescape/auth/AuthService.java new file mode 100644 index 000000000..ba4eddbb8 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthService.java @@ -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); + } +} diff --git a/src/main/java/roomescape/auth/AuthSession.java b/src/main/java/roomescape/auth/AuthSession.java new file mode 100644 index 000000000..cb52676e7 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthSession.java @@ -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 { +} diff --git a/src/main/java/roomescape/auth/AuthWebConfig.java b/src/main/java/roomescape/auth/AuthWebConfig.java new file mode 100644 index 000000000..74ef85e56 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthWebConfig.java @@ -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 resolvers) { + resolvers.add(authMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authAdminInteceptor).addPathPatterns("/admin"); + } +} diff --git a/src/main/java/roomescape/auth/AuthorizationException.java b/src/main/java/roomescape/auth/AuthorizationException.java new file mode 100644 index 000000000..bb7efbb6e --- /dev/null +++ b/src/main/java/roomescape/auth/AuthorizationException.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/auth/JwtUtils.java b/src/main/java/roomescape/auth/JwtUtils.java new file mode 100644 index 000000000..2066b40b0 --- /dev/null +++ b/src/main/java/roomescape/auth/JwtUtils.java @@ -0,0 +1,68 @@ +package roomescape.auth; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import roomescape.member.Member; + +public class JwtUtils { + @Value("${security.jwt.token.secret-key}") + private String secretKey; + @Value("${security.jwt.token.expire-length}") + private long validityInMilliseconds; + + private final String INVALID_COOKIES = "쿠키 정보가 없습니다."; + + public String createToken(Member member) { + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setSubject(member.getId().toString()) + .claim("email", member.getEmail()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(Keys.hmacShaKeyFor(secretKey.getBytes())) + .compact(); + } + + public Long getPayload(Cookie[] cookies) { + String token = extractTokenFromCookie(cookies); + return Long.valueOf(Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + } + + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token); + + return !claims.getBody().getExpiration().before(new Date()); + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } + + private String extractTokenFromCookie(Cookie[] cookies) { + if(cookies == null) { + throw new IllegalArgumentException(INVALID_COOKIES); + } + return Arrays.stream(cookies) + .filter(cookie -> "token".equals(cookie.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(""); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/auth/jwt/TokenRequest.java b/src/main/java/roomescape/auth/jwt/TokenRequest.java new file mode 100644 index 000000000..21066a8c6 --- /dev/null +++ b/src/main/java/roomescape/auth/jwt/TokenRequest.java @@ -0,0 +1,22 @@ +package roomescape.auth.jwt; + +public class TokenRequest { + private String email; + private String password; + + public TokenRequest() { + } + + public TokenRequest(String email, String password) { + this.email = email; + this.password = password; + } + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/auth/jwt/TokenResponse.java b/src/main/java/roomescape/auth/jwt/TokenResponse.java new file mode 100644 index 000000000..1440ee003 --- /dev/null +++ b/src/main/java/roomescape/auth/jwt/TokenResponse.java @@ -0,0 +1,16 @@ +package roomescape.auth.jwt; + +public class TokenResponse { + private String accessToken; + + public TokenResponse() { + } + + public TokenResponse(String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/member/LoginMember.java b/src/main/java/roomescape/member/LoginMember.java new file mode 100644 index 000000000..0acfe7a79 --- /dev/null +++ b/src/main/java/roomescape/member/LoginMember.java @@ -0,0 +1,31 @@ +package 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; + } +} diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b0..38c129a1a 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,10 +1,25 @@ package roomescape.member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; + + @Column(unique = true) private String email; + private String password; + private String role; public Member(Long id, String name, String email, String role) { @@ -21,6 +36,8 @@ public Member(String name, String email, String password, String role) { this.role = role; } + public Member() {} + public Long getId() { return id; } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..0bb769fea 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -10,13 +10,18 @@ import org.springframework.web.bind.annotation.RestController; import java.net.URI; +import roomescape.auth.AuthService; +import roomescape.auth.jwt.TokenRequest; +import roomescape.auth.jwt.TokenResponse; @RestController public class MemberController { - private MemberService memberService; + private final MemberService memberService; + private final AuthService authService; - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, AuthService authService) { this.memberService = memberService; + this.authService = authService; } @PostMapping("/members") @@ -25,6 +30,31 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } + @PostMapping("/login") + public ResponseEntity login(@RequestBody TokenRequest request, HttpServletResponse response) { + TokenResponse tokenResponse = authService.createToken(request); + + Cookie cookie = new Cookie("token", tokenResponse.getAccessToken()); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + + // HttpHeaders 객체 생성 +// HttpHeaders headers = new HttpHeaders(); +// headers.add("Set-Cookie", "token=" + tokenResponse.getAccessToken() + "; Path=/; HttpOnly"); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + Long memberId = authService.findMemberIdByToken(request.getCookies()); + Member member = memberService.findMemberById(memberId); + MemberResponse response = new MemberResponse(member.getName()); + return ResponseEntity.ok() + .body(response); + } + @PostMapping("/logout") public ResponseEntity logout(HttpServletResponse response) { Cookie cookie = new Cookie("token", ""); diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java deleted file mode 100644 index 81f77f4cd..000000000 --- a/src/main/java/roomescape/member/MemberDao.java +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.member; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -public class MemberDao { - private JdbcTemplate jdbcTemplate; - - 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) { - 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 - ); - } - - 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 - ); - } -} diff --git a/src/main/java/roomescape/member/MemberRepository.java b/src/main/java/roomescape/member/MemberRepository.java new file mode 100644 index 000000000..1a0611429 --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,7 @@ +package roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MemberRepository extends JpaRepository { + Member findByEmailAndPassword(String email, String password); +} diff --git a/src/main/java/roomescape/member/MemberResponse.java b/src/main/java/roomescape/member/MemberResponse.java index b9fa3b97a..fe6cec414 100644 --- a/src/main/java/roomescape/member/MemberResponse.java +++ b/src/main/java/roomescape/member/MemberResponse.java @@ -1,10 +1,17 @@ package roomescape.member; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) public class MemberResponse { private Long id; private String name; private String email; + public MemberResponse(String name) { + this.name = name; + } + public MemberResponse(Long id, String name, String email) { this.id = id; this.name = name; diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..bd57e7945 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,19 @@ package roomescape.member; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MemberService { - private MemberDao memberDao; - - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; - } + @Autowired + private MemberRepository memberRepository; 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 Member findMemberById(Long memberId) { + return memberRepository.findById(memberId).orElseThrow(); + } } diff --git a/src/main/java/roomescape/reservation/MyReservationResponse.java b/src/main/java/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 000000000..4f0eb79bf --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,41 @@ +package roomescape.reservation; + +public class MyReservationResponse { + private Long id; + private String theme; + private String date; + private String time; + private String status; + + public MyReservationResponse(Long id, + String theme, + String date, + String time, + String status) { + this.id = id; + this.theme = theme; + this.date = date; + this.time = time; + this.status = status; + } + + public Long getId() { + return id; + } + + public String getTheme() { + return theme; + } + + public String getDate() { + return date; + } + + public String getTime() { + return time; + } + + public String getStatus() { + return status; + } +} diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1b..48033d956 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,28 +1,47 @@ package roomescape.reservation; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import roomescape.member.Member; import roomescape.theme.Theme; import roomescape.time.Time; +@Entity public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String date; + + @ManyToOne private Time time; + + @ManyToOne private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { + @ManyToOne + private Member member; + + public Reservation(Long id, String name, String date, Time time, Theme theme, Member member) { this.id = id; this.name = name; this.date = date; this.time = time; this.theme = theme; + this.member = member; } - public Reservation(String name, String date, Time time, Theme theme) { + public Reservation(String name, String date, Time time, Theme theme, Member member) { this.name = name; this.date = date; this.time = time; this.theme = theme; + this.member = member; } public Reservation() { @@ -48,4 +67,8 @@ public Time getTime() { public Theme getTheme() { return theme; } + + public Member getMember() { + return member; + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..ff83946fb 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -10,6 +10,8 @@ import java.net.URI; import java.util.List; +import roomescape.auth.AuthSession; +import roomescape.member.LoginMember; @RestController public class ReservationController { @@ -26,14 +28,13 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, @AuthSession LoginMember loginMember) { + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } - ReservationResponse reservation = reservationService.save(reservationRequest); + ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); } @@ -43,4 +44,9 @@ public ResponseEntity delete(@PathVariable Long id) { reservationService.deleteById(id); return ResponseEntity.noContent().build(); } + + @GetMapping("/reservations-mine") + public List myList(@AuthSession LoginMember loginMember) { + return reservationService.findMyList(loginMember); + } } diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java deleted file mode 100644 index a4972430c..000000000 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ /dev/null @@ -1,127 +0,0 @@ -package roomescape.reservation; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.theme.Theme; -import roomescape.time.Time; - -import java.sql.PreparedStatement; -import java.util.List; - -@Repository -public class ReservationDao { - - private final JdbcTemplate jdbcTemplate; - - public ReservationDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id", - - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public Reservation save(ReservationRequest reservationRequest) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement("INSERT INTO reservation(date, name, theme_id, time_id) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, reservationRequest.getDate()); - ps.setString(2, reservationRequest.getName()); - ps.setLong(3, reservationRequest.getTheme()); - ps.setLong(4, reservationRequest.getTime()); - return ps; - }, keyHolder); - - Time time = jdbcTemplate.queryForObject("SELECT * FROM time WHERE id = ?", - (rs, rowNum) -> new Time(rs.getLong("id"), rs.getString("time_value")), - reservationRequest.getTime()); - - Theme theme = jdbcTemplate.queryForObject("SELECT * FROM theme WHERE id = ?", - (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), - reservationRequest.getTheme()); - - return new Reservation( - keyHolder.getKey().longValue(), - reservationRequest.getName(), - reservationRequest.getDate(), - time, - theme - ); - } - - public void deleteById(Long id) { - jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); - } - - public List findReservationsByDateAndTheme(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id" + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public List findByDateAndThemeId(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id " + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } -} diff --git a/src/main/java/roomescape/reservation/ReservationRepository.java b/src/main/java/roomescape/reservation/ReservationRepository.java new file mode 100644 index 000000000..4cc9a5e07 --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,9 @@ +package roomescape.reservation; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReservationRepository extends JpaRepository { + List findByDateAndThemeId(String date, Long themeId); + List findByMemberId(Long id); +} diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f441246..fae7d5ee2 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -5,6 +5,7 @@ public class ReservationRequest { private String date; private Long theme; private Long time; + private Long memberId; public String getName() { return name; @@ -21,4 +22,16 @@ public Long getTheme() { public Long getTime() { return time; } + + public long getMemberId() { + return memberId; + } + + public void setName(String name) { + this.name = name; + } + + public void setMemberId(Long memberId) { + this.memberId = memberId; + } } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..7637e3005 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,87 @@ package roomescape.reservation; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; +import roomescape.member.LoginMember; +import roomescape.member.Member; +import roomescape.member.MemberRepository; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; +import roomescape.waiting.WaitingRepository; @Service public class ReservationService { - private ReservationDao reservationDao; + @Autowired + private ReservationRepository reservationRepository; + @Autowired + private TimeRepository timeRepository; + @Autowired + private ThemeRepository themeRepository; + @Autowired + private MemberRepository memberRepository; + @Autowired + private WaitingRepository waitingRepository; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; - } + public ReservationResponse save(ReservationRequest reservationRequest, LoginMember loginMember) { + Time time = timeRepository.findById(reservationRequest.getTime()).orElseThrow(); + Theme theme = themeRepository.findById(reservationRequest.getTheme()).orElseThrow(); + Member member = memberRepository.findById(loginMember.getId()).orElseThrow(); + + reservationRequest.setName(loginMember.getName()); + reservationRequest.setMemberId(loginMember.getId()); - public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + Reservation reservation = + reservationRepository.save( + new Reservation(reservationRequest.getName(), + reservationRequest.getDate(), + time, + theme, + member)); - 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 void deleteById(Long id) { - reservationDao.deleteById(id); + reservationRepository.deleteById(id); } public List findAll() { - return reservationDao.findAll().stream() - .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) + return reservationRepository.findAll().stream() + .map(it -> new ReservationResponse(it.getId(), + it.getName(), + it.getTheme().getName(), + it.getDate(), + it.getTime().getValue())) .toList(); } + + public List findMyList(LoginMember loginMember) { + List reservationResponseList = Stream.concat( + reservationRepository.findByMemberId(loginMember.getId()).stream() + .map(it -> new MyReservationResponse( + it.getId(), + it.getTheme().getName(), + it.getDate(), + it.getTime().getValue(), + "예약")), + waitingRepository.findWaitingsWithRankByMemberId(loginMember.getId()).stream() + .map(it -> new MyReservationResponse( + it.getWaiting().getId(), + it.getWaiting().getTheme().getName(), + it.getWaiting().getDate(), + it.getWaiting().getTime().getValue(), + (it.getRank() + 1) + "번째 예약대기")) + ).collect(Collectors.toList()); + return reservationResponseList; + } } diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239c..8be4abd92 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,6 +1,15 @@ package roomescape.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Theme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String description; diff --git a/src/main/java/roomescape/theme/ThemeController.java b/src/main/java/roomescape/theme/ThemeController.java index 03bca41a6..f336ffe85 100644 --- a/src/main/java/roomescape/theme/ThemeController.java +++ b/src/main/java/roomescape/theme/ThemeController.java @@ -13,26 +13,26 @@ @RestController public class ThemeController { - private ThemeDao themeDao; + private ThemeRepository themeRepository; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; + public ThemeController(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; } @PostMapping("/themes") public ResponseEntity createTheme(@RequestBody Theme theme) { - Theme newTheme = themeDao.save(theme); + Theme newTheme = themeRepository.save(theme); return ResponseEntity.created(URI.create("/themes/" + newTheme.getId())).body(newTheme); } @GetMapping("/themes") public ResponseEntity> list() { - return ResponseEntity.ok(themeDao.findAll()); + return ResponseEntity.ok(themeRepository.findAll()); } @DeleteMapping("/themes/{id}") public ResponseEntity deleteTheme(@PathVariable Long id) { - themeDao.deleteById(id); + themeRepository.deleteById(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/roomescape/theme/ThemeDao.java b/src/main/java/roomescape/theme/ThemeDao.java deleted file mode 100644 index 945341d8d..000000000 --- a/src/main/java/roomescape/theme/ThemeDao.java +++ /dev/null @@ -1,41 +0,0 @@ -package roomescape.theme; - -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.util.List; - -@Repository -public class ThemeDao { - private JdbcTemplate jdbcTemplate; - - public ThemeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query("SELECT * FROM theme where deleted = false", (rs, rowNum) -> new Theme( - rs.getLong("id"), - rs.getString("name"), - rs.getString("description") - )); - } - - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO theme(name, description) VALUES (?, ?)", new String[]{"id"}); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getDescription()); - return ps; - }, keyHolder); - - return new Theme(keyHolder.getKey().longValue(), theme.getName(), theme.getDescription()); - } - - public void deleteById(Long id) { - jdbcTemplate.update("UPDATE theme SET deleted = true WHERE id = ?", id); - } -} diff --git a/src/main/java/roomescape/theme/ThemeRepository.java b/src/main/java/roomescape/theme/ThemeRepository.java new file mode 100644 index 000000000..cbdb21a3d --- /dev/null +++ b/src/main/java/roomescape/theme/ThemeRepository.java @@ -0,0 +1,6 @@ +package roomescape.theme; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ThemeRepository extends JpaRepository { +} diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/Time.java index 008ed93cf..b3b27ab11 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,7 +1,21 @@ package roomescape.time; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "time") public class Time { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "time_value", nullable = false) private String value; public Time(Long id, String value) { diff --git a/src/main/java/roomescape/time/TimeDao.java b/src/main/java/roomescape/time/TimeDao.java deleted file mode 100644 index f39a9a328..000000000 --- a/src/main/java/roomescape/time/TimeDao.java +++ /dev/null @@ -1,41 +0,0 @@ -package 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