diff --git a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java index 16fc5cf9f..48ce881cd 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java +++ b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java @@ -9,11 +9,11 @@ import in.koreatech.koin.domain.coop.dto.DiningImageRequest; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.coop.dto.SoldOutRequest; import in.koreatech.koin.domain.coop.service.CoopService; import in.koreatech.koin.global.auth.Auth; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @RestController @@ -26,7 +26,7 @@ public class CoopController implements CoopApi { @PatchMapping("/dining/soldout") public ResponseEntity changeSoldOut( @Auth(permit = {COOP}) Long userId, - @RequestBody SoldOutRequest soldOutRequest + @Valid @RequestBody SoldOutRequest soldOutRequest ) { coopService.changeSoldOut(soldOutRequest); return ResponseEntity.ok().build(); diff --git a/src/main/java/in/koreatech/koin/domain/coop/dto/SoldOutRequest.java b/src/main/java/in/koreatech/koin/domain/coop/dto/SoldOutRequest.java index a82e3c449..3e78cebd6 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/dto/SoldOutRequest.java +++ b/src/main/java/in/koreatech/koin/domain/coop/dto/SoldOutRequest.java @@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class) public record SoldOutRequest( @Schema(description = "메뉴 고유 ID", example = "1") Long menuId, + @NotNull @Schema(description = "품절 여부", example = "true") Boolean soldOut ) { 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 ec7b1e809..7e5740fd4 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 @@ -1,5 +1,8 @@ package in.koreatech.koin.domain.coop.service; +import java.time.Clock; +import java.time.LocalDateTime; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,10 +19,18 @@ public class CoopService { private final DiningRepository diningRepository; + private final Clock clock; + @Transactional public void changeSoldOut(SoldOutRequest soldOutRequest) { Dining dining = diningRepository.getById(soldOutRequest.menuId()); - dining.setSoldOut(soldOutRequest.soldOut()); + + if (Boolean.TRUE.equals(soldOutRequest.soldOut())) { + dining.setSoldOut(LocalDateTime.now(clock)); + } + else { + dining.setSoldOut(null); + } } @Transactional diff --git a/src/main/java/in/koreatech/koin/domain/dining/dto/DiningResponse.java b/src/main/java/in/koreatech/koin/domain/dining/dto/DiningResponse.java index f4c35d535..11dbfcea1 100644 --- a/src/main/java/in/koreatech/koin/domain/dining/dto/DiningResponse.java +++ b/src/main/java/in/koreatech/koin/domain/dining/dto/DiningResponse.java @@ -52,11 +52,13 @@ public record DiningResponse( @Schema(description = "최신화 일자", example = "2024-03-15 14:02:48") LocalDateTime updatedAt, - @Schema(description = "품절 여부", example = "true") - Boolean soldOut, + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "품절 시각", example = "2024-04-04 23:01:52") + LocalDateTime soldOut, - @Schema(description = "메뉴 변경 여부", example = "true") - Boolean isChanged + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Schema(description = "메뉴 변경 시각", example = "2024-04-04 23:01:52") + LocalDateTime isChanged ) { public static DiningResponse from(Dining dining) { diff --git a/src/main/java/in/koreatech/koin/domain/dining/model/Dining.java b/src/main/java/in/koreatech/koin/domain/dining/model/Dining.java index 3edef47e2..046b8e69c 100644 --- a/src/main/java/in/koreatech/koin/domain/dining/model/Dining.java +++ b/src/main/java/in/koreatech/koin/domain/dining/model/Dining.java @@ -1,5 +1,7 @@ package in.koreatech.koin.domain.dining.model; +import java.time.LocalDateTime; + import in.koreatech.koin.global.domain.BaseEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -52,17 +54,15 @@ public class Dining extends BaseEntity { @Column(name = "image_url") private String imageUrl; - @NotNull - @Column(name = "sold_out", nullable = false) - private Boolean soldOut = false; + @Column(name = "sold_out") + private LocalDateTime soldOut; - @NotNull - @Column(name = "is_changed", nullable = false) - private Boolean isChanged = false; + @Column(name = "is_changed") + private LocalDateTime isChanged; @Builder private Dining(Long id, String date, String type, String place, Integer priceCard, Integer priceCash, - Integer kcal, String menu, String imageUrl, Boolean soldOut, Boolean isChanged) { + Integer kcal, String menu, String imageUrl, LocalDateTime soldOut, LocalDateTime isChanged) { this.id = id; this.date = date; this.type = type; @@ -80,7 +80,7 @@ public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; } - public void setSoldOut(Boolean soldout) { + public void setSoldOut(LocalDateTime soldout) { this.soldOut = soldout; } } diff --git a/src/main/resources/db/migration/V7__alter_diningmenus_change_field_type.sql b/src/main/resources/db/migration/V7__alter_diningmenus_change_field_type.sql new file mode 100644 index 000000000..c67ba00be --- /dev/null +++ b/src/main/resources/db/migration/V7__alter_diningmenus_change_field_type.sql @@ -0,0 +1,11 @@ +ALTER TABLE `dining_menus` + MODIFY COLUMN sold_out BOOLEAN NULL, + MODIFY COLUMN is_changed BOOLEAN NULL; + +UPDATE `dining_menus` +SET sold_out = NULL, + is_changed = NULL; + +ALTER TABLE `dining_menus` + MODIFY COLUMN sold_out DATETIME NULL, + MODIFY COLUMN is_changed DATETIME NULL; diff --git a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java index 4e025b215..6556f8a9b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.when; import java.time.Clock; +import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.List; @@ -54,8 +55,6 @@ void findDinings() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining request2 = Dining.builder() @@ -68,8 +67,6 @@ void findDinings() { .kcal(881) .menu(""" ["혼합잡곡밥", "가쓰오장국", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining request3 = Dining.builder() @@ -82,8 +79,6 @@ void findDinings() { .kcal(300) .menu(""" ["참치김치볶음밥", "유부된장국", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(true) .build(); Dining dining1 = diningRepository.save(request1); @@ -119,7 +114,7 @@ void findDinings() { .isEqualTo(dining1.getUpdatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); softly.assertThat(response.body().jsonPath().getList("[0].menu", String.class)) .containsExactlyInAnyOrderElementsOf(menus1); - softly.assertThat(response.body().jsonPath().getBoolean("[0].is_changed")) + softly.assertThat((LocalDateTime)response.body().jsonPath().get("[0].is_changed")) .isEqualTo(dining1.getIsChanged()); softly.assertThat(response.body().jsonPath().getLong("[1].id")).isEqualTo(dining3.getId()); @@ -137,7 +132,7 @@ void findDinings() { .isEqualTo(dining3.getUpdatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); softly.assertThat(response.body().jsonPath().getList("[1].menu", String.class)) .containsExactlyInAnyOrderElementsOf(menus2); - softly.assertThat(response.body().jsonPath().getBoolean("[1].is_changed")) + softly.assertThat((LocalDateTime)response.body().jsonPath().get("[1].is_changed")) .isEqualTo(dining3.getIsChanged()); } @@ -157,8 +152,6 @@ void invalidFormatDate() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining dining = diningRepository.save(request); @@ -188,8 +181,6 @@ void nullDate() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "고구마순들깨볶음", "총각김치"]""") - .soldOut(false) - .isChanged(false) .build(); Dining request2 = Dining.builder() @@ -202,8 +193,6 @@ void nullDate() { .kcal(881) .menu(""" ["혼합잡곡밥", "가쓰오장국", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining dining1 = diningRepository.save(request1); @@ -242,7 +231,7 @@ void nullDate() { } @Test - @DisplayName("영양사 권한으로 품절 요청을 보낸다") + @DisplayName("영양사 권한으로 품절 요청을 보내고 메뉴를 변경한다.") void requestSoldOut() { User user = User.builder() .password("1234") @@ -259,6 +248,13 @@ void requestSoldOut() { String token = jwtProvider.createToken(user); + when(clock.instant()).thenReturn(ZonedDateTime.parse( + "2024-04-04 18:00:00 KST", + ofPattern("yyyy-MM-dd " + "HH:mm:ss z") + ) + .toInstant()); + when(clock.getZone()).thenReturn(Clock.systemDefaultZone().getZone()); + Dining dining1 = Dining.builder() .date("2024-03-11") .type("LUNCH") @@ -268,8 +264,7 @@ void requestSoldOut() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) + .isChanged(LocalDateTime.now(clock)) .build(); Dining dining2 = Dining.builder() @@ -281,8 +276,6 @@ void requestSoldOut() { .kcal(881) .menu(""" ["병아리", "소고기", "땡초", "탕"]""") - .soldOut(false) - .isChanged(false) .build(); diningRepository.save(dining1); @@ -301,7 +294,12 @@ void requestSoldOut() { .extract(); SoftAssertions.assertSoftly( - softly -> softly.assertThat(diningRepository.getById(2L).getSoldOut()).isEqualTo(true) + softly -> { + softly.assertThat(diningRepository.getById(1L).getIsChanged()).isEqualTo(LocalDateTime.now(clock)); + + softly.assertThat(diningRepository.getById(2L).getSoldOut()).isEqualTo(LocalDateTime.now(clock)); + softly.assertThat(diningRepository.getById(2L).getIsChanged()).isNull(); + } ); } @@ -332,8 +330,6 @@ void requestSoldOutNoAuth() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); diningRepository.save(dining1); @@ -379,8 +375,6 @@ void ImageUpload() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining dining = diningRepository.save(request); @@ -433,8 +427,6 @@ void ImageUploadWithNoAuth() { .kcal(881) .menu(""" ["병아리콩밥", "(탕)소고기육개장", "땡초부추전", "누룽지탕"]""") - .soldOut(false) - .isChanged(false) .build(); Dining dining = diningRepository.save(request);