diff --git a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java index a21a27627..455b71023 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java +++ b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java @@ -48,7 +48,6 @@ ResponseEntity changeSoldOut( ); @ApiResponses( - value = { @ApiResponse(responseCode = "200"), @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), diff --git a/src/main/java/in/koreatech/koin/domain/coop/model/DiningNotifyCache.java b/src/main/java/in/koreatech/koin/domain/coop/model/DiningNotifyCache.java new file mode 100644 index 000000000..7f365f557 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coop/model/DiningNotifyCache.java @@ -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(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coop/repository/DiningNotifyCacheRepository.java b/src/main/java/in/koreatech/koin/domain/coop/repository/DiningNotifyCacheRepository.java new file mode 100644 index 000000000..e89db50e2 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coop/repository/DiningNotifyCacheRepository.java @@ -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 save(DiningNotifyCache diningNotifyCache); + + boolean existsById(String diningNotifyId); + + Optional findById(String diningPlace); + + default DiningNotifyCache getById(String diningPlace) { + return findById(diningPlace).orElseThrow( + () -> DiningCacheNotFoundException.withDetail("diningSoldOutCache: " + diningPlace)); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java index 9e72a3b09..a1182e3c1 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java +++ b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java @@ -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; @@ -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; @@ -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 placeFilters = Arrays.asList("A코너", "B코너", "C코너"); public static final LocalDate LIMIT_DATE = LocalDate.of(2022, 11, 29); private final int EXCEL_COLUMN_COUNT = 8; @@ -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 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 dinings) { + diningNotifyCacheRepository.save(DiningNotifyCache.from(diningNotifyId)); + eventPublisher.publishEvent(new DiningImageUploadEvent(dinings.get(0).getId(), dinings.get(0).getImageUrl())); } @Transactional @@ -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("시작일은 종료일 이전으로 설정해주세요."); } @@ -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 menu) { return String.join("\n", menu); diff --git a/src/main/java/in/koreatech/koin/domain/coop/util/CoopScheduler.java b/src/main/java/in/koreatech/koin/domain/coop/util/CoopScheduler.java new file mode 100644 index 000000000..c37965b35 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coop/util/CoopScheduler.java @@ -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("식단 이미지 알림 과정에서 오류가 발생했습니다."); + } + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/exception/DiningTypeNotFoundException.java b/src/main/java/in/koreatech/koin/domain/coopshop/exception/DiningTypeNotFoundException.java new file mode 100644 index 000000000..0a29a4ec1 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coopshop/exception/DiningTypeNotFoundException.java @@ -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); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java index 6e3414569..55858ccff 100644 --- a/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java +++ b/src/main/java/in/koreatech/koin/domain/coopshop/service/CoopShopService.java @@ -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; @@ -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; @@ -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); diff --git a/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java b/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java index c43547c07..e525a3d33 100644 --- a/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java +++ b/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java @@ -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; @@ -36,4 +37,15 @@ default Dining getById(Integer id) { List findByDateBetween(LocalDate startDate, LocalDate endDate); List findByDateBetweenAndPlaceIn(LocalDate startDate, LocalDate endDate, List placeFilters); + + Optional> findByDate(LocalDate now); + + default List 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 places); } diff --git a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java index acf081641..626250b09 100644 --- a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java @@ -1,5 +1,6 @@ package in.koreatech.koin.acceptance; +import static in.koreatech.koin.domain.dining.model.DiningType.LUNCH; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; @@ -19,8 +20,11 @@ import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.coop.model.DiningNotifyCache; import in.koreatech.koin.domain.coop.model.DiningSoldOutCache; +import in.koreatech.koin.domain.coop.repository.DiningNotifyCacheRepository; import in.koreatech.koin.domain.coop.repository.DiningSoldOutCacheRepository; +import in.koreatech.koin.domain.coop.service.CoopService; import in.koreatech.koin.domain.dining.model.Dining; import in.koreatech.koin.domain.dining.repository.DiningRepository; import in.koreatech.koin.domain.user.model.User; @@ -48,7 +52,14 @@ class DiningApiTest extends AcceptanceTest { @Autowired private CoopShopFixture coopShopFixture; + @Autowired + private CoopService coopService; + + @Autowired + private DiningNotifyCacheRepository diningNotifyCacheRepository; + private Dining A코너_점심; + private Dining B코너_점심; private User coop_준기; private String token_준기; private User owner_현수; @@ -63,6 +74,7 @@ void setUp() { owner_현수 = userFixture.현수_사장님().getUser(); token_현수 = userFixture.getToken(owner_현수); A코너_점심 = diningFixture.A코너_점심(LocalDate.parse("2024-01-15")); + B코너_점심 = diningFixture.B코너_점심(LocalDate.parse("2024-01-15")); } @Test @@ -88,6 +100,28 @@ void setUp() { "땡초부추전", "누룽지탕" ], + "image_url": "https://stage.koreatech.in/image.jpg", + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00", + "soldout_at": null, + "changed_at": null, + "likes": 0, + "is_liked" : false + }, + { + "id": 2, + "date": "2024-01-15", + "type": "LUNCH", + "place": "B코너", + "price_card": 6000, + "price_cash": 6000, + "kcal": 881, + "menu": [ + "병아리", + "소고기", + "땡초", + "탕" + ], "image_url": null, "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00", @@ -118,30 +152,52 @@ void setUp() { ) .andExpect(status().isOk()) .andExpect(content().json(""" - [ - { - "id": 1, - "date": "2024-01-15", - "type": "LUNCH", - "place": "A코너", - "price_card": 6000, - "price_cash": 6000, - "kcal": 881, - "menu": [ - "병아리콩밥", - "(탕)소고기육개장", - "땡초부추전", - "누룽지탕" - ], - "image_url": null, - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00", - "soldout_at": null, - "changed_at": null, - "likes": 0, - "is_liked" : false - } - ] + [ + { + "id": 1, + "date": "2024-01-15", + "type": "LUNCH", + "place": "A코너", + "price_card": 6000, + "price_cash": 6000, + "kcal": 881, + "menu": [ + "병아리콩밥", + "(탕)소고기육개장", + "땡초부추전", + "누룽지탕" + ], + "image_url": "https://stage.koreatech.in/image.jpg", + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00", + "soldout_at": null, + "changed_at": null, + "likes": 0, + "is_liked" : false + }, + { + "id": 2, + "date": "2024-01-15", + "type": "LUNCH", + "place": "B코너", + "price_card": 6000, + "price_cash": 6000, + "kcal": 881, + "menu": [ + "병아리", + "소고기", + "땡초", + "탕" + ], + "image_url": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00", + "soldout_at": null, + "changed_at": null, + "likes": 0, + "is_liked" : false + } + ] """)); } @@ -345,13 +401,35 @@ void setUp() { "땡초부추전", "누룽지탕" ], - "image_url": null, + "image_url": "https://stage.koreatech.in/image.jpg", "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00", "soldout_at": null, "changed_at": null, "likes": 1, "is_liked" : true + }, + { + "id": 2, + "date": "2024-01-15", + "type": "LUNCH", + "place": "B코너", + "price_card": 6000, + "price_cash": 6000, + "kcal": 881, + "menu": [ + "병아리", + "소고기", + "땡초", + "탕" + ], + "image_url": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00", + "soldout_at": null, + "changed_at": null, + "likes": 0, + "is_liked" : false } ] """)) @@ -383,6 +461,28 @@ void setUp() { "땡초부추전", "누룽지탕" ], + "image_url": "https://stage.koreatech.in/image.jpg", + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00", + "soldout_at": null, + "changed_at": null, + "likes": 0, + "is_liked" : false + }, + { + "id": 2, + "date": "2024-01-15", + "type": "LUNCH", + "place": "B코너", + "price_card": 6000, + "price_cash": 6000, + "kcal": 881, + "menu": [ + "병아리", + "소고기", + "땡초", + "탕" + ], "image_url": null, "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00", @@ -397,7 +497,8 @@ void setUp() { } @Test - void 이미지_업로드를_한다_품절_알림이_발송된다() throws Exception { + void 식단_이미지를_업로드_한다() throws Exception { + Dining A코너_저녁 = diningFixture.A코너_저녁(LocalDate.parse("2024-01-15")); String imageUrl = "https://stage.koreatech.in/image.jpg"; mockMvc.perform( patch("/coop/dining/image") @@ -407,38 +508,46 @@ void setUp() { "menu_id": "%s", "image_url": "%s" } - """, A코너_점심.getId(), imageUrl)) + """, A코너_저녁.getId(), imageUrl)) .param("diningId", String.valueOf(1)) .contentType(MediaType.APPLICATION_JSON) ) .andExpect(status().isOk()); - forceVerify(() -> verify(coopEventListener).onDiningImageUploadRequest(any())); clear(); setUp(); } @Test - void 해당_식사시간_외에_이미지_업로드를_한다_품절_알림이_발송되지_않는다() throws Exception { - Dining A코너_저녁 = diningFixture.A코너_저녁(LocalDate.parse("2024-01-15")); - String imageUrl = "https://stage.koreatech.in/image.jpg"; - mockMvc.perform( - patch("/coop/dining/image") - .header("Authorization", "Bearer " + token_준기) - .content(String.format(""" - { - "menu_id": "%s", - "image_url": "%s" - } - """, A코너_저녁.getId(), imageUrl)) - .param("diningId", String.valueOf(1)) - .contentType(MediaType.APPLICATION_JSON) - ) - .andExpect(status().isOk()); + void 이미지가_모두_존재하지_않으면_알림이_발송되지_않는다() throws Exception { + coopService.sendDiningNotify(); + forceVerify(() -> verify(coopEventListener, never()).onDiningImageUploadRequest(any())); clear(); setUp(); } + @Test + void 이미지가_모두_존재하고_오픈시간이고_Redis에_키가_있으면_알림이_발송되지_않는다() throws Exception { + String diningNotifyId = LocalDate.now(clock).toString() + LUNCH; + diningNotifyCacheRepository.save(DiningNotifyCache.from(diningNotifyId)); + coopService.sendDiningNotify(); + + forceVerify(() -> verify(coopEventListener, never()).onDiningImageUploadRequest(any())); + clear(); + setUp(); + } + + @Test + void 이미지가_모두_존재하고_오픈시간이고_Redis에_키가_없으면_알림이_발송된다() throws Exception { + B코너_점심.setImageUrl("https://stage.koreatech.in/image.jpg"); + diningRepository.save(B코너_점심); + coopService.sendDiningNotify(); + + forceVerify(() -> verify(coopEventListener).onDiningImageUploadRequest(any())); + clear(); + setUp(); + } + @Test void 특정_메뉴_특정_코너의_식단을_검색한다() throws Exception { mockMvc.perform( @@ -466,7 +575,7 @@ void setUp() { "땡초부추전", "누룽지탕" ], - "image_url": null, + "image_url": "https://stage.koreatech.in/image.jpg", "created_at": "2024-01-15 12:00:00", "soldout_at": "2024-01-15 12:00:00", "changed_at": "2024-01-15 12:00:00", @@ -529,7 +638,7 @@ void setUp() { "땡초부추전", "누룽지탕" ], - "image_url": null, + "image_url": "https://stage.koreatech.in/image.jpg", "created_at": "2024-01-15 12:00:00", "soldout_at": "2024-01-15 12:00:00", "changed_at": "2024-01-15 12:00:00", diff --git a/src/test/java/in/koreatech/koin/fixture/DiningFixture.java b/src/test/java/in/koreatech/koin/fixture/DiningFixture.java index 9889e8668..c89829a4e 100644 --- a/src/test/java/in/koreatech/koin/fixture/DiningFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/DiningFixture.java @@ -63,6 +63,7 @@ public DiningFixture(DiningRepository diningRepository) { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") + .imageUrl("https://stage.koreatech.in/image.jpg") .likes(0) .build() );