diff --git a/build.gradle b/build.gradle index 8d52aebc6..3551ce1ca 100644 --- a/build.gradle +++ b/build.gradle @@ -15,16 +15,20 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' - implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0' - implementation 'io.jsonwebtoken:jjwt-api:0.11.2' implementation 'io.jsonwebtoken:jjwt-impl:0.11.2' implementation 'io.jsonwebtoken:jjwt-gson:0.11.2' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'com.h2database:h2' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + + + runtimeOnly 'com.h2database:h2' } diff --git a/src/main/java/roomescape/advice/ErrorResponse.java b/src/main/java/roomescape/advice/ErrorResponse.java new file mode 100644 index 000000000..be921e8f1 --- /dev/null +++ b/src/main/java/roomescape/advice/ErrorResponse.java @@ -0,0 +1,4 @@ +package roomescape.advice; + +public record ErrorResponse(String message) { +} diff --git a/src/main/java/roomescape/advice/GlobalExceptionHandler.java b/src/main/java/roomescape/advice/GlobalExceptionHandler.java new file mode 100644 index 000000000..cfff65a7d --- /dev/null +++ b/src/main/java/roomescape/advice/GlobalExceptionHandler.java @@ -0,0 +1,28 @@ +package roomescape.advice; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity handleIllegalArgumentException(IllegalArgumentException e) { + return ResponseEntity.badRequest() + .body(new ErrorResponse(e.getMessage())); + } + + + @ExceptionHandler(NotFoundException.class) + public ResponseEntity handleNotFoundException(RuntimeException e) { + return ResponseEntity.status(404) + .body(new ErrorResponse(e.getMessage())); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception e) { + return ResponseEntity.internalServerError() + .body(new ErrorResponse("오류가 발생했습니다.")); + } +} diff --git a/src/main/java/roomescape/advice/NotFoundException.java b/src/main/java/roomescape/advice/NotFoundException.java new file mode 100644 index 000000000..8e173b382 --- /dev/null +++ b/src/main/java/roomescape/advice/NotFoundException.java @@ -0,0 +1,7 @@ +package roomescape.advice; + +public class NotFoundException extends RuntimeException { + public NotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/roomescape/auth/AuthMember.java b/src/main/java/roomescape/auth/AuthMember.java new file mode 100644 index 000000000..a52262a06 --- /dev/null +++ b/src/main/java/roomescape/auth/AuthMember.java @@ -0,0 +1,11 @@ +package roomescape.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthMember { +} diff --git a/src/main/java/roomescape/infrastructure/DataInitializer.java b/src/main/java/roomescape/infrastructure/DataInitializer.java new file mode 100644 index 000000000..66ab341e9 --- /dev/null +++ b/src/main/java/roomescape/infrastructure/DataInitializer.java @@ -0,0 +1,106 @@ +package roomescape.infrastructure; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +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; + +@Component +public class DataInitializer implements CommandLineRunner { + + private final MemberRepository memberRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + private final ReservationRepository reservationRepository; + private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DataInitializer.class); + + public DataInitializer( + MemberRepository memberRepository, + ThemeRepository themeRepository, + TimeRepository timeRepository, + ReservationRepository reservationRepository + ) { + this.memberRepository = memberRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + this.reservationRepository = reservationRepository; + } + + @Override + @Transactional + public void run(String... args) throws Exception { + // 1. 관리자 계정 생성 + if (memberRepository.findByEmail("admin").isEmpty()) { + Member admin = new Member("admin", "admin", "admin", "ADMIN"); + Member user = new Member("user", "user", "user", "USER"); + memberRepository.save(admin); + memberRepository.save(user); + log.info("관리자 계정이 생성되었습니다."); + } + + // 2. 테마 데이터 생성 + if (themeRepository.count() == 0) { + Theme theme1 = new Theme("테마1", "테마1입니다."); + Theme theme2 = new Theme("테마2", "테마2입니다."); + Theme theme3 = new Theme("테마3", "테마3입니다."); + + themeRepository.save(theme1); + themeRepository.save(theme2); + themeRepository.save(theme3); + log.info("테마 데이터가 생성되었습니다."); + } + + // 3. 시간 데이터 생성 + if (timeRepository.count() == 0) { + Time time1 = new Time("10:00"); + Time time2 = new Time("12:00"); + Time time3 = new Time("14:00"); + Time time4 = new Time("16:00"); + Time time5 = new Time("18:00"); + Time time6 = new Time("20:00"); + + timeRepository.save(time1); + timeRepository.save(time2); + timeRepository.save(time3); + timeRepository.save(time4); + timeRepository.save(time5); + timeRepository.save(time6); + log.info("시간 데이터가 생성되었습니다."); + } + + if (reservationRepository.count() == 0) { + Member admin = memberRepository.findByEmail("admin") + .orElseThrow(() -> new RuntimeException("Admin not found")); + + Time time1 = timeRepository.findById(1L).orElseThrow(); + Time time2 = timeRepository.findById(2L).orElseThrow(); + Time time3 = timeRepository.findById(3L).orElseThrow(); + + Theme theme1 = themeRepository.findById(1L).orElseThrow(); + Theme theme2 = themeRepository.findById(2L).orElseThrow(); + Theme theme3 = themeRepository.findById(3L).orElseThrow(); + + Reservation reservation1 = new Reservation("", "2024-03-01", time1, theme1, admin); + Reservation reservation2 = new Reservation("", "2024-03-01", time2, theme2, admin); + Reservation reservation3 = new Reservation("", "2024-03-01", time3, theme3, admin); + + reservationRepository.save(reservation1); + reservationRepository.save(reservation2); + reservationRepository.save(reservation3); + + Reservation reservation4 = new Reservation("브라운", "2024-03-01", time1, theme2); + + reservationRepository.save(reservation4); + log.info("예약 데이터가 생성되었습니다."); + } + + log.info("초기 데이터 로딩이 완료되었습니다."); + } +} diff --git a/src/main/java/roomescape/infrastructure/JwtTokenProvider.java b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java new file mode 100644 index 000000000..89fbc775b --- /dev/null +++ b/src/main/java/roomescape/infrastructure/JwtTokenProvider.java @@ -0,0 +1,52 @@ +package roomescape.infrastructure; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtTokenProvider { + @Value("${security.jwt.token.secret-key:this-is-a-sample-secret-key-at-least-32-bytes-long}") + private String secretKey; + @Value("${security.jwt.token.expire-length:3600000}") + private long validityInMilliseconds; + + public String createToken(String payload) { + Claims claims = Jwts.claims().setSubject(payload); + Date now = new Date(); + Date validity = new Date(now.getTime() + validityInMilliseconds); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(validity) + .signWith(SignatureAlgorithm.HS256, secretKey) + .compact(); + } + + public String getPayload(String token) { + return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject(); + } + + public boolean validateToken(String token) { + try { + Jws claims = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token); + + return !claims.getBody() + .getExpiration() + .before(new Date()); + + } catch (JwtException | IllegalArgumentException e) { + return false; + } + } +} + diff --git a/src/main/java/roomescape/member/Member.java b/src/main/java/roomescape/member/Member.java index 903aaa9b0..8d00e91b9 100644 --- a/src/main/java/roomescape/member/Member.java +++ b/src/main/java/roomescape/member/Member.java @@ -1,12 +1,37 @@ package roomescape.member; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "member") public class Member { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false) private String name; + + @Column(nullable = false, unique = true) private String email; + + @Column(nullable = false) private String password; + + @Column(nullable = false) private String role; + @Column(name = "deleted") + private boolean deleted = false; + + protected Member() { + } + public Member(Long id, String name, String email, String role) { this.id = id; this.name = name; @@ -40,4 +65,8 @@ public String getPassword() { public String getRole() { return role; } + + public void delete() { + this.deleted = true; + } } diff --git a/src/main/java/roomescape/member/MemberController.java b/src/main/java/roomescape/member/MemberController.java index 881ae5e0d..559c7e0e0 100644 --- a/src/main/java/roomescape/member/MemberController.java +++ b/src/main/java/roomescape/member/MemberController.java @@ -1,13 +1,14 @@ package roomescape.member; import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.AuthMember; import java.net.URI; @@ -19,6 +20,39 @@ public MemberController(MemberService memberService) { this.memberService = memberService; } + @PostMapping("/login") + public ResponseEntity login( + @RequestBody MemberRequest memberRequest, + HttpServletResponse response + ) { + + String tokenValue = memberService.login(memberRequest); + + Cookie cookie = new Cookie("token", tokenValue); + cookie.setHttpOnly(true); + cookie.setPath("/"); + cookie.setMaxAge(3600); + response.addCookie(cookie); + + return ResponseEntity.ok() + .header("Keep-Alive", "timeout=60") + .build(); + } + + @GetMapping("/login/check") + public ResponseEntity check( + @AuthMember Member member + ) { + if (member == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } + + return ResponseEntity.ok() + .header("Connection", "keep-alive") + .header("Keep-Alive", "timeout=60") + .body(new MemberResponse(null, member.getName(), null)); + } + @PostMapping("/members") public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) { MemberResponse member = memberService.createMember(memberRequest); diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java deleted file mode 100644 index 81f77f4cd..000000000 --- a/src/main/java/roomescape/member/MemberDao.java +++ /dev/null @@ -1,55 +0,0 @@ -package roomescape.member; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -@Repository -public class MemberDao { - private JdbcTemplate jdbcTemplate; - - public MemberDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public Member save(Member member) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO member(name, email, password, role) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, member.getName()); - ps.setString(2, member.getEmail()); - ps.setString(3, member.getPassword()); - ps.setString(4, member.getRole()); - return ps; - }, keyHolder); - - return new Member(keyHolder.getKey().longValue(), member.getName(), member.getEmail(), "USER"); - } - - public Member findByEmailAndPassword(String email, String password) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE email = ? AND password = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - email, password - ); - } - - public Member findByName(String name) { - return jdbcTemplate.queryForObject( - "SELECT id, name, email, role FROM member WHERE name = ?", - (rs, rowNum) -> new Member( - rs.getLong("id"), - rs.getString("name"), - rs.getString("email"), - rs.getString("role") - ), - name - ); - } -} diff --git a/src/main/java/roomescape/member/MemberRepository.java b/src/main/java/roomescape/member/MemberRepository.java new file mode 100644 index 000000000..0746e48ec --- /dev/null +++ b/src/main/java/roomescape/member/MemberRepository.java @@ -0,0 +1,55 @@ +package roomescape.member; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Repository +@Transactional(readOnly = true) +public class MemberRepository { + + @PersistenceContext + private EntityManager em; + + @Transactional + public Member save(Member member) { + em.persist(member); + return member; + } + + public Optional findByEmailAndPassword(String email, String password) { + try { + Member member = em.createQuery( + "SELECT m FROM Member m WHERE m.email = :email AND m.password = :password", + Member.class) + .setParameter("email", email) + .setParameter("password", password) + .getSingleResult(); + return Optional.of(member); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findByEmail(String email) { + try { + Member member = em.createQuery( + "SELECT m FROM Member m WHERE m.email = :email", + Member.class) + .setParameter("email", email) + .getSingleResult(); + return Optional.of(member); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + public Optional findById(Long id) { + Member member = em.find(Member.class, id); + return Optional.ofNullable(member); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/member/MemberResponse.java b/src/main/java/roomescape/member/MemberResponse.java index b9fa3b97a..dd5eb89b9 100644 --- a/src/main/java/roomescape/member/MemberResponse.java +++ b/src/main/java/roomescape/member/MemberResponse.java @@ -1,5 +1,8 @@ package roomescape.member; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) public class MemberResponse { private Long id; private String name; diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..47fa44df4 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,51 @@ package roomescape.member; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.infrastructure.JwtTokenProvider; @Service +@Transactional(readOnly = true) public class MemberService { - private MemberDao memberDao; + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; - public MemberService(MemberDao memberDao) { - this.memberDao = memberDao; + public MemberService(MemberRepository memberRepository, JwtTokenProvider jwtTokenProvider) { + this.memberRepository = memberRepository; + this.jwtTokenProvider = jwtTokenProvider; } + @Transactional public MemberResponse createMember(MemberRequest memberRequest) { - Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); + Member member = memberRepository.save( + new Member( + memberRequest.getName(), + memberRequest.getEmail(), + memberRequest.getPassword(), + "USER" + ) + ); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } -} + + public Member findByToken(String token) { + if (!jwtTokenProvider.validateToken(token)) { + throw new RuntimeException("유효하지 않은 토큰입니다."); + } + + String email = jwtTokenProvider.getPayload(token); + + return memberRepository.findByEmail(email) + .orElseThrow(() -> new RuntimeException("존재하지 않는 회원입니다.")); + } + + public String login(MemberRequest memberRequest) { + Member member = memberRepository.findByEmailAndPassword( + memberRequest.getEmail(), + memberRequest.getPassword() + ) + .orElseThrow(() -> new RuntimeException("이메일 또는 비밀번호가 일치하지 않습니다.")); + + return jwtTokenProvider.createToken(member.getEmail()); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/presentation/AdminInterceptor.java b/src/main/java/roomescape/presentation/AdminInterceptor.java new file mode 100644 index 000000000..1b9dd789f --- /dev/null +++ b/src/main/java/roomescape/presentation/AdminInterceptor.java @@ -0,0 +1,54 @@ +package roomescape.presentation; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.infrastructure.JwtTokenProvider; +import roomescape.member.Member; +import roomescape.member.MemberRepository; + +import java.util.Arrays; + +@Component +public class AdminInterceptor implements HandlerInterceptor { + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + + public AdminInterceptor(JwtTokenProvider jwtTokenProvider, MemberRepository MemberRepository) { + this.jwtTokenProvider = jwtTokenProvider; + this.memberRepository = MemberRepository; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = extractToken(request); + + if (token == null || !jwtTokenProvider.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + String email = jwtTokenProvider.getPayload(token); + Member member = memberRepository.findByEmail(email) + .orElse(null); + + if (member == null || !"ADMIN".equals(member.getRole())) { + response.setStatus(401); + return false; + } + + return true; + } + + private String extractToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) return null; + return Arrays.stream(cookies) + .filter(c -> "token".equals(c.getName())) + .map(Cookie::getValue) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/roomescape/presentation/AuthenticationPrincipalArgumentResolver.java b/src/main/java/roomescape/presentation/AuthenticationPrincipalArgumentResolver.java new file mode 100644 index 000000000..3e56580c1 --- /dev/null +++ b/src/main/java/roomescape/presentation/AuthenticationPrincipalArgumentResolver.java @@ -0,0 +1,54 @@ +package roomescape.presentation; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import roomescape.auth.AuthMember; +import roomescape.infrastructure.JwtTokenProvider; +import roomescape.member.Member; +import roomescape.member.MemberService; + +@Component +public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private final JwtTokenProvider jwtTokenProvider; + private final MemberService memberService; + + public AuthenticationPrincipalArgumentResolver(JwtTokenProvider jwtTokenProvider, MemberService memberService) { + this.jwtTokenProvider = jwtTokenProvider; + this.memberService = memberService; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthMember.class) + && Member.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + Cookie[] cookies = request.getCookies(); + String token = ""; + if (cookies != null) { + for (Cookie cookie : cookies) { + if ("token".equals(cookie.getName())) { + token = cookie.getValue(); + } + } + } + + if (!jwtTokenProvider.validateToken(token)) { + return null; + //throw new RuntimeException("인증되지 않은 사용자입니다."); + } + + return memberService.findByToken(token); + } +} diff --git a/src/main/java/roomescape/presentation/WebMvcConfig.java b/src/main/java/roomescape/presentation/WebMvcConfig.java new file mode 100644 index 000000000..fc1c15718 --- /dev/null +++ b/src/main/java/roomescape/presentation/WebMvcConfig.java @@ -0,0 +1,31 @@ +package roomescape.presentation; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + private final AuthenticationPrincipalArgumentResolver authResolver; + private final AdminInterceptor adminInterceptor; + + public WebMvcConfig(AuthenticationPrincipalArgumentResolver authResolver, AdminInterceptor adminInterceptor) { + this.authResolver = authResolver; + this.adminInterceptor = adminInterceptor; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(authResolver); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminInterceptor) + .addPathPatterns("/admin/**"); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/reservation/Reservation.java b/src/main/java/roomescape/reservation/Reservation.java index 83a7edf1b..c535d008c 100644 --- a/src/main/java/roomescape/reservation/Reservation.java +++ b/src/main/java/roomescape/reservation/Reservation.java @@ -1,21 +1,55 @@ package roomescape.reservation; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import roomescape.member.Member; import roomescape.theme.Theme; import roomescape.time.Time; +@Entity +@Table(name = "Reservation") public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + + @Column(nullable = false) private String name; + + @Column(nullable = false) private String date; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "time_id", nullable = false) private Time time; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id", nullable = false) private Theme theme; - public Reservation(Long id, String name, String date, Time time, Theme theme) { - this.id = id; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @Column(name = "deleted") + private boolean deleted = false; + + protected Reservation() { + } + + 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) { @@ -25,10 +59,6 @@ public Reservation(String name, String date, Time time, Theme theme) { this.theme = theme; } - public Reservation() { - - } - public Long getId() { return id; } @@ -48,4 +78,8 @@ public Time getTime() { public Theme getTheme() { return theme; } + + public Member getMember() { + return member; + } } diff --git a/src/main/java/roomescape/reservation/ReservationController.java b/src/main/java/roomescape/reservation/ReservationController.java index b3bef3990..1c0343167 100644 --- a/src/main/java/roomescape/reservation/ReservationController.java +++ b/src/main/java/roomescape/reservation/ReservationController.java @@ -7,6 +7,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.AuthMember; +import roomescape.member.Member; +import roomescape.reservation.dto.MyReservationResponse; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.reservation.dto.ReservationResponse; import java.net.URI; import java.util.List; @@ -26,16 +31,14 @@ public List list() { } @PostMapping("/reservations") - public ResponseEntity create(@RequestBody ReservationRequest reservationRequest) { - if (reservationRequest.getName() == null - || reservationRequest.getDate() == null - || reservationRequest.getTheme() == null - || reservationRequest.getTime() == null) { - return ResponseEntity.badRequest().build(); + public ResponseEntity create(@AuthMember Member member, @RequestBody ReservationRequest request) { + Long id; + if (member != null) { + id = reservationService.saveByMember(request, member); + } else { + id = reservationService.saveByAdmin(request); } - ReservationResponse reservation = reservationService.save(reservationRequest); - - return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation); + return ResponseEntity.created(URI.create("/reservations/" + id)).build(); } @DeleteMapping("/reservations/{id}") @@ -43,4 +46,9 @@ public ResponseEntity delete(@PathVariable Long id) { reservationService.deleteById(id); return ResponseEntity.noContent().build(); } + + @GetMapping("/reservations-mine") + public List list(@AuthMember Member member) { + return reservationService.findByMember(member); + } } diff --git a/src/main/java/roomescape/reservation/ReservationDao.java b/src/main/java/roomescape/reservation/ReservationDao.java deleted file mode 100644 index a4972430c..000000000 --- a/src/main/java/roomescape/reservation/ReservationDao.java +++ /dev/null @@ -1,127 +0,0 @@ -package roomescape.reservation; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; -import roomescape.theme.Theme; -import roomescape.time.Time; - -import java.sql.PreparedStatement; -import java.util.List; - -@Repository -public class ReservationDao { - - private final JdbcTemplate jdbcTemplate; - - public ReservationDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id", - - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public Reservation save(ReservationRequest reservationRequest) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - PreparedStatement ps = connection.prepareStatement("INSERT INTO reservation(date, name, theme_id, time_id) VALUES (?, ?, ?, ?)", new String[]{"id"}); - ps.setString(1, reservationRequest.getDate()); - ps.setString(2, reservationRequest.getName()); - ps.setLong(3, reservationRequest.getTheme()); - ps.setLong(4, reservationRequest.getTime()); - return ps; - }, keyHolder); - - Time time = jdbcTemplate.queryForObject("SELECT * FROM time WHERE id = ?", - (rs, rowNum) -> new Time(rs.getLong("id"), rs.getString("time_value")), - reservationRequest.getTime()); - - Theme theme = jdbcTemplate.queryForObject("SELECT * FROM theme WHERE id = ?", - (rs, rowNum) -> new Theme(rs.getLong("id"), rs.getString("name"), rs.getString("description")), - reservationRequest.getTheme()); - - return new Reservation( - keyHolder.getKey().longValue(), - reservationRequest.getName(), - reservationRequest.getDate(), - time, - theme - ); - } - - public void deleteById(Long id) { - jdbcTemplate.update("DELETE FROM reservation WHERE id = ?", id); - } - - public List findReservationsByDateAndTheme(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id" + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } - - public List findByDateAndThemeId(String date, Long themeId) { - return jdbcTemplate.query( - "SELECT r.id AS reservation_id, r.name as reservation_name, r.date as reservation_date, " + - "t.id AS theme_id, t.name AS theme_name, t.description AS theme_description, " + - "ti.id AS time_id, ti.time_value AS time_value " + - "FROM reservation r " + - "JOIN theme t ON r.theme_id = t.id " + - "JOIN time ti ON r.time_id = ti.id " + - "WHERE r.date = ? AND r.theme_id = ?", - new Object[]{date, themeId}, - (rs, rowNum) -> new Reservation( - rs.getLong("reservation_id"), - rs.getString("reservation_name"), - rs.getString("reservation_date"), - new Time( - rs.getLong("time_id"), - rs.getString("time_value") - ), - new Theme( - rs.getLong("theme_id"), - rs.getString("theme_name"), - rs.getString("theme_description") - ))); - } -} diff --git a/src/main/java/roomescape/reservation/ReservationRepository.java b/src/main/java/roomescape/reservation/ReservationRepository.java new file mode 100644 index 000000000..005d5e47f --- /dev/null +++ b/src/main/java/roomescape/reservation/ReservationRepository.java @@ -0,0 +1,77 @@ +package roomescape.reservation; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.theme.Theme; +import roomescape.time.Time; + +import java.util.List; + +@Repository +public class ReservationRepository { + + private final EntityManager em; + + public ReservationRepository(EntityManager em) { + this.em = em; + } + + public long count() { + return em.createQuery("SELECT COUNT(t) FROM Reservation t", Long.class) + .getSingleResult(); + } + + + public List findAll() { + String jpql = "SELECT r FROM Reservation r JOIN FETCH r.time JOIN FETCH r.theme WHERE r.deleted = false"; + + return em.createQuery(jpql, Reservation.class) + .getResultList(); + } + + public Reservation save(ReservationRequest reservationRequest, String reservationName) { + Reservation reservation = new Reservation( + reservationName, + reservationRequest.getDate(), + new Time(reservationRequest.getTime().toString()), + new Theme(reservationName, reservationRequest.getTheme().toString()) + ); + em.persist(reservation); + return reservation; + } + + public void save(Reservation reservation) { + em.persist(reservation); + } + + public void deleteById(Long id) { + Reservation reservation = em.find(Reservation.class, id); + if (reservation != null) { + reservation.setDeleted(true); + } + } + + public List findByDateAndThemeId(String date, Long themeId) { + return em.createQuery( + "SELECT r FROM Reservation r WHERE r.date = :date AND r.theme.id = :themeId AND r.deleted = false", + Reservation.class) + .setParameter("date", date) + .setParameter("themeId", themeId) + .getResultList(); + } + + public List findByMemberId(Long memberId) { + + String jpql = """ + SELECT r FROM Reservation r + JOIN FETCH r.time + JOIN FETCH r.theme + WHERE r.member.id = :memberId AND r.deleted = false + """; + + return em.createQuery(jpql, Reservation.class) + .setParameter("memberId", memberId) + .getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/roomescape/reservation/ReservationService.java b/src/main/java/roomescape/reservation/ReservationService.java index bd3313328..463015e96 100644 --- a/src/main/java/roomescape/reservation/ReservationService.java +++ b/src/main/java/roomescape/reservation/ReservationService.java @@ -1,30 +1,107 @@ package roomescape.reservation; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import roomescape.member.Member; +import roomescape.reservation.dto.MyReservationResponse; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.reservation.dto.ReservationResponse; +import roomescape.reservation.waiting.Waiting; +import roomescape.reservation.waiting.WaitingRepository; +import roomescape.reservation.waiting.WaitingWithRank; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; import java.util.List; @Service +@Transactional(readOnly = true) public class ReservationService { - private ReservationDao reservationDao; + private final ReservationRepository reservationRepository; + private final TimeRepository timeRepository; + private final ThemeRepository themeRepository; + private final WaitingRepository waitingRepository; - public ReservationService(ReservationDao reservationDao) { - this.reservationDao = reservationDao; + public ReservationService(ReservationRepository reservationRepository, + TimeRepository timeRepository, + ThemeRepository themeRepository, + WaitingRepository waitingRepository) { + this.reservationRepository = reservationRepository; + this.timeRepository = timeRepository; + this.themeRepository = themeRepository; + this.waitingRepository = waitingRepository; } - public ReservationResponse save(ReservationRequest reservationRequest) { - Reservation reservation = reservationDao.save(reservationRequest); + @Transactional + public Long saveByMember(ReservationRequest request, Member member) { + Theme theme = findTheme(request.getTheme()); + Time time = findTime(request.getTime()); - return new ReservationResponse(reservation.getId(), reservationRequest.getName(), reservation.getTheme().getName(), reservation.getDate(), reservation.getTime().getValue()); + if (waitingRepository.existsByMemberAndDateTime(member.getId(), request.getDate(), theme.getId(), time.getId())) { + throw new IllegalArgumentException("이미 대기 신청을 한 타임입니다."); + } + + List existing = reservationRepository.findByDateAndThemeId(request.getDate(), theme.getId()); + + if (existing.isEmpty()) { + Reservation reservation = new Reservation(member.getName(), request.getDate(), time, theme, member); + reservationRepository.save(reservation); + return reservation.getId(); + } + + Waiting waiting = new Waiting(member, theme, time, request.getDate()); + waitingRepository.save(waiting); + return waiting.getId(); + } + + @Transactional + public Long saveByAdmin(ReservationRequest request) { + Theme theme = findTheme(request.getTheme()); + Time time = findTime(request.getTime()); + + Reservation reservation = new Reservation(request.getName(), request.getDate(), time, theme); + reservationRepository.save(reservation); + return reservation.getId(); + } + + public List findByMember(Member member) { + List responses = new java.util.ArrayList<>( + reservationRepository.findByMemberId(member.getId()).stream() + .map(MyReservationResponse::from) + .toList() + ); + + List waitingsWithRank = waitingRepository.findWaitingsWithRankByMemberId(member.getId()); + + for (WaitingWithRank wr : waitingsWithRank) { + Waiting w = wr.getWaiting(); + responses.add(MyReservationResponse.from(w, wr.getRank() + )); + } + return responses; + } + + + private Theme findTheme(Long id) { + return themeRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("테마 없음")); + } + + private Time findTime(Long id) { + return timeRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("시간 없음")); } + + @Transactional 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 -> ReservationResponse.from(it)) .toList(); } } diff --git a/src/main/java/roomescape/reservation/dto/MyReservationResponse.java b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java new file mode 100644 index 000000000..705cf62f2 --- /dev/null +++ b/src/main/java/roomescape/reservation/dto/MyReservationResponse.java @@ -0,0 +1,34 @@ +package roomescape.reservation.dto; + +import roomescape.reservation.Reservation; +import roomescape.reservation.waiting.Waiting; + + +public record MyReservationResponse( + Long reservationId, + String theme, + String date, + String time, + String status +) { + public static MyReservationResponse from(Reservation reservation) { + return new MyReservationResponse( + reservation.getId(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue(), + "예약" + ); + } + + public static MyReservationResponse from(Waiting waiting, Long rank) { + return new MyReservationResponse( + waiting.getId(), + waiting.getTheme().getName(), + waiting.getDate(), + waiting.getTime().getValue(), + rank + "번째 대기" + ); + + } +} diff --git a/src/main/java/roomescape/reservation/ReservationRequest.java b/src/main/java/roomescape/reservation/dto/ReservationRequest.java similarity index 90% rename from src/main/java/roomescape/reservation/ReservationRequest.java rename to src/main/java/roomescape/reservation/dto/ReservationRequest.java index 19f441246..aca06b48b 100644 --- a/src/main/java/roomescape/reservation/ReservationRequest.java +++ b/src/main/java/roomescape/reservation/dto/ReservationRequest.java @@ -1,4 +1,4 @@ -package roomescape.reservation; +package roomescape.reservation.dto; public class ReservationRequest { private String name; @@ -21,4 +21,4 @@ public Long getTheme() { public Long getTime() { return time; } -} +} \ No newline at end of file diff --git a/src/main/java/roomescape/reservation/ReservationResponse.java b/src/main/java/roomescape/reservation/dto/ReservationResponse.java similarity index 61% rename from src/main/java/roomescape/reservation/ReservationResponse.java rename to src/main/java/roomescape/reservation/dto/ReservationResponse.java index 41360a363..e723d67d6 100644 --- a/src/main/java/roomescape/reservation/ReservationResponse.java +++ b/src/main/java/roomescape/reservation/dto/ReservationResponse.java @@ -1,4 +1,6 @@ -package roomescape.reservation; +package roomescape.reservation.dto; + +import roomescape.reservation.Reservation; public class ReservationResponse { private Long id; @@ -15,6 +17,16 @@ public ReservationResponse(Long id, String name, String theme, String date, Stri this.time = time; } + public static ReservationResponse from(Reservation reservation) { + return new ReservationResponse( + reservation.getId(), + reservation.getName(), + reservation.getTheme().getName(), + reservation.getDate(), + reservation.getTime().getValue() + ); + } + public Long getId() { return id; } diff --git a/src/main/java/roomescape/reservation/waiting/Waiting.java b/src/main/java/roomescape/reservation/waiting/Waiting.java new file mode 100644 index 000000000..0773301e7 --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/Waiting.java @@ -0,0 +1,72 @@ +package roomescape.reservation.waiting; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import roomescape.member.Member; +import roomescape.theme.Theme; +import roomescape.time.Time; + +import java.time.LocalDateTime; + +@Entity +public class Waiting { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "theme_id") + private Theme theme; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "time_id") + private Time time; + + private String date; + + private LocalDateTime createdAt = LocalDateTime.now(); + + + public Waiting() { + } + + public Waiting(Member member, Theme theme, Time time, String date) { + this.member = member; + this.theme = theme; + this.time = time; + this.date = date; + } + + public Long getId() { + return id; + } + + public Member getMember() { + return member; + } + + public Theme getTheme() { + return theme; + } + + public Time getTime() { + return time; + } + + public String getDate() { + return date; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } +} diff --git a/src/main/java/roomescape/reservation/waiting/WaitingController.java b/src/main/java/roomescape/reservation/waiting/WaitingController.java new file mode 100644 index 000000000..0b84a446c --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/WaitingController.java @@ -0,0 +1,36 @@ +package roomescape.reservation.waiting; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.auth.AuthMember; +import roomescape.member.Member; +import roomescape.reservation.dto.ReservationRequest; + +import java.net.URI; + +@RestController +public class WaitingController { + private final WaitingService waitingService; + + public WaitingController(WaitingService waitingService) { + this.waitingService = waitingService; + } + + @PostMapping("/waitings") + public ResponseEntity create(@AuthMember Member member, + @RequestBody ReservationRequest request) { + WaitingResponse response = waitingService.save(request, member); + return ResponseEntity.created(URI.create("/waitings/" + response.id())) + .body(response); + } + + @DeleteMapping("/waitings/{id}") + public ResponseEntity delete(@PathVariable Long id) { + waitingService.deleteById(id); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/roomescape/reservation/waiting/WaitingRepository.java b/src/main/java/roomescape/reservation/waiting/WaitingRepository.java new file mode 100644 index 000000000..40a87fc82 --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/WaitingRepository.java @@ -0,0 +1,88 @@ +package roomescape.reservation.waiting; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class WaitingRepository { + + private final EntityManager em; + + public WaitingRepository(EntityManager em) { + this.em = em; + } + + public void save(Waiting waiting) { + em.persist(waiting); + } + + public Optional findById(Long id) { + return Optional.ofNullable(em.find(Waiting.class, id)); + } + + public void delete(Waiting waiting) { + em.remove(waiting); + } + + public List findByMemberId(Long memberId) { + return em.createQuery( + "SELECT w FROM Waiting w JOIN FETCH w.time JOIN FETCH w.theme " + + "WHERE w.member.id = :memberId", Waiting.class) + .setParameter("memberId", memberId) + .getResultList(); + } + + public Long findRank(Waiting waiting) { + String jpql = """ + SELECT COUNT(w) + 1 + FROM Waiting w + WHERE w.date = :date + AND w.theme.id = :themeId + AND w.time.id = :timeId + AND w.createdAt < :createdAt + """; + + return em.createQuery(jpql, Long.class) + .setParameter("date", waiting.getDate()) + .setParameter("themeId", waiting.getTheme().getId()) + .setParameter("timeId", waiting.getTime().getId()) + .setParameter("createdAt", waiting.getCreatedAt()) + .getSingleResult(); + } + + public boolean existsByMemberAndDateTime(Long memberId, String date, Long themeId, Long timeId) { + String jpql = """ + SELECT COUNT(w) > 0 + FROM Waiting w + WHERE w.member.id = :memberId + AND w.date = :date + AND w.theme.id = :themeId + AND w.time.id = :timeId + """; + return em.createQuery(jpql, Boolean.class) + .setParameter("memberId", memberId) + .setParameter("date", date) + .setParameter("themeId", themeId) + .setParameter("timeId", timeId) + .getSingleResult(); + } + + public List findWaitingsWithRankByMemberId(Long memberId) { + String jpql = "SELECT new roomescape.reservation.waiting.WaitingWithRank(" + + " w, " + + " (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)) " + + "FROM Waiting w " + + "WHERE w.member.id = :memberId"; + + return em.createQuery(jpql, WaitingWithRank.class) + .setParameter("memberId", memberId) + .getResultList(); + } +} diff --git a/src/main/java/roomescape/reservation/waiting/WaitingResponse.java b/src/main/java/roomescape/reservation/waiting/WaitingResponse.java new file mode 100644 index 000000000..e04a5b3a2 --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/WaitingResponse.java @@ -0,0 +1,17 @@ +package roomescape.reservation.waiting; + +public record WaitingResponse( + Long id, + String theme, + String date, + String time +) { + public static WaitingResponse from(Waiting waiting) { + return new WaitingResponse( + waiting.getId(), + waiting.getTheme().getName(), + waiting.getDate(), + waiting.getTime().getValue() + ); + } +} diff --git a/src/main/java/roomescape/reservation/waiting/WaitingService.java b/src/main/java/roomescape/reservation/waiting/WaitingService.java new file mode 100644 index 000000000..2f113d83f --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/WaitingService.java @@ -0,0 +1,60 @@ +package roomescape.reservation.waiting; + +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; +import roomescape.member.Member; +import roomescape.reservation.ReservationRepository; +import roomescape.reservation.dto.ReservationRequest; +import roomescape.theme.Theme; +import roomescape.theme.ThemeRepository; +import roomescape.time.Time; +import roomescape.time.TimeRepository; + +@Service +@Transactional +public class WaitingService { + private final WaitingRepository waitingRepository; + private final ReservationRepository reservationRepository; + private final ThemeRepository themeRepository; + private final TimeRepository timeRepository; + + public WaitingService(WaitingRepository waitingRepository, + ReservationRepository reservationRepository, + ThemeRepository themeRepository, + TimeRepository timeRepository) { + this.waitingRepository = waitingRepository; + this.reservationRepository = reservationRepository; + this.themeRepository = themeRepository; + this.timeRepository = timeRepository; + } + + public WaitingResponse save(ReservationRequest request, Member member) { + Theme theme = themeRepository.findById(request.getTheme()) + .orElseThrow(() -> new IllegalArgumentException("테마 없음")); + Time time = timeRepository.findById(request.getTime()) + .orElseThrow(() -> new IllegalArgumentException("시간 없음")); + + if (waitingRepository.existsByMemberAndDateTime(member.getId(), request.getDate(), theme.getId(), time.getId())) { + throw new IllegalArgumentException("이미 해당 시간에 대기/예약이 존재합니다."); + } + + var existing = reservationRepository.findByDateAndThemeId(request.getDate(), theme.getId()); + boolean hasReservationAtTime = existing.stream() + .anyMatch(r -> r.getTime().getId().equals(time.getId())); + + if (!hasReservationAtTime) { + throw new IllegalArgumentException("예약자가 없는 시간에는 대기를 걸 수 없습니다. 바로 예약해 주세요."); + } + + Waiting waiting = new Waiting(member, theme, time, request.getDate()); + waitingRepository.save(waiting); + + return WaitingResponse.from(waiting); + } + + public void deleteById(Long id) { + Waiting waiting = waitingRepository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 대기입니다.")); + waitingRepository.delete(waiting); + } +} diff --git a/src/main/java/roomescape/reservation/waiting/WaitingWithRank.java b/src/main/java/roomescape/reservation/waiting/WaitingWithRank.java new file mode 100644 index 000000000..c6b9a863d --- /dev/null +++ b/src/main/java/roomescape/reservation/waiting/WaitingWithRank.java @@ -0,0 +1,19 @@ +package roomescape.reservation.waiting; + +public class WaitingWithRank { + private Waiting waiting; + private Long rank; + + public WaitingWithRank(Waiting waiting, Long rank) { + this.waiting = waiting; + this.rank = rank + 1; + } + + public Waiting getWaiting() { + return waiting; + } + + public Long getRank() { + return rank; + } +} diff --git a/src/main/java/roomescape/theme/Theme.java b/src/main/java/roomescape/theme/Theme.java index 430a6239c..091e56859 100644 --- a/src/main/java/roomescape/theme/Theme.java +++ b/src/main/java/roomescape/theme/Theme.java @@ -1,10 +1,21 @@ package roomescape.theme; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Theme { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; + private String name; private String description; + private boolean deleted = false; + public Theme() { } @@ -30,4 +41,8 @@ public String getName() { public String getDescription() { return description; } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } } diff --git a/src/main/java/roomescape/theme/ThemeController.java b/src/main/java/roomescape/theme/ThemeController.java index 03bca41a6..fee0ad28f 100644 --- a/src/main/java/roomescape/theme/ThemeController.java +++ b/src/main/java/roomescape/theme/ThemeController.java @@ -13,26 +13,26 @@ @RestController public class ThemeController { - private ThemeDao themeDao; + private ThemeService themeService; - public ThemeController(ThemeDao themeDao) { - this.themeDao = themeDao; + public ThemeController(ThemeService themeService) { + this.themeService = themeService; } @PostMapping("/themes") public ResponseEntity createTheme(@RequestBody Theme theme) { - Theme newTheme = themeDao.save(theme); + Theme newTheme = themeService.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(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/roomescape/theme/ThemeDao.java b/src/main/java/roomescape/theme/ThemeDao.java deleted file mode 100644 index 945341d8d..000000000 --- a/src/main/java/roomescape/theme/ThemeDao.java +++ /dev/null @@ -1,41 +0,0 @@ -package roomescape.theme; - -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.support.GeneratedKeyHolder; -import org.springframework.jdbc.support.KeyHolder; -import org.springframework.stereotype.Repository; - -import java.util.List; - -@Repository -public class ThemeDao { - private JdbcTemplate jdbcTemplate; - - public ThemeDao(JdbcTemplate jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - public List findAll() { - return jdbcTemplate.query("SELECT * FROM theme where deleted = false", (rs, rowNum) -> new Theme( - rs.getLong("id"), - rs.getString("name"), - rs.getString("description") - )); - } - - public Theme save(Theme theme) { - KeyHolder keyHolder = new GeneratedKeyHolder(); - jdbcTemplate.update(connection -> { - var ps = connection.prepareStatement("INSERT INTO theme(name, description) VALUES (?, ?)", new String[]{"id"}); - ps.setString(1, theme.getName()); - ps.setString(2, theme.getDescription()); - return ps; - }, keyHolder); - - return new Theme(keyHolder.getKey().longValue(), theme.getName(), theme.getDescription()); - } - - public void deleteById(Long id) { - jdbcTemplate.update("UPDATE theme SET deleted = true WHERE id = ?", id); - } -} diff --git a/src/main/java/roomescape/theme/ThemeRepository.java b/src/main/java/roomescape/theme/ThemeRepository.java new file mode 100644 index 000000000..05f313b66 --- /dev/null +++ b/src/main/java/roomescape/theme/ThemeRepository.java @@ -0,0 +1,43 @@ +package roomescape.theme; + +import jakarta.persistence.EntityManager; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public class ThemeRepository { + private EntityManager em; + + public long count() { + return em.createQuery("SELECT COUNT(t) FROM Theme t", Long.class) + .getSingleResult(); + } + + public ThemeRepository(EntityManager em) { + this.em = em; + } + + public List findAll() { + return em.createQuery("SELECT t FROM Theme t WHERE t.deleted = false", Theme.class).getResultList(); + } + + public Theme save(Theme theme) { + em.persist(theme); + return theme; + } + + public void deleteById(Long id) { + Theme theme = em.find(Theme.class, id); + if (theme != null) { + theme.setDeleted(true); + } + } + + public Optional findById(Long id) { + Theme theme = em.find(Theme.class, id); + return Optional.of(theme); + } +} + diff --git a/src/main/java/roomescape/theme/ThemeService.java b/src/main/java/roomescape/theme/ThemeService.java new file mode 100644 index 000000000..383fd0888 --- /dev/null +++ b/src/main/java/roomescape/theme/ThemeService.java @@ -0,0 +1,33 @@ +package roomescape.theme; + +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Transactional +public class ThemeService { + + private ThemeRepository themeRepository; + + @Autowired + public ThemeService(ThemeRepository themeRepository) { + this.themeRepository = themeRepository; + } + + @Transactional + public Theme save(Theme theme) { + return themeRepository.save(theme); + } + + public List findAll() { + return themeRepository.findAll(); + } + + @Transactional + public void deleteById(Long id) { + themeRepository.deleteById(id); + } +} diff --git a/src/main/java/roomescape/time/Time.java b/src/main/java/roomescape/time/Time.java index 008ed93cf..ce4c6ad5f 100644 --- a/src/main/java/roomescape/time/Time.java +++ b/src/main/java/roomescape/time/Time.java @@ -1,9 +1,22 @@ package roomescape.time; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity public class Time { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) private Long id; + + @Column(name = "time_value") private String value; + private boolean deleted = false; + public Time(Long id, String value) { this.id = id; this.value = value; @@ -24,4 +37,8 @@ public Long getId() { public String getValue() { return value; } + + public void setDeleted(boolean deleted) { + this.deleted = deleted; + } } diff --git a/src/main/java/roomescape/time/TimeController.java b/src/main/java/roomescape/time/TimeController.java index 2343114d1..4f5a55f4b 100644 --- a/src/main/java/roomescape/time/TimeController.java +++ b/src/main/java/roomescape/time/TimeController.java @@ -30,7 +30,6 @@ public ResponseEntity