Skip to content

Commit

Permalink
🔧 목표 금액 유즈 케이스 리팩토링 (#112)
Browse files Browse the repository at this point in the history
* fix: 배포 파이프라인 이미지 빌드 버전 추가

* refactor: create_target_amount() 리팩토링

* rename: recent_target_amount_search_service -> target_amount_search_service

* refactor: get_target_amount_and_total_spending()

* refactor: get_target_amounts_and_total_spendings()

* refactor: update_target_amount 리팩토링

* refactor: target_amount_delete_service() 리팩토링

* style: 불필요한 transactional 어노테이션 제거

* refactor: 월별 총 지출 내역 조회 메서드 분리

* refactor: 지출 조회, 지출 목표 금액 조회 서비스 로직 분리

* refactor: 목표금액&월별 지출 내역 리스트 조회 메서드 분리

* fix: target_amount_mapper start_at 사용자 회원가입 일자 -> 가장 오래된 목표 금액 데이터 기반으로 수정

* docs: 목표금액 리스트 조회 스웨거 문서 요약 수정
  • Loading branch information
psychology50 authored Jun 25, 2024
1 parent 6458798 commit a69fc70
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public interface TargetAmountApi {
})
ResponseEntity<?> getTargetAmountAndTotalSpending(@PathVariable LocalDate date, @AuthenticationPrincipal SecurityUserDetails user);

