Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.fastcampus.book_bot.domain.noti;

import com.fastcampus.book_bot.domain.book.Book;
import com.fastcampus.book_bot.domain.user.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

import java.time.LocalDateTime;

@Entity
@Table(name = "notification_logs")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NotificationLogs {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "sub_id", nullable = false)
private Integer subId;

@Column(name = "user_id", nullable = false)
private Integer userId;

@Column(name = "book_id", nullable = false)
private Integer bookId;

@Column(name = "threshold_quantity", nullable = false)
private Integer thresholdQuantity;

@Column(name = "current_stock", nullable = false)
private Integer currentStock;

@Column(name = "message", nullable = false, columnDefinition = "TEXT")
private String message;

@CreatedDate
@Column(name = "sent_at", updatable = false)
private LocalDateTime sentAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "sub_id", insertable = false, updatable = false)
private NotificationSub notificationSub;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", insertable = false, updatable = false)
private Book book;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.fastcampus.book_bot.domain.noti;

import com.fastcampus.book_bot.domain.book.Book;
import com.fastcampus.book_bot.domain.user.User;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;

import java.time.LocalDateTime;
import java.util.List;

@Entity
@Table(name = "notification_sub")
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class NotificationSub {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@Column(name = "user_id", nullable = false)
private Integer userId;

@Column(name = "book_id", nullable = false)
private Integer bookId;

@Column(name = "threshold_quantity", nullable = false)
@Builder.Default
private Integer thresholdQuantity = 5;

@Column(name = "is_active", nullable = false)
@Builder.Default
private Boolean isActive = true;

@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "book_id", insertable = false, updatable = false)
private Book book;

