Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
implementation "com.querydsl:querydsl-collections:5.0.0"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.ClubAccount_BE.core.s3;

import static com.ClubAccount_BE.core.constant.CommonConstant.IMAGE_KEY_DELIMITER;

import java.util.UUID;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

Expand All @@ -16,4 +19,9 @@ public class DefaultS3UrlBuilder implements S3UrlBuilder {
public String toUrl(String key) {
return "https://" + bucket + ".s3." + region + ".amazonaws.com/" + key;
}

@Override
public String createObjectName(String objectName) {
return UUID.randomUUID() + IMAGE_KEY_DELIMITER + objectName;
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/ClubAccount_BE/core/s3/S3UrlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@

public interface S3UrlBuilder {
String toUrl(String key);

String createObjectName(String objectName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ public class FindReceiptController implements FindReceiptApi {
private final FindReceiptUseCase findReceiptUseCase;

@GetMapping("/{link}/receipts")
public PagingResponse<ReceiptResponse> getReceiptList(
public PagingResponse<ReceiptResponse> getReceiptsByDate(
@PathVariable(value = "link") UUID link,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
@PageableDefault(page = 1, sort = "createdAt", direction = Sort.Direction.ASC) Pageable pageable
) {
return findReceiptUseCase.getReceiptList(link, startDate, endDate, pageable);
return findReceiptUseCase.getReceiptsByDate(link, startDate, endDate, pageable);
}

@GetMapping("/{link}/receipts/{receiptId}")
Expand All @@ -47,18 +47,18 @@ public List<ReceiptItemResponse> getReceiptItem(
return findReceiptUseCase.getReceiptItem(link, receiptId);
}

@GetMapping("/{link}/receipts/category-expense")
public ReceiptCategoryExpenseResponse getReceiptCategoryExpense(
@PathVariable(value = "link") UUID link
) {
return findReceiptUseCase.getReceiptCategoryExpense(link);
}

@GetMapping("/{link}/receipts/monthly-expense")
public List<ReceiptMonthlyExpenseResponse> getReceiptMonthlyExpenseList(
public List<ReceiptMonthlyExpenseResponse> getReceiptExpenseByMonth(
@PathVariable(value = "link") UUID link,
@Positive(message = "유효하지 않은 연도입니다.") @RequestParam int year
) {
return findReceiptUseCase.getReceiptMonthlyExpenseList(link, year);
return findReceiptUseCase.getReceiptExpenseByMonth(link, year);
}

@GetMapping("/{link}/receipts/category-expense")
public List<ReceiptCategoryExpenseResponse> getReceiptExpenseByCategory(
@PathVariable(value = "link") UUID link
) {
return findReceiptUseCase.getReceiptExpenseByCategory(link);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public interface FindReceiptApi {
@Operation(
summary = "영수증 목록 조회",
description = "시작일과 종료일을 기준으로 파싱된 영수증을 조회한다. 시작일과 종료일에 정보가 없을 경우 모든 영수증을 조회한다.")
PagingResponse<ReceiptResponse> getReceiptList(
PagingResponse<ReceiptResponse> getReceiptsByDate(
@PathVariable(value = "link") UUID link,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
Expand All @@ -37,14 +37,14 @@ List<ReceiptItemResponse> getReceiptItem(
@PathVariable("receiptId") Long receiptId
);

@Operation(summary = "영수증 카테고리별 지출 조회", description = "등록된 영수증의 카테고리별 지출을 조회한다.")
ReceiptCategoryExpenseResponse getReceiptCategoryExpense(
@PathVariable(value = "link") UUID link
);

@Operation(summary = "영수증 월별 지출 목록 조회", description = "등록된 영수증의 월별 지출을 조회한다.")
List<ReceiptMonthlyExpenseResponse> getReceiptMonthlyExpenseList(
List<ReceiptMonthlyExpenseResponse> getReceiptExpenseByMonth(
@PathVariable(value = "link") UUID link,
@Positive(message = "유효하지 않은 연도입니다.") @RequestParam int year
);

@Operation(summary = "영수증 카테고리별 지출 조회", description = "등록된 영수증의 카테고리별 지출을 조회한다.")
List<ReceiptCategoryExpenseResponse> getReceiptExpenseByCategory(
@PathVariable(value = "link") UUID link
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ClubAccount_BE.receipt.adapter.in.web.dto.request;

import com.ClubAccount_BE.receipt.domain.type.ReceiptCategory;
import com.ClubAccount_BE.receipt.domain.ReceiptCategory;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
package com.ClubAccount_BE.receipt.adapter.in.web.dto.response;

import com.ClubAccount_BE.receipt.domain.ReceiptCategoryExpenseResult;
import com.ClubAccount_BE.receipt.domain.CategoryExpenseResult;
import com.ClubAccount_BE.receipt.domain.ReceiptCategory;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import lombok.Builder;

@Builder
public record ReceiptCategoryExpenseResponse(
@Schema(description = "회식비 카테고리 총 지출")
BigDecimal groupDiningExpense,
@Schema(description = "영수증 카테고리 종류")
ReceiptCategory category,

@Schema(description = "물품 구매비 카테고리 총 지출")
BigDecimal supplyPurchaseExpense,

@Schema(description = "정기 구독비 카테고리 총 지출")
BigDecimal subscriptionExpense,

@Schema(description = "대관비 카테고리 총 지출")
BigDecimal venueRentalExpense,

@Schema(description = "기타 카테고리 총 지출")
BigDecimal otherExpense
@Schema(description = "해당 카테고리 총 지출 금액")
BigDecimal totalExpense
) {

public static ReceiptCategoryExpenseResponse of(ReceiptCategoryExpenseResult result) {
public static ReceiptCategoryExpenseResponse of(CategoryExpenseResult result) {
return ReceiptCategoryExpenseResponse.builder()
.groupDiningExpense(result.getGroupDiningExpense())
.supplyPurchaseExpense(result.getSupplyPurchaseExpense())
.subscriptionExpense(result.getSubscriptionExpense())
.venueRentalExpense(result.getVenueRentalExpense())
.otherExpense(result.getOtherExpense())
.category(result.category())
.totalExpense(result.totalExpense())
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.ClubAccount_BE.receipt.adapter.in.web.dto.response;

import com.ClubAccount_BE.receipt.domain.ReceiptMonthlyExpenseResult;
import com.ClubAccount_BE.receipt.domain.MonthlyExpenseResult;
import java.math.BigDecimal;
import java.util.UUID;
import lombok.Builder;
Expand All @@ -13,12 +13,12 @@ public record ReceiptMonthlyExpenseResponse(
BigDecimal totalExpense
) {

public static ReceiptMonthlyExpenseResponse of(ReceiptMonthlyExpenseResult result) {
public static ReceiptMonthlyExpenseResponse of(MonthlyExpenseResult result) {
return ReceiptMonthlyExpenseResponse.builder()
.id(UUID.nameUUIDFromBytes((result.getYear() + "-" + result.getMonth()).getBytes()))
.year(result.getYear())
.month(result.getMonth())
.totalExpense(result.getTotalExpense())
.id(UUID.nameUUIDFromBytes((result.year() + "-" + result.month()).getBytes()))
.year(result.year())
.month(result.month())
.totalExpense(result.totalExpense())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ClubAccount_BE.receipt.adapter.in.web.dto.response;

import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.receipt.domain.type.ReceiptCategory;
import com.ClubAccount_BE.receipt.domain.ReceiptCategory;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.Builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static com.ClubAccount_BE.core.exception.ErrorCode.S3_UPLOAD_FAIL;

import com.ClubAccount_BE.core.exception.ApiException;
import com.ClubAccount_BE.core.s3.S3KeyExtractor;
import com.ClubAccount_BE.core.s3.S3UrlBuilder;
import com.ClubAccount_BE.receipt.application.port.out.DeleteReceiptImagePort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptImagePort;
import java.io.IOException;
Expand All @@ -24,21 +26,22 @@
public class ReceiptImageAdapter implements UploadReceiptImagePort, DeleteReceiptImagePort {

private final S3Client amazonS3;
private final S3KeyExtractor keyExtractor;
private final S3UrlBuilder urlBuilder;

@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Override
public String uploadReceipt(MultipartFile image) {
String imageName = createImageName(image.getOriginalFilename());
String imageName = urlBuilder.createObjectName(image.getOriginalFilename());

try {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(imageName)
.contentType(image.getContentType())
.build();

amazonS3.putObject(
putObjectRequest,
RequestBody.fromInputStream(image.getInputStream(), image.getSize())
Expand All @@ -47,34 +50,18 @@ public String uploadReceipt(MultipartFile image) {
} catch (IOException e) {
throw new ApiException(S3_UPLOAD_FAIL);
}

return getImageUrl(imageName);
return urlBuilder.toUrl(imageName);
}

// TODO: S3에서 이미지 삭제 로직 비동기 처리
@Override
public void deleteImages(List<String> receiptImage) {
receiptImage.forEach(url -> {
String key = extractKey(url);
String key = keyExtractor.extractKey(url);
DeleteObjectRequest request = DeleteObjectRequest.builder()
.bucket(bucket)
.key(key)
.build();
amazonS3.deleteObject(request);
});
}

private String createImageName(String originalFilename) {
return UUID.randomUUID() + IMAGE_KEY_DELIMITER + originalFilename;
}

private String getImageUrl(String fileName) {
return amazonS3.utilities()
.getUrl(builder -> builder.bucket(bucket).key(fileName))
.toExternalForm();
}

private String extractKey(String url) {
return URI.create(url).getPath().substring(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
import com.ClubAccount_BE.receipt.application.port.out.DeleteReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.FindReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.UpdateReceiptPort;
import com.ClubAccount_BE.receipt.domain.CategoryExpenseResult;
import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.receipt.domain.ReceiptItem;
import com.ClubAccount_BE.receipt.domain.MonthlyExpenseResult;
import com.ClubAccount_BE.receipt.mapper.ReceiptItemMapper;
import com.ClubAccount_BE.receipt.mapper.ReceiptMapper;
import com.ClubAccount_BE.user.domain.User;
Expand All @@ -24,32 +26,16 @@

@Component
@RequiredArgsConstructor
public class ReceiptRepositoryAdapter
implements CreateReceiptPort, FindReceiptPort, UpdateReceiptPort, DeleteReceiptPort {
public class ReceiptRepositoryAdapter implements CreateReceiptPort, FindReceiptPort, UpdateReceiptPort, DeleteReceiptPort {

private final ReceiptRepository receiptRepository;

@Override
public Long createReceipt(Receipt receipt, List<ReceiptItem> receiptItems) {
ReceiptEntity receiptEntity = ReceiptMapper.toEntity(receipt);
receiptItems.stream()
.map(ReceiptItemMapper::toEntity)
.forEach(receiptEntity::addReceiptItem);
return receiptRepository
.save(receiptEntity)
.getId();
}

@Override
public Page<Receipt> getReceiptList(
User user,
LocalDate startDate,
LocalDate endDate,
Pageable pageable
) {
return receiptRepository
.findAllByDate(user.getId(), startDate, endDate, pageable)
.map(ReceiptMapper::toDomain);
public List<Receipt> getAllReceipts(User user) {
return receiptRepository.findAllByUserId(user.getId())
.stream()
.map(ReceiptMapper::toDomain)
.toList();
}

@Override
Expand All @@ -61,29 +47,26 @@ public Receipt getReceipt(User user, Long receiptId) {
}

@Override
public List<Receipt> getReceiptMonthlyExpenseList(User user, int year) {
public Page<Receipt> getReceiptsByDate(
User user,
LocalDate startDate,
LocalDate endDate,
Pageable pageable
) {
return receiptRepository
.findByUserIdAndYear(user.getId(), year)
.stream()
.map(ReceiptMapper::toDomain)
.toList();
}

@Override
public List<Receipt> getAllReceipts(User user) {
return receiptRepository.findAllByUserId(user.getId())
.stream()
.map(ReceiptMapper::toDomain)
.toList();
.findByDate(user.getId(), startDate, endDate, pageable)
.map(ReceiptMapper::toDomain);
}

@Override
public List<Receipt> getReceiptCategoryList(User user) {
public Long createReceipt(Receipt receipt, List<ReceiptItem> receiptItems) {
ReceiptEntity receiptEntity = ReceiptMapper.toEntity(receipt);
receiptItems.stream()
.map(ReceiptItemMapper::toEntity)
.forEach(receiptEntity::addReceiptItem);
return receiptRepository
.findAllByUserId(user.getId())
.stream()
.map(ReceiptMapper::toDomain)
.toList();
.save(receiptEntity)
.getId();
}

@Override
Expand All @@ -103,7 +86,7 @@ public Long updateReceipt(
}

@Override
public List<Receipt> deleteReceiptList(User user, List<Long> receiptIds) {
public List<Receipt> deleteReceipts(User user, List<Long> receiptIds) {
List<ReceiptEntity> receipts = receiptRepository.findAllById(receiptIds);

boolean hasInvalidOwner = receipts.stream()
Expand All @@ -116,4 +99,15 @@ public List<Receipt> deleteReceiptList(User user, List<Long> receiptIds) {
receiptRepository.deleteAll(receipts);
return receipts.stream().map(ReceiptMapper::toDomain).toList();
}

@Override
public List<MonthlyExpenseResult> getReceiptExpenseByMonth(User user, int year) {
return receiptRepository.calculateExpensesByMonth(user.getId(), year);
}


@Override
public List<CategoryExpenseResult> getReceiptExpenseByCategory(User user) {
return receiptRepository.calculateExpensesByCategory(user.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.ClubAccount_BE.core.entity.TimeBaseEntity;
import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.receipt.domain.type.ReceiptCategory;
import com.ClubAccount_BE.receipt.domain.ReceiptCategory;
import com.ClubAccount_BE.user.adapter.out.persistence.entity.UserEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
Expand Down
Loading