Skip to content

Commit

Permalink
refactor: 이미지 업로드 알림 로직 분리 (#1124)
Browse files Browse the repository at this point in the history
* feat: DiningTypeNotFoundException 클래스 구현

* feat: CoopShopService 클래스 diningType 찾는 메서드 구현

* feat: DiningNotifyCache Redis 클래스 생성

* feat: DiningNotifyCacheRepository Redis용 클래스 구현

* feat: DiningRepository 날짜, 타입, 장소 기준으로 이미지 업로드 여부 확인 메서드 구현

* feat: 식단 이미지 저장시 알림 발송 삭제, 스케줄러용 알림 발송 로직 구현

* feat: CoopScheduler 클래스 구현

* feat: LocalTime clock 추가

* feat: test 완성

* fix: 10분전 알림 로직 수정

* fix: 테스트용 코드 삭제

* fix: 테스트 수정

* fix: conflict 해결

* fix: 리뷰 반영
  • Loading branch information
dradnats1012 authored Dec 15, 2024
1 parent 885875d commit b4a0e4c
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ ResponseEntity<Void> changeSoldOut(
);

@ApiResponses(

value = {
@ApiResponse(responseCode = "200"),
@ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package in.koreatech.koin.domain.coop.model;

import java.util.concurrent.TimeUnit;

import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;

import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;

@Getter
@RedisHash("DiningNotify")
public class DiningNotifyCache {

private static final long CACHE_EXPIRE_HOUR_BY_COOP = 3L;

@Id
private String id;

@TimeToLive(unit = TimeUnit.HOURS)
private final Long expiration;

@Builder
private DiningNotifyCache(String id, Long expiration){
this.id = id;
this.expiration = CACHE_EXPIRE_HOUR_BY_COOP;
}

public static DiningNotifyCache from(String diningId){
return DiningNotifyCache.builder()
.id(diningId)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package in.koreatech.koin.domain.coop.repository;

import java.util.Optional;

import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.coop.exception.DiningCacheNotFoundException;
import in.koreatech.koin.domain.coop.model.DiningNotifyCache;

public interface DiningNotifyCacheRepository extends Repository<DiningNotifyCache, String> {

DiningNotifyCache save(DiningNotifyCache diningNotifyCache);

boolean existsById(String diningNotifyId);

Optional<DiningNotifyCache> findById(String diningPlace);

default DiningNotifyCache getById(String diningPlace) {
return findById(diningPlace).orElseThrow(
() -> DiningCacheNotFoundException.withDetail("diningSoldOutCache: " + diningPlace));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
Expand Down Expand Up @@ -38,12 +39,15 @@
import in.koreatech.koin.domain.coop.exception.StartDateAfterEndDateException;
import in.koreatech.koin.domain.coop.model.Coop;
import in.koreatech.koin.domain.coop.model.DiningImageUploadEvent;
import in.koreatech.koin.domain.coop.model.DiningNotifyCache;
import in.koreatech.koin.domain.coop.model.DiningSoldOutEvent;
import in.koreatech.koin.domain.coop.repository.CoopRepository;
import in.koreatech.koin.domain.coop.repository.DiningNotifyCacheRepository;
import in.koreatech.koin.domain.coop.repository.DiningSoldOutCacheRepository;
import in.koreatech.koin.domain.coopshop.model.CoopShopType;
import in.koreatech.koin.domain.coopshop.service.CoopShopService;
import in.koreatech.koin.domain.dining.model.Dining;
import in.koreatech.koin.domain.dining.model.DiningType;
import in.koreatech.koin.domain.dining.repository.DiningRepository;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.domain.user.model.UserToken;
Expand All @@ -61,11 +65,13 @@ public class CoopService {
private final ApplicationEventPublisher eventPublisher;
private final DiningRepository diningRepository;
private final DiningSoldOutCacheRepository diningSoldOutCacheRepository;
private final DiningNotifyCacheRepository diningNotifyCacheRepository;
private final CoopRepository coopRepository;
private final UserTokenRepository userTokenRepository;
private final CoopShopService coopShopService;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final List<String> placeFilters = Arrays.asList("A코너", "B코너", "C코너");

public static final LocalDate LIMIT_DATE = LocalDate.of(2022, 11, 29);
private final int EXCEL_COLUMN_COUNT = 8;
Expand All @@ -90,15 +96,56 @@ public void changeSoldOut(SoldOutRequest soldOutRequest) {
@Transactional
public void saveDiningImage(DiningImageRequest imageRequest) {
Dining dining = diningRepository.getById(imageRequest.menuId());
boolean isImageExist = diningRepository.existsByDateAndTypeAndImageUrlIsNotNull(dining.getDate(),
dining.getType());
dining.setImageUrl(imageRequest.imageUrl());
}

public void sendDiningNotify() {
DiningType diningType = coopShopService.getDiningType();
LocalDate nowDate = LocalDate.now(clock);
List<Dining> dinings = diningRepository.findAllByDateAndType(nowDate, diningType);

LocalDateTime now = LocalDateTime.now(clock);
boolean isOpened = coopShopService.getIsOpened(now, CoopShopType.CAFETERIA, dining.getType(), true);
if (isOpened && !isImageExist) {
eventPublisher.publishEvent(new DiningImageUploadEvent(dining.getId(), dining.getImageUrl()));
if (dinings.isEmpty()) {
return;
}
dining.setImageUrl(imageRequest.imageUrl());

boolean allImageExist = diningRepository.allExistsByDateAndTypeAndPlacesAndImageUrlIsNotNull(
nowDate, diningType, placeFilters
);

boolean isOpened = coopShopService.getIsOpened(
LocalDateTime.now(clock), CoopShopType.CAFETERIA, diningType, true
);

String diningNotifyId = nowDate.toString() + diningType;

if (isOpened && allImageExist) {
if (alreadyNotify(diningNotifyId))
return;

if (!diningNotifyCacheRepository.existsById(diningNotifyId)) {
sendNotify(diningNotifyId, dinings);
}
}

if (LocalTime.now().isAfter(diningType.getStartTime().minusMinutes(10))
&& LocalTime.now().isBefore(diningType.getStartTime())
&& !diningNotifyCacheRepository.existsById(diningNotifyId)
&& diningRepository.existsByDateAndTypeAndImageUrlIsNotNull(nowDate, diningType)
) {
sendNotify(diningNotifyId, dinings);
}
}

private boolean alreadyNotify(String diningNotifyId) {
if (diningNotifyCacheRepository.existsById(diningNotifyId)) {
return true;
}
return false;
}

private void sendNotify(String diningNotifyId, List<Dining> dinings) {
diningNotifyCacheRepository.save(DiningNotifyCache.from(diningNotifyId));
eventPublisher.publishEvent(new DiningImageUploadEvent(dinings.get(0).getId(), dinings.get(0).getImageUrl()));
}

@Transactional
Expand Down Expand Up @@ -145,6 +192,7 @@ private void validateDates(LocalDate startDate, LocalDate endDate) {
if (startDate.isAfter(today) || endDate.isAfter(today)) {
throw new DiningNowDateException("오늘 날짜 이후 기간은 설정할 수 없어요.");
}

if (startDate.isAfter(endDate)) {
throw new StartDateAfterEndDateException("시작일은 종료일 이전으로 설정해주세요.");
}
Expand Down Expand Up @@ -217,10 +265,10 @@ private void fillDiningRow(Dining dining, Row row, CellStyle commonStyle) {
row.createCell(6).setCellValue(Optional.ofNullable(dining.getSoldOut()).map(Object::toString).orElse(""));
row.createCell(7).setCellValue(Optional.ofNullable(dining.getIsChanged()).map(Object::toString).orElse(""));

for (int i = 0; i < 8; i++) {
row.getCell(i).setCellStyle(commonStyle);
for (int i = 0; i < EXCEL_COLUMN_COUNT; i++) {
row.getCell(i).setCellStyle(commonStyle);
}
}
}

private String formatMenu(List<String> menu) {
return String.join("\n", menu);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package in.koreatech.koin.domain.coop.util;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import in.koreatech.koin.domain.coop.service.CoopService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class CoopScheduler {

private final CoopService coopService;

@Scheduled(cron = "0 0/6 7 * * *")
@Scheduled(cron = "0 30/6 10-11 * * *")
@Scheduled(cron = "0 30/6 16-17 * * *")
public void notifyDiningImageUpload() {
try {
coopService.sendDiningNotify();
} catch (Exception e) {
log.warn("식단 이미지 알림 과정에서 오류가 발생했습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package in.koreatech.koin.domain.coopshop.exception;

import in.koreatech.koin.global.exception.DataNotFoundException;

public class DiningTypeNotFoundException extends DataNotFoundException {

private static final String DEFAULT_MESSAGE = "해당하는 식단 타입이 존재하지 않습니다.";

public DiningTypeNotFoundException(String message) {
super(message);
}

public DiningTypeNotFoundException(String message, String detail) {
super(message, detail);
}

public static DiningTypeNotFoundException withDetail(String detail) {
return new DiningTypeNotFoundException(DEFAULT_MESSAGE, detail);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package in.koreatech.koin.domain.coopshop.service;

import static in.koreatech.koin.domain.dining.model.DiningType.*;

import java.time.Clock;
import java.time.DayOfWeek;
import java.time.LocalDate;
Expand All @@ -14,14 +16,15 @@
import in.koreatech.koin.domain.coopshop.dto.CoopShopResponse;
import in.koreatech.koin.domain.coopshop.dto.CoopShopsResponse;
import in.koreatech.koin.domain.coopshop.exception.CoopSemesterNotFoundException;
import in.koreatech.koin.domain.coopshop.exception.DiningTypeNotFoundException;
import in.koreatech.koin.domain.coopshop.model.CoopOpen;
import in.koreatech.koin.domain.coopshop.model.CoopSemester;
import in.koreatech.koin.domain.coopshop.model.CoopShop;
import in.koreatech.koin.domain.coopshop.model.CoopShopType;
import in.koreatech.koin.domain.coopshop.model.DayType;
import in.koreatech.koin.domain.coopshop.repository.CoopOpenRepository;
import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository;
import in.koreatech.koin.domain.coopshop.repository.CoopSemesterRepository;
import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository;
import in.koreatech.koin.domain.dining.model.DiningType;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -71,6 +74,23 @@ public boolean getIsOpened(LocalDateTime now, CoopShopType coopShopType, DiningT
}
}

public DiningType getDiningType(){
if(LocalTime.now(clock).isAfter(BREAKFAST.getStartTime().minusHours(1))
&& LocalTime.now(clock).isBefore(BREAKFAST.getEndTime())){
return BREAKFAST;
}
if(LocalTime.now(clock).isAfter(LUNCH.getStartTime().minusHours(1))
&& LocalTime.now(clock).isBefore(LUNCH.getEndTime())){
return LUNCH;
}
if(LocalTime.now(clock).isAfter(DINNER.getStartTime().minusHours(1))
&& LocalTime.now(clock).isBefore(DINNER.getEndTime())){
return DINNER;
}

throw DiningTypeNotFoundException.withDetail(LocalTime.now() + "");
}

@Transactional
public void updateSemester() {
CoopSemester currentSemester = coopSemesterRepository.getByIsApplied(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

import in.koreatech.koin.domain.coop.exception.MenuNotFoundException;
Expand Down Expand Up @@ -36,4 +37,15 @@ default Dining getById(Integer id) {
List<Dining> findByDateBetween(LocalDate startDate, LocalDate endDate);

List<Dining> findByDateBetweenAndPlaceIn(LocalDate startDate, LocalDate endDate, List<String> placeFilters);

Optional<List<Dining>> findByDate(LocalDate now);

default List<Dining> getByDate(LocalDate now){
return findByDate(now)
.orElseThrow(()-> MenuNotFoundException.withDetail("menuId: " + now));
}

@Query("SELECT COUNT(d) = (SELECT COUNT(d2) FROM Dining d2 WHERE d2.date = :date AND d2.type = :type AND d2.place IN :places) " +
"FROM Dining d WHERE d.date = :date AND d.type = :type AND d.place IN :places AND d.imageUrl IS NOT NULL")
boolean allExistsByDateAndTypeAndPlacesAndImageUrlIsNotNull(LocalDate date, DiningType type, List<String> places);
}
Loading

0 comments on commit b4a0e4c

Please sign in to comment.