From d79db4dfe3837c80cf2257023dcf8f2cc3ed7ab6 Mon Sep 17 00:00:00 2001 From: baekjaehyuk Date: Wed, 14 May 2025 16:49:18 +0900 Subject: [PATCH] =?UTF-8?q?TB-28/feat:=20=EC=98=81=EC=88=98=EC=A6=9D=20?= =?UTF-8?q?=EC=9B=94=EB=B3=84=20=EC=A7=80=EC=B6=9C=EC=95=A1=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/FindReceiptController.java | 11 ++++++++ .../adapter/in/web/api/FindReceiptApi.java | 9 ++++++ .../dto/response/ReceiptExpenseResponse.java | 24 ++++++++++++++++ .../adapter/out/ReceiptRepositoryAdapter.java | 9 ++++++ .../repository/ReceiptCustomRepository.java | 3 ++ .../ReceiptCustomRepositoryImpl.java | 27 ++++++++++++------ .../port/in/FindReceiptUseCase.java | 4 +++ .../application/port/out/FindReceiptPort.java | 2 ++ .../service/FindReceiptService.java | 13 +++++++++ .../receipt/domain/DetailExpenseResult.java | 28 +++++++++++++++++++ .../receipt/domain/service/ReceiptEditor.java | 20 +++++++++++++ 11 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/dto/response/ReceiptExpenseResponse.java create mode 100644 src/main/java/com/ClubAccount_BE/receipt/domain/DetailExpenseResult.java diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/FindReceiptController.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/FindReceiptController.java index c3538b1..f0d7651 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/FindReceiptController.java +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/FindReceiptController.java @@ -4,9 +4,12 @@ import com.ClubAccount_BE.receipt.adapter.in.web.api.FindReceiptApi; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptCategoryResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptDetailResponse; +import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptExpenseResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptResponse; import com.ClubAccount_BE.receipt.application.port.in.FindReceiptUseCase; +import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import java.util.List; import java.util.UUID; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -50,4 +53,12 @@ public ReceiptCategoryResponse getReceiptCategoryRatio( ) { return findReceiptUseCase.getReceiptCategoryRatio(link); } + + @GetMapping("/{link}/receipts/expense") + public List getReceiptExpenseList( + @PathVariable(value = "link") UUID link, + @Positive(message = "유효하지 않은 연도입니다.") @RequestParam int year + ) { + return findReceiptUseCase.getReceiptExpenseList(link, year); + } } diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/api/FindReceiptApi.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/api/FindReceiptApi.java index 98a00ae..267e058 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/api/FindReceiptApi.java +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/api/FindReceiptApi.java @@ -3,10 +3,13 @@ import com.ClubAccount_BE.core.response.PagingResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptCategoryResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptDetailResponse; +import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptExpenseResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Positive; import java.time.LocalDate; +import java.util.List; import java.util.UUID; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -39,4 +42,10 @@ ReceiptDetailResponse getReceipt( ReceiptCategoryResponse getReceiptCategoryRatio( @PathVariable(value = "link") UUID link ); + + @Operation(summary = "영수증 월별 지출 목록 조회", description = "등록된 영수증의 월별 지출을 조회한다.") + List getReceiptExpenseList( + @PathVariable(value = "link") UUID link, + @Positive(message = "유효하지 않은 연도입니다.") @RequestParam int year + ); } diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/dto/response/ReceiptExpenseResponse.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/dto/response/ReceiptExpenseResponse.java new file mode 100644 index 0000000..dc0edc0 --- /dev/null +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/in/web/dto/response/ReceiptExpenseResponse.java @@ -0,0 +1,24 @@ +package com.ClubAccount_BE.receipt.adapter.in.web.dto.response; + +import com.ClubAccount_BE.receipt.domain.DetailExpenseResult; +import java.math.BigDecimal; +import java.util.UUID; +import lombok.Builder; + +@Builder +public record ReceiptExpenseResponse( + UUID id, + int year, + int month, + BigDecimal totalExpense +) { + + public static ReceiptExpenseResponse of(DetailExpenseResult result) { + return ReceiptExpenseResponse.builder() + .id(UUID.nameUUIDFromBytes((result.getYear() + "-" + result.getMonth()).getBytes())) + .year(result.getYear()) + .month(result.getMonth()) + .totalExpense(result.getTotalExpense()) + .build(); + } +} diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/ReceiptRepositoryAdapter.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/ReceiptRepositoryAdapter.java index 7bcc6dc..2af406a 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/ReceiptRepositoryAdapter.java +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/ReceiptRepositoryAdapter.java @@ -58,6 +58,15 @@ public Receipt getReceipt(User user, Long receiptId) { .orElseThrow(() -> new ApiException(RECEIPT_NOT_FOUND)); } + @Override + public List getReceiptExpenseList(User user, int year) { + return receiptRepository + .findByUserIdAndYear(user.getId(), year) + .stream() + .map(ReceiptMapper::toDomain) + .toList(); + } + @Override public List getReceiptCategoryList(User user) { return receiptRepository diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepository.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepository.java index b099acf..bdb5283 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepository.java +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepository.java @@ -2,6 +2,7 @@ import com.ClubAccount_BE.receipt.adapter.out.persistence.entity.ReceiptEntity; import java.time.LocalDate; +import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -13,4 +14,6 @@ Page findAllByDate( LocalDate endDate, Pageable pageable ); + + List findByUserIdAndYear(Long userId, int year); } diff --git a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepositoryImpl.java b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepositoryImpl.java index 0df6073..a5b1032 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepositoryImpl.java +++ b/src/main/java/com/ClubAccount_BE/receipt/adapter/out/persistence/repository/ReceiptCustomRepositoryImpl.java @@ -1,6 +1,7 @@ package com.ClubAccount_BE.receipt.adapter.out.persistence.repository; -import com.ClubAccount_BE.receipt.adapter.out.persistence.entity.QReceiptEntity; +import static com.ClubAccount_BE.receipt.adapter.out.persistence.entity.QReceiptEntity.receiptEntity; + import com.ClubAccount_BE.receipt.adapter.out.persistence.entity.ReceiptEntity; import com.querydsl.core.BooleanBuilder; import com.querydsl.jpa.impl.JPAQuery; @@ -24,28 +25,38 @@ public Page findAllByDate( LocalDate endDate, Pageable pageable ) { - QReceiptEntity receipt = QReceiptEntity.receiptEntity; BooleanBuilder where = new BooleanBuilder(); - where.and(receipt.user.id.eq(userId)); + where.and(receiptEntity.user.id.eq(userId)); if (startDate != null && endDate != null) { - where.and(receipt.date.between(startDate, endDate)); + where.and(receiptEntity.date.between(startDate, endDate)); } List content = queryFactory - .selectFrom(receipt) + .selectFrom(receiptEntity) .where(where) - .orderBy(receipt.date.desc()) + .orderBy(receiptEntity.date.desc()) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); JPAQuery count = queryFactory - .select(receipt.count()) - .from(receipt) + .select(receiptEntity.count()) + .from(receiptEntity) .where(where); return PageableExecutionUtils.getPage(content, pageable, count::fetchOne); } + + @Override + public List findByUserIdAndYear(Long userId, int year) { + return queryFactory + .selectFrom(receiptEntity) + .where( + receiptEntity.user.id.eq(userId), + receiptEntity.date.year().eq(year) + ) + .fetch(); + } } diff --git a/src/main/java/com/ClubAccount_BE/receipt/application/port/in/FindReceiptUseCase.java b/src/main/java/com/ClubAccount_BE/receipt/application/port/in/FindReceiptUseCase.java index f6e4213..9a6f4ea 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/application/port/in/FindReceiptUseCase.java +++ b/src/main/java/com/ClubAccount_BE/receipt/application/port/in/FindReceiptUseCase.java @@ -3,8 +3,10 @@ import com.ClubAccount_BE.core.response.PagingResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptCategoryResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptDetailResponse; +import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptExpenseResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptResponse; import java.time.LocalDate; +import java.util.List; import java.util.UUID; import org.springframework.data.domain.Pageable; @@ -20,4 +22,6 @@ PagingResponse getReceiptList( ); ReceiptDetailResponse getReceipt(UUID link, Long receiptId); + + List getReceiptExpenseList(UUID link, int year); } diff --git a/src/main/java/com/ClubAccount_BE/receipt/application/port/out/FindReceiptPort.java b/src/main/java/com/ClubAccount_BE/receipt/application/port/out/FindReceiptPort.java index 91b078c..98ecf24 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/application/port/out/FindReceiptPort.java +++ b/src/main/java/com/ClubAccount_BE/receipt/application/port/out/FindReceiptPort.java @@ -19,4 +19,6 @@ Page getReceiptList( List getReceiptCategoryList(User user); Receipt getReceipt(User user, Long receiptId); + + List getReceiptExpenseList(User user, int year); } diff --git a/src/main/java/com/ClubAccount_BE/receipt/application/service/FindReceiptService.java b/src/main/java/com/ClubAccount_BE/receipt/application/service/FindReceiptService.java index a0912e3..34d9e16 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/application/service/FindReceiptService.java +++ b/src/main/java/com/ClubAccount_BE/receipt/application/service/FindReceiptService.java @@ -6,10 +6,12 @@ import com.ClubAccount_BE.core.response.PagingResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptCategoryResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptDetailResponse; +import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptExpenseResponse; import com.ClubAccount_BE.receipt.adapter.in.web.dto.response.ReceiptResponse; import com.ClubAccount_BE.receipt.application.port.in.FindReceiptUseCase; import com.ClubAccount_BE.receipt.application.port.out.FindReceiptPort; import com.ClubAccount_BE.receipt.domain.DetailCategoryResult; +import com.ClubAccount_BE.receipt.domain.DetailExpenseResult; import com.ClubAccount_BE.receipt.domain.Receipt; import com.ClubAccount_BE.receipt.domain.service.ReceiptEditor; import com.ClubAccount_BE.user.application.port.out.FindUserPort; @@ -40,6 +42,17 @@ public ReceiptDetailResponse getReceipt(UUID link, Long receiptId) { return ReceiptDetailResponse.of(receipt); } + @Override + public List getReceiptExpenseList(UUID link, int year) { + + User user = findUserPort.getUserByLink(link); + List receiptList = findReceiptPort.getReceiptExpenseList(user, year); + List results = receiptEditor.calculateExpense(receiptList, year); + return results.stream() + .map(ReceiptExpenseResponse::of) + .toList(); + } + @Override public PagingResponse getReceiptList( diff --git a/src/main/java/com/ClubAccount_BE/receipt/domain/DetailExpenseResult.java b/src/main/java/com/ClubAccount_BE/receipt/domain/DetailExpenseResult.java new file mode 100644 index 0000000..33c99a0 --- /dev/null +++ b/src/main/java/com/ClubAccount_BE/receipt/domain/DetailExpenseResult.java @@ -0,0 +1,28 @@ +package com.ClubAccount_BE.receipt.domain; + +import java.math.BigDecimal; +import lombok.Builder; +import lombok.Getter; + +@Getter +public class DetailExpenseResult { + + private final int year; + private final int month; + private final BigDecimal totalExpense; + + @Builder + private DetailExpenseResult(int year, int month, BigDecimal totalExpense) { + this.year = year; + this.month = month; + this.totalExpense = totalExpense; + } + + public static DetailExpenseResult of(int year, int month, BigDecimal totalExpense) { + return DetailExpenseResult.builder() + .year(year) + .month(month) + .totalExpense(totalExpense) + .build(); + } +} diff --git a/src/main/java/com/ClubAccount_BE/receipt/domain/service/ReceiptEditor.java b/src/main/java/com/ClubAccount_BE/receipt/domain/service/ReceiptEditor.java index 73f3660..6d80bd1 100644 --- a/src/main/java/com/ClubAccount_BE/receipt/domain/service/ReceiptEditor.java +++ b/src/main/java/com/ClubAccount_BE/receipt/domain/service/ReceiptEditor.java @@ -1,6 +1,7 @@ package com.ClubAccount_BE.receipt.domain.service; import com.ClubAccount_BE.receipt.domain.DetailCategoryResult; +import com.ClubAccount_BE.receipt.domain.DetailExpenseResult; import com.ClubAccount_BE.receipt.domain.Receipt; import com.ClubAccount_BE.receipt.domain.ReceiptItem; import com.ClubAccount_BE.receipt.domain.type.ReceiptCategory; @@ -8,6 +9,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.springframework.stereotype.Service; @Service @@ -46,6 +48,24 @@ public DetailCategoryResult calculateCategoryRatio(List receipts) { ); } + /** + * 영수증 월별 지출 계산 + */ + public List calculateExpense(List receiptList, int year) { + Map monthlyExpense = receiptList.stream() + .collect(Collectors.groupingBy( + receipt -> receipt.getDate().getMonthValue(), + Collectors.reducing(BigDecimal.ZERO, Receipt::getAmount, BigDecimal::add) + )); + + return IntStream.rangeClosed(1, 12) + .mapToObj(month -> DetailExpenseResult.of( + year, + month, + monthlyExpense.getOrDefault(month, BigDecimal.ZERO))) + .collect(Collectors.toList()); + } + private float ratio(Long count, int total) { return count == null ? 0f : (count * 100f / total); }