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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum ErrorCode {
// 영수증 관련 에러 코드
RECEIPT_INVALID_START_DATE("2001", "시작일은 종료일보다 빠르거나 같아야 합니다.", HttpStatus.BAD_REQUEST),
RECEIPT_NOT_FOUND("2002", "해당 영수증을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
RECEIPT_NOT_DELETE("2003", "요청한 영수증을 삭제할 수 없습니다.", HttpStatus.FORBIDDEN),

// S3 관련 에러 코드
S3_UPLOAD_FAIL("3001", "S3에 이미지 업로드 에러입니다.", HttpStatus.INTERNAL_SERVER_ERROR);
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/ClubAccount_BE/core/meta/LoginUser.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ClubAccount_BE.core.meta;

import io.swagger.v3.oas.annotations.Parameter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand All @@ -9,6 +10,7 @@
@Target({ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Parameter(hidden = true)
public @interface LoginUser {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class CreateReceiptController implements CreateReceiptApi {

private final CreateReceiptUseCase createReceiptUseCase;

@PostMapping("/create")
@PostMapping
public Long createReceipt(
@LoginUser User user,
@RequestPart(value = "image", required = false) MultipartFile image,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.ClubAccount_BE.receipt.adapter.in.web;

import com.ClubAccount_BE.core.meta.LoginUser;
import com.ClubAccount_BE.receipt.adapter.in.web.api.DeleteReceiptApi;
import com.ClubAccount_BE.receipt.application.port.in.DeleteReceiptUseCase;
import com.ClubAccount_BE.user.domain.User;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/receipts")
public class DeleteReceiptController implements DeleteReceiptApi {

private final DeleteReceiptUseCase deleteReceiptUseCase;

@DeleteMapping
public ResponseEntity<Void> deleteReceiptList(
@LoginUser User user,
@RequestParam @NotEmpty List<Long> receiptIds
) {
deleteReceiptUseCase.deleteReceiptList(user, receiptIds);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ClubAccount_BE.receipt.adapter.in.web.api;

import com.ClubAccount_BE.core.meta.LoginUser;
import com.ClubAccount_BE.user.domain.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestParam;

@Tag(name = "Delete Receipt", description = "영수증 삭제 API")
public interface DeleteReceiptApi {

@Operation(summary = "영수증 삭제", description = "등록된 영수증 정보를 삭제한다.")
ResponseEntity<Void> deleteReceiptList(
@LoginUser User user,
@RequestParam @NotEmpty List<Long> receiptIds
);
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
package com.ClubAccount_BE.receipt.adapter.out;


import static com.ClubAccount_BE.core.constant.CommonConstant.IMAGE_KEY_DELIMITER;
import static com.ClubAccount_BE.core.exception.ErrorCode.S3_UPLOAD_FAIL;

import com.ClubAccount_BE.core.exception.ApiException;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.DeleteReceiptImagePort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptImagePort;
import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
@RequiredArgsConstructor
public class ReceiptImageRepositoryAdapter implements UploadReceiptPort {
public class ReceiptImageRepositoryAdapter implements UploadReceiptImagePort, DeleteReceiptImagePort {

private final S3Client amazonS3;

Expand Down Expand Up @@ -48,6 +51,19 @@ public String uploadReceipt(MultipartFile image) {
return getImageUrl(imageName);
}

// TODO: S3에서 이미지 삭제 로직 비동기 처리
@Override
public void deleteImages(List<String> receiptImage) {
receiptImage.forEach(url -> {
String key = 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;
}
Expand All @@ -57,4 +73,8 @@ private String getImageUrl(String fileName) {
.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
@@ -1,11 +1,13 @@
package com.ClubAccount_BE.receipt.adapter.out;

import static com.ClubAccount_BE.core.exception.ErrorCode.RECEIPT_NOT_DELETE;
import static com.ClubAccount_BE.core.exception.ErrorCode.RECEIPT_NOT_FOUND;

import com.ClubAccount_BE.core.exception.ApiException;
import com.ClubAccount_BE.receipt.adapter.out.persistence.entity.ReceiptEntity;
import com.ClubAccount_BE.receipt.adapter.out.persistence.repository.ReceiptRepository;
import com.ClubAccount_BE.receipt.application.port.out.CreateReceiptPort;
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.Receipt;
Expand All @@ -23,7 +25,7 @@
@Component
@RequiredArgsConstructor
public class ReceiptRepositoryAdapter
implements CreateReceiptPort, FindReceiptPort, UpdateReceiptPort {
implements CreateReceiptPort, FindReceiptPort, UpdateReceiptPort, DeleteReceiptPort {

private final ReceiptRepository receiptRepository;

Expand Down Expand Up @@ -91,4 +93,19 @@ public Long updateReceipt(
.forEach(receiptEntity::addReceiptItem);
return receiptEntity.getId();
}

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

boolean hasInvalidOwner = receipts.stream()
.anyMatch(receipt -> !receipt.getUser().getId().equals(user.getId()));

if (hasInvalidOwner || receipts.size() != receiptIds.size()) {
throw new ApiException(RECEIPT_NOT_DELETE);
}

receiptRepository.deleteAll(receipts);
return receipts.stream().map(ReceiptMapper::toDomain).toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.ClubAccount_BE.receipt.application.port.in;

import com.ClubAccount_BE.user.domain.User;
import java.util.List;

public interface DeleteReceiptUseCase {

void deleteReceiptList(User user, List<Long> receiptIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.ClubAccount_BE.receipt.application.port.out;

import java.util.List;

public interface DeleteReceiptImagePort {

void deleteImages(List<String> receiptImage);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.ClubAccount_BE.receipt.application.port.out;

import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.user.domain.User;
import java.util.List;

public interface DeleteReceiptPort {

List<Receipt> deleteReceiptList(User user, List<Long> receiptIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import org.springframework.web.multipart.MultipartFile;

public interface UploadReceiptPort {
public interface UploadReceiptImagePort {

String uploadReceipt(MultipartFile image);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import com.ClubAccount_BE.receipt.adapter.in.web.dto.request.ReceiptRequest;
import com.ClubAccount_BE.receipt.application.port.in.CreateReceiptUseCase;
import com.ClubAccount_BE.receipt.application.port.out.CreateReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptImagePort;
import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.receipt.domain.ReceiptItem;
import com.ClubAccount_BE.receipt.domain.service.ReceiptEditor;
Expand All @@ -23,7 +23,7 @@
public class CreateReceiptService implements CreateReceiptUseCase {

private final CreateReceiptPort createReceiptPort;
private final UploadReceiptPort uploadReceiptPort;
private final UploadReceiptImagePort uploadReceiptImagePort;
private final ReceiptEditor receiptEditor;
private final ReceiptItemEditor receiptItemEditor;

Expand All @@ -40,7 +40,7 @@ public Long createReceipt(
receiptRequest.date(),
receiptRequest.amount(),
receiptRequest.etc(),
image == null ? DEFAULT_IMAGE : uploadReceiptPort.uploadReceipt(image)
image == null ? DEFAULT_IMAGE : uploadReceiptImagePort.uploadReceipt(image)
);
List<ReceiptItem> receiptItems = receiptItemEditor.toReceiptItems(receiptRequest, receipt);
boolean isAmountMatched = receiptEditor.checkAmountMatch(receipt, receiptItems);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.ClubAccount_BE.receipt.application.service;

import com.ClubAccount_BE.receipt.application.port.in.DeleteReceiptUseCase;
import com.ClubAccount_BE.receipt.application.port.out.DeleteReceiptImagePort;
import com.ClubAccount_BE.receipt.application.port.out.DeleteReceiptPort;
import com.ClubAccount_BE.receipt.domain.Receipt;
import com.ClubAccount_BE.receipt.domain.service.ReceiptEditor;
import com.ClubAccount_BE.user.domain.User;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class DeleteReceiptService implements DeleteReceiptUseCase {

private final DeleteReceiptPort deleteReceiptPort;
private final DeleteReceiptImagePort deleteReceiptImagePort;
private final ReceiptEditor receiptEditor;

@Override
public void deleteReceiptList(User user, List<Long> receiptIds) {
List<Receipt> receiptList = deleteReceiptPort.deleteReceiptList(user, receiptIds);
List<String> receiptImage = receiptEditor.deleteReceiptImage(receiptList);
deleteReceiptImagePort.deleteImages(receiptImage);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.ClubAccount_BE.receipt.domain.service;

import static com.ClubAccount_BE.core.constant.CommonConstant.DEFAULT_IMAGE;
import static com.ClubAccount_BE.receipt.domain.type.ReceiptCategory.GROUP_DINING;
import static com.ClubAccount_BE.receipt.domain.type.ReceiptCategory.OTHER;
import static com.ClubAccount_BE.receipt.domain.type.ReceiptCategory.SUBSCRIPTION;
Expand Down Expand Up @@ -72,4 +73,14 @@ public List<ReceiptMonthlyExpenseResult> calculateMonthlyExpense(
monthlyExpense.getOrDefault(month, BigDecimal.ZERO)))
.collect(Collectors.toList());
}

/**
* 영수증 이미지 삭제 시 필요한 URL 리스트 생성
*/
public List<String> deleteReceiptImage(List<Receipt> receiptList) {
return receiptList.stream()
.map(Receipt::getReceiptImageUrl)
.filter(url -> url != null && !url.equals(DEFAULT_IMAGE))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.ClubAccount_BE.receipt.application.service;

import com.ClubAccount_BE.receipt.application.port.out.CreateReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptPort;
import com.ClubAccount_BE.receipt.application.port.out.UploadReceiptImagePort;
import com.ClubAccount_BE.user.domain.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -16,7 +16,7 @@ class CreateReceiptServiceTest {
private CreateReceiptPort createReceiptPort;

@Mock
private UploadReceiptPort uploadReceiptPort;
private UploadReceiptImagePort uploadReceiptImagePort;

@InjectMocks
private CreateReceiptService createReceiptService;
Expand Down