diff --git a/build.gradle b/build.gradle index 6b61c3f..ad7b228 100644 --- a/build.gradle +++ b/build.gradle @@ -20,7 +20,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter:3.4.4' implementation 'org.springframework.boot:spring-boot-starter-web:3.4.4' - implementation 'org.springframework.boot:spring-boot-starter-jdbc:3.4.4' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.4.4' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.5.1' diff --git a/src/main/java/com/yourssu/roomescape/auth/AuthController.java b/src/main/java/com/yourssu/roomescape/auth/AuthController.java index 750dfcc..6e22683 100644 --- a/src/main/java/com/yourssu/roomescape/auth/AuthController.java +++ b/src/main/java/com/yourssu/roomescape/auth/AuthController.java @@ -21,7 +21,7 @@ public AuthController(AuthService authService) { public ResponseEntity login(@RequestBody AuthRequest request, HttpServletResponse response) { AuthResponse authResponse = authService.login(request); - Cookie cookie = new Cookie(CookieUtil.TOKEN, authResponse.getToken()); + Cookie cookie = new Cookie(CookieUtil.TOKEN, authResponse.token()); cookie.setHttpOnly(true); cookie.setPath("/"); response.addCookie(cookie); @@ -32,8 +32,6 @@ public ResponseEntity login(@RequestBody AuthRequest request, Http @GetMapping("/login/check") public ResponseEntity checkLogin(HttpServletRequest request) { String token = CookieUtil.extractTokenFromCookies(request.getCookies()); - if (token == null || token.isEmpty()) return ResponseEntity.status(401).build(); - String name = authService.getNameFromToken(token); return ResponseEntity.ok(new LoginCheckResponse(name)); } @@ -47,4 +45,5 @@ public ResponseEntity logout(HttpServletResponse response) { response.addCookie(cookie); return ResponseEntity.ok().build(); } -} \ No newline at end of file + +} diff --git a/src/main/java/com/yourssu/roomescape/auth/AuthRequest.java b/src/main/java/com/yourssu/roomescape/auth/AuthRequest.java index b015933..b2e37af 100644 --- a/src/main/java/com/yourssu/roomescape/auth/AuthRequest.java +++ b/src/main/java/com/yourssu/roomescape/auth/AuthRequest.java @@ -1,14 +1,5 @@ package com.yourssu.roomescape.auth; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +public record AuthRequest(String email, String password) { +} -@Getter -@AllArgsConstructor -@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) -public class AuthRequest { - private final String email; - private final String password; -} \ No newline at end of file diff --git a/src/main/java/com/yourssu/roomescape/auth/AuthResponse.java b/src/main/java/com/yourssu/roomescape/auth/AuthResponse.java index 4277209..754af78 100644 --- a/src/main/java/com/yourssu/roomescape/auth/AuthResponse.java +++ b/src/main/java/com/yourssu/roomescape/auth/AuthResponse.java @@ -1,10 +1,4 @@ package com.yourssu.roomescape.auth; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class AuthResponse { - private final String token; -} \ No newline at end of file +public record AuthResponse(String token) { +} diff --git a/src/main/java/com/yourssu/roomescape/auth/AuthService.java b/src/main/java/com/yourssu/roomescape/auth/AuthService.java index 5c9f586..dbe9b6a 100644 --- a/src/main/java/com/yourssu/roomescape/auth/AuthService.java +++ b/src/main/java/com/yourssu/roomescape/auth/AuthService.java @@ -1,33 +1,35 @@ package com.yourssu.roomescape.auth; import com.yourssu.roomescape.member.Member; -import com.yourssu.roomescape.member.MemberDao; import com.yourssu.roomescape.exception.MemberNotFoundException; +import com.yourssu.roomescape.exception.ErrorCode; +import com.yourssu.roomescape.member.MemberRepository; import com.yourssu.roomescape.util.JwtTokenProvider; import org.springframework.stereotype.Service; + @Service public class AuthService { - private final MemberDao memberDao; + private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; - public AuthService(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) { - this.memberDao = memberDao; + public AuthService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { + this.memberRepository = memberRepository; this.jwtTokenProvider = jwtTokenProvider; } public AuthResponse login(AuthRequest request) { - Member member = memberDao.findByEmailAndPassword(request.getEmail(), request.getPassword()) - .orElseThrow(() -> new MemberNotFoundException("이메일 또는 비밀번호가 틀렸습니다.")); + Member member = memberRepository.findByEmailAndPassword(request.email(), request.password()) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.INVALID_LOGIN, "이메일: " + request.email())); String token = jwtTokenProvider.createToken(member); return new AuthResponse(token); } public String getNameFromToken(String token) { String email = jwtTokenProvider.getEmail(token); - Member member = memberDao.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException("토큰에 해당하는 사용자를 찾을 수 없습니다.")); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일: " + email)); return member.getName(); } -} \ No newline at end of file +} diff --git a/src/main/java/com/yourssu/roomescape/auth/LoginCheckResponse.java b/src/main/java/com/yourssu/roomescape/auth/LoginCheckResponse.java index 9b85cbf..50bf074 100644 --- a/src/main/java/com/yourssu/roomescape/auth/LoginCheckResponse.java +++ b/src/main/java/com/yourssu/roomescape/auth/LoginCheckResponse.java @@ -1,10 +1,4 @@ package com.yourssu.roomescape.auth; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class LoginCheckResponse { - private final String name; -} \ No newline at end of file +public record LoginCheckResponse(String name) { +} diff --git a/src/main/java/com/yourssu/roomescape/auth/LoginMember.java b/src/main/java/com/yourssu/roomescape/auth/LoginMember.java index ba6aa2d..a614e01 100644 --- a/src/main/java/com/yourssu/roomescape/auth/LoginMember.java +++ b/src/main/java/com/yourssu/roomescape/auth/LoginMember.java @@ -1,13 +1,4 @@ package com.yourssu.roomescape.auth; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class LoginMember { - private final Long id; - private final String name; - private final String email; - private final String role; -} \ No newline at end of file +public record LoginMember(Long id, String name, String email, String role) { +} diff --git a/src/main/java/com/yourssu/roomescape/auth/LoginMemberAnnotation.java b/src/main/java/com/yourssu/roomescape/auth/LoginMemberAnnotation.java new file mode 100644 index 0000000..ac1fa83 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/auth/LoginMemberAnnotation.java @@ -0,0 +1,9 @@ +package com.yourssu.roomescape.auth; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LoginMemberAnnotation { +} diff --git a/src/main/java/com/yourssu/roomescape/auth/LoginMemberArgumentResolver.java b/src/main/java/com/yourssu/roomescape/auth/LoginMemberArgumentResolver.java index a88bb91..9484d97 100644 --- a/src/main/java/com/yourssu/roomescape/auth/LoginMemberArgumentResolver.java +++ b/src/main/java/com/yourssu/roomescape/auth/LoginMemberArgumentResolver.java @@ -1,42 +1,46 @@ package com.yourssu.roomescape.auth; +import com.yourssu.roomescape.exception.ErrorCode; +import com.yourssu.roomescape.exception.MemberNotFoundException; import com.yourssu.roomescape.member.Member; -import com.yourssu.roomescape.member.MemberDao; +import com.yourssu.roomescape.member.MemberRepository; import com.yourssu.roomescape.util.CookieUtil; import com.yourssu.roomescape.util.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Configuration; import org.springframework.core.MethodParameter; 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; +@Configuration public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { - private final MemberDao memberDao; + private final MemberRepository memberRepository; private final JwtTokenProvider jwtTokenProvider; - public LoginMemberArgumentResolver(MemberDao memberDao, JwtTokenProvider jwtTokenProvider) { - this.memberDao = memberDao; + public LoginMemberArgumentResolver(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { + this.memberRepository = memberRepository; this.jwtTokenProvider = jwtTokenProvider; } @Override public boolean supportsParameter(MethodParameter parameter) { - return parameter.getParameterType().equals(LoginMember.class); + return parameter.hasParameterAnnotation(LoginMemberAnnotation.class) && + parameter.getParameterType().equals(com.yourssu.roomescape.auth.LoginMember.class); } @Override - public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + public Object resolveArgument(@NotNull MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); String token = CookieUtil.extractTokenFromCookies(request.getCookies()); - if (token == null || token.isBlank()) return null; - String email = jwtTokenProvider.getEmail(token); - Member member = memberDao.findByEmail(email) - .orElseThrow(() -> new RuntimeException("사용자 정보를 찾을 수 없습니다.")); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일: " + email)); return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); } diff --git a/src/main/java/com/yourssu/roomescape/config/AdminInterceptor.java b/src/main/java/com/yourssu/roomescape/config/AdminInterceptor.java index 483c23c..a79b110 100644 --- a/src/main/java/com/yourssu/roomescape/config/AdminInterceptor.java +++ b/src/main/java/com/yourssu/roomescape/config/AdminInterceptor.java @@ -4,6 +4,7 @@ import com.yourssu.roomescape.util.JwtTokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -17,7 +18,7 @@ public AdminInterceptor(JwtTokenProvider jwtTokenProvider) { } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) { String token = CookieUtil.extractTokenFromCookies(request.getCookies()); if (token == null || token.isBlank()) { diff --git a/src/main/java/com/yourssu/roomescape/config/WebConfig.java b/src/main/java/com/yourssu/roomescape/config/WebConfig.java index 1e94390..7a8c8ad 100644 --- a/src/main/java/com/yourssu/roomescape/config/WebConfig.java +++ b/src/main/java/com/yourssu/roomescape/config/WebConfig.java @@ -1,8 +1,6 @@ package com.yourssu.roomescape.config; import com.yourssu.roomescape.auth.LoginMemberArgumentResolver; -import com.yourssu.roomescape.member.MemberDao; -import com.yourssu.roomescape.util.JwtTokenProvider; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.*; @@ -12,19 +10,17 @@ @Configuration public class WebConfig implements WebMvcConfigurer { - private final MemberDao memberDao; - private final JwtTokenProvider jwtTokenProvider; private final AdminInterceptor adminInterceptor; + private final LoginMemberArgumentResolver loginMemberArgumentResolver; - public WebConfig(MemberDao memberDao, JwtTokenProvider jwtTokenProvider, AdminInterceptor adminInterceptor) { - this.memberDao = memberDao; - this.jwtTokenProvider = jwtTokenProvider; + public WebConfig(AdminInterceptor adminInterceptor, LoginMemberArgumentResolver loginMemberArgumentResolver) { this.adminInterceptor = adminInterceptor; + this.loginMemberArgumentResolver = loginMemberArgumentResolver; } @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(new LoginMemberArgumentResolver(memberDao, jwtTokenProvider)); + resolvers.add(loginMemberArgumentResolver); } @Override @@ -33,3 +29,4 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/admin"); } } + diff --git a/src/main/java/com/yourssu/roomescape/exception/DuplicateException.java b/src/main/java/com/yourssu/roomescape/exception/DuplicateException.java new file mode 100644 index 0000000..31f2f9f --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/exception/DuplicateException.java @@ -0,0 +1,13 @@ +package com.yourssu.roomescape.exception; + +import lombok.Getter; + +@Getter +public class DuplicateException extends RuntimeException { + private final ErrorCode errorCode; + + public DuplicateException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/yourssu/roomescape/exception/ErrorCode.java b/src/main/java/com/yourssu/roomescape/exception/ErrorCode.java new file mode 100644 index 0000000..2697cec --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/exception/ErrorCode.java @@ -0,0 +1,44 @@ +package com.yourssu.roomescape.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum ErrorCode { + + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 생겼습니다."), + + // Auth + COOKIE_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다."), + TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키에 토큰이 존재하지 않습니다."), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다."), + UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "지원하지 않는 토큰 형식입니다."), + MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "손상된 JWT 토큰입니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다."), + + // Member + MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 정보를 찾을 수 없습니다."), + INVALID_LOGIN(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 틀렸습니다."), + NOT_ADMIN(HttpStatus.FORBIDDEN, "관리자 권한이 필요합니다."), + + // Reservation + RESERVATION_NOT_FOUND(HttpStatus.NOT_FOUND, "예약 정보를 찾을 수 없습니다."), + NO_PERMISSION_FOR_RESERVATION(HttpStatus.FORBIDDEN, "해당 예약에 대한 권한이 없습니다."), + RESERVATION_ALREADY_EXISTS(HttpStatus.CONFLICT, "예약 정보가 이미 존재합니다."), + WAITING_ALREADY_EXISTS(HttpStatus.CONFLICT, "예약 대기 정보가 이미 존재합니다."), + + // Time + TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "시간 정보를 찾을 수 없습니다."), + + // Theme + THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "테마 정보를 찾을 수 없습니다."); + + private final HttpStatus status; + private final String message; + + ErrorCode(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + +} diff --git a/src/main/java/com/yourssu/roomescape/exception/ErrorResponse.java b/src/main/java/com/yourssu/roomescape/exception/ErrorResponse.java index ee1959a..ada24b9 100644 --- a/src/main/java/com/yourssu/roomescape/exception/ErrorResponse.java +++ b/src/main/java/com/yourssu/roomescape/exception/ErrorResponse.java @@ -1,14 +1,7 @@ package com.yourssu.roomescape.exception; -import lombok.Getter; - -@Getter -public class ErrorResponse { - - private final String message; - - public ErrorResponse(String message) { - this.message = message; +public record ErrorResponse(int status, String message) { + public ErrorResponse(ErrorCode errorCode) { + this(errorCode.getStatus().value(), errorCode.getMessage()); } - -} \ No newline at end of file +} diff --git a/src/main/java/com/yourssu/roomescape/exception/ExceptionController.java b/src/main/java/com/yourssu/roomescape/exception/ExceptionController.java index 991896d..b3f5267 100644 --- a/src/main/java/com/yourssu/roomescape/exception/ExceptionController.java +++ b/src/main/java/com/yourssu/roomescape/exception/ExceptionController.java @@ -1,6 +1,5 @@ package com.yourssu.roomescape.exception; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -9,15 +8,46 @@ public class ExceptionController { @ExceptionHandler(MemberNotFoundException.class) - public ResponseEntity handleMemberNotFound(MemberNotFoundException ex) { - return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(new ErrorResponse(ex.getMessage())); + public ResponseEntity handleMemberNotFound(MemberNotFoundException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(new ErrorResponse(e.getErrorCode())); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleTimeNotFound(TimeNotFoundException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(new ErrorResponse(e.getErrorCode())); + } + + @ExceptionHandler(MemberNotFoundException.class) + public ResponseEntity handleThemeNotFound(ThemeNotFoundException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(new ErrorResponse(e.getErrorCode())); + } + + @ExceptionHandler(UnauthenticatedException.class) + public ResponseEntity handleUnauthenticated(UnauthenticatedException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(new ErrorResponse(e.getErrorCode())); } @ExceptionHandler(Exception.class) public ResponseEntity handleGeneric(Exception ex) { ex.printStackTrace(); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(new ErrorResponse("서버 오류가 발생했습니다.")); + return ResponseEntity + .status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus()) + .body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR)); } + + @ExceptionHandler(DuplicateException.class) + public ResponseEntity handleDuplicate(DuplicateException e) { + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(new ErrorResponse(e.getErrorCode())); + } + } diff --git a/src/main/java/com/yourssu/roomescape/exception/MemberNotFoundException.java b/src/main/java/com/yourssu/roomescape/exception/MemberNotFoundException.java index cf1c265..8ecd889 100644 --- a/src/main/java/com/yourssu/roomescape/exception/MemberNotFoundException.java +++ b/src/main/java/com/yourssu/roomescape/exception/MemberNotFoundException.java @@ -1,7 +1,14 @@ package com.yourssu.roomescape.exception; +import lombok.Getter; + +@Getter public class MemberNotFoundException extends RuntimeException { - public MemberNotFoundException(String message) { - super(message); + + private final ErrorCode errorCode; + + public MemberNotFoundException(ErrorCode errorCode, String detail) { + super(errorCode.getMessage() + " " + detail); + this.errorCode = errorCode; } -} \ No newline at end of file +} diff --git a/src/main/java/com/yourssu/roomescape/exception/ThemeNotFoundException.java b/src/main/java/com/yourssu/roomescape/exception/ThemeNotFoundException.java new file mode 100644 index 0000000..a35481d --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/exception/ThemeNotFoundException.java @@ -0,0 +1,14 @@ +package com.yourssu.roomescape.exception; + +import lombok.Getter; + +@Getter +public class ThemeNotFoundException extends RuntimeException { + + private final ErrorCode errorCode; + + public ThemeNotFoundException(ErrorCode errorCode, String detail) { + super(errorCode.getMessage() + " " + detail); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/yourssu/roomescape/exception/TimeNotFoundException.java b/src/main/java/com/yourssu/roomescape/exception/TimeNotFoundException.java new file mode 100644 index 0000000..85bba97 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/exception/TimeNotFoundException.java @@ -0,0 +1,14 @@ +package com.yourssu.roomescape.exception; + +import lombok.Getter; + +@Getter +public class TimeNotFoundException extends RuntimeException { + + private final ErrorCode errorCode; + + public TimeNotFoundException(ErrorCode errorCode, String detail) { + super(errorCode.getMessage() + " " + detail); + this.errorCode = errorCode; + } +} diff --git a/src/main/java/com/yourssu/roomescape/exception/UnauthenticatedException.java b/src/main/java/com/yourssu/roomescape/exception/UnauthenticatedException.java new file mode 100644 index 0000000..2c60d65 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/exception/UnauthenticatedException.java @@ -0,0 +1,14 @@ +package com.yourssu.roomescape.exception; + +import lombok.Getter; + +@Getter +public class UnauthenticatedException extends RuntimeException { + private final ErrorCode errorCode; + + public UnauthenticatedException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + +} diff --git a/src/main/java/com/yourssu/roomescape/member/Member.java b/src/main/java/com/yourssu/roomescape/member/Member.java index b3b7a37..d9829eb 100644 --- a/src/main/java/com/yourssu/roomescape/member/Member.java +++ b/src/main/java/com/yourssu/roomescape/member/Member.java @@ -1,25 +1,27 @@ package com.yourssu.roomescape.member; +import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@Entity +@Table(name = "member") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { - private final Long id; - private final String name; - private final String email; - private final String password; - private final String role; + @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 password, String role) { - this.id = id; + public Member(String name, String email, String password, String role) { this.name = name; this.email = email; this.password = password; this.role = role; } - - public Member(String name, String email, String password, String role) { - this(null, name, email, password, role); - } - } diff --git a/src/main/java/com/yourssu/roomescape/member/MemberController.java b/src/main/java/com/yourssu/roomescape/member/MemberController.java index af530be..4ae2842 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberController.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberController.java @@ -18,7 +18,7 @@ public MemberController(MemberService memberService) { @PostMapping public ResponseEntity create(@RequestBody MemberRequest request) { MemberResponse response = memberService.createMember(request); - return ResponseEntity.created(URI.create("/members/" + response.getId())).body(response); + return ResponseEntity.created(URI.create("/members/" + response.id())).body(response); } } diff --git a/src/main/java/com/yourssu/roomescape/member/MemberDao.java b/src/main/java/com/yourssu/roomescape/member/MemberDao.java deleted file mode 100644 index 26122ff..0000000 --- a/src/main/java/com/yourssu/roomescape/member/MemberDao.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.yourssu.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; -import org.springframework.dao.EmptyResultDataAccessException; - -import java.util.Objects; -import java.util.Optional; - -@Repository -public class MemberDao { - private final 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(Objects.requireNonNull(keyHolder.getKey()).longValue(), member.getName(), member.getEmail(), member.getPassword(), member.getRole()); - } - - public Optional findByEmailAndPassword(String email, String password) { - return query("SELECT * FROM member WHERE email = ? AND password = ?", email, password); - } - - public Optional findByName(String name) { - return query("SELECT * FROM member WHERE name = ?", name); - } - - public Optional findByEmail(String email) { - return query("SELECT * FROM member WHERE email = ?", email); - } - - private Optional query(String sql, Object... args) { - try { - Member member = jdbcTemplate.queryForObject(sql, - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - null, // password는 조회 안 함 - rs.getString("role") - ), args); - return Optional.ofNullable(member); - } catch (EmptyResultDataAccessException e) { - return Optional.empty(); - } - } -} diff --git a/src/main/java/com/yourssu/roomescape/member/MemberRepository.java b/src/main/java/com/yourssu/roomescape/member/MemberRepository.java new file mode 100644 index 0000000..7b58e4e --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/member/MemberRepository.java @@ -0,0 +1,14 @@ +package com.yourssu.roomescape.member; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + Optional findByEmail(String email); + + Optional findByName(String name); + + Optional findByEmailAndPassword(String email, String password); +} diff --git a/src/main/java/com/yourssu/roomescape/member/MemberRequest.java b/src/main/java/com/yourssu/roomescape/member/MemberRequest.java index 6d5f55c..c130c2c 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberRequest.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberRequest.java @@ -1,15 +1,4 @@ package com.yourssu.roomescape.member; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@AllArgsConstructor -@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) -public class MemberRequest { - private final String name; - private final String email; - private final String password; +public record MemberRequest(String name, String email, String password) { } diff --git a/src/main/java/com/yourssu/roomescape/member/MemberResponse.java b/src/main/java/com/yourssu/roomescape/member/MemberResponse.java index 5b230f1..b19fae1 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberResponse.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberResponse.java @@ -1,12 +1,4 @@ package com.yourssu.roomescape.member; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class MemberResponse { - private final Long id; - private final String name; - private final String email; -} \ No newline at end of file +public record MemberResponse(Long id, String name, String email) { +} diff --git a/src/main/java/com/yourssu/roomescape/member/MemberService.java b/src/main/java/com/yourssu/roomescape/member/MemberService.java index a36472c..0c3f913 100644 --- a/src/main/java/com/yourssu/roomescape/member/MemberService.java +++ b/src/main/java/com/yourssu/roomescape/member/MemberService.java @@ -1,29 +1,21 @@ package com.yourssu.roomescape.member; -import com.yourssu.roomescape.exception.MemberNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class MemberService { - private final MemberDao memberDao; + private final MemberRepository memberRepository; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; } + @Transactional public MemberResponse createMember(MemberRequest request) { - Member member = new Member(request.getName(), request.getEmail(), request.getPassword(), "USER"); - Member saved = memberDao.save(member); + Member member = new Member(request.name(), request.email(), request.password(), "USER"); + Member saved = memberRepository.save(member); return new MemberResponse(saved.getId(), saved.getName(), saved.getEmail()); } - public Member findByEmail(String email) { - return memberDao.findByEmail(email) - .orElseThrow(() -> new MemberNotFoundException("이메일이 일치하는 회원이 없습니다.")); - } - - public Member findByName(String name) { - return memberDao.findByName(name) - .orElseThrow(() -> new MemberNotFoundException("이름이 일치하는 회원이 없습니다.")); - } } diff --git a/src/main/java/com/yourssu/roomescape/reservation/MyReservationResponse.java b/src/main/java/com/yourssu/roomescape/reservation/MyReservationResponse.java new file mode 100644 index 0000000..e373b64 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/MyReservationResponse.java @@ -0,0 +1,36 @@ +package com.yourssu.roomescape.reservation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.yourssu.roomescape.reservation.waiting.Waiting; +import com.yourssu.roomescape.reservation.waiting.WaitingWithRank; + +public record MyReservationResponse( + @JsonProperty("id") Long Id, + String theme, + String date, + String time, + String status +) { + + public static MyReservationResponse of(Reservation reservation) { + return new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getTimeValue(), + "예약" + ); + } + + public static MyReservationResponse fromWaiting(WaitingWithRank waitingWithRank) { + Waiting waiting = waitingWithRank.waiting(); + String status = waitingWithRank.rank() + "번째 예약대기"; + return new MyReservationResponse( + waiting.getId(), + waiting.getTheme().getName(), + waiting.getDate(), + waiting.getTime().getTimeValue(), + status + ); + } +} diff --git a/src/main/java/com/yourssu/roomescape/reservation/Reservation.java b/src/main/java/com/yourssu/roomescape/reservation/Reservation.java index 84f7f5d..1333df3 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/Reservation.java +++ b/src/main/java/com/yourssu/roomescape/reservation/Reservation.java @@ -2,33 +2,36 @@ import com.yourssu.roomescape.theme.Theme; import com.yourssu.roomescape.time.Time; +import com.yourssu.roomescape.member.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; +@Entity @Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String name; + private String date; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) private Time time; - private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { - this.id = id; - this.name = name; - this.date = date; - this.time = time; - this.theme = theme; - } + @ManyToOne(fetch = FetchType.LAZY) + private Theme theme; - public Reservation(String name, String date, Time time, Theme theme) { - this.name = name; + public Reservation(Member member, String date, Time time, Theme theme) { + this.member = member; this.date = date; this.time = time; this.theme = theme; } - public Reservation() { - - } - } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java index 68b89f4..f637218 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationController.java @@ -1,6 +1,9 @@ package com.yourssu.roomescape.reservation; import com.yourssu.roomescape.auth.LoginMember; +import com.yourssu.roomescape.auth.LoginMemberAnnotation; +import com.yourssu.roomescape.reservation.waiting.WaitingRequest; +import com.yourssu.roomescape.reservation.waiting.WaitingResponse; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -23,10 +26,9 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@Valid @RequestBody ReservationRequest reservationRequest, LoginMember loginMember) { + public ResponseEntity create(@Valid @RequestBody ReservationRequest reservationRequest, @LoginMemberAnnotation LoginMember loginMember) { ReservationResponse reservation = reservationService.save(reservationRequest, loginMember); - - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation); } @DeleteMapping("/reservations/{id}") @@ -34,4 +36,22 @@ public ResponseEntity delete(@PathVariable Long id) { reservationService.deleteById(id); return ResponseEntity.noContent().build(); } + + @GetMapping("/reservations-mine") + public ResponseEntity> myReservations(@LoginMemberAnnotation LoginMember loginMember) { + return ResponseEntity.ok(reservationService.findMyReservations(loginMember)); + } + + @PostMapping("/waitings") + public ResponseEntity createWaiting(@Valid @RequestBody WaitingRequest request, @LoginMemberAnnotation LoginMember loginMember) { + WaitingResponse waitingResponse = reservationService.createWaiting(request, loginMember); + return ResponseEntity.created(URI.create("/waitings/" + waitingResponse.id())).body(waitingResponse); + } + + @DeleteMapping("/waitings/{id}") + public ResponseEntity cancelWaiting(@PathVariable Long id, @LoginMemberAnnotation LoginMember loginMember) { + reservationService.cancelWaiting(id, loginMember); + return ResponseEntity.noContent().build(); + } + } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationDao.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationDao.java deleted file mode 100644 index 733bb51..0000000 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationDao.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.yourssu.roomescape.reservation; - -import com.yourssu.roomescape.theme.Theme; -import com.yourssu.roomescape.time.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; -import java.util.Objects; - -@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( - Objects.requireNonNull(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/com/yourssu/roomescape/reservation/ReservationRepository.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationRepository.java new file mode 100644 index 0000000..3120626 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,21 @@ +package com.yourssu.roomescape.reservation; + +import org.jetbrains.annotations.NotNull; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ReservationRepository extends JpaRepository { + + List findByDateAndThemeId(String date, Long themeId); + + @EntityGraph(attributePaths = {"theme", "time"}) + List findByMemberId(Long memberId); + + @NotNull + @EntityGraph(attributePaths = {"member","theme", "time"}) + List findAll(); + + boolean existsByMemberIdAndDateAndTimeIdAndThemeId(Long memberId, String date, Long timeId, Long themeId); +} diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationRequest.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationRequest.java index c68c6f4..bf918e9 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationRequest.java @@ -2,28 +2,18 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -@Getter -@AllArgsConstructor -@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) -public class ReservationRequest { - private final String name; // Optional: 관리자만 사용 +public record ReservationRequest( - @NotBlank(message = "예약 날짜는 필수입니다.") - private final String date; + String name, // Optional: 관리자만 사용 - @NotNull(message = "예약 시간은 필수입니다.") - private final Long time; + @NotBlank(message = "예약 날짜는 필수입니다.") + String date, - @NotNull(message = "테마 ID는 필수입니다.") - private final Long theme; - - public ReservationRequest withName(String name) { - return new ReservationRequest(name, this.date, this.time, this.theme); - } + @NotNull(message = "예약 시간은 필수입니다.") + Long time, + @NotNull(message = "테마 ID는 필수입니다.") + Long theme +) { } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationResponse.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationResponse.java index 42521f8..e467b42 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationResponse.java @@ -1,23 +1,12 @@ package com.yourssu.roomescape.reservation; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public class ReservationResponse { - private final Long id; - private final String name; - private final String date; - private final String time; - private final String theme; - +public record ReservationResponse(Long id, String name, String date, String time, String theme) { public static ReservationResponse of(Reservation reservation) { return new ReservationResponse( reservation.getId(), - reservation.getName(), + reservation.getMember().getName(), reservation.getDate(), - reservation.getTime().getValue(), + reservation.getTime().getTimeValue(), reservation.getTheme().getName() ); } diff --git a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java index 2151126..2b2f597 100644 --- a/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java +++ b/src/main/java/com/yourssu/roomescape/reservation/ReservationService.java @@ -1,44 +1,149 @@ package com.yourssu.roomescape.reservation; import com.yourssu.roomescape.auth.LoginMember; +import com.yourssu.roomescape.exception.DuplicateException; +import com.yourssu.roomescape.exception.ErrorCode; +import com.yourssu.roomescape.exception.MemberNotFoundException; +import com.yourssu.roomescape.exception.ThemeNotFoundException; +import com.yourssu.roomescape.exception.TimeNotFoundException; import com.yourssu.roomescape.member.Member; -import com.yourssu.roomescape.member.MemberService; +import com.yourssu.roomescape.member.MemberRepository; +import com.yourssu.roomescape.reservation.waiting.Waiting; +import com.yourssu.roomescape.reservation.waiting.WaitingRepository; +import com.yourssu.roomescape.reservation.waiting.WaitingRequest; +import com.yourssu.roomescape.reservation.waiting.WaitingResponse; +import com.yourssu.roomescape.theme.Theme; +import com.yourssu.roomescape.theme.ThemeRepository; +import com.yourssu.roomescape.time.Time; +import com.yourssu.roomescape.time.TimeRepository; + import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Service public class ReservationService { - private final ReservationDao reservationDao; - private final MemberService memberService; + private final ReservationRepository reservationRepository; + private final MemberRepository memberRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + private final WaitingRepository waitingRepository; public ReservationService( - ReservationDao reservationDao, - MemberService memberService + ReservationRepository reservationRepository, + MemberRepository memberRepository, + ThemeRepository themeRepository, + TimeRepository timeRepository, + WaitingRepository waitingRepository ) { - this.reservationDao = reservationDao; - this.memberService = memberService; + this.reservationRepository = reservationRepository; + this.memberRepository = memberRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + this.waitingRepository = waitingRepository; } + @Transactional public ReservationResponse save(ReservationRequest request, LoginMember loginMember) { - Member member = (request.getName() != null && !request.getName().isBlank()) - ? memberService.findByName(request.getName()) - : memberService.findByEmail(loginMember.getEmail()); + Member member = (request.name() != null && !request.name().isBlank()) + ? memberRepository.findByName(request.name()) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이름: "+ request.name())) + : memberRepository.findByEmail(loginMember.email()) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일: " + loginMember.email())); + + Time time = timeRepository.findById(request.time()).orElseThrow(); + Theme theme = themeRepository.findById(request.theme()).orElseThrow(); - ReservationRequest filledRequest = request.withName(member.getName()); - Reservation reservation = reservationDao.save(filledRequest); + validateNotDuplicateReservation(member.getId(), request.date(), time.getId(), theme.getId()); + Reservation reservation = new Reservation(member, request.date(), time, theme); + reservationRepository.save(reservation); return ReservationResponse.of(reservation); } + private void validateNotDuplicateReservation(Long memberId, String date, Long timeId, Long themeId) { + boolean exists = reservationRepository.existsByMemberIdAndDateAndTimeIdAndThemeId(memberId, date, timeId, themeId); + if (exists) { + throw new DuplicateException(ErrorCode.RESERVATION_ALREADY_EXISTS); + } + } public void deleteById(Long id) { - reservationDao.deleteById(id); + reservationRepository.deleteById(id); } public List findAll() { - return reservationDao.findAll().stream() - .map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) + return reservationRepository.findAll().stream() + .map(it -> new ReservationResponse( + it.getId(), + it.getMember().getName(), + it.getTheme().getName(), + it.getDate(), + it.getTime().getTimeValue() + )) .toList(); } + + @Transactional(readOnly = true) + public List findMyReservations(LoginMember loginMember) { + Long memberId = loginMember.id(); + + List reservations = reservationRepository.findByMemberId(memberId).stream() + .map(MyReservationResponse::of) + .toList(); + + List waitings = waitingRepository.findWithRankByMemberId(memberId).stream() + .map(MyReservationResponse::fromWaiting) + .toList(); + + List result = new ArrayList<>(reservations); + result.addAll(waitings); + return result; + } + + @Transactional + public WaitingResponse createWaiting(WaitingRequest request, LoginMember loginMember) { + Member member = memberRepository.findByEmail(loginMember.email()) + .orElseThrow(() -> new MemberNotFoundException(ErrorCode.MEMBER_NOT_FOUND, "이메일"+loginMember.email())); + + Time time = timeRepository.findById(request.time()) + .orElseThrow(() -> new TimeNotFoundException(ErrorCode.TIME_NOT_FOUND, "time:"+request.time())); + + Theme theme = themeRepository.findById(request.theme()) + .orElseThrow(() -> new ThemeNotFoundException(ErrorCode.THEME_NOT_FOUND, "theme:"+request.theme())); + + validateNotDuplicateReservation(member.getId(), request.date(), time.getId(), theme.getId()); + validateNotDuplicateWaiting(member.getId(), request.date(), time, theme); + + Waiting waiting = new Waiting(member, request.date(), time, theme); + Waiting savedWaiting = waitingRepository.save(waiting); + + int rank = waitingRepository.countByThemeAndDateAndTimeAndIdLessThan( + theme, request.date(), time, savedWaiting.getId() + ) + 1; + + return WaitingResponse.of(savedWaiting, rank); + } + + private void validateNotDuplicateWaiting(Long memberId, String date, Time time, Theme theme) { + boolean exists = waitingRepository.existsByMemberIdAndDateAndTimeAndTheme(memberId, date, time, theme); + if (exists) { + throw new DuplicateException(ErrorCode.WAITING_ALREADY_EXISTS); + } + } + + @Transactional + public void cancelWaiting(Long waitingId, LoginMember loginMember) { + Waiting waiting = waitingRepository.findById(waitingId) + .orElseThrow(() -> new IllegalArgumentException("예약 대기를 찾을 수 없습니다.")); + + if (!waiting.getMember().getId().equals(loginMember.id())) { + throw new IllegalArgumentException("본인의 예약 대기만 취소할 수 있습니다."); + } + + waitingRepository.delete(waiting); + } + } diff --git a/src/main/java/com/yourssu/roomescape/reservation/waiting/Waiting.java b/src/main/java/com/yourssu/roomescape/reservation/waiting/Waiting.java new file mode 100644 index 0000000..e692fe9 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/waiting/Waiting.java @@ -0,0 +1,36 @@ +package com.yourssu.roomescape.reservation.waiting; + +import com.yourssu.roomescape.member.Member; +import com.yourssu.roomescape.theme.Theme; +import com.yourssu.roomescape.time.Time; +import jakarta.persistence.Entity; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class Waiting { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String date; + + @ManyToOne(fetch = FetchType.LAZY) + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + private Theme theme; + + @ManyToOne(fetch = FetchType.LAZY) + private Time time; + + public Waiting(Member member, String date, Time time, Theme theme) { + this.member = member; + this.date = date; + this.time = time; + this.theme = theme; + } +} diff --git a/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRepository.java b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRepository.java new file mode 100644 index 0000000..941b266 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRepository.java @@ -0,0 +1,47 @@ +package com.yourssu.roomescape.reservation.waiting; + +import com.yourssu.roomescape.theme.Theme; +import com.yourssu.roomescape.time.Time; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface WaitingRepository extends JpaRepository { + + boolean existsByMemberIdAndDateAndTimeAndTheme(Long memberId, String date, Time time, Theme theme); + + @Query(""" + SELECT new com.yourssu.roomescape.reservation.waiting.WaitingWithRank( + w, + CAST( + (SELECT COUNT(w2) + FROM Waiting w2 + WHERE w2.theme = w.theme + AND w2.date = w.date + AND w2.time = w.time + AND w2.id < w.id + ) AS int) + 1) + FROM Waiting w + WHERE w.member.id = :memberId + """) + @EntityGraph(attributePaths = {"theme", "time"}) + List findWithRankByMemberId(@Param("memberId") Long memberId); + + @Query(""" + SELECT COUNT(w) + FROM Waiting w + WHERE w.theme = :theme + AND w.date = :date + AND w.time = :time + AND w.id < :id + """) + int countByThemeAndDateAndTimeAndIdLessThan( + @Param("theme") Theme theme, + @Param("date") String date, + @Param("time") Time time, + @Param("id") Long id + ); +} diff --git a/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRequest.java b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRequest.java new file mode 100644 index 0000000..e9eb37c --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingRequest.java @@ -0,0 +1,17 @@ +package com.yourssu.roomescape.reservation.waiting; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record WaitingRequest( + + @NotBlank + String date, + + @NotNull + Long time, + + @NotNull + Long theme +) { +} diff --git a/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingResponse.java b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingResponse.java new file mode 100644 index 0000000..1985e01 --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingResponse.java @@ -0,0 +1,9 @@ +package com.yourssu.roomescape.reservation.waiting; + +public record WaitingResponse(Long id, int waitingNumber) { + + public static WaitingResponse of(Waiting waiting, int waitingNumber) { + return new WaitingResponse(waiting.getId(), waitingNumber); + } +} + diff --git a/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingWithRank.java b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingWithRank.java new file mode 100644 index 0000000..505f4be --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/reservation/waiting/WaitingWithRank.java @@ -0,0 +1,5 @@ +package com.yourssu.roomescape.reservation.waiting; + +public record WaitingWithRank(Waiting waiting, int rank) { + +} diff --git a/src/main/java/com/yourssu/roomescape/theme/Theme.java b/src/main/java/com/yourssu/roomescape/theme/Theme.java index 3103408..d321437 100644 --- a/src/main/java/com/yourssu/roomescape/theme/Theme.java +++ b/src/main/java/com/yourssu/roomescape/theme/Theme.java @@ -1,25 +1,37 @@ package com.yourssu.roomescape.theme; +import jakarta.persistence.*; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@Entity +@Table(name = "theme") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Theme { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String name; private String description; - public Theme() { - } + @Column(nullable = false) + private boolean deleted = false; - public Theme(Long id, String name, String description) { - this.id = id; + public Theme(String name, String description) { this.name = name; this.description = description; } - public Theme(String name, String description) { + public Theme(Long id, String name, String description) { + this.id = id; this.name = name; this.description = description; } + public void markDeleted() { + this.deleted = true; + } } diff --git a/src/main/java/com/yourssu/roomescape/theme/ThemeController.java b/src/main/java/com/yourssu/roomescape/theme/ThemeController.java index d55b8c4..7feb02b 100644 --- a/src/main/java/com/yourssu/roomescape/theme/ThemeController.java +++ b/src/main/java/com/yourssu/roomescape/theme/ThemeController.java @@ -8,26 +8,26 @@ @RestController public class ThemeController { - private final ThemeDao themeDao; + private final ThemeService themeService; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; + public ThemeController(ThemeService timeService) { + this.themeService = timeService; } @PostMapping("/themes") public ResponseEntity createTheme(@RequestBody Theme theme) { - Theme newTheme = themeDao.save(theme); + Theme newTheme = themeService.createTheme(theme); return ResponseEntity.created(URI.create("/themes/" + newTheme.getId())).body(newTheme); } @GetMapping("/themes") public ResponseEntity> list() { - return ResponseEntity.ok(themeDao.findAll()); + return ResponseEntity.ok(themeService.findAll()); } @DeleteMapping("/themes/{id}") public ResponseEntity deleteTheme(@PathVariable Long id) { - themeDao.deleteById(id); + themeService.deleteById(id); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/yourssu/roomescape/theme/ThemeDao.java b/src/main/java/com/yourssu/roomescape/theme/ThemeDao.java deleted file mode 100644 index b1afa9b..0000000 --- a/src/main/java/com/yourssu/roomescape/theme/ThemeDao.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.yourssu.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; -import java.util.Objects; - -@Repository -public class ThemeDao { - private final 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(Objects.requireNonNull(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/com/yourssu/roomescape/theme/ThemeRepository.java b/src/main/java/com/yourssu/roomescape/theme/ThemeRepository.java new file mode 100644 index 0000000..0c9764f --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/theme/ThemeRepository.java @@ -0,0 +1,9 @@ +package com.yourssu.roomescape.theme; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ThemeRepository extends JpaRepository { + List findByDeletedFalse(); +} diff --git a/src/main/java/com/yourssu/roomescape/theme/ThemeService.java b/src/main/java/com/yourssu/roomescape/theme/ThemeService.java new file mode 100644 index 0000000..0fcc70b --- /dev/null +++ b/src/main/java/com/yourssu/roomescape/theme/ThemeService.java @@ -0,0 +1,29 @@ +package com.yourssu.roomescape.theme; + +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ThemeService { + private final ThemeRepository themeRepository; + + public ThemeService(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; + } + + public Theme createTheme(Theme theme) { + return themeRepository.save(theme); + } + + public List findAll() { + return themeRepository.findByDeletedFalse(); + } + + public void deleteById(Long id) { + themeRepository.findById(id).ifPresent(theme -> { + theme.markDeleted(); + themeRepository.save(theme); + }); + } +} diff --git a/src/main/java/com/yourssu/roomescape/time/AvailableTime.java b/src/main/java/com/yourssu/roomescape/time/AvailableTime.java index aceb215..807f70d 100644 --- a/src/main/java/com/yourssu/roomescape/time/AvailableTime.java +++ b/src/main/java/com/yourssu/roomescape/time/AvailableTime.java @@ -1,17 +1,5 @@ package com.yourssu.roomescape.time; -import lombok.Getter; - -@Getter -public class AvailableTime { - private Long timeId; - private String time; - private boolean booked; - - public AvailableTime(Long timeId, String time, boolean booked) { - this.timeId = timeId; - this.time = time; - this.booked = booked; - } +public record AvailableTime (Long timeId, String time, boolean booked){ } diff --git a/src/main/java/com/yourssu/roomescape/time/Time.java b/src/main/java/com/yourssu/roomescape/time/Time.java index 4b298e2..91de872 100644 --- a/src/main/java/com/yourssu/roomescape/time/Time.java +++ b/src/main/java/com/yourssu/roomescape/time/Time.java @@ -1,27 +1,35 @@ package com.yourssu.roomescape.time; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@Table(name = "times") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Time { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private String value; - - public Time(Long id, String value) { - this.id = id; - this.value = value; - } - public Time(String value) { - this.value = value; - } + @Column(name = "time_value", nullable = false) + private String timeValue; - public Time() { + @Column(nullable = false) + private boolean deleted = false; + public Time(String timeValue) { + this.timeValue = timeValue; } - public Long getId() { - return id; + public Time(Long id, String timeValue) { + this.id = id; + this.timeValue = timeValue; } - public String getValue() { - return value; + public void markDeleted() { + this.deleted = true; } } diff --git a/src/main/java/com/yourssu/roomescape/time/TimeController.java b/src/main/java/com/yourssu/roomescape/time/TimeController.java index 228b805..2cfce89 100644 --- a/src/main/java/com/yourssu/roomescape/time/TimeController.java +++ b/src/main/java/com/yourssu/roomescape/time/TimeController.java @@ -21,7 +21,7 @@ public List