diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java index a2c835cc..cff4f4d3 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/converter/ConsumptionGoalConverter.java @@ -1,9 +1,13 @@ package com.bbteam.budgetbuddies.domain.consumptiongoal.converter; +import java.time.LocalDate; +import java.util.List; + import org.springframework.stereotype.Component; import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseDto; +import com.bbteam.budgetbuddies.domain.consumptiongoal.dto.ConsumptionGoalResponseListDto; import com.bbteam.budgetbuddies.domain.consumptiongoal.entity.ConsumptionGoal; @Component @@ -25,4 +29,23 @@ public ConsumptionGoalResponseDto toConsumptionGoalResponseDto(ConsumptionGoal c .consumeAmount(consumptionGoal.getConsumeAmount()) .build(); } + + public ConsumptionGoalResponseListDto toConsumptionGoalResponseListDto( + List consumptionGoalList, LocalDate goalMonth) { + return ConsumptionGoalResponseListDto.builder() + .goalMonth(goalMonth) + .totalGoalAmount(sumTotalGoalAmount(consumptionGoalList)) + .totalConsumptionAmount(sumTotalConsumptionAmount(consumptionGoalList)) + .consumptionGoalList(consumptionGoalList) + .build(); + + } + + private Long sumTotalConsumptionAmount(List consumptionGoalList) { + return consumptionGoalList.stream().reduce(0L, (sum, c2) -> sum + c2.getConsumeAmount(), Long::sum); + } + + private Long sumTotalGoalAmount(List consumptionGoalList) { + return consumptionGoalList.stream().reduce(0L, (sum, c2) -> sum + c2.getGoalAmount(), Long::sum); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/dto/ConsumptionGoalResponseListDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/dto/ConsumptionGoalResponseListDto.java index e7f5a54e..f09ffc90 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/dto/ConsumptionGoalResponseListDto.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/dto/ConsumptionGoalResponseListDto.java @@ -4,17 +4,18 @@ import java.util.List; import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@Builder public class ConsumptionGoalResponseListDto { private LocalDate goalMonth; + private Long totalGoalAmount; + private Long totalConsumptionAmount; private List consumptionGoalList; - - public ConsumptionGoalResponseListDto(LocalDate goalMonth, List consumptionGoalList) { - this.goalMonth = goalMonth; - this.consumptionGoalList = consumptionGoalList; - } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java index 7ca9cc5e..4afa9f99 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceImpl.java @@ -97,45 +97,6 @@ public ConsumptionAnalysisResponseDTO getTopCategoryAndConsumptionAmount(Long us return ConsumptionAnalysisConverter.fromEntity(topConsumptionGoal, totalConsumptionAmountForCurrentWeek); } - @Override - @Transactional - public ConsumptionGoalResponseListDto updateConsumptionGoals(Long userId, - ConsumptionGoalListRequestDto consumptionGoalListRequestDto) { - LocalDate thisMonth = LocalDate.now().withDayOfMonth(1); - User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Not found user")); - - List updatedConsumptionGoal = consumptionGoalListRequestDto.getConsumptionGoalList() - .stream() - .map(c -> updateConsumptionGoalWithRequestDto(user, c, thisMonth)) - .toList(); - - List response = consumptionGoalRepository.saveAll(updatedConsumptionGoal) - .stream() - .map(consumptionGoalConverter::toConsumptionGoalResponseDto) - .toList(); - - return new ConsumptionGoalResponseListDto(thisMonth, response); - } - - @Override - @Transactional(readOnly = true) - public ConsumptionGoalResponseListDto findUserConsumptionGoal(Long userId, LocalDate date) { - LocalDate goalMonth = date.withDayOfMonth(1); - Map goalMap = initializeGoalMap(userId, goalMonth); - - updateGoalMapWithPreviousMonth(userId, goalMonth, goalMap); - updateGoalMapWithCurrentMonth(userId, goalMonth, goalMap); - - return new ConsumptionGoalResponseListDto(goalMonth, new ArrayList<>(goalMap.values())); - } - - private Map initializeGoalMap(Long userId, LocalDate goalMonth) { - return categoryRepository.findUserCategoryByUserId(userId) - .stream() - .collect(Collectors.toMap(Category::getId, - category -> consumptionGoalConverter.toConsumptionGoalResponseDto(category))); - } - private User findUserById(Long userId) { Optional user = userRepository.findById(userId); @@ -179,21 +140,24 @@ private void setAgeGroupByUser(int userAge) { } } - private void updateGoalMapWithPreviousMonth(Long userId, LocalDate goalMonth, - Map goalMap) { - updateGoalMap(userId, goalMonth.minusMonths(1), goalMap); - } + @Override + @Transactional + public ConsumptionGoalResponseListDto updateConsumptionGoals(Long userId, + ConsumptionGoalListRequestDto consumptionGoalListRequestDto) { + LocalDate thisMonth = LocalDate.now().withDayOfMonth(1); + User user = userRepository.findById(userId).orElseThrow(() -> new IllegalArgumentException("Not found user")); - private void updateGoalMapWithCurrentMonth(Long userId, LocalDate goalMonth, - Map goalMap) { - updateGoalMap(userId, goalMonth, goalMap); - } + List updatedConsumptionGoal = consumptionGoalListRequestDto.getConsumptionGoalList() + .stream() + .map(c -> updateConsumptionGoalWithRequestDto(user, c, thisMonth)) + .toList(); - private void updateGoalMap(Long userId, LocalDate month, Map goalMap) { - consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(userId, month) + List response = consumptionGoalRepository.saveAll(updatedConsumptionGoal) .stream() .map(consumptionGoalConverter::toConsumptionGoalResponseDto) - .forEach(goal -> goalMap.put(goal.getCategoryId(), goal)); + .toList(); + + return consumptionGoalConverter.toConsumptionGoalResponseListDto(response, thisMonth); } private ConsumptionGoal updateConsumptionGoalWithRequestDto(User user, @@ -216,4 +180,40 @@ private ConsumptionGoal findOrElseGenerateConsumptionGoal(User user, Category ca private ConsumptionGoal generateConsumptionGoal(User user, Category category, LocalDate goalMonth) { return ConsumptionGoal.builder().goalMonth(goalMonth).user(user).category(category).consumeAmount(0L).build(); } + + @Override + @Transactional(readOnly = true) + public ConsumptionGoalResponseListDto findUserConsumptionGoal(Long userId, LocalDate date) { + LocalDate goalMonth = date.withDayOfMonth(1); + Map goalMap = initializeGoalMap(userId, goalMonth); + + updateGoalMapWithPreviousMonth(userId, goalMonth, goalMap); + updateGoalMapWithCurrentMonth(userId, goalMonth, goalMap); + + return consumptionGoalConverter.toConsumptionGoalResponseListDto(new ArrayList<>(goalMap.values()), goalMonth); + } + + private Map initializeGoalMap(Long userId, LocalDate goalMonth) { + return categoryRepository.findUserCategoryByUserId(userId) + .stream() + .collect(Collectors.toMap(Category::getId, + category -> consumptionGoalConverter.toConsumptionGoalResponseDto(category))); + } + + private void updateGoalMapWithPreviousMonth(Long userId, LocalDate goalMonth, + Map goalMap) { + updateGoalMap(userId, goalMonth.minusMonths(1), goalMap); + } + + private void updateGoalMapWithCurrentMonth(Long userId, LocalDate goalMonth, + Map goalMap) { + updateGoalMap(userId, goalMonth, goalMap); + } + + private void updateGoalMap(Long userId, LocalDate month, Map goalMap) { + consumptionGoalRepository.findConsumptionGoalByUserIdAndGoalMonth(userId, month) + .stream() + .map(consumptionGoalConverter::toConsumptionGoalResponseDto) + .forEach(goal -> goalMap.put(goal.getCategoryId(), goal)); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java new file mode 100644 index 00000000..48fbdaac --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseApi.java @@ -0,0 +1,42 @@ +package com.bbteam.budgetbuddies.domain.expense.controller; + +import java.time.LocalDate; + +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; + +import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; +import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +public interface ExpenseApi { + @Operation(summary = "소비 내역 추가", description = "사용자가 소비 내역을 추가합니다.") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + }) + ResponseEntity createExpense( + @Parameter(description = "user_id, category_id, amount, description, expenseDate") + ExpenseRequestDto expenseRequestDto); + + @Operation(summary = "월별 소비 조회", description = "무한 스크롤을 통한 조회로 예상하여 Slice를 통해서 조회") + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), + @ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) + }) + ResponseEntity findExpensesForMonth( + Pageable pageable, + Long userId, + LocalDate date); +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java index 159c6fc9..a28bfabc 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/controller/ExpenseController.java @@ -1,37 +1,47 @@ package com.bbteam.budgetbuddies.domain.expense.controller; +import java.time.LocalDate; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.query.Param; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; import com.bbteam.budgetbuddies.domain.expense.service.ExpenseService; -import io.swagger.v3.oas.annotations.Operation; + import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @RequestMapping("/expenses") -public class ExpenseController { - - private final ExpenseService expenseService; - - @Operation(summary = "소비 내역 추가", description = "사용자가 소비 내역을 추가합니다.") - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공"), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH003", description = "access 토큰을 주세요!", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH004", description = "access 토큰 만료", content = @Content(schema = @Schema(implementation = ApiResponse.class))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "AUTH006", description = "access 토큰 모양이 이상함", content = @Content(schema = @Schema(implementation = ApiResponse.class))) - }) - @PostMapping("/add") - public ResponseEntity createExpense( - @Parameter(description = "user_id, category_id, amount, description, expenseDate") - @RequestBody ExpenseRequestDto expenseRequestDto) { - ExpenseResponseDto response = expenseService.createExpense(expenseRequestDto); - return ResponseEntity.ok(response); - } +public class ExpenseController implements ExpenseApi { + private final ExpenseService expenseService; + + @Override + @PostMapping("/add") + public ResponseEntity createExpense( + @Parameter(description = "user_id, category_id, amount, description, expenseDate") @RequestBody ExpenseRequestDto expenseRequestDto) { + ExpenseResponseDto response = expenseService.createExpense(expenseRequestDto); + return ResponseEntity.ok(response); + } + + @Override + @GetMapping("/{userId}") + public ResponseEntity findExpensesForMonth(Pageable pageable, + @PathVariable @Param("userId") Long userId, + @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) { + + return ResponseEntity.ok(expenseService.getMonthlyExpense(pageable, userId, date)); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java index ac88217c..57bb2a64 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/converter/ExpenseConverter.java @@ -1,34 +1,64 @@ package com.bbteam.budgetbuddies.domain.expense.converter; +import java.time.LocalDate; +import java.util.List; + +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Component; + import com.bbteam.budgetbuddies.domain.category.entity.Category; +import com.bbteam.budgetbuddies.domain.expense.dto.CompactExpenseResponseDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.user.entity.User; -import org.springframework.stereotype.Component; @Component public class ExpenseConverter { - public Expense toExpenseEntity(ExpenseRequestDto expenseRequestDto, User user, Category category) { - return Expense.builder() - .user(user) - .category(category) - .amount(expenseRequestDto.getAmount()) - .description(expenseRequestDto.getDescription()) - .expenseDate(expenseRequestDto.getExpenseDate()) - .build(); - } - - public ExpenseResponseDto toExpenseResponseDto(Expense expense) { - return ExpenseResponseDto.builder() - .expenseId(expense.getId()) - .userId(expense.getUser().getId()) - .categoryId(expense.getCategory().getId()) - .amount(expense.getAmount()) - .description(expense.getDescription()) - .expenseDate(expense.getExpenseDate()) - .build(); - } + public Expense toExpenseEntity(ExpenseRequestDto expenseRequestDto, User user, Category category) { + return Expense.builder() + .user(user) + .category(category) + .amount(expenseRequestDto.getAmount()) + .description(expenseRequestDto.getDescription()) + .expenseDate(expenseRequestDto.getExpenseDate()) + .build(); + } + + public ExpenseResponseDto toExpenseResponseDto(Expense expense) { + return ExpenseResponseDto.builder() + .expenseId(expense.getId()) + .userId(expense.getUser().getId()) + .categoryId(expense.getCategory().getId()) + .amount(expense.getAmount()) + .description(expense.getDescription()) + .expenseDate(expense.getExpenseDate()) + .build(); + } + + public MonthlyExpenseCompactResponseDto toMonthlyExpenseCompactResponseDto(Slice expenseSlice, + LocalDate startOfMonth) { + List compactResponseList = expenseSlice.getContent().stream() + .map(this::toExpenseCompactResponseDto).toList(); + + return MonthlyExpenseCompactResponseDto + .builder() + .expenseMonth(startOfMonth) + .currentPage(expenseSlice.getPageable().getPageNumber()) + .hasNext(expenseSlice.hasNext()) + .expenseList(compactResponseList) + .build(); + } + + private CompactExpenseResponseDto toExpenseCompactResponseDto(Expense expense) { + return CompactExpenseResponseDto.builder() + .expenseId(expense.getId()) + .description(expense.getDescription()) + .amount(expense.getAmount()) + .expenseDate(expense.getExpenseDate()) + .build(); + } } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/CompactExpenseResponseDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/CompactExpenseResponseDto.java new file mode 100644 index 00000000..e2f26ee7 --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/CompactExpenseResponseDto.java @@ -0,0 +1,20 @@ +package com.bbteam.budgetbuddies.domain.expense.dto; + +import java.time.LocalDateTime; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@Builder +public class CompactExpenseResponseDto { + private Long expenseId; + private String description; + private Long amount; + private LocalDateTime expenseDate; +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/MonthlyExpenseCompactResponseDto.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/MonthlyExpenseCompactResponseDto.java new file mode 100644 index 00000000..bd7ce82e --- /dev/null +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/dto/MonthlyExpenseCompactResponseDto.java @@ -0,0 +1,21 @@ +package com.bbteam.budgetbuddies.domain.expense.dto; + +import java.time.LocalDate; +import java.util.List; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@AllArgsConstructor +@Builder +public class MonthlyExpenseCompactResponseDto { + private LocalDate expenseMonth; + private int currentPage; + private boolean hasNext; + private List expenseList; +} diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java index c846f92e..ab9721e0 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepository.java @@ -1,18 +1,27 @@ package com.bbteam.budgetbuddies.domain.expense.repository; -import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; +import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import com.bbteam.budgetbuddies.domain.user.entity.User; public interface ExpenseRepository extends JpaRepository { - // 추후 적용 예정 - @Query("SELECT e FROM Expense e WHERE e.user.id = :userId AND e.category.id = :categoryId") - List findByUserIdAndCategoryId(@Param("userId") Long userId, @Param("categoryId") Long categoryId); + // 추후 적용 예정 + @Query("SELECT e FROM Expense e WHERE e.user.id = :userId AND e.category.id = :categoryId") + List findByUserIdAndCategoryId(@Param("userId") Long userId, @Param("categoryId") Long categoryId); + + @Query("SELECT e FROM Expense e WHERE e.user.id = :userId") + List findByUserId(@Param("userId") Long userId); - @Query("SELECT e FROM Expense e WHERE e.user.id = :userId") - List findByUserId(@Param("userId") Long userId); + @Query("SELECT e FROM Expense e WHERE e.user = :user AND e.expenseDate BETWEEN :startDate AND :endDate ORDER BY e.expenseDate DESC") + Slice findAllByUserIdForPeriod(Pageable pageable, @Param("user") User user, + @Param("startDate") LocalDateTime startDate, @Param("endDate") LocalDateTime endDate); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java index cbcddb72..aa4007ca 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseService.java @@ -1,11 +1,15 @@ package com.bbteam.budgetbuddies.domain.expense.service; +import java.time.LocalDate; + +import org.springframework.data.domain.Pageable; + import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; -import com.bbteam.budgetbuddies.domain.category.dto.CategoryResponseDTO; - -import java.util.List; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; public interface ExpenseService { - ExpenseResponseDto createExpense(ExpenseRequestDto expenseRequestDto); + ExpenseResponseDto createExpense(ExpenseRequestDto expenseRequestDto); + + MonthlyExpenseCompactResponseDto getMonthlyExpense(Pageable pageable, Long userId, LocalDate localDate); } diff --git a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java index b4675ebe..37c1e178 100644 --- a/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java +++ b/src/main/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImpl.java @@ -1,36 +1,59 @@ package com.bbteam.budgetbuddies.domain.expense.service; +import java.time.LocalDate; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.bbteam.budgetbuddies.domain.category.entity.Category; import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; import com.bbteam.budgetbuddies.domain.expense.converter.ExpenseConverter; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseRequestDto; import com.bbteam.budgetbuddies.domain.expense.dto.ExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; import com.bbteam.budgetbuddies.domain.expense.entity.Expense; import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; import com.bbteam.budgetbuddies.domain.user.entity.User; import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; + import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor public class ExpenseServiceImpl implements ExpenseService { - private final ExpenseRepository expenseRepository; - private final UserRepository userRepository; - private final CategoryRepository categoryRepository; - private final ExpenseConverter expenseConverter; + private final ExpenseRepository expenseRepository; + private final UserRepository userRepository; + private final CategoryRepository categoryRepository; + private final ExpenseConverter expenseConverter; + + @Override + public ExpenseResponseDto createExpense(ExpenseRequestDto expenseRequestDto) { + User user = userRepository.findById(expenseRequestDto.getUserId()) + .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); + Category category = categoryRepository.findById(expenseRequestDto.getCategoryId()) + .orElseThrow(() -> new IllegalArgumentException("Invalid category ID")); + + Expense expense = expenseConverter.toExpenseEntity(expenseRequestDto, user, category); + expenseRepository.save(expense); + + return expenseConverter.toExpenseResponseDto(expense); + } + + @Override + @Transactional(readOnly = true) + public MonthlyExpenseCompactResponseDto getMonthlyExpense(Pageable pageable, Long userId, LocalDate localDate) { + LocalDate startOfMonth = localDate.withDayOfMonth(1); + LocalDate endOfMonth = localDate.withDayOfMonth(startOfMonth.lengthOfMonth()); - @Override - public ExpenseResponseDto createExpense(ExpenseRequestDto expenseRequestDto) { - User user = userRepository.findById(expenseRequestDto.getUserId()) - .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); - Category category = categoryRepository.findById(expenseRequestDto.getCategoryId()) - .orElseThrow(() -> new IllegalArgumentException("Invalid category ID")); + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("Invalid user ID")); - Expense expense = expenseConverter.toExpenseEntity(expenseRequestDto, user, category); - expenseRepository.save(expense); + Slice expenseSlice = expenseRepository.findAllByUserIdForPeriod(pageable, user, + startOfMonth.atStartOfDay(), endOfMonth.atStartOfDay()); - return expenseConverter.toExpenseResponseDto(expense); - } + return expenseConverter.toMonthlyExpenseCompactResponseDto(expenseSlice, startOfMonth); + } } diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java index ca282360..23601804 100644 --- a/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java +++ b/src/test/java/com/bbteam/budgetbuddies/domain/consumptiongoal/service/ConsumptionGoalServiceTest.java @@ -62,9 +62,14 @@ void setUp() { int randomDay = random.nextInt(30) + 1; goalMonthRandomDay = LocalDate.of(GOAL_MONTH.getYear(), GOAL_MONTH.getMonth(), randomDay); - user = Mockito.spy(User.builder().email("email").age(24).name("name").gender(Gender.MALE).phoneNumber("010-1234-5678").build()); + user = Mockito.spy(User.builder() + .email("email") + .age(24) + .name("name") + .gender(Gender.MALE) + .phoneNumber("010-1234-5678") + .build()); given(user.getId()).willReturn(-1L); - given(userRepository.findById(user.getId())).willReturn(Optional.ofNullable(user)); } @Test @@ -185,6 +190,8 @@ void updateConsumptionGoal_Success() { Long defaultGoalAmount = 100L; Long userGoalAmount = 200L; + given(userRepository.findById(user.getId())).willReturn(Optional.ofNullable(user)); + ConsumptionGoalListRequestDto request = new ConsumptionGoalListRequestDto( List.of(new ConsumptionGoalRequestDto(-1L, defaultGoalAmount), new ConsumptionGoalRequestDto(-2L, userGoalAmount))); @@ -299,11 +306,11 @@ void getTopCategoryAndConsumptionAmount_Success() { LocalDate endOfWeek = goalMonthRandomDay.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(consumptionGoalRepository.findTopCategoriesAndGoalAmount(1, 23, 25, Gender.MALE)) - .willReturn(List.of(topConsumptionGoal)); - given(consumptionGoalRepository.findTopConsumptionByCategoryIdAndCurrentWeek( - defaultCategory.getId(), startOfWeek, endOfWeek)) - .willReturn(Optional.of(currentWeekConsumptionGoal)); + given(consumptionGoalRepository.findTopCategoriesAndGoalAmount(1, 23, 25, Gender.MALE)).willReturn( + List.of(topConsumptionGoal)); + given( + consumptionGoalRepository.findTopConsumptionByCategoryIdAndCurrentWeek(defaultCategory.getId(), startOfWeek, + endOfWeek)).willReturn(Optional.of(currentWeekConsumptionGoal)); // when ConsumptionAnalysisResponseDTO result = consumptionGoalService.getTopCategoryAndConsumptionAmount(user.getId()); @@ -318,9 +325,12 @@ void getTopCategoryAndConsumptionAmount_Success() { void getTopGoalCategories_Success() { // given Category defaultCategory = Mockito.spy(Category.builder().name("디폴트 카테고리").user(null).isDefault(true).build()); - Category defaultCategory2 = Mockito.spy(Category.builder().name("디폴트 카테고리2").user(null).isDefault(true).build()); - Category defaultCategory3 = Mockito.spy(Category.builder().name("디폴트 카테고리3").user(null).isDefault(true).build()); - Category defaultCategory4 = Mockito.spy(Category.builder().name("디폴트 카테고리4").user(null).isDefault(true).build()); + Category defaultCategory2 = Mockito.spy( + Category.builder().name("디폴트 카테고리2").user(null).isDefault(true).build()); + Category defaultCategory3 = Mockito.spy( + Category.builder().name("디폴트 카테고리3").user(null).isDefault(true).build()); + Category defaultCategory4 = Mockito.spy( + Category.builder().name("디폴트 카테고리4").user(null).isDefault(true).build()); ConsumptionGoal topConsumptionGoal1 = ConsumptionGoal.builder() .goalAmount(5000L) @@ -355,11 +365,12 @@ void getTopGoalCategories_Success() { .build(); given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); - given(consumptionGoalRepository.findTopCategoriesAndGoalAmount(4, 23, 25, Gender.MALE)) - .willReturn(List.of(topConsumptionGoal1, topConsumptionGoal2, topConsumptionGoal3, topConsumptionGoal4)); + given(consumptionGoalRepository.findTopCategoriesAndGoalAmount(4, 23, 25, Gender.MALE)).willReturn( + List.of(topConsumptionGoal1, topConsumptionGoal2, topConsumptionGoal3, topConsumptionGoal4)); // when - List result = consumptionGoalService.getTopGoalCategories(4, user.getId(), 0, 0, "none"); + List result = consumptionGoalService.getTopGoalCategories(4, user.getId(), 0, 0, + "none"); // then assertThat(result).hasSize(4); diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java new file mode 100644 index 00000000..9c1b1156 --- /dev/null +++ b/src/test/java/com/bbteam/budgetbuddies/domain/expense/repository/ExpenseRepositoryTest.java @@ -0,0 +1,92 @@ +package com.bbteam.budgetbuddies.domain.expense.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; + +import com.bbteam.budgetbuddies.domain.category.entity.Category; +import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; +import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import com.bbteam.budgetbuddies.domain.user.entity.User; +import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; + +@DisplayName("Expense 레포지토리 테스트의 ") +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class ExpenseRepositoryTest { + @Autowired + private ExpenseRepository expenseRepository; + @Autowired + private UserRepository userRepository; + @Autowired + private CategoryRepository categoryRepository; + + @Test + @DisplayName("findAllByUserIdFOrPeriod 성공") + void findAllByUserIdForPeriod_Success() { + // given + User user = userRepository.save( + User.builder().email("email").age(24).name("name").phoneNumber("010-1234-5678").build()); + + Category userCategory = categoryRepository.save( + Category.builder().name("유저 카테고리").user(user).isDefault(false).build()); + + LocalDate startDate = LocalDate.of(2024, 07, 01); + LocalDate endDate = startDate.withDayOfMonth(startDate.lengthOfMonth()); + + List expected = setExpense(user, userCategory, startDate); + + Pageable pageable = PageRequest.of(0, 5); + + Slice result = expenseRepository.findAllByUserIdForPeriod(pageable, user, + startDate.atStartOfDay(), endDate.atStartOfDay()); + + assertThat(result.getContent()).usingRecursiveComparison().isEqualTo(expected); + } + + private List setExpense(User user, Category userCategory, LocalDate startDate) { + setUnexpectedExpense(user, userCategory, startDate); + + return setExpectedExpenseOrderByDateDesc(user, userCategory, startDate); + } + + private List setExpectedExpenseOrderByDateDesc(User user, Category userCategory, LocalDate startDate) { + List expenses = new ArrayList<>(); + + for (int i = startDate.lengthOfMonth(); i > startDate.lengthOfMonth() - 5; i--) { + Expense expense = Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L * i) + .expenseDate(startDate.withDayOfMonth(i).atStartOfDay()) + .build(); + + expenses.add(expenseRepository.save(expense)); + } + return expenses; + } + + private void setUnexpectedExpense(User user, Category userCategory, LocalDate startDate) { + for (int i = 1; i <= 5; i++) { + Expense expense = Expense.builder() + .user(user) + .category(userCategory) + .amount(100000L * i) + .expenseDate(startDate.withMonth(8).atStartOfDay()) + .build(); + + expenseRepository.save(expense); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java b/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java new file mode 100644 index 00000000..82083dfa --- /dev/null +++ b/src/test/java/com/bbteam/budgetbuddies/domain/expense/service/ExpenseServiceImplTest.java @@ -0,0 +1,118 @@ +package com.bbteam.budgetbuddies.domain.expense.service; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; + +import com.bbteam.budgetbuddies.domain.category.entity.Category; +import com.bbteam.budgetbuddies.domain.category.repository.CategoryRepository; +import com.bbteam.budgetbuddies.domain.expense.converter.ExpenseConverter; +import com.bbteam.budgetbuddies.domain.expense.dto.CompactExpenseResponseDto; +import com.bbteam.budgetbuddies.domain.expense.dto.MonthlyExpenseCompactResponseDto; +import com.bbteam.budgetbuddies.domain.expense.entity.Expense; +import com.bbteam.budgetbuddies.domain.expense.repository.ExpenseRepository; +import com.bbteam.budgetbuddies.domain.user.entity.User; +import com.bbteam.budgetbuddies.domain.user.repository.UserRepository; + +@ExtendWith(MockitoExtension.class) +@DisplayName("ExpenseService 테스트의 ") +class ExpenseServiceImplTest { + @InjectMocks + private ExpenseServiceImpl expenseService; + @Mock + private ExpenseRepository expenseRepository; + @Mock + private UserRepository userRepository; + @Mock + private CategoryRepository categoryRepository; + @Spy + private ExpenseConverter expenseConverter; + + @Test + @DisplayName("getMonthlyExpense : 성공") + void getMonthlyExpense_Success() { + // given + final int pageSize = 5; + + User user = Mockito.spy(User.builder().build()); + given(user.getId()).willReturn(-1L); + given(userRepository.findById(user.getId())).willReturn(Optional.of(user)); + + Category userCategory = Mockito.spy(Category.builder().build()); + + LocalDate requestMonth = LocalDate.of(2024, 07, 8); + Pageable requestPage = PageRequest.of(0, pageSize); + + List expenses = generateExpenseList(requestMonth, user, userCategory, pageSize); + + Slice expenseSlice = new SliceImpl<>(expenses, requestPage, false); + given(expenseRepository.findAllByUserIdForPeriod(any(Pageable.class), any(User.class), any(LocalDateTime.class), + any(LocalDateTime.class))).willReturn(expenseSlice); + + MonthlyExpenseCompactResponseDto expected = generateExpectation(requestMonth, pageSize); + + // when + MonthlyExpenseCompactResponseDto result = expenseService.getMonthlyExpense(requestPage, user.getId(), + requestMonth); + + // then + assertThat(result).usingRecursiveComparison().isEqualTo(expected); + } + + private List generateExpenseList(LocalDate month, User user, Category userCategory, int repeat) { + List expenses = new ArrayList<>(); + for (int i = repeat; i > 0; i--) { + Expense expense = Mockito.spy(Expense.builder() + .amount(i * 100000L) + .description("User 소비" + i) + .expenseDate(month.withDayOfMonth(i).atStartOfDay()) + .user(user) + .category(userCategory) + .build()); + given(expense.getId()).willReturn((long)-i); + + expenses.add(expense); + } + return expenses; + } + + private MonthlyExpenseCompactResponseDto generateExpectation(LocalDate month, int count) { + return MonthlyExpenseCompactResponseDto.builder() + .expenseMonth(month.withDayOfMonth(1)) + .hasNext(false) + .currentPage(0) + .expenseList(generateCompactExpenseResponseList(month, count)) + .build(); + } + + private List generateCompactExpenseResponseList(LocalDate month, int count) { + List compactExpenses = new ArrayList<>(); + for (int i = count; i > 0; i--) { + compactExpenses.add(CompactExpenseResponseDto.builder() + .description("User 소비" + i) + .expenseId((long)-i) + .expenseDate(month.withDayOfMonth(i).atStartOfDay()) + .amount(i * 100000L) + .build()); + } + return compactExpenses; + } +} \ No newline at end of file