From a78ee145edc8ce46d34799fc5a7f06c1c35c2c36 Mon Sep 17 00:00:00 2001 From: kkang_h00n Date: Sun, 6 Jul 2025 22:07:57 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20=ED=8A=B9=EC=A0=95=20=EC=95=A0?= =?UTF-8?q?=EC=99=84=20=EC=8B=9D=EB=AC=BC=20=ED=8A=B9=EC=A0=95=20=EC=9B=94?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PetPlantController.java | 16 ++++ .../DailyRecordQueryRepository.java | 14 +++ .../repository/WateringQueryRepository.java | 15 ++++ .../request/FindPetPlantDateRequestDto.java | 13 +++ .../response/FindPetPlantDateResponseDto.java | 15 ++++ .../DailyRecordJpaQueryRepository.java | 29 +++++++ .../WateringJpaQueryRepository.java | 29 +++++++ .../service/PetPlantQueryService.java | 87 +++++++++++++++++++ 8 files changed, 218 insertions(+) create mode 100644 src/main/java/swyp/team5/greening/petPlant/domain/repository/DailyRecordQueryRepository.java create mode 100644 src/main/java/swyp/team5/greening/petPlant/domain/repository/WateringQueryRepository.java create mode 100644 src/main/java/swyp/team5/greening/petPlant/dto/request/FindPetPlantDateRequestDto.java create mode 100644 src/main/java/swyp/team5/greening/petPlant/dto/response/FindPetPlantDateResponseDto.java create mode 100644 src/main/java/swyp/team5/greening/petPlant/infrastructure/DailyRecordJpaQueryRepository.java create mode 100644 src/main/java/swyp/team5/greening/petPlant/infrastructure/WateringJpaQueryRepository.java diff --git a/src/main/java/swyp/team5/greening/petPlant/controller/PetPlantController.java b/src/main/java/swyp/team5/greening/petPlant/controller/PetPlantController.java index 704e95f..8e5cb24 100644 --- a/src/main/java/swyp/team5/greening/petPlant/controller/PetPlantController.java +++ b/src/main/java/swyp/team5/greening/petPlant/controller/PetPlantController.java @@ -8,6 +8,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -17,8 +18,10 @@ import swyp.team5.greening.common.dto.response.ApiResponseDto; import swyp.team5.greening.common.resolver.LogIn; import swyp.team5.greening.petPlant.dto.request.CreatePetPlantRequestDto; +import swyp.team5.greening.petPlant.dto.request.FindPetPlantDateRequestDto; import swyp.team5.greening.petPlant.dto.response.CreatePetPlantResponseDto; import swyp.team5.greening.petPlant.dto.response.FindAllPetPlantResponseDto; +import swyp.team5.greening.petPlant.dto.response.FindPetPlantDateResponseDto; import swyp.team5.greening.petPlant.service.PetPlantCommandService; import swyp.team5.greening.petPlant.service.PetPlantQueryService; @@ -58,4 +61,17 @@ public void deletePetPlant( petPlantCommandService.deletePetPlant(userId, petPlantId); } + @Operation(summary = "특정 애완 식물 특정 월 정보 조회 API") + @GetMapping("/{petPlantId}") + @ResponseStatus(HttpStatus.OK) + public ApiResponseDto> getMyPetPlantCalender( + @LogIn Long userId, + @PathVariable Long petPlantId, + @ModelAttribute FindPetPlantDateRequestDto requestDto + ) { + return ApiResponseDto.of(petPlantQueryService.findMyPetPlantCalender( + userId, petPlantId, requestDto.year(), requestDto.month() + )); + } + } diff --git a/src/main/java/swyp/team5/greening/petPlant/domain/repository/DailyRecordQueryRepository.java b/src/main/java/swyp/team5/greening/petPlant/domain/repository/DailyRecordQueryRepository.java new file mode 100644 index 0000000..36c2f37 --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/domain/repository/DailyRecordQueryRepository.java @@ -0,0 +1,14 @@ +package swyp.team5.greening.petPlant.domain.repository; + +import java.time.LocalDate; +import java.util.List; +import swyp.team5.greening.petPlant.domain.entity.DailyRecord; + +public interface DailyRecordQueryRepository { + + List findByPetPlantAndWriteDate( + Long petPlantId, + LocalDate startDate, + LocalDate endDate + ); +} diff --git a/src/main/java/swyp/team5/greening/petPlant/domain/repository/WateringQueryRepository.java b/src/main/java/swyp/team5/greening/petPlant/domain/repository/WateringQueryRepository.java new file mode 100644 index 0000000..b61433d --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/domain/repository/WateringQueryRepository.java @@ -0,0 +1,15 @@ +package swyp.team5.greening.petPlant.domain.repository; + +import java.time.LocalDate; +import java.util.List; +import swyp.team5.greening.petPlant.domain.entity.Watering; + +public interface WateringQueryRepository { + + List findByPetPlantAndWriteDate( + Long petPlantId, + LocalDate startDate, + LocalDate endDate + ); + +} diff --git a/src/main/java/swyp/team5/greening/petPlant/dto/request/FindPetPlantDateRequestDto.java b/src/main/java/swyp/team5/greening/petPlant/dto/request/FindPetPlantDateRequestDto.java new file mode 100644 index 0000000..57acaa1 --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/dto/request/FindPetPlantDateRequestDto.java @@ -0,0 +1,13 @@ +package swyp.team5.greening.petPlant.dto.request; + +import jakarta.validation.constraints.NotNull; + +public record FindPetPlantDateRequestDto( + @NotNull + Integer year, + + @NotNull + Integer month +) { + +} diff --git a/src/main/java/swyp/team5/greening/petPlant/dto/response/FindPetPlantDateResponseDto.java b/src/main/java/swyp/team5/greening/petPlant/dto/response/FindPetPlantDateResponseDto.java new file mode 100644 index 0000000..0cc57d9 --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/dto/response/FindPetPlantDateResponseDto.java @@ -0,0 +1,15 @@ +package swyp.team5.greening.petPlant.dto.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDate; + +public record FindPetPlantDateResponseDto( + @JsonFormat(pattern = "yyyy-MM-dd") + LocalDate date, + + Boolean watering, + + Long dailyRecordId +) { + +} diff --git a/src/main/java/swyp/team5/greening/petPlant/infrastructure/DailyRecordJpaQueryRepository.java b/src/main/java/swyp/team5/greening/petPlant/infrastructure/DailyRecordJpaQueryRepository.java new file mode 100644 index 0000000..f4f2ac3 --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/infrastructure/DailyRecordJpaQueryRepository.java @@ -0,0 +1,29 @@ +package swyp.team5.greening.petPlant.infrastructure; + +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import swyp.team5.greening.petPlant.domain.entity.DailyRecord; +import swyp.team5.greening.petPlant.domain.repository.DailyRecordQueryRepository; + +public interface DailyRecordJpaQueryRepository extends JpaRepository, + DailyRecordQueryRepository { + + @Override + @Query(""" + SELECT dailyRecord + FROM DailyRecord dailyRecord + WHERE dailyRecord.petPlantId = :petPlantId + AND dailyRecord.writeDate >= :startDate + AND dailyRecord.writeDate < :endDate + ORDER BY dailyRecord.writeDate asc + """) + List findByPetPlantAndWriteDate( + @Param("petPlantId") Long petPlantId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + +} diff --git a/src/main/java/swyp/team5/greening/petPlant/infrastructure/WateringJpaQueryRepository.java b/src/main/java/swyp/team5/greening/petPlant/infrastructure/WateringJpaQueryRepository.java new file mode 100644 index 0000000..2ecddcf --- /dev/null +++ b/src/main/java/swyp/team5/greening/petPlant/infrastructure/WateringJpaQueryRepository.java @@ -0,0 +1,29 @@ +package swyp.team5.greening.petPlant.infrastructure; + +import java.time.LocalDate; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import swyp.team5.greening.petPlant.domain.entity.Watering; +import swyp.team5.greening.petPlant.domain.repository.WateringQueryRepository; + +public interface WateringJpaQueryRepository extends JpaRepository, + WateringQueryRepository { + + @Override + @Query(""" + SELECT watering + FROM Watering watering + WHERE watering.petPlantId = :petPlantId + AND watering.writeDate >= :startDate + AND watering.writeDate < :endDate + ORDER BY watering.writeDate asc + """) + List findByPetPlantAndWriteDate( + @Param("petPlantId") Long petPlantId, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); + +} diff --git a/src/main/java/swyp/team5/greening/petPlant/service/PetPlantQueryService.java b/src/main/java/swyp/team5/greening/petPlant/service/PetPlantQueryService.java index ac31355..eba2fb8 100644 --- a/src/main/java/swyp/team5/greening/petPlant/service/PetPlantQueryService.java +++ b/src/main/java/swyp/team5/greening/petPlant/service/PetPlantQueryService.java @@ -1,21 +1,108 @@ package swyp.team5.greening.petPlant.service; +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import swyp.team5.greening.common.exception.GreeningGlobalException; +import swyp.team5.greening.petPlant.domain.entity.DailyRecord; +import swyp.team5.greening.petPlant.domain.entity.PetPlant; +import swyp.team5.greening.petPlant.domain.entity.PetPlantState; +import swyp.team5.greening.petPlant.domain.entity.Watering; +import swyp.team5.greening.petPlant.domain.repository.DailyRecordQueryRepository; import swyp.team5.greening.petPlant.domain.repository.PetPlantQueryRepository; +import swyp.team5.greening.petPlant.domain.repository.PetPlantRepository; +import swyp.team5.greening.petPlant.domain.repository.WateringQueryRepository; import swyp.team5.greening.petPlant.dto.response.FindAllPetPlantResponseDto; +import swyp.team5.greening.petPlant.dto.response.FindPetPlantDateResponseDto; +import swyp.team5.greening.petPlant.exception.PetPlantExceptionMessage; @Service @RequiredArgsConstructor public class PetPlantQueryService { + private final PetPlantRepository petPlantRepository; private final PetPlantQueryRepository petPlantQueryRepository; + private final DailyRecordQueryRepository dailyRecordQueryRepository; + private final WateringQueryRepository wateringQueryRepository; + + //나의 애완 식물 목록 조회 @Transactional(readOnly = true) public List findMyPetPlants(Long userId) { return petPlantQueryRepository.findMyPetPlants(userId); } + //특정 애완 식물 특정 월 정보 조회 + //1. 애완 식물 조회 + //2. 사용자 유효성 검증 + //3. 해당 월에 대한 물 주기 스탬프 목록 조회 + //4. 해당 월에 대한 오늘의 일기 식별자 조회 + //5. 조합 + @Transactional(readOnly = true) + public List findMyPetPlantCalender( + Long userId, + Long petPlantId, + Integer year, + Integer month + ) { + //1 + PetPlant petPlant = petPlantRepository.findByIdAndState(petPlantId, + PetPlantState.IN_PROGRESS) + .orElseThrow(() -> new GreeningGlobalException( + PetPlantExceptionMessage.NOT_FOUND_PET_PLANT)); + + //2 + if (!Objects.equals(petPlant.getUserId(), userId)) { + throw new GreeningGlobalException( + PetPlantExceptionMessage.BAD_REQUEST_PET_PLANT_WRITER); + } + + YearMonth requestDate = YearMonth.of(year, month); + + LocalDate start = requestDate.atDay(1); + LocalDate end = requestDate.plusMonths(1).atDay(1); + + //3 + List dailyRecordsByWriteDate = dailyRecordQueryRepository + .findByPetPlantAndWriteDate(petPlantId, start, end); + + //4 + List wateringByWriteDate = wateringQueryRepository + .findByPetPlantAndWriteDate(petPlantId, start, end); + + //5 + //날짜 별 일기 정보 조회 + Map dailyRecordIdMap = dailyRecordsByWriteDate.stream() + .collect(Collectors.toMap(DailyRecord::getWriteDate, DailyRecord::getId)); + + //날짜 별 물 주기 스탬프 조회 + Set wateredDates = wateringByWriteDate.stream() + .map(Watering::getWriteDate) + .collect(Collectors.toSet()); + + //해당 월에 대한 정보 존재 날짜 집합 정리 + Set allDates = new HashSet<>(); + allDates.addAll(dailyRecordIdMap.keySet()); + allDates.addAll(wateredDates); + + //조합 + return allDates.stream() + .map(date -> { + Long dailyRecordId = dailyRecordIdMap.getOrDefault(date, -1L); + boolean isWatered = wateredDates.contains(date); + return new FindPetPlantDateResponseDto(date, isWatered, dailyRecordId); + }) + .sorted(Comparator.comparing(FindPetPlantDateResponseDto::date)) // 오름차순 정렬 + .toList(); + } + } From 04fa2ab5d35cc19ef1344f447c51bad03b52c8a4 Mon Sep 17 00:00:00 2001 From: kkang_h00n Date: Sun, 6 Jul 2025 22:08:36 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Test:=20=ED=8A=B9=EC=A0=95=20=EC=95=A0?= =?UTF-8?q?=EC=99=84=20=EC=8B=9D=EB=AC=BC=20=ED=8A=B9=EC=A0=95=20=EC=9B=94?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20API=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PetPlantControllerTest.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/test/java/swyp/team5/greening/petPlant/controller/PetPlantControllerTest.java diff --git a/src/test/java/swyp/team5/greening/petPlant/controller/PetPlantControllerTest.java b/src/test/java/swyp/team5/greening/petPlant/controller/PetPlantControllerTest.java new file mode 100644 index 0000000..d92a4ca --- /dev/null +++ b/src/test/java/swyp/team5/greening/petPlant/controller/PetPlantControllerTest.java @@ -0,0 +1,115 @@ +package swyp.team5.greening.petPlant.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.test.web.servlet.ResultActions; +import swyp.team5.greening.petPlant.domain.entity.DailyRecord; +import swyp.team5.greening.petPlant.domain.entity.PetPlant; +import swyp.team5.greening.petPlant.domain.entity.PetPlantState; +import swyp.team5.greening.petPlant.domain.entity.Watering; +import swyp.team5.greening.petPlant.domain.repository.DailyRecordRepository; +import swyp.team5.greening.petPlant.domain.repository.PetPlantRepository; +import swyp.team5.greening.petPlant.domain.repository.WateringRepository; +import swyp.team5.greening.support.ApiTestSupport; + +class PetPlantControllerTest extends ApiTestSupport { + + @Autowired + private PetPlantRepository petPlantRepository; + + @Autowired + private WateringRepository wateringRepository; + + @Autowired + private DailyRecordRepository dailyRecordRepository; + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @BeforeEach + void init() { + petPlantRepository.deleteAll(); + wateringRepository.deleteAll(); + dailyRecordRepository.deleteAll(); + } + + @Nested + @DisplayName("사용자는 애완 식물을 1개 등록해 놓은 상태이다. 7월 3일에 물을 주었고, 7월 5일에 오늘의 일기를 작성하였다.") + class TestCase1 { + + PetPlant petPlant; + Watering day3Watering; + DailyRecord day5DailyRecord; + + LocalDate day3 = LocalDate.of(2025, 7, 3); + LocalDate day5 = LocalDate.of(2025, 7, 5); + + @BeforeEach + void init() { + petPlant = PetPlant.builder() + .name("기요미") + .plantType("민들레") + .state(PetPlantState.IN_PROGRESS) + .userId(loginUser.getId()) + .build(); + petPlantRepository.save(petPlant); + + day3Watering = Watering.builder() + .petPlantId(petPlant.getId()) + .writeDate(day3) + .build(); + wateringRepository.save(day3Watering); + + day5DailyRecord = DailyRecord.builder() + .title("제목") + .writeDate(day5) + .petPlantId(petPlant.getId()) + .build(); + dailyRecordRepository.save(day5DailyRecord); + } + + @Test + @DisplayName("7월 3일과 7월 5일의 정보가 잘 조회된다.") + void getMyPetPlantCalender1() throws Exception { + //when + ResultActions perform = mockMvc.perform( + get("/api/pet-plants/{petPlantId}", petPlant.getId()) + .header(HttpHeaders.AUTHORIZATION, accessToken) + .param("year", "2025") + .param("month", "7")); + + //then + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.size()").value(2)) + .andExpect(jsonPath("$.data[0].date").value(day3.format(formatter))) + .andExpect(jsonPath("$.data[0].watering").value(true)) + .andExpect(jsonPath("$.data[0].dailyRecordId").value(-1)) + .andExpect(jsonPath("$.data[1].date").value(day5.format(formatter))) + .andExpect(jsonPath("$.data[1].watering").value(false)) + .andExpect(jsonPath("$.data[1].dailyRecordId").value(day5DailyRecord.getId())); + } + + @Test + @DisplayName("다른 월로 조회할 경우, 조회되지 않는다.") + void getMyPetPlantCalender2() throws Exception { + //when + ResultActions perform = mockMvc.perform( + get("/api/pet-plants/{petPlantId}", petPlant.getId()) + .header(HttpHeaders.AUTHORIZATION, accessToken) + .param("year", "2025") + .param("month", "6")); + + //then + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.size()").value(0)); + } + } +} \ No newline at end of file From 2b4a0a11030f53b3f5bd84b4ceed3e4878522640 Mon Sep 17 00:00:00 2001 From: kkang_h00n Date: Sun, 6 Jul 2025 22:11:30 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Fix:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EC=8A=A4=ED=8E=99=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/dto/response/FindAllCommentResponseDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/swyp/team5/greening/comment/dto/response/FindAllCommentResponseDto.java b/src/main/java/swyp/team5/greening/comment/dto/response/FindAllCommentResponseDto.java index 1beda09..5c79784 100644 --- a/src/main/java/swyp/team5/greening/comment/dto/response/FindAllCommentResponseDto.java +++ b/src/main/java/swyp/team5/greening/comment/dto/response/FindAllCommentResponseDto.java @@ -12,7 +12,7 @@ public record FindAllCommentResponseDto( String comment, - @JsonFormat(pattern = "YYYY-MM-DD'T'hh:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd'T'hh:mm:ss") LocalDateTime createdAt, boolean isWriter