diff --git a/src/main/java/com/fastcampus/book_bot/common/config/RedisConfig.java b/src/main/java/com/fastcampus/book_bot/common/config/RedisConfig.java new file mode 100644 index 0000000..3b503c6 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/common/config/RedisConfig.java @@ -0,0 +1,35 @@ +package com.fastcampus.book_bot.common.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + + // ObjectMapper에 JSR310 모듈 추가 + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + // Key는 String으로, Value는 JSON으로 직렬화 + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/fastcampus/book_bot/common/config/WebConfig.java b/src/main/java/com/fastcampus/book_bot/common/config/WebConfig.java new file mode 100644 index 0000000..9802ac8 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/common/config/WebConfig.java @@ -0,0 +1,21 @@ +package com.fastcampus.book_bot.common.config; + +import com.fastcampus.book_bot.common.intercept.AuthInterceptor; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/order/**", "/api/order/**") + .excludePathPatterns("/login", "/register"); + } +} diff --git a/src/main/java/com/fastcampus/book_bot/common/intercept/AuthInterceptor.java b/src/main/java/com/fastcampus/book_bot/common/intercept/AuthInterceptor.java new file mode 100644 index 0000000..c85ac78 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/common/intercept/AuthInterceptor.java @@ -0,0 +1,56 @@ +package com.fastcampus.book_bot.common.intercept; + +import com.fastcampus.book_bot.common.utils.JwtUtil; +import com.fastcampus.book_bot.domain.user.User; +import com.fastcampus.book_bot.service.auth.UserService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AuthInterceptor implements HandlerInterceptor { + + private final UserService userService; + private final JwtUtil jwtUtil; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + try { + String authorization = request.getHeader("Authorization"); + if (authorization == null || !authorization.startsWith("Bearer ")) { + response.sendRedirect("/login"); + return false; + } + + String accessToken = authorization.substring(7); + + if (jwtUtil.isTokenExpired(accessToken)) { + response.sendRedirect("/login"); + return false; + } + + Integer userId = jwtUtil.extractUserId(accessToken); + + User currentUser = userService.getUserById(userId); + + request.setAttribute("currentUser", currentUser); + + return true; + } catch (Exception e) { + log.error("interceptor: 인증 실패: {}", e.getMessage()); + try { + response.sendRedirect("/login"); + } catch (IOException ioException) { + log.error("리다이렉트 실패", ioException); + } + return false; + } + } +} diff --git a/src/main/java/com/fastcampus/book_bot/controller/MainController.java b/src/main/java/com/fastcampus/book_bot/controller/MainController.java index 8254ba4..8a8ac65 100644 --- a/src/main/java/com/fastcampus/book_bot/controller/MainController.java +++ b/src/main/java/com/fastcampus/book_bot/controller/MainController.java @@ -1,15 +1,46 @@ package com.fastcampus.book_bot.controller; +import com.fastcampus.book_bot.domain.book.Book; +import com.fastcampus.book_bot.service.order.BestSellerService; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; +import java.util.List; + @Controller +@RequiredArgsConstructor @Slf4j public class MainController { + private final BestSellerService bestSellerService; + @GetMapping - public String main() { + public String main(Model model) { + try { + // 월간 베스트셀러 데이터 조회 및 캐시 업데이트 + bestSellerService.getMonthBestSeller(); + List monthlyBestSellers = bestSellerService.getMonthBestSeller(); + + // 주간 베스트셀러 데이터 조회 및 캐시 업데이트 + bestSellerService.getWeekBestSeller(); + List weeklyBestSellers = bestSellerService.getWeekBestSeller(); + + // 모델에 데이터 추가 + model.addAttribute("monthlyBestSellers", monthlyBestSellers); + model.addAttribute("weeklyBestSellers", weeklyBestSellers); + + log.info("BestSellers loaded - Monthly: {}, Weekly: {}", + monthlyBestSellers.size(), weeklyBestSellers.size()); + + } catch (Exception e) { + log.error("Error loading bestsellers", e); + // 에러 발생시 빈 리스트로 처리 + model.addAttribute("monthlyBestSellers", List.of()); + model.addAttribute("weeklyBestSellers", List.of()); + } return "index"; } diff --git a/src/main/java/com/fastcampus/book_bot/controller/order/OrderController.java b/src/main/java/com/fastcampus/book_bot/controller/order/OrderController.java index 4fddbfe..e3caed1 100644 --- a/src/main/java/com/fastcampus/book_bot/controller/order/OrderController.java +++ b/src/main/java/com/fastcampus/book_bot/controller/order/OrderController.java @@ -1,8 +1,10 @@ package com.fastcampus.book_bot.controller.order; import com.fastcampus.book_bot.common.response.SuccessApiResponse; +import com.fastcampus.book_bot.domain.user.User; import com.fastcampus.book_bot.dto.order.OrdersDTO; import com.fastcampus.book_bot.service.order.OrderService; +import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -17,10 +19,13 @@ public class OrderController { private final OrderService orderService; @PostMapping("/complete") - public ResponseEntity> orderComplete(OrdersDTO ordersDTO) { + public ResponseEntity> orderComplete(OrdersDTO ordersDTO, + HttpServletRequest request) { - orderService.orderBook(ordersDTO); + User user = (User) request.getAttribute("currentUser"); - return ResponseEntity.ok(SuccessApiResponse.of("")); + orderService.saveOrder(user, ordersDTO); + + return ResponseEntity.ok(SuccessApiResponse.of("엔티티 저장 완료")); } } diff --git a/src/main/java/com/fastcampus/book_bot/controller/order/OrderViewController.java b/src/main/java/com/fastcampus/book_bot/controller/order/OrderViewController.java index d84da5e..8a1fea3 100644 --- a/src/main/java/com/fastcampus/book_bot/controller/order/OrderViewController.java +++ b/src/main/java/com/fastcampus/book_bot/controller/order/OrderViewController.java @@ -1,16 +1,37 @@ package com.fastcampus.book_bot.controller.order; +import com.fastcampus.book_bot.domain.user.User; +import com.fastcampus.book_bot.dto.order.OrderCalculationResult; import com.fastcampus.book_bot.dto.order.OrdersDTO; +import com.fastcampus.book_bot.service.grade.GradeStrategyFactory; +import com.fastcampus.book_bot.service.order.OrderService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @Controller +@RequiredArgsConstructor public class OrderViewController { + private final OrderService orderService; + @PostMapping("/order") - public String order(@ModelAttribute("orderForm") OrdersDTO ordersDTO) { + public String order(@ModelAttribute("orderForm") OrdersDTO ordersDTO, + HttpServletRequest request, + Model model) { + + User user = (User) request.getAttribute("currentUser"); + OrderCalculationResult result = orderService.calculateOrder(ordersDTO, user); + + model.addAttribute("calculationResult", result); + model.addAttribute("currentUser", user); + model.addAttribute("userGrade", user.getUserGrade()); + model.addAttribute("userPoints", user.getPoint()); + return "order/order"; } - } diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/User.java b/src/main/java/com/fastcampus/book_bot/domain/user/User.java index 36fba16..f42a143 100644 --- a/src/main/java/com/fastcampus/book_bot/domain/user/User.java +++ b/src/main/java/com/fastcampus/book_bot/domain/user/User.java @@ -23,7 +23,7 @@ public class User { @Column(name = "USER_ID", nullable = false, updatable = false) private Integer userId; - @ManyToOne(fetch = FetchType.LAZY) + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "GRADE_ID", nullable = false, insertable = false) private UserGrade userGrade; diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/BronzeGradeStrategy.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/BronzeGradeStrategy.java deleted file mode 100644 index 3efdf05..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/BronzeGradeStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; - -@Component -public class BronzeGradeStrategy implements GradeStrategy { - - @Override - public BigDecimal calculateDiscount(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0)); - } - - @Override - public Integer calculateMileage(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0)).intValue(); - } - - @Override - public boolean canFreeShipping() { - return false; - } - - @Override - public String getGradeName() { - return "Bronze"; - } -} diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GoldGradeStrategy.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GoldGradeStrategy.java deleted file mode 100644 index fc3c4bb..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GoldGradeStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; - -@Component -public class GoldGradeStrategy implements GradeStrategy { - - @Override - public BigDecimal calculateDiscount(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.05)); - } - - @Override - public Integer calculateMileage(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.05)).intValue(); - } - - @Override - public boolean canFreeShipping() { - return true; - } - - @Override - public String getGradeName() { - return "GOLD"; - } -} diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategy.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategy.java deleted file mode 100644 index 2887a12..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategy.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import java.math.BigDecimal; - -public interface GradeStrategy { - BigDecimal calculateDiscount(BigDecimal orderAmount); - Integer calculateMileage(BigDecimal orderAmount); - boolean canFreeShipping(); - String getGradeName(); -} diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategyFactory.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategyFactory.java deleted file mode 100644 index 4b1e995..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/GradeStrategyFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import com.fastcampus.book_bot.domain.user.UserGrade; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -@Component -public class GradeStrategyFactory { - - private final Map strategies; - - public GradeStrategyFactory(List strategyList) { - this.strategies = strategyList.stream() - .collect(Collectors.toMap( - GradeStrategy::getGradeName, - strategy -> strategy - )); - } - - public GradeStrategy getStrategy(String gradeName) { - GradeStrategy strategy = strategies.get(gradeName.toUpperCase()); - if (strategy == null) { - throw new IllegalArgumentException("지원하지 않는 등급입니다." + gradeName); - } - return strategy; - } - - public GradeStrategy getStrategy(UserGrade userGrade) { - return getStrategy(userGrade.getGradeName()); - } -} diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/PlatinumGradeStrategy.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/PlatinumGradeStrategy.java deleted file mode 100644 index 46c65a2..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/PlatinumGradeStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; - -@Component -public class PlatinumGradeStrategy implements GradeStrategy { - - @Override - public BigDecimal calculateDiscount(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.08)); - } - - @Override - public Integer calculateMileage(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.08)).intValue(); - } - - @Override - public boolean canFreeShipping() { - return true; - } - - @Override - public String getGradeName() { - return "PLATINUM"; - } -} diff --git a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/SilverGradeStrategy.java b/src/main/java/com/fastcampus/book_bot/domain/user/strategy/SilverGradeStrategy.java deleted file mode 100644 index 21a63e3..0000000 --- a/src/main/java/com/fastcampus/book_bot/domain/user/strategy/SilverGradeStrategy.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fastcampus.book_bot.domain.user.strategy; - -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; - -@Component -public class SilverGradeStrategy implements GradeStrategy { - - @Override - public BigDecimal calculateDiscount(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.03)); - } - - @Override - public Integer calculateMileage(BigDecimal orderAmount) { - return orderAmount.multiply(BigDecimal.valueOf(0.03)).intValue(); - } - - @Override - public boolean canFreeShipping() { - return false; - } - - @Override - public String getGradeName() { - return "SILVER"; - } -} diff --git a/src/main/java/com/fastcampus/book_bot/dto/book/BookSalesDTO.java b/src/main/java/com/fastcampus/book_bot/dto/book/BookSalesDTO.java new file mode 100644 index 0000000..954c544 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/dto/book/BookSalesDTO.java @@ -0,0 +1,16 @@ +package com.fastcampus.book_bot.dto.book; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class BookSalesDTO { + private Integer bookId; + private Long totalQuantity; + +} diff --git a/src/main/java/com/fastcampus/book_bot/dto/grade/GradeInfo.java b/src/main/java/com/fastcampus/book_bot/dto/grade/GradeInfo.java new file mode 100644 index 0000000..924ef1c --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/dto/grade/GradeInfo.java @@ -0,0 +1,21 @@ +package com.fastcampus.book_bot.dto.grade; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GradeInfo implements Serializable { + private String gradeName; + private int minUsage; + private int orderCount; + private BigDecimal discount; + private BigDecimal mileageRate; +} diff --git a/src/main/java/com/fastcampus/book_bot/dto/order/OrderCalculationResult.java b/src/main/java/com/fastcampus/book_bot/dto/order/OrderCalculationResult.java new file mode 100644 index 0000000..872a323 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/dto/order/OrderCalculationResult.java @@ -0,0 +1,17 @@ +package com.fastcampus.book_bot.dto.order; + +import lombok.Builder; +import lombok.Getter; + +@Builder +@Getter +public class OrderCalculationResult { + private Integer originalAmount; // 원가 + private Integer gradeDiscountAmount; // 등급 할인 금액 + private Integer usedPoints; // 사용한 포인트 + private Integer shippingCost; // 배송비 + private Integer finalAmount; // 최종 결제 금액 + private Integer earnedMileage; // 적립될 마일리지 + private String gradeName; // 등급명 + private Integer afterDiscountAmount; // 등급 할인 후 금액 +} diff --git a/src/main/java/com/fastcampus/book_bot/repository/OrderBookRepository.java b/src/main/java/com/fastcampus/book_bot/repository/OrderBookRepository.java index 265e1ea..38e4141 100644 --- a/src/main/java/com/fastcampus/book_bot/repository/OrderBookRepository.java +++ b/src/main/java/com/fastcampus/book_bot/repository/OrderBookRepository.java @@ -1,7 +1,37 @@ package com.fastcampus.book_bot.repository; import com.fastcampus.book_bot.domain.orders.OrderBook; +import com.fastcampus.book_bot.dto.book.BookSalesDTO; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.time.LocalDateTime; +import java.util.List; public interface OrderBookRepository extends JpaRepository { -} + + @Query(""" + SELECT new com.fastcampus.book_bot.dto.book.BookSalesDTO( + ob.book.bookId, SUM(ob.quantity) + ) + FROM OrderBook ob + JOIN ob.order o + WHERE o.orderDate >= :weekAgo + GROUP BY ob.book.bookId + ORDER BY SUM(ob.quantity) DESC + """) + List findWeeklyBestSellers(@Param("weekAgo") LocalDateTime weekAgo); + + @Query(""" + SELECT new com.fastcampus.book_bot.dto.book.BookSalesDTO( + ob.book.bookId, SUM(ob.quantity)) + FROM OrderBook ob + JOIN ob.order o + WHERE o.orderDate >= :monthAgo + GROUP BY ob.book.bookId + ORDER BY SUM(ob.quantity) DESC + """) + List findMonthlyBestSellers(@Param("monthAgo") LocalDateTime monthAgo); + } + diff --git a/src/main/java/com/fastcampus/book_bot/repository/UserGradeRepository.java b/src/main/java/com/fastcampus/book_bot/repository/UserGradeRepository.java index c50592d..796539c 100644 --- a/src/main/java/com/fastcampus/book_bot/repository/UserGradeRepository.java +++ b/src/main/java/com/fastcampus/book_bot/repository/UserGradeRepository.java @@ -3,5 +3,8 @@ import com.fastcampus.book_bot.domain.user.UserGrade; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; + public interface UserGradeRepository extends JpaRepository { + Optional findByGradeName(String gradeName); } diff --git a/src/main/java/com/fastcampus/book_bot/repository/UserRepository.java b/src/main/java/com/fastcampus/book_bot/repository/UserRepository.java index d762387..9099c24 100644 --- a/src/main/java/com/fastcampus/book_bot/repository/UserRepository.java +++ b/src/main/java/com/fastcampus/book_bot/repository/UserRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface UserRepository extends JpaRepository { @@ -11,4 +12,6 @@ public interface UserRepository extends JpaRepository { boolean existsByUserNickname(String userNickname); User findByUserEmail(String userEmail); + + Optional findByUserId(Integer userId); } diff --git a/src/main/java/com/fastcampus/book_bot/service/auth/UserService.java b/src/main/java/com/fastcampus/book_bot/service/auth/UserService.java new file mode 100644 index 0000000..5bee8e7 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/auth/UserService.java @@ -0,0 +1,65 @@ +package com.fastcampus.book_bot.service.auth; + +import com.fastcampus.book_bot.common.exception.user.UserDomainException; +import com.fastcampus.book_bot.common.exception.user.UserErrorCode; +import com.fastcampus.book_bot.common.utils.JwtUtil; +import com.fastcampus.book_bot.domain.user.User; +import com.fastcampus.book_bot.repository.UserRepository; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class UserService { + + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Transactional + public User getCurrentUser(HttpServletRequest request) { + try { + // Authorization 헤더에서 토큰 추출 + String authorization = request.getHeader("Authorization"); + if (authorization == null || !authorization.startsWith("Bearer ")) { + throw new UserDomainException(UserErrorCode.UNAUTHORIZED_ACCESS.getMessage(), + UserErrorCode.UNAUTHORIZED_ACCESS.getCode(), HttpStatus.UNAUTHORIZED); + } + + String accessToken = authorization.substring(7); + + // 토큰 유효성 검사 + if (jwtUtil.isTokenExpired(accessToken)) { + throw new UserDomainException(UserErrorCode.UNAUTHORIZED_ACCESS.getMessage(), + UserErrorCode.UNAUTHORIZED_ACCESS.getCode(), HttpStatus.UNAUTHORIZED); + } + + // 토큰에서 사용자 ID 추출 + Integer userId = jwtUtil.extractUserId(accessToken); + + // DB에서 사용자 정보 조회 (등급 정보 포함) + return (User) userRepository.findByUserId(userId) + .orElseThrow(() -> new UserDomainException(UserErrorCode.UNAUTHORIZED_ACCESS.getMessage(), + UserErrorCode.UNAUTHORIZED_ACCESS.getCode(), HttpStatus.UNAUTHORIZED)); + + } catch (Exception e) { + log.error("현재 사용자 정보 조회 실패: {}", e.getMessage()); + throw new UserDomainException(UserErrorCode.UNAUTHORIZED_ACCESS.getMessage(), + UserErrorCode.UNAUTHORIZED_ACCESS.getCode(), HttpStatus.UNAUTHORIZED); + } + } + + /** + * 사용자 ID로 사용자 정보 조회 (내부 서비스용) + */ + @Transactional + public User getUserById(Integer userId) { + return (User) userRepository.findByUserId(userId) + .orElseThrow(() -> new UserDomainException(UserErrorCode.UNAUTHORIZED_ACCESS.getMessage(), + UserErrorCode.UNAUTHORIZED_ACCESS.getCode(), HttpStatus.UNAUTHORIZED)); + } +} diff --git a/src/main/java/com/fastcampus/book_bot/service/grade/GradeCacheService.java b/src/main/java/com/fastcampus/book_bot/service/grade/GradeCacheService.java new file mode 100644 index 0000000..4be4613 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/grade/GradeCacheService.java @@ -0,0 +1,77 @@ +package com.fastcampus.book_bot.service.grade; + +import com.fastcampus.book_bot.domain.user.UserGrade; +import com.fastcampus.book_bot.dto.grade.GradeInfo; +import com.fastcampus.book_bot.repository.UserGradeRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Set; + +@Service +@RequiredArgsConstructor +public class GradeCacheService { + + private final RedisTemplate redisTemplate; + private final UserGradeRepository userGradeRepository; + + private static final String GRADE_CACHE_KEY = "grade:"; + private static final Duration CACHE_TTL = Duration.ofHours(24); + + @Transactional + public GradeInfo getGradeInfo(String gradeName) { + + String cacheKey = GRADE_CACHE_KEY + gradeName; + + // Redis 조회 + GradeInfo cached = (GradeInfo) redisTemplate.opsForValue().get(cacheKey); + if (cached != null) { + return cached; + } + + // 캐시에 없으면 DB에서 조회 후 캐시 저장 + return userGradeRepository.findByGradeName(gradeName) + .map(this::convertToGradeInfo) + .map(gradeInfo -> { + redisTemplate.opsForValue().set(cacheKey, gradeInfo, CACHE_TTL); + return gradeInfo; + }) + .orElse(getDefaultGradeInfo()); + + } + + public void evictGradeCache(String gradeName) { + redisTemplate.delete(GRADE_CACHE_KEY + gradeName); + } + + public void evictAllGradeCache() { + Set keys = redisTemplate.keys(GRADE_CACHE_KEY + "*"); + if (keys != null && !keys.isEmpty()) { + redisTemplate.delete(keys); + } + } + + private GradeInfo convertToGradeInfo(UserGrade userGrade) { + return GradeInfo.builder() + .gradeName(userGrade.getGradeName()) + .minUsage(userGrade.getMinUsage()) + .orderCount(userGrade.getOrderCount()) + .discount(userGrade.getDiscount()) + .mileageRate(userGrade.getMileageRate()) + .build(); + } + + private GradeInfo getDefaultGradeInfo() { + return GradeInfo.builder() + .gradeName("BRONZE") + .minUsage(100) + .orderCount(1) + .discount(BigDecimal.valueOf(0.0)) + .mileageRate(BigDecimal.valueOf(0.0)) + .build(); + } +} diff --git a/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategy.java b/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategy.java new file mode 100644 index 0000000..43e2398 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategy.java @@ -0,0 +1,9 @@ +package com.fastcampus.book_bot.service.grade; + +public interface GradeStrategy { + + int calculateDiscountedPrice(int originalAmount); + int calculateShippingCost(int orderAmount); + int calculateMileage(int orderAmount); + String getGradeName(); +} diff --git a/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategyFactory.java b/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategyFactory.java new file mode 100644 index 0000000..a3df369 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/grade/GradeStrategyFactory.java @@ -0,0 +1,17 @@ +package com.fastcampus.book_bot.service.grade; + +import com.fastcampus.book_bot.dto.grade.GradeInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class GradeStrategyFactory { + + private final GradeCacheService gradeCacheService; + + public GradeStrategy getStrategy(String gradeName) { + GradeInfo gradeInfo = gradeCacheService.getGradeInfo(gradeName); + return new RedisGradeStrategy(gradeInfo); + } +} diff --git a/src/main/java/com/fastcampus/book_bot/service/grade/RedisGradeStrategy.java b/src/main/java/com/fastcampus/book_bot/service/grade/RedisGradeStrategy.java new file mode 100644 index 0000000..dc0bb82 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/grade/RedisGradeStrategy.java @@ -0,0 +1,38 @@ +package com.fastcampus.book_bot.service.grade; + +import com.fastcampus.book_bot.dto.grade.GradeInfo; + +import java.math.BigDecimal; + +public class RedisGradeStrategy implements GradeStrategy { + + private static final int SHIPPING_COST = 3000; + private final GradeInfo gradeInfo; + + public RedisGradeStrategy(GradeInfo gradeInfo) { + this.gradeInfo = gradeInfo; + } + + @Override + public int calculateDiscountedPrice(int originalAmount) { + return BigDecimal.valueOf(originalAmount) + .multiply(BigDecimal.ONE.subtract(gradeInfo.getDiscount())) + .intValue(); + } + + @Override + public int calculateShippingCost(int orderAmount) { + return orderAmount >= gradeInfo.getMinUsage() ? 0 : SHIPPING_COST; + } + + public int calculateMileage(int orderAmount) { + return BigDecimal.valueOf(orderAmount) + .multiply(gradeInfo.getMileageRate()) + .intValue(); + } + + @Override + public String getGradeName() { + return gradeInfo.getGradeName(); + } +} diff --git a/src/main/java/com/fastcampus/book_bot/service/order/BestSellerService.java b/src/main/java/com/fastcampus/book_bot/service/order/BestSellerService.java new file mode 100644 index 0000000..777d683 --- /dev/null +++ b/src/main/java/com/fastcampus/book_bot/service/order/BestSellerService.java @@ -0,0 +1,68 @@ +package com.fastcampus.book_bot.service.order; + +import com.fastcampus.book_bot.domain.book.Book; +import com.fastcampus.book_bot.dto.book.BookSalesDTO; +import com.fastcampus.book_bot.repository.BookRepository; +import com.fastcampus.book_bot.repository.OrderBookRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +@Slf4j +public class BestSellerService { + + private final OrderBookRepository orderBookRepository; + private final BookRepository bookRepository; + private final RedisTemplate redisTemplate; + + private static final String WEEKLY_BESTSELLER_KEY = "bestseller:weekly"; + private static final String MONTHLY_BESTSELLER_KEY = "bestseller:monthly"; + + @Transactional + public List getWeekBestSeller() { + + LocalDateTime weekAgo = LocalDateTime.now().minusDays(7); + + List weekSalesList = orderBookRepository.findWeeklyBestSellers(weekAgo); + + List bookList = new ArrayList<>(); + + for (BookSalesDTO bookSalesDTO : weekSalesList) { + Optional book = bookRepository.findById(bookSalesDTO.getBookId()); + book.ifPresent(bookList::add); + } + + redisTemplate.opsForValue().set(WEEKLY_BESTSELLER_KEY, bookList, Duration.ofDays(7)); + + return bookList; + } + + @Transactional + public List getMonthBestSeller() { + + LocalDateTime monthAgo = LocalDateTime.now().minusDays(30); + + List monthlySalesList = orderBookRepository.findMonthlyBestSellers(monthAgo); + + List bookList = new ArrayList<>(); + + for (BookSalesDTO bookSalesDTO : monthlySalesList) { + Optional book = bookRepository.findById(bookSalesDTO.getBookId()); + book.ifPresent(bookList::add); + } + + redisTemplate.opsForValue().set(MONTHLY_BESTSELLER_KEY, bookList, Duration.ofDays(30)); + + return bookList; + } +} diff --git a/src/main/java/com/fastcampus/book_bot/service/order/OrderService.java b/src/main/java/com/fastcampus/book_bot/service/order/OrderService.java index fc0291d..1494d39 100644 --- a/src/main/java/com/fastcampus/book_bot/service/order/OrderService.java +++ b/src/main/java/com/fastcampus/book_bot/service/order/OrderService.java @@ -1,27 +1,140 @@ package com.fastcampus.book_bot.service.order; +import com.fastcampus.book_bot.domain.book.Book; +import com.fastcampus.book_bot.domain.orders.OrderBook; +import com.fastcampus.book_bot.domain.orders.Orders; +import com.fastcampus.book_bot.domain.user.User; +import com.fastcampus.book_bot.dto.order.OrderCalculationResult; import com.fastcampus.book_bot.dto.order.OrdersDTO; +import com.fastcampus.book_bot.repository.BookRepository; import com.fastcampus.book_bot.repository.OrderBookRepository; import com.fastcampus.book_bot.repository.OrderRepository; +import com.fastcampus.book_bot.service.grade.GradeStrategy; +import com.fastcampus.book_bot.service.grade.GradeStrategyFactory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.time.LocalDateTime; + @Service -@Slf4j @RequiredArgsConstructor +@Slf4j public class OrderService { - private final OrderRepository orderRepository; + private final GradeStrategyFactory gradeStrategyFactory; private final OrderBookRepository orderBookRepository; + private final OrderRepository orderRepository; + private final BookRepository bookRepository; + /** + * 주문 금액 계산 (User 객체 기반) + */ + public OrderCalculationResult calculateOrder(OrdersDTO ordersDTO, User user) { + return calculateOrder( + user.getUserGrade().getGradeName(), + ordersDTO.getPrice() * ordersDTO.getQuantity(), + 0 // 포인트 사용은 기본값 0 (나중에 추가 가능) + ); + } - @Transactional - public void orderBook(OrdersDTO ordersDTO) { + /** + * 주문 금액 계산 (포인트 사용 포함) + */ + public OrderCalculationResult calculateOrder(OrdersDTO ordersDTO, User user, Integer usedPoints) { + return calculateOrder( + user.getUserGrade().getGradeName(), + ordersDTO.getPrice() * ordersDTO.getQuantity(), + usedPoints != null ? usedPoints : 0 + ); + } + + /** + * 주문 금액 계산 (기본 메서드) + */ + public OrderCalculationResult calculateOrder(String gradeName, Integer originalAmount, Integer usedPoints) { + GradeStrategy strategy = gradeStrategyFactory.getStrategy(gradeName); + + // 등급 할인 적용 + Integer discountedPrice = strategy.calculateDiscountedPrice(originalAmount); + Integer gradeDiscountAmount = originalAmount - discountedPrice; + + // 포인트 사용 적용 + Integer afterPointUsage = Math.max(0, discountedPrice - usedPoints); + + // 배송비 계산 + Integer shippingCost = strategy.calculateShippingCost(afterPointUsage); + + // 최종 결제 금액 + Integer finalAmount = afterPointUsage + shippingCost; - /* ordersDTO to order Entity */ - + // 마일리지 적립 (원가 기준) + Integer earnedMileage = strategy.calculateMileage(originalAmount); + + return OrderCalculationResult.builder() + .originalAmount(originalAmount) + .gradeDiscountAmount(gradeDiscountAmount) + .usedPoints(usedPoints) + .afterDiscountAmount(discountedPrice) + .shippingCost(shippingCost) + .finalAmount(finalAmount) + .earnedMileage(earnedMileage) + .gradeName(strategy.getGradeName()) + .build(); } + @Transactional + public void saveOrder(User user, OrdersDTO ordersDTO) { + + try { + OrderCalculationResult calculationResult = calculateOrder(ordersDTO, user); + log.info("주문 금액 계산 완료 - 최종 결제금액: {}", calculationResult.getFinalAmount()); + + Book book = bookRepository.findById(ordersDTO.getBookId().intValue()) + .orElseThrow(() -> { + log.error("도서 조회 실패 - 존재하지 않는 도서ID: {}", ordersDTO.getBookId()); + return new IllegalArgumentException("존재하지 않는 도서입니다: " + ordersDTO.getBookId()); + }); + log.info("도서 조회 성공 - 도서명: {}, 재고: {}", book.getBookName(), book.getBookQuantity()); + + if (book.getBookQuantity() < ordersDTO.getQuantity()) { + log.error("재고 부족 - 요청수량: {}, 현재재고: {}", ordersDTO.getQuantity(), book.getBookQuantity()); + throw new IllegalStateException("재고가 부족합니다. 현재 재고: " + book.getBookQuantity()); + } + + Orders order = Orders.builder() + .user(user) + .orderStatus("ORDER_READY") + .totalPrice(calculationResult.getFinalAmount()) + .orderDay(LocalDate.now()) + .orderDate(LocalDateTime.now()) + .createdAt(LocalDateTime.now()) + .build(); + + Orders savedOrder = orderRepository.save(order); + log.info("주문 저장 성공 - 주문ID: {}, 상태: {}, 총금액: {}", + savedOrder.getOrderId(), savedOrder.getOrderStatus(), savedOrder.getTotalPrice()); + + OrderBook orderBook = OrderBook.builder() + .order(savedOrder) + .book(book) + .quantity(ordersDTO.getQuantity()) + .price(ordersDTO.getPrice()) + .build(); + + OrderBook savedOrderBook = orderBookRepository.save(orderBook); + log.info("주문상품 저장 성공 - 주문상품ID: {}, 수량: {}, 가격: {}", + savedOrderBook.getOrderBookId(), savedOrderBook.getQuantity(), savedOrderBook.getPrice()); + + log.info("=== 주문 저장 프로세스 완료 ==="); + + } catch (Exception e) { + log.error("주문 저장 중 오류 발생", e); + log.error("오류 상세 정보 - 사용자ID: {}, 상품ID: {}, 오류메시지: {}", + user.getUserId(), ordersDTO.getBookId(), e.getMessage()); + throw e; // 트랜잭션 롤백을 위해 예외 재발생 + } + } } diff --git a/src/main/resources/db/migration/V14__Alter_table_orderbooks.sql b/src/main/resources/db/migration/V14__Alter_table_orderbooks.sql new file mode 100644 index 0000000..7dc3e6e --- /dev/null +++ b/src/main/resources/db/migration/V14__Alter_table_orderbooks.sql @@ -0,0 +1,2 @@ +-- order_book_id 컬럼에 AUTO_INCREMENT 추가 +ALTER TABLE order_book MODIFY COLUMN order_book_id INT NOT NULL AUTO_INCREMENT; diff --git a/src/main/resources/db/migration/V15__Insert_table_orders.sql b/src/main/resources/db/migration/V15__Insert_table_orders.sql new file mode 100644 index 0000000..7dc0808 --- /dev/null +++ b/src/main/resources/db/migration/V15__Insert_table_orders.sql @@ -0,0 +1,52 @@ +-- Orders 테이블 더미 데이터 (50개) +INSERT INTO `orders` (`USER_ID`, `ORDER_STATUS`, `TOTAL_PRICE`, `ORDER_DAY`, `ORDER_DATE`, `CREATED_AT`, `UPDATED_AT`) VALUES + (1, 'ORDER_READY', 45000, '2025-08-15', '2025-08-15 14:30:00', '2025-08-15 14:30:00', '2025-08-15 14:30:00'), + (1, 'ORDER_READY', 32000, '2025-08-17', '2025-08-17 09:15:00', '2025-08-17 09:15:00', '2025-08-17 09:15:00'), + (1, 'ORDER_READY', 28000, '2025-08-20', '2025-08-20 16:45:00', '2025-08-20 16:45:00', '2025-08-20 16:45:00'), + (1, 'ORDER_READY', 55000, '2025-08-22', '2025-08-22 11:20:00', '2025-08-22 11:20:00', '2025-08-22 11:20:00'), + (1, 'ORDER_READY', 41000, '2025-08-25', '2025-08-25 13:10:00', '2025-08-25 13:10:00', '2025-08-25 13:10:00'), + (1, 'ORDER_READY', 67000, '2025-08-28', '2025-08-28 15:30:00', '2025-08-28 15:30:00', '2025-08-28 15:30:00'), + (1, 'ORDER_READY', 39000, '2025-08-30', '2025-08-30 10:45:00', '2025-08-30 10:45:00', '2025-08-30 10:45:00'), + (1, 'ORDER_READY', 52000, '2025-09-02', '2025-09-02 14:20:00', '2025-09-02 14:20:00', '2025-09-02 14:20:00'), + (1, 'ORDER_READY', 33000, '2025-09-05', '2025-09-05 16:00:00', '2025-09-05 16:00:00', '2025-09-05 16:00:00'), + (1, 'ORDER_READY', 44000, '2025-09-07', '2025-09-07 12:15:00', '2025-09-07 12:15:00', '2025-09-07 12:15:00'), + (1, 'ORDER_READY', 29000, '2025-09-10', '2025-09-10 09:30:00', '2025-09-10 09:30:00', '2025-09-10 09:30:00'), + (1, 'ORDER_READY', 58000, '2025-09-12', '2025-09-12 17:45:00', '2025-09-12 17:45:00', '2025-09-12 17:45:00'), + (1, 'ORDER_READY', 47000, '2025-09-14', '2025-09-14 11:50:00', '2025-09-14 11:50:00', '2025-09-14 11:50:00'), + (1, 'ORDER_READY', 36000, '2025-08-14', '2025-08-14 13:25:00', '2025-08-14 13:25:00', '2025-08-14 13:25:00'), + (1, 'ORDER_READY', 61000, '2025-08-16', '2025-08-16 15:40:00', '2025-08-16 15:40:00', '2025-08-16 15:40:00'), + (1, 'ORDER_READY', 42000, '2025-08-19', '2025-08-19 10:10:00', '2025-08-19 10:10:00', '2025-08-19 10:10:00'), + (1, 'ORDER_READY', 35000, '2025-08-21', '2025-08-21 14:55:00', '2025-08-21 14:55:00', '2025-08-21 14:55:00'), + (1, 'ORDER_READY', 50000, '2025-08-24', '2025-08-24 16:20:00', '2025-08-24 16:20:00', '2025-08-24 16:20:00'), + (1, 'ORDER_READY', 38000, '2025-08-26', '2025-08-26 12:35:00', '2025-08-26 12:35:00', '2025-08-26 12:35:00'), + (1, 'ORDER_READY', 64000, '2025-08-29', '2025-08-29 09:05:00', '2025-08-29 09:05:00', '2025-08-29 09:05:00'), + (1, 'ORDER_READY', 43000, '2025-09-01', '2025-09-01 15:15:00', '2025-09-01 15:15:00', '2025-09-01 15:15:00'), + (1, 'ORDER_READY', 31000, '2025-09-03', '2025-09-03 11:40:00', '2025-09-03 11:40:00', '2025-09-03 11:40:00'), + (1, 'ORDER_READY', 57000, '2025-09-06', '2025-09-06 13:50:00', '2025-09-06 13:50:00', '2025-09-06 13:50:00'), + (1, 'ORDER_READY', 46000, '2025-09-08', '2025-09-08 16:25:00', '2025-09-08 16:25:00', '2025-09-08 16:25:00'), + (1, 'ORDER_READY', 34000, '2025-09-11', '2025-09-11 10:30:00', '2025-09-11 10:30:00', '2025-09-11 10:30:00'), + (1, 'ORDER_READY', 59000, '2025-09-13', '2025-09-13 14:45:00', '2025-09-13 14:45:00', '2025-09-13 14:45:00'), + (1, 'ORDER_READY', 48000, '2025-09-13', '2025-09-13 17:10:00', '2025-09-13 17:10:00', '2025-09-13 17:10:00'), + (1, 'ORDER_READY', 37000, '2025-08-18', '2025-08-18 12:20:00', '2025-08-18 12:20:00', '2025-08-18 12:20:00'), + (1, 'ORDER_READY', 53000, '2025-08-23', '2025-08-23 15:55:00', '2025-08-23 15:55:00', '2025-08-23 15:55:00'), + (1, 'ORDER_READY', 40000, '2025-08-27', '2025-08-27 09:40:00', '2025-08-27 09:40:00', '2025-08-27 09:40:00'), + (1, 'ORDER_READY', 62000, '2025-08-31', '2025-08-31 13:05:00', '2025-08-31 13:05:00', '2025-08-31 13:05:00'), + (1, 'ORDER_READY', 45000, '2025-09-04', '2025-09-04 16:30:00', '2025-09-04 16:30:00', '2025-09-04 16:30:00'), + (1, 'ORDER_READY', 33000, '2025-09-09', '2025-09-09 11:15:00', '2025-09-09 11:15:00', '2025-09-09 11:15:00'), + (1, 'ORDER_READY', 56000, '2025-08-13', '2025-08-13 14:00:00', '2025-08-13 14:00:00', '2025-08-13 14:00:00'), + (1, 'ORDER_READY', 41000, '2025-08-12', '2025-08-12 16:45:00', '2025-08-12 16:45:00', '2025-08-12 16:45:00'), + (1, 'ORDER_READY', 49000, '2025-08-11', '2025-08-11 10:25:00', '2025-08-11 10:25:00', '2025-08-11 10:25:00'), + (1, 'ORDER_READY', 35000, '2025-08-10', '2025-08-10 13:35:00', '2025-08-10 13:35:00', '2025-08-10 13:35:00'), + (1, 'ORDER_READY', 54000, '2025-08-09', '2025-08-09 15:20:00', '2025-08-09 15:20:00', '2025-08-09 15:20:00'), + (1, 'ORDER_READY', 42000, '2025-08-08', '2025-08-08 11:50:00', '2025-08-08 11:50:00', '2025-08-08 11:50:00'), + (1, 'ORDER_READY', 38000, '2025-08-07', '2025-08-07 14:10:00', '2025-08-07 14:10:00', '2025-08-07 14:10:00'), + (1, 'ORDER_READY', 60000, '2025-08-06', '2025-08-06 16:35:00', '2025-08-06 16:35:00', '2025-08-06 16:35:00'), + (1, 'ORDER_READY', 46000, '2025-08-05', '2025-08-05 12:05:00', '2025-08-05 12:05:00', '2025-08-05 12:05:00'), + (1, 'ORDER_READY', 32000, '2025-08-04', '2025-08-04 09:25:00', '2025-08-04 09:25:00', '2025-08-04 09:25:00'), + (1, 'ORDER_READY', 51000, '2025-08-03', '2025-08-03 15:40:00', '2025-08-03 15:40:00', '2025-08-03 15:40:00'), + (1, 'ORDER_READY', 39000, '2025-08-02', '2025-08-02 13:15:00', '2025-08-02 13:15:00', '2025-08-02 13:15:00'), + (1, 'ORDER_READY', 55000, '2025-08-01', '2025-08-01 17:20:00', '2025-08-01 17:20:00', '2025-08-01 17:20:00'), + (1, 'ORDER_READY', 44000, '2025-07-31', '2025-07-31 11:30:00', '2025-07-31 11:30:00', '2025-07-31 11:30:00'), + (1, 'ORDER_READY', 37000, '2025-07-30', '2025-07-30 14:50:00', '2025-07-30 14:50:00', '2025-07-30 14:50:00'), + (1, 'ORDER_READY', 63000, '2025-07-29', '2025-07-29 16:05:00', '2025-07-29 16:05:00', '2025-07-29 16:05:00'), + (1, 'ORDER_READY', 48000, '2025-07-28', '2025-07-28 10:40:00', '2025-07-28 10:40:00', '2025-07-28 10:40:00'); \ No newline at end of file diff --git a/src/main/resources/db/migration/V16__Insert_table_orderbook.sql b/src/main/resources/db/migration/V16__Insert_table_orderbook.sql new file mode 100644 index 0000000..df54411 --- /dev/null +++ b/src/main/resources/db/migration/V16__Insert_table_orderbook.sql @@ -0,0 +1,61 @@ +-- Order_book 테이블 더미 데이터 (ORDER_ID 103~152, BOOK_ID는 1~100 사이) +INSERT INTO `order_book` (`ORDER_ID`, `BOOK_ID`, `QUANTITY`, `PRICE`) VALUES + (103, 15, 2, 18000), + (103, 43, 1, 27000), + (104, 89, 1, 32000), + (105, 56, 1, 28000), + (106, 34, 2, 22000), + (106, 67, 1, 33000), + (107, 23, 1, 41000), + (108, 78, 3, 19000), + (108, 1, 1, 10000), + (109, 34, 1, 39000), + (110, 45, 2, 26000), + (111, 67, 1, 33000), + (112, 92, 2, 22000), + (113, 78, 1, 29000), + (114, 56, 2, 29000), + (115, 89, 1, 47000), + (116, 98, 1, 36000), + (117, 12, 3, 17000), + (117, 45, 1, 10000), + (118, 76, 2, 21000), + (119, 34, 1, 35000), + (120, 3, 2, 25000), + (121, 87, 1, 38000), + (122, 56, 2, 24000), + (122, 89, 1, 16000), + (123, 45, 1, 43000), + (124, 67, 1, 31000), + (125, 34, 3, 19000), + (126, 98, 2, 23000), + (127, 76, 1, 34000), + (128, 23, 2, 26000), + (128, 67, 1, 7000), + (129, 34, 1, 48000), + (130, 89, 1, 37000), + (131, 45, 3, 15000), + (131, 12, 1, 8000), + (132, 56, 2, 31000), + (133, 78, 2, 31000), + (134, 1, 2, 27000), + (134, 34, 1, 8000), + (135, 67, 1, 33000), + (136, 89, 3, 18000), + (136, 45, 1, 2000), + (137, 23, 2, 20500), + (138, 34, 1, 49000), + (139, 98, 1, 35000), + (140, 78, 3, 18000), + (141, 67, 2, 21000), + (142, 45, 1, 38000), + (143, 23, 2, 30000), + (144, 89, 1, 46000), + (145, 56, 1, 32000), + (146, 1, 3, 17000), + (147, 34, 1, 39000), + (148, 67, 2, 27500), + (149, 78, 1, 44000), + (150, 45, 1, 37000), + (151, 23, 3, 21000), + (152, 89, 2, 24000); \ No newline at end of file diff --git a/src/main/resources/templates/book/detail.html b/src/main/resources/templates/book/detail.html index 53c8c38..2df830e 100644 --- a/src/main/resources/templates/book/detail.html +++ b/src/main/resources/templates/book/detail.html @@ -99,7 +99,6 @@ min-width: 150px; } - /* 주문 버튼 특별 스타일 */ .btn-order { background: linear-gradient(135deg, #28a745 0%, #20c997 100%); border: none; @@ -117,6 +116,12 @@ color: white; } + .btn-order:disabled { + background: #6c757d; + cursor: not-allowed; + transform: none; + } + .breadcrumb-custom { background-color: transparent; padding: 0; @@ -163,6 +168,14 @@ margin-top: 30px; } + .loading { + display: none; + } + + .loading.show { + display: inline-block; + } + @media (max-width: 768px) { .book-title { font-size: 1.5rem; @@ -258,18 +271,15 @@

도서 제목

- -
- - - - - - - -
+ + 도서 소개
- +