diff --git a/.gitignore b/.gitignore index c2065bc2..94c92c7e 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,5 @@ out/ ### VS Code ### .vscode/ + +src/main/resources/application-oauth.properties \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8d52aebc..9bc129a0 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ 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-data-jpa' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' diff --git a/src/main/java/roomescape/TestDataLoader.java b/src/main/java/roomescape/TestDataLoader.java new file mode 100644 index 00000000..72c1ca4a --- /dev/null +++ b/src/main/java/roomescape/TestDataLoader.java @@ -0,0 +1,53 @@ +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 final MemberRepository memberRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + private final ReservationRepository reservationRepository; + + public TestDataLoader(MemberRepository memberRepository, ThemeRepository themeRepository, TimeRepository timeRepository, ReservationRepository reservationRepository) { + this.memberRepository = memberRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + this.reservationRepository = reservationRepository; + } + + @Override + public void run(String... args) throws Exception { + final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + final Member member2 = 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, member1)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", time2, theme2, member1)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", time3, theme3, member1)); + + reservationRepository.save(new Reservation("브라운", "2024-03-01", time4, theme1, member2)); + } +} diff --git a/src/main/java/roomescape/auth/AdminAccessInterceptor.java b/src/main/java/roomescape/auth/AdminAccessInterceptor.java new file mode 100644 index 00000000..59567180 --- /dev/null +++ b/src/main/java/roomescape/auth/AdminAccessInterceptor.java @@ -0,0 +1,53 @@ +package roomescape.auth; + +import io.jsonwebtoken.Claims; +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; +import roomescape.member.MemberService; +import roomescape.member.ViewMemberResponse; + +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Optional; + +@Component +public class AdminAccessInterceptor implements HandlerInterceptor { + + private final JwtProvider jwtProvider; + private final MemberService memberService; + + public AdminAccessInterceptor(JwtProvider jwtProvider, MemberService memberService) { + this.jwtProvider = jwtProvider; + this.memberService = memberService; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + Optional token = Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("token")) + .findFirst() + .map(Cookie::getValue); + + if(token.isEmpty()) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + String tokenValue = token.get(); + Claims claims = jwtProvider.getClaimsFromToken(tokenValue); + String role = claims.get("role", String.class); + ViewMemberResponse member = memberService.findMemberById(Long.parseLong(claims.getSubject())); + + if(member == null || !member.getRole().equalsIgnoreCase("admin")) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + 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 00000000..b05e584d --- /dev/null +++ b/src/main/java/roomescape/auth/AuthConfig.java @@ -0,0 +1,15 @@ +package roomescape.auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuthConfig { + + @Bean + public JwtUtils jwtUtils(@Value("${roomescape.auth.jwt.secret}") String secretKey, + @Value("${roomescape.auth.jwt.expiration}") String expiration) { + return new JwtUtils(secretKey, expiration); + } +} diff --git a/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java b/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java new file mode 100644 index 00000000..ea463c4c --- /dev/null +++ b/src/main/java/roomescape/auth/AuthMemberArgumentResolver.java @@ -0,0 +1,34 @@ +package roomescape.auth; + +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; + +import java.util.Arrays; + +@Component +public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null; + boolean isUserClass = LoginMember.class.equals(parameter.getParameterType()); + return isLoginUserAnnotation && isUserClass; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + Arrays.stream(request.getCookies()) + .filter(cookie -> cookie.getName().equals("token")) + .findFirst() + .map(Cookie::getValue) + .orElseThrow(()-> new IllegalArgumentException("로그인이 필요합니다.")); + LoginMember loginUser = (LoginMember) request.getAttribute("loginUser"); + return loginUser; + } +} diff --git a/src/main/java/roomescape/auth/DataLoader.java b/src/main/java/roomescape/auth/DataLoader.java new file mode 100644 index 00000000..ba15f04a --- /dev/null +++ b/src/main/java/roomescape/auth/DataLoader.java @@ -0,0 +1,25 @@ +package roomescape.auth; + +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 final MemberRepository memberRepository; + + public DataLoader(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @Override + public void run(String... args) throws Exception { + final Member member1 = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + final Member member2 = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + } + +} diff --git a/src/main/java/roomescape/auth/JwtProvider.java b/src/main/java/roomescape/auth/JwtProvider.java new file mode 100644 index 00000000..a77a2339 --- /dev/null +++ b/src/main/java/roomescape/auth/JwtProvider.java @@ -0,0 +1,26 @@ +package roomescape.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +@Component +public class JwtProvider { + @Value("${roomescape.auth.jwt.secret}") + private String secretKey; + @Value("${roomescape.auth.jwt.expiration}") + private Long expiration; + + public Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/roomescape/auth/JwtUtils.java b/src/main/java/roomescape/auth/JwtUtils.java new file mode 100644 index 00000000..9136ce7d --- /dev/null +++ b/src/main/java/roomescape/auth/JwtUtils.java @@ -0,0 +1,28 @@ +package roomescape.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +public class JwtUtils { + private String secretKey; + private String expiration; + + public JwtUtils(String secretKey, String expiration) { + this.secretKey = secretKey; + this.expiration = expiration; + } + + public Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} diff --git a/src/main/java/roomescape/auth/LoginMember.java b/src/main/java/roomescape/auth/LoginMember.java new file mode 100644 index 00000000..e3eeac16 --- /dev/null +++ b/src/main/java/roomescape/auth/LoginMember.java @@ -0,0 +1,50 @@ +package roomescape.auth; + +import java.nio.file.attribute.UserPrincipal; + +public class LoginMember implements UserPrincipal { + + 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 void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} diff --git a/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java new file mode 100644 index 00000000..6ded4b07 --- /dev/null +++ b/src/main/java/roomescape/auth/LoginMemberArgumentResolver.java @@ -0,0 +1,70 @@ +package roomescape.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwt; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +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.MemberService; +import roomescape.member.ViewMemberResponse; + +@Component +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final MemberService memberService; + private final JwtProvider jwtProvider; + public LoginMemberArgumentResolver(MemberService memberService, JwtProvider jwtProvider) { + this.memberService = memberService; + this.jwtProvider = jwtProvider; + } + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean isLoginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class) != null; + boolean isUserClass = LoginMember.class.equals(parameter.getParameterType()); + return isLoginUserAnnotation && isUserClass; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + Cookie[] cookies = request.getCookies(); + + if (cookies != null) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + String token = cookie.getValue(); + System.out.println("=========="); + System.out.println("token == " + token); + System.out.println("=========="); + + Claims claims = jwtProvider.getClaimsFromToken(token); + if (claims != null && claims.getSubject() != null) { + System.out.println("claims == " + claims.getSubject()); + Long memberId = Long.parseLong(claims.getSubject()); + ViewMemberResponse member = memberService.findMemberById(memberId); + + if (member != null) { + System.out.println("=========="); + System.out.println("member == " + member.getEmail()); + System.out.println("=========="); + + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } else { + throw new IllegalArgumentException("해당 ID의 회원을 찾을 수 없습니다."); + } + } else { + throw new IllegalArgumentException("토큰에서 유효한 클레임을 추출할 수 없습니다."); + } + } + } + } + throw new IllegalArgumentException("유효한 인증 정보가 없습니다."); + } +} diff --git a/src/main/java/roomescape/auth/LoginUser.java b/src/main/java/roomescape/auth/LoginUser.java new file mode 100644 index 00000000..df47530d --- /dev/null +++ b/src/main/java/roomescape/auth/LoginUser.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; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface LoginUser { +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 00000000..9c81aa56 --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,41 @@ +package roomescape.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.auth.AdminAccessInterceptor; +import roomescape.auth.AuthMemberArgumentResolver; +import roomescape.auth.LoginMemberArgumentResolver; + +import java.util.List; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final LoginMemberArgumentResolver loginMemberArgumentResolver; + private final AuthMemberArgumentResolver authMemberArgumentResolver; + private final AdminAccessInterceptor adminAccessInterceptor; + + @Autowired + public WebConfig(LoginMemberArgumentResolver loginMemberArgumentResolver, + AuthMemberArgumentResolver authMemberArgumentResolver, + AdminAccessInterceptor adminAccessInterceptor) { + this.loginMemberArgumentResolver = loginMemberArgumentResolver; + this.authMemberArgumentResolver = authMemberArgumentResolver; + this.adminAccessInterceptor = adminAccessInterceptor; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginMemberArgumentResolver); + resolvers.add(authMemberArgumentResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAccessInterceptor) + .addPathPatterns("/admin"); + } +} diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b..d666964d 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,6 +1,17 @@ package roomescape.member; +import jakarta.persistence.*; +import roomescape.reservation.Reservation; + +import java.util.List; + +import static jakarta.persistence.GenerationType.*; + +@Entity public class Member { + + @Id + @GeneratedValue(strategy = IDENTITY) private Long id; private String name; private String email; @@ -21,6 +32,10 @@ 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 881ae5e0..297f1e2b 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,5 +1,9 @@ package roomescape.member; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -10,14 +14,18 @@ import org.springframework.web.bind.annotation.RestController; import java.net.URI; +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; @RestController public class MemberController { private MemberService memberService; - public MemberController(MemberService memberService) { this.memberService = memberService; } + private static final String SECRET_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; @PostMapping("/members") public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { @@ -25,6 +33,61 @@ public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member); } + @PostMapping("/login") + public ResponseEntity login(@RequestBody Map params, HttpServletResponse response) { + + String email = params.get("email"); + String password = params.get("password"); + + ViewMemberResponse viewMemberResponse = memberService.findMemberByEmailAndPassword(email, password); + + String assessToken = Jwts.builder() + .setSubject(viewMemberResponse.getId().toString()) + .claim("name", viewMemberResponse.getName()) + .claim("role", viewMemberResponse.getRole()) + .signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes()) + .compact(); + + Cookie cookie = new Cookie("token", assessToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + + return ResponseEntity.ok().build(); + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies != null) { + String token = extractTokenFromCookie(cookies); + if (!token.isEmpty()) { + try { + Long memberId = Long.valueOf(Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) + .build() + .parseClaimsJws(token) + .getBody().getSubject()); + + ViewMemberResponse memberResponse = memberService.findMemberById(memberId); + return ResponseEntity.ok(memberResponse); + } catch (Exception e) { + return ResponseEntity.status(401).build(); + } + } + } + return ResponseEntity.status(401).build(); + } + private String extractTokenFromCookie(Cookie[] cookies) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + + return ""; + } + @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 81f77f4c..00000000 --- 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 00000000..ca6bbaef --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,10 @@ +package roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + Optional findByEmailAndPassword(String email, String password); + Optional findByName(String name); +} diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba..3da097d7 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -4,14 +4,24 @@ @Service public class MemberService { - private MemberDao memberDao; + private MemberRepository memberRepository; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberRepository) { + this.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 ViewMemberResponse findMemberByEmailAndPassword (String email, String password) { + Member member = memberRepository.findByEmailAndPassword(email, password).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + return new ViewMemberResponse(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } + + public ViewMemberResponse findMemberById (Long id) { + Member member = memberRepository.findById(id).orElseThrow(()-> new IllegalArgumentException("찾을 수 없는 member id 입니다.")); + return new ViewMemberResponse(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } } diff --git a/src/main/java/roomescape/member/ViewMemberResponse.java b/src/main/java/roomescape/member/ViewMemberResponse.java new file mode 100644 index 00000000..aad33156 --- /dev/null +++ b/src/main/java/roomescape/member/ViewMemberResponse.java @@ -0,0 +1,33 @@ +package roomescape.member; + +public class ViewMemberResponse { + + private Long id; + private String name; + private String email; + private String role; + + public ViewMemberResponse(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/reservation/MyReservationResponse.java b/src/main/java/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 00000000..cf71694f --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,72 @@ +package roomescape.reservation; + +import roomescape.waiting.WaitingWithRank; + +import java.util.List; +import java.util.stream.Collectors; + +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; + } + + public static List from(List reservationResponses) { + return reservationResponses.stream() + .map(reservation -> new MyReservationResponse( + reservation.getId(), reservation.getTheme(), reservation.getDate(), reservation.getTime(), "예약")) + .collect(Collectors.toList()); + } + + public static List of(List reservations, List waitingWithRanks) { + List myReservationResponses = reservations.stream() + .map(reservation -> new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue(), + "예약")) + .collect(Collectors.toList()); + + waitingWithRanks.stream().map(reservation -> new MyReservationResponse( + reservation.getWaiting().getId(), + reservation.getWaiting().getTheme().getName(), + reservation.getWaiting().getDate(), + reservation.getWaiting().getTime().getValue(), + String.format("%d번째 예약대기", reservation.getRank() + 1))) + .forEach(myReservationResponses::add); + + return myReservationResponses; + + } +} diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1..0c9dd55c 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,28 +1,46 @@ package roomescape.reservation; +import jakarta.persistence.*; +import roomescape.member.Member; import roomescape.theme.Theme; import roomescape.time.Time; +import static jakarta.persistence.GenerationType.*; + +@Entity public class Reservation { + @Id + @GeneratedValue(strategy = IDENTITY) private Long id; private String name; private String date; + @ManyToOne + @JoinColumn(name = "time_id") private Time time; + + @ManyToOne + @JoinColumn(name = "theme_id") private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { + @ManyToOne + @JoinColumn(name = "member_id") + 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() { diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef399..d2ff5931 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,12 +1,15 @@ package roomescape.reservation; import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.LoginMember; +import roomescape.auth.LoginUser; import java.net.URI; import java.util.List; @@ -20,19 +23,29 @@ public ReservationController(ReservationService reservationService) { this.reservationService = reservationService; } + @GetMapping("/reservations-mine") + public List myReservation( + @LoginUser LoginMember loginMember + ) { + List reservationResponses = reservationService.findAllByMemberName(loginMember.getName()); + return reservationService.findMyReservationsByMemberName(loginMember.getName()); + } @GetMapping("/reservations") public List list() { return reservationService.findAll(); } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null + public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, @LoginUser LoginMember member) { + + if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) { return ResponseEntity.badRequest().build(); } + if (reservationRequest.getName() == null) { + reservationRequest.setName(member.getName()); + } ReservationResponse reservation = reservationService.save(reservationRequest); return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java deleted file mode 100644 index a4972430..00000000 --- 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 00000000..23eb0bed --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,15 @@ +package roomescape.reservation; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ReservationRepository extends JpaRepository { + List findByDateAndThemeId(String date, Long themeId); + List findByName(String name); + + Optional findByDateAndThemeIdAndTimeId(ReservationRequest reservationRequest, Long themeId, Long timeId); + + List findByMemberId(Long memberId); +} diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index 19f44124..c2af43b1 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -3,8 +3,8 @@ public class ReservationRequest { private String name; private String date; - private Long theme; - private Long time; + private String theme; + private String time; public String getName() { return name; @@ -14,11 +14,15 @@ public String getDate() { return date; } - public Long getTheme() { + public String getTheme() { return theme; } - public Long getTime() { + public String getTime() { return time; } + + public void setName(String name) { + this.name = name; + } } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd331332..6d05d4e0 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,76 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +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; +import roomescape.waiting.WaitingWithRank; import java.util.List; +import java.util.stream.Collectors; @Service public class ReservationService { - private ReservationDao reservationDao; + private final ReservationRepository reservationRepository; + private final TimeRepository timeRepository; + private final ThemeRepository themeRepository; + private final MemberRepository memberRepository; + private final WaitingRepository waitingRepository; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; - } + public ReservationService(ReservationRepository reservationRepository, TimeRepository timeRepository, ThemeRepository themeRepository, MemberRepository memberRepository, WaitingRepository waitingRepository) { + this.reservationRepository = reservationRepository; + this.timeRepository = timeRepository; + this.themeRepository = themeRepository; + this.memberRepository = memberRepository; + this.waitingRepository = waitingRepository; + } public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); - + Time time = timeRepository.findByValue(reservationRequest.getTime()).orElseThrow(() -> new IllegalArgumentException("없는 시간입니다.")); + Theme theme = themeRepository.findByName(reservationRequest.getTheme()).orElseThrow(() -> new IllegalArgumentException("없는 테마입니다.")); + Member member = memberRepository.findByName(reservationRequest.getName()).orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + validateDuplicate(reservationRequest, theme.getId(), time.getId()); + Reservation reservation = new Reservation(reservationRequest.getName(), reservationRequest.getDate(), time, theme, member); + reservationRepository.save(reservation); 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() + return reservationRepository.findAll().stream() .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) .toList(); } + + public List findAllByMemberName(String name) { + return reservationRepository.findByName(name) + .stream() + .map(reservation -> new ReservationResponse( + reservation.getId(), reservation.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue() + )).collect(Collectors.toList()); + } + + private void validateDuplicate(ReservationRequest reservationRequest, Long themeId, Long timeId) { + reservationRepository.findByDateAndThemeIdAndTimeId(reservationRequest, themeId, timeId) + .ifPresent(it -> { + throw new IllegalArgumentException("이미 예약된 시간입니다."); + }); + } + + public List findMyReservationsByMemberName(String name) { + Member member = memberRepository.findByName(name) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다.")); + + final List reservations = reservationRepository.findByMemberId(member.getId()); + final List waitingWithRanks = waitingRepository.findWaitingsWithRankByMemberId(member.getId()); + + return MyReservationResponse.of(reservations, waitingWithRanks); + } } diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239..c8602167 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,6 +1,16 @@ package roomescape.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +import static jakarta.persistence.GenerationType.*; + +@Entity public class Theme { + @Id + @GeneratedValue(strategy = 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 03bca41a..f336ffe8 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 945341d8..00000000 --- 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 00000000..717125f7 --- /dev/null +++ b/src/main/java/roomescape/theme/ThemeRepository.java @@ -0,0 +1,9 @@ +package roomescape.theme; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ThemeRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/Time.java index 008ed93c..ba3d8265 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,7 +1,16 @@ package roomescape.time; +import jakarta.persistence.*; + +import static jakarta.persistence.GenerationType.IDENTITY; + +@Entity public class Time { + @Id + @GeneratedValue(strategy = IDENTITY) private Long id; + + @Column(name = "time_value") 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 f39a9a32..00000000 --- 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