[Java] 지출 CRUD 구현

TNFUDS·2025년 12월 1일

FinTrack 프로젝트

목록 보기
9/14

목표

사용자가 자신의 지출 내역을 등록하고 → 읽고 → 수정하고 → 삭제할 수 있도록 만드는 것(CRUD)

엔티티

package doHoaSen.FinTrack.expense.entity;

import doHoaSen.FinTrack.user.entity.User;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.springframework.cglib.core.Local;

import java.time.LocalDateTime;

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Expense {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private User user;

    private Long amount;
    private String category;
    private String memo;
    private LocalDateTime dateTime;

    @CreationTimestamp
    private LocalDateTime createdAt;

    @UpdateTimestamp
    private LocalDateTime updatedAt;

}

DTO

ExpenseCreateRequest, ExpenseUpdateRequestExpenseResponse를 작성했다.
지출금액, 카테고리, 메모와 일시를 필요로 한다.

ExpenseCreateRequest

public record ExpenseCreateRequest(
    Integer amount,
    String category,
    String memo,
    LocalDateTime dateTime
) {}

ExpenseUpdateRequest

public record ExpenseUpdateRequest(
    Integer amount,
    String category,
    String memo,
    LocalDateTime dateTime
) {}

ExpenseResponse

public record ExpenseResponse(
    Long id,
    Integer amount,
    String category,
    String memo,
    LocalDateTime dateTime
) {}

Service

@Service
@RequiredArgsConstructor
public class ExpenseService {

    private final ExpenseRepository expenseRepository;
    private final UserRepository userRepository;

    public Long createExpense(Long userId, ExpenseCreateRequest req) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new NotFoundException("User not found"));

        Expense expense = Expense.builder()
                .user(user)
                .amount(req.amount())
                .category(req.category())
                .memo(req.memo())
                .dateTime(req.dateTime())
                .build();

        expenseRepository.save(expense);
        return expense.getId();
    }

    public List<ExpenseResponse> getMonthlyExpenses(Long userId, int year, int month) {
        LocalDateTime start = LocalDateTime.of(year, month, 1, 0, 0);
        LocalDateTime end = start.plusMonths(1);

        return expenseRepository.findByUserIdAndDateTimeBetween(userId, start, end)
                .stream()
                .map(ExpenseMapper::toResponse)
                .toList();
    }

    public void updateExpense(Long userId, Long expenseId, ExpenseUpdateRequest req) {
        Expense exp = getUserExpense(userId, expenseId);

        if (req.amount() != null) exp.setAmount(req.amount());
        if (req.category() != null) exp.setCategory(req.category());
        if (req.memo() != null) exp.setMemo(req.memo());
        if (req.dateTime() != null) exp.setDateTime(req.dateTime());
    }

    public void deleteExpense(Long userId, Long expenseId) {
        Expense exp = getUserExpense(userId, expenseId);
        expenseRepository.delete(exp);
    }

    private Expense getUserExpense(Long userId, Long expenseId) {
        Expense expense = expenseRepository.findById(expenseId)
                .orElseThrow(() -> new NotFoundException("Expense not found"));

        if (!expense.getUser().getId().equals(userId))
            throw new ForbiddenException("본인의 지출만 수정 가능합니다.");

        return expense;
    }
}

getMonthlyExpenses()

특정 유저의 특정 월에 해당하는 지출 목록을 가져오는 기능을 한다.
추후 월별 소비 통계 / 카테고리별 통계 / 시간대별 분석에 필요한 기능으로 구현해두었다.
즉, 지출 데이터를 분석하기 위한 가장 기본이 되는 조회 메서드이다.

이를 위해 ExpenseRepository

List<Expense> findByUserIdAndDateTimeBetween(
            Long userId,
            LocalDateTime start,
            LocalDateTime end
    );

findByUserIdAndDateTimeBetween를 작성했다.

그리고 Expense 엔티티 → ExpenseResponse DTO로 변환하기 위해

public class ExpenseMapper {
    public static ExpenseResponse toResponse(Expense expense){
        return new ExpenseResponse(
                expense.getId(),
                expense.getAmount(),
                expense.getCategory(),
                expense.getMemo(),
                expense.getDateTime()
        );
    }

ExpenseMapper를 위와 같이 작성했다.


Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/expenses")
public class ExpenseController {

    private final ExpenseService expenseService;

    /** 지출 등록 */
    @PostMapping
    public ResponseEntity<ApiResponse<Long>> createExpense(
            @AuthenticationPrincipal User user, 
            @RequestBody ExpenseCreateRequest req) {

        Long id = expenseService.createExpense(user.getId(), req);

        return ResponseEntity.ok(
                ApiResponse.success("지출이 등록되었습니다.", id)
        );
    }

    /*월별 지출 조회*/ 
    @GetMapping 
    public ResponseEntity<ApiResponse<List<ExpenseResponse>>> getMonthlyExpenses( 
    		@AuthenticationPrincipal User user, 
    		@RequestParam int year, 
    		@RequestParam int month ) {
            
        List<ExpenseResponse> expenses = expenseService.getMonthlyExpensess(user.getId(), year, month); 
        
    	return ResponseEntity.ok( 
        		ApiResponse.success("해당 월 목록 조회 성공", expenses)
        ); 
     } 
    
    // 월별 지출 목록 
    public List<ExpenseResponse> getMonthlyExpenses(Long userId, int year, int month){ 
    		LocalDateTime start = LocalDateTime.of(year, month, 1, 0, 0); 
    		LocalDateTime end = start.plusMonths(1); 
    
    	return expenseRepository
    			.findByUserIdAndDateTimeBetween(userId, start, end) 
    			.stream() 
    			.map(ExpenseMapper::toResponse) 
    			.toList(); 
    }
    
    
    /** 지출 수정 */
    @PatchMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> updateExpense(
            @AuthenticationPrincipal User user, 
            @PathVariable Long id,
            @RequestBody ExpenseUpdateRequest req) {

        expenseService.updateExpense(user.getId(), id, req);

        return ResponseEntity.ok(
                ApiResponse.success("지출 수정 완료", null)
        );
    }

    /** 지출 삭제 */
    @DeleteMapping("/{id}")
    public ResponseEntity<ApiResponse<Void>> deleteExpense(
            @AuthenticationPrincipal User user, 
            @PathVariable Long id) {

        expenseService.deleteExpense(user.getId(), id);

        return ResponseEntity.ok(
                ApiResponse.success("지출 삭제 완료", null)
        );
    }
}
profile
내 세상을 넓혀가는 중

0개의 댓글