diff --git a/build.gradle b/build.gradle index d5f9249b..fcf72cb6 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 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/roomescape/DataLoader.java b/src/main/java/roomescape/DataLoader.java new file mode 100644 index 00000000..3dbe0796 --- /dev/null +++ b/src/main/java/roomescape/DataLoader.java @@ -0,0 +1,25 @@ +package roomescape; + +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +public class DataLoader implements CommandLineRunner { + + private final MemberRepository memberRepository; + + @Override + public void run(String... args) throws Exception { + Member member1 = new Member("어드민", "admin@email.com", "password", "ADMIN"); + Member member2 = new Member("브라운", "brown@email.com", "password", "USER"); + + memberRepository.save(member1); + memberRepository.save(member2); + } +} diff --git a/src/main/java/roomescape/TestDataLoader.java b/src/main/java/roomescape/TestDataLoader.java new file mode 100644 index 00000000..44eb675b --- /dev/null +++ b/src/main/java/roomescape/TestDataLoader.java @@ -0,0 +1,48 @@ +package roomescape; + +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +public class TestDataLoader implements CommandLineRunner { + + private final TimeRepository timeRepository; + private final ThemeRepository themeRepository; + private final MemberRepository memberRepository; + private final ReservationRepository reservationRepository; + + @Override + public void run(String... args) throws Exception { + Member adminMember = memberRepository.save(new Member("어드민", "admin@email.com", "password", "ADMIN")); + Member userMember = memberRepository.save(new Member("브라운", "brown@email.com", "password", "USER")); + + 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")); + + 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입니다.")); + + reservationRepository.save(new Reservation("어드민", "2024-03-01", time1, theme1, adminMember)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", time2, theme2, adminMember)); + reservationRepository.save(new Reservation("어드민", "2024-03-01", time3, theme3, adminMember)); + + reservationRepository.save(new Reservation("브라운", "2024-03-01", time1, theme2, userMember)); + } +} diff --git a/src/main/java/roomescape/authentication/AuthenticationExtractor.java b/src/main/java/roomescape/authentication/AuthenticationExtractor.java new file mode 100644 index 00000000..0c6c9d90 --- /dev/null +++ b/src/main/java/roomescape/authentication/AuthenticationExtractor.java @@ -0,0 +1,10 @@ +package roomescape.authentication; + +import jakarta.servlet.http.Cookie; + +public interface AuthenticationExtractor { + + MemberAuthInfo extractMemberAuthInfoFromToken(String token); + + AuthenticationResponse extractTokenFromCookie(Cookie[] cookies); +} diff --git a/src/main/java/roomescape/authentication/AuthenticationService.java b/src/main/java/roomescape/authentication/AuthenticationService.java index d50fc2fb..856984e4 100644 --- a/src/main/java/roomescape/authentication/AuthenticationService.java +++ b/src/main/java/roomescape/authentication/AuthenticationService.java @@ -3,7 +3,6 @@ import jakarta.servlet.http.Cookie; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import roomescape.authentication.jwt.JwtAuthenticationInfoExtractor; import roomescape.member.Member; @Service @@ -11,17 +10,22 @@ public class AuthenticationService { private final AuthenticationProvider authenticationProvider; - private final JwtAuthenticationInfoExtractor jwtAuthenticationInfoExtractor; + private final AuthenticationExtractor authenticationExtractor; public AuthenticationResponse createToken(Member member) { return authenticationProvider.createAuthenticationMethod(member); } public AuthenticationResponse extractToken(Cookie [] cookies){ - return jwtAuthenticationInfoExtractor.extractTokenFromCookie(cookies); + return authenticationExtractor.extractTokenFromCookie(cookies); } public MemberAuthInfo extractMemberInfo(String token) { - return jwtAuthenticationInfoExtractor.extractMemberAuthInfoFromToken(token); + return authenticationExtractor.extractMemberAuthInfoFromToken(token); + } + + public MemberAuthInfo getMemberAuthInfoFromCookies(Cookie[] cookies) { + AuthenticationResponse authenticationResponse = extractToken(cookies); + return extractMemberInfo(authenticationResponse.accessToken()); } } diff --git a/src/main/java/roomescape/authentication/AuthenticationConfig.java b/src/main/java/roomescape/authentication/AuthenticationWebConfig.java similarity index 93% rename from src/main/java/roomescape/authentication/AuthenticationConfig.java rename to src/main/java/roomescape/authentication/AuthenticationWebConfig.java index afd0bc58..8b108c6a 100644 --- a/src/main/java/roomescape/authentication/AuthenticationConfig.java +++ b/src/main/java/roomescape/authentication/AuthenticationWebConfig.java @@ -10,7 +10,7 @@ @Configuration @RequiredArgsConstructor -public class AuthenticationConfig implements WebMvcConfigurer { +public class AuthenticationWebConfig implements WebMvcConfigurer { private final LoginMemberArgumentResolver loginMemberArgumentResolver; private final AuthAdminRoleInterceptor authAdminRoleInterceptor; diff --git a/src/main/java/roomescape/authentication/LoginMemberArgumentResolver.java b/src/main/java/roomescape/authentication/LoginMemberArgumentResolver.java index 15569dc6..4ae25d57 100644 --- a/src/main/java/roomescape/authentication/LoginMemberArgumentResolver.java +++ b/src/main/java/roomescape/authentication/LoginMemberArgumentResolver.java @@ -25,11 +25,12 @@ public boolean supportsParameter(MethodParameter parameter) { public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = ((ServletWebRequest) webRequest).getRequest(); - AuthenticationResponse authenticationResponse = authenticationService.extractToken(request.getCookies()); - if (authenticationResponse.accessToken() == null) { + MemberAuthInfo memberAuthInfo = authenticationService.getMemberAuthInfoFromCookies(request.getCookies()); + + if (memberAuthInfo == null){ return null; } - return authenticationService.extractMemberInfo(authenticationResponse.accessToken()); + return memberAuthInfo; } } diff --git a/src/main/java/roomescape/authentication/MemberAuthInfo.java b/src/main/java/roomescape/authentication/MemberAuthInfo.java index d2d2027f..c5dbaec7 100644 --- a/src/main/java/roomescape/authentication/MemberAuthInfo.java +++ b/src/main/java/roomescape/authentication/MemberAuthInfo.java @@ -1,6 +1,7 @@ package roomescape.authentication; public record MemberAuthInfo( + Long id, String name, String role) { } diff --git a/src/main/java/roomescape/authentication/WebConfig.java b/src/main/java/roomescape/authentication/WebConfig.java new file mode 100644 index 00000000..30c227b2 --- /dev/null +++ b/src/main/java/roomescape/authentication/WebConfig.java @@ -0,0 +1,22 @@ +package roomescape.authentication; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import roomescape.authentication.jwt.JwtAuthenticationInfoExtractor; +import roomescape.authentication.jwt.JwtAuthenticationProvider; + +@Configuration +@RequiredArgsConstructor +public class WebConfig { + + @Bean + AuthenticationProvider authenticationProvider(){ + return new JwtAuthenticationProvider(); + } + + @Bean + AuthenticationExtractor authenticationExtractor(){ + return new JwtAuthenticationInfoExtractor(); + } +} diff --git a/src/main/java/roomescape/authentication/jwt/JwtAuthenticationInfoExtractor.java b/src/main/java/roomescape/authentication/jwt/JwtAuthenticationInfoExtractor.java index a64bc8d2..6c4eb7b7 100644 --- a/src/main/java/roomescape/authentication/jwt/JwtAuthenticationInfoExtractor.java +++ b/src/main/java/roomescape/authentication/jwt/JwtAuthenticationInfoExtractor.java @@ -7,16 +7,15 @@ import jakarta.servlet.http.Cookie; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; +import roomescape.authentication.AuthenticationExtractor; import roomescape.authentication.AuthenticationResponse; import roomescape.authentication.MemberAuthInfo; import roomescape.exception.JwtValidationException; import java.util.Arrays; -@Component @RequiredArgsConstructor -public class JwtAuthenticationInfoExtractor { +public class JwtAuthenticationInfoExtractor implements AuthenticationExtractor { @Value("${roomescape.auth.jwt.secret}") private String secretKey; @@ -33,10 +32,11 @@ public MemberAuthInfo extractMemberAuthInfoFromToken(String token) { .parseClaimsJws(token) .getBody(); + Long id = Long.valueOf(claims.getSubject()); String name = claims.get("name", String.class); String role = claims.get("role", String.class); - return new MemberAuthInfo(name, role); + return new MemberAuthInfo(id, name, role); } catch (JwtException e) { throw new JwtValidationException("유효하지 않은 JWT 토큰입니다.", e); } diff --git a/src/main/java/roomescape/authentication/jwt/JwtAuthenticationProvider.java b/src/main/java/roomescape/authentication/jwt/JwtAuthenticationProvider.java index 30725af5..8513a78c 100644 --- a/src/main/java/roomescape/authentication/jwt/JwtAuthenticationProvider.java +++ b/src/main/java/roomescape/authentication/jwt/JwtAuthenticationProvider.java @@ -5,13 +5,11 @@ import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; import roomescape.authentication.AuthenticationProvider; import roomescape.authentication.AuthenticationResponse; import roomescape.exception.JwtProviderException; import roomescape.member.Member; -@Component @RequiredArgsConstructor public class JwtAuthenticationProvider implements AuthenticationProvider { diff --git a/src/main/java/roomescape/exception/DuplicateReservationException.java b/src/main/java/roomescape/exception/DuplicateReservationException.java new file mode 100644 index 00000000..255d8b28 --- /dev/null +++ b/src/main/java/roomescape/exception/DuplicateReservationException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class DuplicateReservationException extends RuntimeException { + public DuplicateReservationException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/exception/ErrorResponse.java b/src/main/java/roomescape/exception/ErrorResponse.java new file mode 100644 index 00000000..0af67d12 --- /dev/null +++ b/src/main/java/roomescape/exception/ErrorResponse.java @@ -0,0 +1,19 @@ +package roomescape.exception; + +public class ErrorResponse { + private final String message; + private final String type; + + public ErrorResponse(String message, String type) { + this.message = message; + this.type = type; + } + + public String getMessage() { + return message; + } + + public String getType() { + return type; + } +} diff --git a/src/main/java/roomescape/exception/GeneralExceptionHandler.java b/src/main/java/roomescape/exception/GeneralExceptionHandler.java index d14e8f5f..c682e55e 100644 --- a/src/main/java/roomescape/exception/GeneralExceptionHandler.java +++ b/src/main/java/roomescape/exception/GeneralExceptionHandler.java @@ -1,31 +1,37 @@ package roomescape.exception; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +@Slf4j @RestControllerAdvice public class GeneralExceptionHandler { - @ExceptionHandler(MemberNotFoundException.class) - public ResponseEntity handleMemberNotFound(MemberNotFoundException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + @ExceptionHandler({MemberNotFoundException.class, JwtValidationException.class, JwtProviderException.class}) + public ResponseEntity handleMemberNotFound(Exception e) { + ErrorResponse authenticationErrorResponse = new ErrorResponse(e.getMessage(), "authentication_error"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(authenticationErrorResponse); } - @ExceptionHandler(JwtValidationException.class) - public ResponseEntity handleJwtValidationException(JwtValidationException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + @ExceptionHandler({TimeNotFoundException.class, ThemeNotFoundException.class}) + public ResponseEntity handleTimeNotFound(Exception e) { + ErrorResponse notFoundErrorResponse = new ErrorResponse(e.getMessage(), "not_found"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(notFoundErrorResponse); } - @ExceptionHandler(JwtProviderException.class) - public ResponseEntity handleJwtProviderException(JwtProviderException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(e.getMessage()); + @ExceptionHandler(DuplicateReservationException.class) + public ResponseEntity handleDuplicatedReservation(DuplicateReservationException e) { + ErrorResponse duplicationErrorResponse = new ErrorResponse(e.getMessage(), "duplicate_reservation"); + return ResponseEntity.status(HttpStatus.CONFLICT).body(duplicationErrorResponse); } @ExceptionHandler(Exception.class) - public ResponseEntity handleGeneralException(Exception e) { - e.printStackTrace(); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + public ResponseEntity handleGeneralException(Exception e) { + log.error("Exception [Err_Location] : {}", e.getStackTrace()[0], e); + ErrorResponse errorResponse = new ErrorResponse("잠깐 문제가 생겼어요. 다음에 다시 시도해주세요.", "internal_server_error"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); } } diff --git a/src/main/java/roomescape/exception/MemberNotFoundException.java b/src/main/java/roomescape/exception/MemberNotFoundException.java index b8a7db7a..2df644f2 100644 --- a/src/main/java/roomescape/exception/MemberNotFoundException.java +++ b/src/main/java/roomescape/exception/MemberNotFoundException.java @@ -1,7 +1,8 @@ package roomescape.exception; public class MemberNotFoundException extends RuntimeException { - public MemberNotFoundException(String message, Throwable cause) { - super(message, cause); + + public MemberNotFoundException(String message) { + super(message); } } diff --git a/src/main/java/roomescape/exception/ThemeNotFoundException.java b/src/main/java/roomescape/exception/ThemeNotFoundException.java new file mode 100644 index 00000000..a0729e1a --- /dev/null +++ b/src/main/java/roomescape/exception/ThemeNotFoundException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class ThemeNotFoundException extends RuntimeException { + public ThemeNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/exception/TimeNotFoundException.java b/src/main/java/roomescape/exception/TimeNotFoundException.java new file mode 100644 index 00000000..6d6bfacb --- /dev/null +++ b/src/main/java/roomescape/exception/TimeNotFoundException.java @@ -0,0 +1,7 @@ +package roomescape.exception; + +public class TimeNotFoundException extends RuntimeException { + public TimeNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/login/LoginController.java b/src/main/java/roomescape/login/LoginController.java index 8938da68..4e588695 100644 --- a/src/main/java/roomescape/login/LoginController.java +++ b/src/main/java/roomescape/login/LoginController.java @@ -9,8 +9,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import roomescape.authentication.AuthenticationService; -import roomescape.authentication.MemberAuthInfo; import roomescape.authentication.AuthenticationResponse; @RestController @@ -18,7 +16,6 @@ public class LoginController { private final LoginService loginService; - private final AuthenticationService authenticationService; @PostMapping("/login") public void login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResponse response) { @@ -32,10 +29,7 @@ public void login(@Valid @RequestBody LoginRequest loginRequest, HttpServletResp @GetMapping("/login/check") public LoginCheckResponse checkLogin(HttpServletRequest request) { - AuthenticationResponse authenticationResponse = authenticationService.extractToken(request.getCookies()); - MemberAuthInfo memberAuthInfo = authenticationService.extractMemberInfo(authenticationResponse.accessToken()); - LoginCheckResponse loginCheckResponse = loginService.checkLogin(memberAuthInfo); - + LoginCheckResponse loginCheckResponse = loginService.checkLogin(request.getCookies()); return loginCheckResponse; } diff --git a/src/main/java/roomescape/login/LoginService.java b/src/main/java/roomescape/login/LoginService.java index f6c41bf6..3256c9f2 100644 --- a/src/main/java/roomescape/login/LoginService.java +++ b/src/main/java/roomescape/login/LoginService.java @@ -1,38 +1,37 @@ package roomescape.login; +import jakarta.servlet.http.Cookie; import lombok.RequiredArgsConstructor; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; import roomescape.authentication.MemberAuthInfo; import roomescape.authentication.AuthenticationResponse; import roomescape.authentication.AuthenticationService; import roomescape.member.Member; -import roomescape.member.MemberDao; import roomescape.exception.MemberNotFoundException; +import roomescape.member.MemberRepository; + @Service @RequiredArgsConstructor public class LoginService { - private final MemberDao memberDao; + private final MemberRepository memberRepository; private final AuthenticationService authenticationService; + public AuthenticationResponse login(LoginRequest loginRequest) { - try { - Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password()); + Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) + .orElseThrow(() -> new MemberNotFoundException("입력한 이메일 혹은 비밀번호로 가입한 회원을 찾을 수 없습니다.")); + return authenticationService.createToken(member); - } catch (EmptyResultDataAccessException e) { - throw new MemberNotFoundException("이메일 혹은 비밀번호가 맞지 않습니다.", e); - } } - public LoginCheckResponse checkLogin(MemberAuthInfo memberAuthInfo) { + public LoginCheckResponse checkLogin(Cookie[] cookies) { + MemberAuthInfo memberAuthInfo = authenticationService.getMemberAuthInfoFromCookies(cookies); + + Member member= memberRepository.findByName(memberAuthInfo.name()) + .orElseThrow(() -> new MemberNotFoundException("로그인 된 회원이 아닙니다.")); - try { - Member member = memberDao.findByName(memberAuthInfo.name()); - return new LoginCheckResponse(member.getName()); - } catch (EmptyResultDataAccessException e) { - throw new MemberNotFoundException("로그인이 되지 않은 상태입니다.", e); - } + return new LoginCheckResponse(member.getName()); } } diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b..0cfae7ef 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,10 +1,26 @@ package roomescape.member; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; + private String email; + private String password; + private String role; public Member(Long id, String name, String email, String role) { 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..c0ee791c --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,12 @@ +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 bec76725..47b4c2b5 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,16 @@ package roomescape.member; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class MemberService { - private MemberDao memberDao; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; - } + private final MemberRepository memberRepository; public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.name(), memberRequest.email(), memberRequest.password(), "USER")); + Member member = memberRepository.save(new Member(memberRequest.name(), memberRequest.email(), memberRequest.password(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } } diff --git a/src/main/java/roomescape/reservation/MyReservationResponse.java b/src/main/java/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 00000000..c7edc9c9 --- /dev/null +++ b/src/main/java/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,10 @@ +package roomescape.reservation; + +public record MyReservationResponse( + Long reservationId, + String theme, + String date, + String time, + String status +) { +} diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1..01b9e1c5 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,23 +1,47 @@ package roomescape.reservation; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import roomescape.member.Member; import roomescape.theme.Theme; import roomescape.time.Time; +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Reservation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; + private String date; + + @ManyToOne + @JoinColumn(name = "time_id") + @OnDelete(action = OnDeleteAction.CASCADE) private Time time; + + @ManyToOne + @JoinColumn(name = "theme_id") + @OnDelete(action = OnDeleteAction.CASCADE) private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { - this.id = id; + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + 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(String name, String date, Time time, Theme theme) { this.name = name; this.date = date; @@ -25,10 +49,6 @@ public Reservation(String name, String date, Time time, Theme theme) { this.theme = theme; } - public Reservation() { - - } - public Long getId() { return id; } @@ -48,4 +68,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 0ef4bae8..8cc35b1b 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -1,5 +1,6 @@ package roomescape.reservation; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -13,14 +14,11 @@ import java.util.List; @RestController +@RequiredArgsConstructor public class ReservationController { private final ReservationService reservationService; - public ReservationController(ReservationService reservationService) { - this.reservationService = reservationService; - } - @GetMapping("/reservations") public List list() { return reservationService.findAll(); @@ -28,20 +26,14 @@ public List list() { @PostMapping("/reservations") public ResponseEntity create(@RequestBody ReservationRequest reservationRequest, MemberAuthInfo memberAuthInfo) { - if ( memberAuthInfo == null - || reservationRequest.getDate() == null - || reservationRequest.getTheme() == null - || reservationRequest.getTime() == null) { - return ResponseEntity.badRequest().build(); - } - - if (reservationRequest.getName() == null) { - reservationRequest.setName(memberAuthInfo.name()); - } - - ReservationResponse reservation = reservationService.save(reservationRequest); + ReservationResponse reservation = reservationService.save(reservationRequest, memberAuthInfo); + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); + } - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + @GetMapping("/reservations-mine") + public List myReservationLists(MemberAuthInfo memberAuthInfo){ + List myReservations = reservationService.findMyReservations(memberAuthInfo); + return myReservations; } @DeleteMapping("/reservations/{id}") 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..f0402302 --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,14 @@ +package roomescape.reservation; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReservationRepository extends JpaRepository { + + List findByDateAndThemeId(String date, Long themeId); + + List findByName(String name); + + boolean existsByDateAndTimeIdAndThemeId(String date, Long timeId, Long themeId); +} diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/ReservationRequest.java index a07a8483..d538ad34 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/ReservationRequest.java @@ -1,28 +1,10 @@ package roomescape.reservation; -public class ReservationRequest { - private String name; - private String date; - private Long theme; - private Long time; - - public String getName() { - return name; - } - - public String getDate() { - return date; - } - - public Long getTheme() { - return theme; - } - - public Long getTime() { - return time; - } - - public void setName(String name) { - this.name = name; - } +public record ReservationRequest( + String name, + String date, + Long theme, + Long time +) { } + diff --git a/src/main/java/roomescape/reservation/ReservationResponse.java b/src/main/java/roomescape/reservation/ReservationResponse.java index 41360a36..4e97aaa6 100644 --- a/src/main/java/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/ReservationResponse.java @@ -1,37 +1,10 @@ package roomescape.reservation; -public class ReservationResponse { - private Long id; - private String name; - private String theme; - private String date; - private String time; - - public ReservationResponse(Long id, String name, String theme, String date, String time) { - this.id = id; - this.name = name; - this.theme = theme; - this.date = date; - this.time = time; - } - - public Long getId() { - return id; - } - - public String getName() { - return name; - } - - public String getTheme() { - return theme; - } - - public String getDate() { - return date; - } - - public String getTime() { - return time; - } +public record ReservationResponse( + Long id, + String name, + String theme, + String date, + String time +) { } diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd331332..99b08aca 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,131 @@ package roomescape.reservation; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import roomescape.authentication.MemberAuthInfo; +import roomescape.exception.DuplicateReservationException; +import roomescape.exception.MemberNotFoundException; +import roomescape.exception.ThemeNotFoundException; +import roomescape.exception.TimeNotFoundException; +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; +import java.util.stream.Stream; @Service +@RequiredArgsConstructor public class ReservationService { - private ReservationDao reservationDao; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; + private final ReservationRepository reservationRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + private final MemberRepository memberRepository; + private final WaitingRepository waitingRepository; + + public ReservationResponse save(ReservationRequest reservationRequest, MemberAuthInfo memberAuthInfo) { + validateReservationRequest(reservationRequest); + + if (reservationRequest.name() == null) { + reservationRequest = new ReservationRequest( + memberAuthInfo.name(), + reservationRequest.date(), + reservationRequest.theme(), + reservationRequest.time()); + } + + Theme theme = themeRepository.findById(reservationRequest.theme()) + .orElseThrow(() -> new ThemeNotFoundException("해당 테마를 찾을 수 없습니다.")); + Time time = timeRepository.findById(reservationRequest.time()) + .orElseThrow(() -> new TimeNotFoundException("예약 시간을 찾을 수 없습니다.")); + + Member member = null; + if (memberAuthInfo.id() != null) { //관리자가 아닌, 사용자일 경우 + member = memberRepository.findById(memberAuthInfo.id()) + .orElseThrow(() -> new MemberNotFoundException("가입된 회원이 아닙니다.")); + } + + validateDuplicateReservation(reservationRequest); + + Reservation reservation = new Reservation ( + reservationRequest.name(), + reservationRequest.date(), + time, + theme, + member + ); + + reservationRepository.save(reservation); + + return new ReservationResponse(reservation.getId(), reservationRequest.name(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + } + + public List findAll() { + return reservationRepository.findAll().stream() + .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) + .toList(); } - public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + public List findMyReservations(MemberAuthInfo memberAuthInfo) { + + List reservationResponses = findAllByMemberName(memberAuthInfo.name()); + List myReservationResponses1 = reservationResponses + .stream() + .map((it -> new MyReservationResponse( + it.id(), + it.theme(), + it.date(), + it.time(), + "예약"))) + .collect(Collectors.toList()); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + List waitingWithRanks = waitingRepository.findWaitingsWithRankByMemberId(memberAuthInfo.id()); + List myReservationResponses2 = waitingWithRanks.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 Stream.concat(myReservationResponses1.stream(), myReservationResponses2.stream()) + .collect(Collectors.toList()); } public void deleteById(Long id) { - reservationDao.deleteById(id); + reservationRepository.deleteById(id); } - public List findAll() { - return reservationDao.findAll().stream() + private List findAllByMemberName(String name) { + return reservationRepository.findByName(name).stream() .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) .toList(); + + } + + private void validateReservationRequest(ReservationRequest reservationRequest) { + if (reservationRequest.date() == null || reservationRequest.theme() == null || reservationRequest.time() == null) { + throw new IllegalArgumentException("유효하지 않은 요청입니다. 필요한 값이 누락되었습니다."); + } + } + + private void validateDuplicateReservation(ReservationRequest reservationRequest) { + boolean exists = reservationRepository.existsByDateAndTimeIdAndThemeId( + reservationRequest.date(), + reservationRequest.time(), + reservationRequest.theme() + ); + + if (exists) { + throw new DuplicateReservationException("이미 예약이 존재합니다."); + } } } diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239..a8d482c7 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,12 +1,23 @@ package roomescape.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Theme { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; - private String description; - public Theme() { - } + private String description; public Theme(Long id, String name, String description) { this.id = id; diff --git a/src/main/java/roomescape/theme/ThemeController.java b/src/main/java/roomescape/theme/ThemeController.java index 03bca41a..9f69c1e9 100644 --- a/src/main/java/roomescape/theme/ThemeController.java +++ b/src/main/java/roomescape/theme/ThemeController.java @@ -1,5 +1,6 @@ package roomescape.theme; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -12,27 +13,25 @@ import java.util.List; @RestController +@RequiredArgsConstructor public class ThemeController { - private ThemeDao themeDao; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; - } + private final 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..cbdb21a3 --- /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 008ed93c..acb75b80 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,7 +1,17 @@ package roomescape.time; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Time { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(name = "time_value") private String value; public Time(Long id, String value) { @@ -13,10 +23,6 @@ public Time(String value) { this.value = value; } - public Time() { - - } - public Long getId() { return id; } 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