@Operation(summary = "사용자 가입 이후 현재까지의 목표 금액 및 총 사용 금액 리스트 조회", method = "GET", description = "일수는 무시하고 년/월 정보만 사용한다. 데이터가 존재하지 않을 때 더미 값을 사용하며, 최신 데이터 순으로 정렬된 응답을 반환한다.")
@Operation(summary = "가장 오래된 목표 금액 이후부터 현재까지의 목표 금액 및 총 사용 금액 리스트 조회", method = "GET", description = "일수는 무시하고 년/월 정보만 사용한다. 데이터가 존재하지 않을 때 더미 값을 사용하며, 최신 데이터 순으로 정렬된 응답을 반환한다.")
@Parameters({
@Parameter(name = "date", description = "현재 날짜(yyyy-MM-dd)", required = true, in = ParameterIn.QUERY),
@Parameter(name = "param", hidden = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ public static TargetAmountDto.WithTotalSpendingRes toWithTotalSpendingResponse(T

/**
* TargetAmount와 TotalSpendingAmount를 이용하여 WithTotalSpendingRes 리스트를 생성한다. <br/>
* startAt부터 endAt까지의 날짜에 대한 WithTotalSpendingRes를 생성하며, 임의의 날짜에 대한 정보가 없을 경우 더미 데이터를 생성한다.
* startAt부터 endAt까지의 날짜에 대한 WithTotalSpendingRes를 생성하며, 임의의 날짜에 대한 정보가 없을 경우 더미 데이터를 생성한다. <br/>
* startAt은 목표 금액 데이터 중 가장 오래된 날짜를 기준으로 잡는다.
*
* @param startAt : 조회 시작 날짜. 이유가 없다면 사용자 생성 날짜를 사용한다.
* @param endAt : 조회 종료 날짜. 이유가 없다면 현재 날짜이며, 클라이언트로 부터 받은 날짜를 사용한다.
* @param endAt : 조회 종료 날짜. 이유가 없다면 현재 날짜이며, 클라이언트로 부터 받은 날짜를 사용한다.
*/
public static List<TargetAmountDto.WithTotalSpendingRes> toWithTotalSpendingResponses(List<TargetAmount> targetAmounts, List<TotalSpendingAmount> totalSpendings, LocalDate startAt, LocalDate endAt) {
public static List<TargetAmountDto.WithTotalSpendingRes> toWithTotalSpendingResponses(List<TargetAmount> targetAmounts, List<TotalSpendingAmount> totalSpendings, LocalDate endAt) {
LocalDate startAt = getOldestDate(targetAmounts);
int monthLength = (endAt.getYear() - startAt.getYear()) * 12 + (endAt.getMonthValue() - startAt.getMonthValue());

Map<YearMonth, TargetAmount> targetAmountsByDates = toYearMonthMap(targetAmounts, targetAmount -> YearMonth.of(targetAmount.getCreatedAt().getYear(), targetAmount.getCreatedAt().getMonthValue()), Function.identity());
Expand Down Expand Up @@ -86,6 +87,19 @@ private static TargetAmountDto.WithTotalSpendingRes createWithTotalSpendingRes(T
.build();
}

private static LocalDate getOldestDate(List<TargetAmount> targetAmounts) {
LocalDate minDate = LocalDate.now();

for (TargetAmount targetAmount : targetAmounts) {
LocalDate date = targetAmount.getCreatedAt().toLocalDate();
if (date.isBefore(minDate)) {
minDate = date;
}
}

return minDate;
}

/**
* List를 YearMonth를 key로 하는 Map으로 변환한다.
*
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.domain.domains.spending.dto.TotalSpendingAmount;
import kr.co.pennyway.domain.domains.spending.service.SpendingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

@Slf4j
@Service
@RequiredArgsConstructor
public class SpendingSearchService {
private final SpendingService spendingService;

@Transactional(readOnly = true)
public Optional<TotalSpendingAmount> readTotalSpendingAmountByUserIdThatMonth(Long userId, LocalDate date) {
return spendingService.readTotalSpendingAmountByUserId(userId, date);
}

@Transactional(readOnly = true)
public List<TotalSpendingAmount> readTotalSpendingsAmountByUserId(Long userId) {
return spendingService.readTotalSpendingsAmountByUserId(userId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.domain.domains.target.domain.TargetAmount;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorCode;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorException;
import kr.co.pennyway.domain.domains.target.service.TargetAmountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class TargetAmountDeleteService {
private final TargetAmountService targetAmountService;

@Transactional
public void execute(Long targetAmountId) {
TargetAmount targetAmount = targetAmountService.readTargetAmount(targetAmountId)
.filter(TargetAmount::isAllocatedAmount)
.orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));

if (!targetAmount.isThatMonth()) {
throw new TargetAmountErrorException(TargetAmountErrorCode.INVALID_TARGET_AMOUNT_DATE);
}

targetAmountService.deleteTargetAmount(targetAmount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,46 @@
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorException;
import kr.co.pennyway.domain.domains.target.service.TargetAmountService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;

@Slf4j
@Service
@RequiredArgsConstructor
public class TargetAmountSaveService {
private final UserService userService;
private final TargetAmountService targetAmountService;

@DistributedLock(key = "#key.concat(#user.getId()).concat('_').concat(#date.getYear()).concat('-').concat(#date.getMonthValue())")
public TargetAmount createTargetAmount(String key, User user, LocalDate date) {
@DistributedLock(key = "#key.concat(#userId).concat('_').concat(#date.getYear()).concat('-').concat(#date.getMonthValue())")
public TargetAmount createTargetAmount(String key, Long userId, LocalDate date) {
User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));

if (targetAmountService.isExistsTargetAmountThatMonth(user.getId(), date)) {
log.info("{}에 대한 날짜의 목표 금액이 이미 존재합니다.", date);
throw new TargetAmountErrorException(TargetAmountErrorCode.ALREADY_EXIST_TARGET_AMOUNT);
}

return targetAmountService.createTargetAmount(TargetAmount.of(-1, user));
}

@Transactional
public TargetAmount updateTargetAmount(Long targetAmountId, Integer amount) {
TargetAmount targetAmount = targetAmountService.readTargetAmount(targetAmountId)
.orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));

if (!targetAmount.isThatMonth()) {
throw new TargetAmountErrorException(TargetAmountErrorCode.INVALID_TARGET_AMOUNT_DATE);
}

targetAmount.updateAmount(amount);

return targetAmount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.co.pennyway.api.apis.ledger.service;

import kr.co.pennyway.domain.domains.target.domain.TargetAmount;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorCode;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorException;
import kr.co.pennyway.domain.domains.target.service.TargetAmountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;

@Service
@RequiredArgsConstructor
public class TargetAmountSearchService {
private final TargetAmountService targetAmountService;

@Transactional(readOnly = true)
public List<TargetAmount> readTargetAmountsByUserId(Long userId) {
return targetAmountService.readTargetAmountsByUserId(userId);
}

@Transactional(readOnly = true)
public TargetAmount readTargetAmountThatMonth(Long userId, LocalDate date) {
return targetAmountService.readTargetAmountThatMonth(userId, date).orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));
}

@Transactional(readOnly = true)
public Integer readRecentTargetAmount(Long userId) {
return targetAmountService.readRecentTargetAmount(userId)
.map(TargetAmount::getAmount)
.orElse(-1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,14 @@

import kr.co.pennyway.api.apis.ledger.dto.TargetAmountDto;
import kr.co.pennyway.api.apis.ledger.mapper.TargetAmountMapper;
import kr.co.pennyway.api.apis.ledger.service.RecentTargetAmountSearchService;
import kr.co.pennyway.api.apis.ledger.service.SpendingSearchService;
import kr.co.pennyway.api.apis.ledger.service.TargetAmountDeleteService;
import kr.co.pennyway.api.apis.ledger.service.TargetAmountSaveService;
import kr.co.pennyway.api.apis.ledger.service.TargetAmountSearchService;
import kr.co.pennyway.common.annotation.UseCase;
import kr.co.pennyway.domain.common.redisson.DistributedLockPrefix;
import kr.co.pennyway.domain.domains.spending.dto.TotalSpendingAmount;
import kr.co.pennyway.domain.domains.spending.service.SpendingService;
import kr.co.pennyway.domain.domains.target.domain.TargetAmount;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorCode;
import kr.co.pennyway.domain.domains.target.exception.TargetAmountErrorException;
import kr.co.pennyway.domain.domains.target.service.TargetAmountService;
import kr.co.pennyway.domain.domains.user.domain.User;
import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
import kr.co.pennyway.domain.domains.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -28,69 +22,46 @@
@UseCase
@RequiredArgsConstructor
public class TargetAmountUseCase {
private final UserService userService;
private final TargetAmountService targetAmountService;
private final SpendingService spendingService;

private final TargetAmountSaveService targetAmountSaveService;
private final RecentTargetAmountSearchService recentTargetAmountSearchService;
private final TargetAmountSearchService targetAmountSearchService;
private final TargetAmountDeleteService targetAmountDeleteService;

private final SpendingSearchService spendingSearchService;

@Transactional
public TargetAmountDto.TargetAmountInfo createTargetAmount(Long userId, int year, int month) {
User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));

TargetAmount targetAmount = targetAmountSaveService.createTargetAmount(DistributedLockPrefix.TARGET_AMOUNT_USER, user, LocalDate.of(year, month, 1));
TargetAmount targetAmount = targetAmountSaveService.createTargetAmount(DistributedLockPrefix.TARGET_AMOUNT_USER, userId, LocalDate.of(year, month, 1));

return TargetAmountDto.TargetAmountInfo.from(targetAmount);
}

@Transactional(readOnly = true)
public TargetAmountDto.WithTotalSpendingRes getTargetAmountAndTotalSpending(Long userId, LocalDate date) {
TargetAmount targetAmount = targetAmountService.readTargetAmountThatMonth(userId, date).orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));
Optional<TotalSpendingAmount> totalSpending = spendingService.readTotalSpendingAmountByUserId(userId, date);

TargetAmount targetAmount = targetAmountSearchService.readTargetAmountThatMonth(userId, date);
Optional<TotalSpendingAmount> totalSpending = spendingSearchService.readTotalSpendingAmountByUserIdThatMonth(userId, date);
return TargetAmountMapper.toWithTotalSpendingResponse(targetAmount, totalSpending.orElse(null), date);
}

@Transactional(readOnly = true)
public List<TargetAmountDto.WithTotalSpendingRes> getTargetAmountsAndTotalSpendings(Long userId, LocalDate date) {
User user = userService.readUser(userId).orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));

List<TargetAmount> targetAmounts = targetAmountService.readTargetAmountsByUserId(userId);
List<TotalSpendingAmount> totalSpendings = spendingService.readTotalSpendingsAmountByUserId(userId);
List<TargetAmount> targetAmounts = targetAmountSearchService.readTargetAmountsByUserId(userId);
List<TotalSpendingAmount> totalSpendings = spendingSearchService.readTotalSpendingsAmountByUserId(userId);

return TargetAmountMapper.toWithTotalSpendingResponses(targetAmounts, totalSpendings, user.getCreatedAt().toLocalDate(), date);
return TargetAmountMapper.toWithTotalSpendingResponses(targetAmounts, totalSpendings, date);
}

@Transactional(readOnly = true)
public TargetAmountDto.RecentTargetAmountRes getRecentTargetAmount(Long userId) {
return TargetAmountMapper.toRecentTargetAmountResponse(recentTargetAmountSearchService.readRecentTargetAmount(userId));
return TargetAmountMapper.toRecentTargetAmountResponse(targetAmountSearchService.readRecentTargetAmount(userId));
}

@Transactional
public TargetAmountDto.TargetAmountInfo updateTargetAmount(Long targetAmountId, Integer amount) {
TargetAmount targetAmount = targetAmountService.readTargetAmount(targetAmountId)
.orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));

if (!targetAmount.isThatMonth()) {
throw new TargetAmountErrorException(TargetAmountErrorCode.INVALID_TARGET_AMOUNT_DATE);
}

targetAmount.updateAmount(amount);
TargetAmount targetAmount = targetAmountSaveService.updateTargetAmount(targetAmountId, amount);

return TargetAmountDto.TargetAmountInfo.from(targetAmount);
}

@Transactional
public void deleteTargetAmount(Long targetAmountId) {
TargetAmount targetAmount = targetAmountService.readTargetAmount(targetAmountId)
.filter(TargetAmount::isAllocatedAmount)
.orElseThrow(() -> new TargetAmountErrorException(TargetAmountErrorCode.NOT_FOUND_TARGET_AMOUNT));

if (!targetAmount.isThatMonth()) {
throw new TargetAmountErrorException(TargetAmountErrorCode.INVALID_TARGET_AMOUNT_DATE);
}

targetAmountService.deleteTargetAmount(targetAmount);
targetAmountDeleteService.execute(targetAmountId);
}
}

0 comments on commit a69fc70

Please sign in to comment.