@OneToMany(mappedBy = "notificationSub", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NotificationLogs> notificationLogs;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

Expand All @@ -17,4 +21,7 @@ public interface BookRepository extends JpaRepository<Book, Integer> {
Page<Book> findByBookPublisherContaining(String bookPublisher, Pageable pageable);
Page<Book> findByBookNameContainingOrBookAuthorContainingOrBookPublisherContaining(String bookTitle, String bookAuthor, String bookPublisher, Pageable pageable);

@Modifying
@Query("UPDATE Book b SET b.bookQuantity = :newQuantity WHERE b.bookId = :bookId")
void updateBookQuantity(@Param("bookId") Integer bookId, @Param("newQuantity") Integer newQuantity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.fastcampus.book_bot.repository;

import com.fastcampus.book_bot.domain.noti.NotificationSub;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface NotificationSubRepository extends JpaRepository<NotificationSub, Integer> {
List<NotificationSub> findActiveByBookIdAndThresholdQuantity(Integer bookId, Integer thresholdQuantity);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.thymeleaf.context.Context;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Random;

Expand Down Expand Up @@ -124,4 +126,37 @@ public boolean verifyCode(String userEmail, String inputCode) {
);
}
}

/**
* 재고 알림 이메일 발송
* @param userEmail 사용자 이메일
* @param bookTitle 도서 제목
* @param currentStock 현재 재고
* @param message 알림 메시지
*/
public void sendStockNotification(String userEmail, String bookTitle, int currentStock, String message) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();

try {
MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
messageHelper.setTo(userEmail);
messageHelper.setSubject("📚 재고 알림 - " + bookTitle);

Context context = new Context();
context.setVariable("bookTitle", bookTitle);
context.setVariable("currentStock", currentStock);
context.setVariable("message", message);
context.setVariable("notificationTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH:mm")));

String htmlContent = templateEngine.process("notification/stock-alert", context);
messageHelper.setText(htmlContent, true);

javaMailSender.send(mimeMessage);
log.info("재고 알림 이메일 발송 성공 - 수신자: {}, 도서: {}, 재고: {}권", userEmail, bookTitle, currentStock);

} catch (Exception e) {
log.error("재고 알림 이메일 발송 실패 - 수신자: {}, 도서: {}", userEmail, bookTitle, e);
throw new RuntimeException("재고 알림 이메일 발송에 실패했습니다.", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@
import com.fastcampus.book_bot.dto.grade.GradeInfo;
import com.fastcampus.book_bot.repository.UserGradeRepository;
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.math.BigDecimal;
import java.time.Duration;
import java.util.LinkedHashMap;
import java.util.Set;

@Service
@RequiredArgsConstructor
@Slf4j
public class GradeCacheService {

private final RedisTemplate<String, Object> redisTemplate;
Expand All @@ -22,36 +25,77 @@ public class GradeCacheService {
private static final String GRADE_CACHE_KEY = "grade:";
private static final Duration CACHE_TTL = Duration.ofHours(24);

@Transactional
@Transactional(readOnly = true)
public GradeInfo getGradeInfo(String gradeName) {

String cacheKey = GRADE_CACHE_KEY + gradeName;

// Redis 조회
GradeInfo cached = (GradeInfo) redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
return cached;
try {
// Redis 조회
Object cachedValue = redisTemplate.opsForValue().get(cacheKey);

if (cachedValue != null) {
// LinkedHashMap인 경우 GradeInfo 객체로 변환
if (cachedValue instanceof LinkedHashMap) {
LinkedHashMap<String, Object> map = (LinkedHashMap<String, Object>) cachedValue;
return mapToGradeInfo(map);
}
// 이미 GradeInfo 객체인 경우
else if (cachedValue instanceof GradeInfo) {
return (GradeInfo) cachedValue;
}
}
} catch (Exception e) {
log.warn("Redis에서 등급 정보 조회 실패: {}, DB에서 조회합니다.", gradeName, e);
}

// 캐시에 없으면 DB에서 조회 후 캐시 저장
// 캐시에 없거나 오류 발생 시 DB에서 조회
return userGradeRepository.findByGradeName(gradeName)
.map(this::convertToGradeInfo)
.map(gradeInfo -> {
redisTemplate.opsForValue().set(cacheKey, gradeInfo, CACHE_TTL);
try {
redisTemplate.opsForValue().set(cacheKey, gradeInfo, CACHE_TTL);
} catch (Exception e) {
log.warn("Redis 캐시 저장 실패: {}", gradeName, e);
}
return gradeInfo;
})
.orElse(getDefaultGradeInfo());

}

public void evictGradeCache(String gradeName) {
redisTemplate.delete(GRADE_CACHE_KEY + gradeName);
try {
redisTemplate.delete(GRADE_CACHE_KEY + gradeName);
} catch (Exception e) {
log.warn("Redis 캐시 삭제 실패: {}", gradeName, e);
}
}

public void evictAllGradeCache() {
Set<String> keys = redisTemplate.keys(GRADE_CACHE_KEY + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
try {
Set<String> keys = redisTemplate.keys(GRADE_CACHE_KEY + "*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
} catch (Exception e) {
log.warn("Redis 전체 캐시 삭제 실패", e);
}
}

// LinkedHashMap을 GradeInfo로 변환하는 헬퍼 메서드
private GradeInfo mapToGradeInfo(LinkedHashMap<String, Object> map) {
try {
return GradeInfo.builder()
.gradeName((String) map.get("gradeName"))
.minUsage(map.get("minUsage") != null ? ((Number) map.get("minUsage")).intValue() : 0)
.orderCount(map.get("orderCount") != null ? ((Number) map.get("orderCount")).intValue() : 0)
.discount(map.get("discount") != null ?
new BigDecimal(map.get("discount").toString()) : BigDecimal.ZERO)
.mileageRate(map.get("mileageRate") != null ?
new BigDecimal(map.get("mileageRate").toString()) : BigDecimal.ZERO)
.build();
} catch (Exception e) {
log.error("LinkedHashMap을 GradeInfo로 변환 실패: {}", map, e);
return getDefaultGradeInfo();
}
}

Expand All @@ -60,8 +104,10 @@ private GradeInfo convertToGradeInfo(UserGrade userGrade) {
.gradeName(userGrade.getGradeName())
.minUsage(userGrade.getMinUsage())
.orderCount(userGrade.getOrderCount())
.discount(userGrade.getDiscount())
.mileageRate(userGrade.getMileageRate())
.discount(userGrade.getDiscount() != null ?
userGrade.getDiscount() : BigDecimal.ZERO)
.mileageRate(userGrade.getMileageRate() != null ?
userGrade.getMileageRate() : BigDecimal.ZERO)
.build();
}

Expand All @@ -70,8 +116,8 @@ private GradeInfo getDefaultGradeInfo() {
.gradeName("BRONZE")
.minUsage(100)
.orderCount(1)
.discount(BigDecimal.valueOf(0.0))
.mileageRate(BigDecimal.valueOf(0.0))
.discount(BigDecimal.ZERO)
.mileageRate(BigDecimal.ZERO)
.build();
}
}
}
Loading