diff --git a/src/main/java/com/werp/sero/approval/command/application/controller/ApprovalCommandController.java b/src/main/java/com/werp/sero/approval/command/application/controller/ApprovalCommandController.java index 01aab9bf..264e6b58 100644 --- a/src/main/java/com/werp/sero/approval/command/application/controller/ApprovalCommandController.java +++ b/src/main/java/com/werp/sero/approval/command/application/controller/ApprovalCommandController.java @@ -10,12 +10,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; @Tag(name = "결재 - Command", description = "결재 관련 API") @RequestMapping("/approvals") @@ -25,11 +21,10 @@ public class ApprovalCommandController { private final ApprovalCommandService approvalCommandService; @Operation(summary = "결재 상신") - @PostMapping(consumes = {MediaType.MULTIPART_FORM_DATA_VALUE}) + @PostMapping public ResponseEntity submitForApproval(@CurrentUser final Employee employee, - @Valid @RequestPart(name = "requestDTO") final ApprovalCreateRequestDTO requestDTO, - @RequestPart(name = "files", required = false) final List files) { - return ResponseEntity.ok(approvalCommandService.submitForApproval(employee, requestDTO, files)); + @Valid @RequestBody final ApprovalCreateRequestDTO requestDTO) { + return ResponseEntity.ok(approvalCommandService.submitForApproval(employee, requestDTO)); } @Operation(summary = "결재 승인") diff --git a/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalAttachmentRequestDTO.java b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalAttachmentRequestDTO.java new file mode 100644 index 00000000..63c8d8ad --- /dev/null +++ b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalAttachmentRequestDTO.java @@ -0,0 +1,15 @@ +package com.werp.sero.approval.command.application.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class ApprovalAttachmentRequestDTO { + @Schema(description = "파일명") + private String originalFileName; + + @Schema(description = "s3 url") + private String s3Url; +} \ No newline at end of file diff --git a/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalCreateRequestDTO.java b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalCreateRequestDTO.java index 4d6f2695..0cd2ce05 100644 --- a/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalCreateRequestDTO.java +++ b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalCreateRequestDTO.java @@ -33,4 +33,6 @@ public class ApprovalCreateRequestDTO { @NotNull(message = "1개 이상의 결재선이 필요합니다.") @Valid List approvalLines; + + List approvalAttachments; } \ No newline at end of file diff --git a/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalResponseDTO.java b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalResponseDTO.java index 3ab2a526..b462e337 100644 --- a/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalResponseDTO.java +++ b/src/main/java/com/werp/sero/approval/command/application/dto/ApprovalResponseDTO.java @@ -7,8 +7,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; - @Builder @Getter @AllArgsConstructor @@ -20,75 +18,10 @@ public class ApprovalResponseDTO { @Schema(description = "결재 코드") private String approvalCode; - @Schema(description = "제목") - private String title; - - @Schema(description = "내용") - private String content; - - @Schema(description = "문서 번호") - private String refCode; - - @Schema(description = "기안일시") - private String draftedAt; - - @Schema(description = "결재 상태") - private String status; - - @Schema(description = "결재 완료일시") - private String completedAt; - - @Schema(description = "기안자 ID(PK)") - private int drafterId; - - @Schema(description = "기안자 이름") - private String drafterName; - - @Schema(description = "기안자 부서") - private String drafterDepartment; - - @Schema(description = "기안자 직책") - private String drafterPosition; - - @Schema(description = "기안자 직급") - private String drafterRank; - - @Schema(description = "결재 첨부파일 목록") - private List approvalAttachments; - - @Schema(description = "결재선 목록 (결재/협조)") - private List approvalLines; - - @Schema(description = "참조자 목록") - private List referenceLines; - - @Schema(description = "수신자 목록") - private List recipientLines; - - public static ApprovalResponseDTO of(final Approval approval, - final List approvalAttachments, - final List approvalLines, - final List referenceLines, - final List recipientLines) { + public static ApprovalResponseDTO of(final Approval approval) { return ApprovalResponseDTO.builder() .approvalId(approval.getId()) .approvalCode(approval.getApprovalCode()) - .title(approval.getTitle()) - .content(approval.getContent()) - .refCode(approval.getRefCode()) - .draftedAt(approval.getDraftedAt()) - .status(approval.getStatus()) - .completedAt(approval.getCompletedAt()) - .drafterId(approval.getEmployee().getId()) - .drafterName(approval.getEmployee().getName()) - .drafterDepartment((approval.getEmployee().getDepartment() != null) ? - approval.getEmployee().getDepartment().getDeptName() : null) - .drafterPosition(approval.getEmployee().getPositionCode()) - .drafterRank(approval.getEmployee().getRankCode()) - .approvalAttachments(approvalAttachments) - .approvalLines(approvalLines) - .referenceLines(referenceLines) - .recipientLines(recipientLines) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandService.java b/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandService.java index e6c90066..1d734034 100644 --- a/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandService.java +++ b/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandService.java @@ -4,13 +4,9 @@ import com.werp.sero.approval.command.application.dto.ApprovalDecisionRequestDTO; import com.werp.sero.approval.command.application.dto.ApprovalResponseDTO; import com.werp.sero.employee.command.domain.aggregate.Employee; -import org.springframework.web.multipart.MultipartFile; - -import java.util.List; public interface ApprovalCommandService { - ApprovalResponseDTO submitForApproval(final Employee employee, final ApprovalCreateRequestDTO requestDTO, - final List files); + ApprovalResponseDTO submitForApproval(final Employee employee, final ApprovalCreateRequestDTO requestDTO); void approve(final Employee employee, final int approvalId, final ApprovalDecisionRequestDTO requestDTO); diff --git a/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandServiceImpl.java b/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandServiceImpl.java index fe097c01..41971662 100644 --- a/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandServiceImpl.java +++ b/src/main/java/com/werp/sero/approval/command/application/service/ApprovalCommandServiceImpl.java @@ -25,7 +25,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import java.util.*; import java.util.stream.Collectors; @@ -49,12 +48,10 @@ public class ApprovalCommandServiceImpl implements ApprovalCommandService { private final S3Uploader s3Uploader; private final ApplicationEventPublisher applicationEventPublisher; private final DocumentSequenceCommandService documentSequenceCommandService; - private final ApplicationEventPublisher eventPublisher; @Transactional @Override - public ApprovalResponseDTO submitForApproval(final Employee employee, final ApprovalCreateRequestDTO requestDTO, - final List files) { + public ApprovalResponseDTO submitForApproval(final Employee employee, final ApprovalCreateRequestDTO requestDTO) { validateDuplicateApproval(requestDTO.getRefCode()); validateApprovalLines(requestDTO.getApprovalLines()); @@ -65,39 +62,20 @@ public ApprovalResponseDTO submitForApproval(final Employee employee, final Appr final Approval approval = saveApproval(employee, approvalCode, requestDTO); - List approvalAttachmentResponseDTOs = new ArrayList<>(); - - if (files != null && !files.isEmpty()) { - approvalAttachmentResponseDTOs = saveApprovalAttachments(approval, files).stream() - .map(ApprovalAttachmentResponseDTO::of) - .collect(Collectors.toList()); + if (requestDTO.getApprovalAttachments() != null && !requestDTO.getApprovalAttachments().isEmpty()) { + saveApprovalAttachments(approval, requestDTO.getApprovalAttachments()); } - final List approvalLines = saveApprovalLines(approval, requestDTO.getApprovalLines()); - - final List approvalLineResponseDTOs = approvalLines.stream() - .filter(approvalLine -> - approvalLine.getLineType().equals(APPROVAL_TYPE_APPROVAL) || - approvalLine.getLineType().equals(APPROVAL_TYPE_REVIEWER)) - .sorted(Comparator.comparingInt(ApprovalLine::getSequence)) - .map(ApprovalLineResponseDTO::of) - .collect(Collectors.toList()); - - final List refLines = approvalLines.stream() - .filter(approvalLine -> approvalLine.getLineType().equals(APPROVAL_TYPE_REFERENCE)) - .map(ApprovalLineResponseDTO::of) - .collect(Collectors.toList()); - - final List rcptLines = approvalLines.stream() - .filter(approvalLine -> approvalLine.getLineType().equals(APPROVAL_TYPE_RECIPIENT)) - .map(ApprovalLineResponseDTO::of) - .collect(Collectors.toList()); + final ApprovalLine firstApprovalLine = + saveApprovalLinesAndGetFirstApprover(approval, requestDTO.getApprovalLines()).stream() + .filter(approvalLine -> "ALS_RVW".equals(approvalLine.getStatus())) + .findFirst().get(); updateRefCode(requestDTO.getApprovalTargetType(), approvalCode, ref); - sendApprovalNotification(approval, ApprovalNotificationType.REQUEST, - approvalLineResponseDTOs.get(0).getApproverId()); - return ApprovalResponseDTO.of(approval, approvalAttachmentResponseDTOs, approvalLineResponseDTOs, refLines, rcptLines); + sendApprovalNotification(approval, ApprovalNotificationType.REQUEST, firstApprovalLine.getEmployee().getId()); + + return ApprovalResponseDTO.of(approval); } @Transactional @@ -195,8 +173,8 @@ private void updateRefDocumentStatus(final String approvalStatus, String documen so.updateApprovalInfo(so.getApprovalCode(), (isRejected ? "ORD_APPR_RJCT" : "ORD_APPR_DONE")); - if(!isRejected){ - eventPublisher.publishEvent(NotificationEvent.forClient( + if (!isRejected) { + applicationEventPublisher.publishEvent(NotificationEvent.forClient( NotificationType.ORDER, "주문 상태 변경", "주문번호 " + so.getSoCode() + "의 상태가 진행중으로 변경되었습니다.", @@ -313,19 +291,20 @@ private int calculateTotalApprovalLineCount(final List r return totalLine; } - private List saveApprovalAttachments(final Approval approval, final List files) { - final List approvalAttachments = files.stream() - .map(file -> { - final String s3Url = s3Uploader.upload("sero/documents/", file); + private List saveApprovalAttachments(final Approval approval, + final List requestDTOs) { + final List approvalAttachments = requestDTOs.stream() + .map(requestDTO -> { + final String s3Url = s3Uploader.copy(requestDTO.getS3Url(), "sero/documents/"); - return new ApprovalAttachment(file.getOriginalFilename(), s3Url, approval); + return new ApprovalAttachment(requestDTO.getOriginalFileName(), s3Url, approval); }) .collect(Collectors.toList()); return approvalAttachmentRepository.saveAll(approvalAttachments); } - private List saveApprovalLines(final Approval approval, final List requestDTOs) { + private List saveApprovalLinesAndGetFirstApprover(final Approval approval, final List requestDTOs) { final List employees = employeeRepository.findByIdIn(requestDTOs.stream() .map(ApprovalLineRequestDTO::getApproverId) .collect(Collectors.toList())); diff --git a/src/main/java/com/werp/sero/common/error/ErrorCode.java b/src/main/java/com/werp/sero/common/error/ErrorCode.java index d4eb465f..52e60c0d 100644 --- a/src/main/java/com/werp/sero/common/error/ErrorCode.java +++ b/src/main/java/com/werp/sero/common/error/ErrorCode.java @@ -119,6 +119,7 @@ public enum ErrorCode { PDF_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE004", "PDF 생성에 실패했습니다."), S3_URL_INVALID(HttpStatus.BAD_REQUEST, "FILE005", "유효하지 않은 S3 URL입니다."), S3_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE006", "S3 파일 삭제에 실패했습니다."), + S3_COPY_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "FILE007", "S3 파일 복사에 실패했습니다."), /* PRODUCTION PLAN */ PP_ALREADY_EXISTS(HttpStatus.CONFLICT, "PRODUCTION101", "이미 해당 생산요청 품목에 대한 생산계획이 존재합니다."), diff --git a/src/main/java/com/werp/sero/common/file/S3Uploader.java b/src/main/java/com/werp/sero/common/file/S3Uploader.java index 78d3a607..f2013047 100644 --- a/src/main/java/com/werp/sero/common/file/S3Uploader.java +++ b/src/main/java/com/werp/sero/common/file/S3Uploader.java @@ -3,6 +3,7 @@ import com.werp.sero.common.error.ErrorCode; import com.werp.sero.common.error.exception.BusinessException; import com.werp.sero.common.error.exception.SystemException; +import com.werp.sero.file.dto.PresignedResponseDTO; import io.awspring.cloud.s3.S3Exception; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -11,18 +12,24 @@ 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.CopyObjectRequest; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.time.Duration; import java.util.UUID; @RequiredArgsConstructor @Component public class S3Uploader { private final S3Client s3Client; + private final S3Presigner s3Presigner; @Value("${spring.cloud.aws.s3.bucket}") private String bucket; @@ -32,7 +39,7 @@ public class S3Uploader { public String upload(final String objectPath, final MultipartFile file) { try { - final String objectKey = generateKey(objectPath, file); + final String objectKey = generateKey(objectPath, file.getOriginalFilename()); final PutObjectRequest putRequest = PutObjectRequest.builder() .bucket(bucket) @@ -48,7 +55,7 @@ public String upload(final String objectPath, final MultipartFile file) { } } - public void delete( final String s3Url) { + public void delete(final String s3Url) { try { final String objectKey = extractKey(s3Url); @@ -63,6 +70,48 @@ public void delete( final String s3Url) { } } + public String copy(final String s3Url, final String targetPath) { + final String objectKey = extractKey(s3Url); + + final String destinationKey = targetPath + objectKey.substring(objectKey.lastIndexOf("/") + 1); + + final CopyObjectRequest copyRequest = CopyObjectRequest.builder() + .sourceBucket(bucket) + .sourceKey(objectKey) + .destinationBucket(bucket) + .destinationKey(destinationKey) + .build(); + try { + s3Client.copyObject(copyRequest); + + return generateS3Url(destinationKey); + } catch (S3Exception e) { + throw new SystemException(ErrorCode.S3_COPY_FAILED); + } + } + + public PresignedResponseDTO createPresignedPutUrl(final String objectPath, final String originalFileName, + final String contentType) { + final String key = generateKey(objectPath, originalFileName); + + final PutObjectRequest objectRequest = PutObjectRequest.builder() + .bucket(bucket) + .contentType(contentType) + .key(key) + .build(); + + final PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(10)) // The URL will expire in 10 minutes. + .putObjectRequest(objectRequest) + .build(); + + final PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); + + final String s3Url = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, region, key); + + return new PresignedResponseDTO(presignedRequest.url().toExternalForm(), s3Url); + } + public String uploadBytes( final String objectPath, final byte[] bytes, @@ -103,8 +152,8 @@ private String extractKey(final String s3Url) { } } - private String generateKey(final String path, final MultipartFile file) { - final String extension = StringUtils.getFilenameExtension(file.getOriginalFilename()); + private String generateKey(final String path, final String originalFileName) { + final String extension = StringUtils.getFilenameExtension(originalFileName); final String fileName = UUID.randomUUID() + "." + extension; return path + fileName; diff --git a/src/main/java/com/werp/sero/file/controller/FileController.java b/src/main/java/com/werp/sero/file/controller/FileController.java new file mode 100644 index 00000000..a971f423 --- /dev/null +++ b/src/main/java/com/werp/sero/file/controller/FileController.java @@ -0,0 +1,31 @@ +package com.werp.sero.file.controller; + +import com.werp.sero.file.dto.PresignedResponseDTO; +import com.werp.sero.file.dto.PresignedUrlRequestDTO; +import com.werp.sero.file.service.FileService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "파일", description = "파일 관련 API") +@RequestMapping("/files") +@RequiredArgsConstructor +@RestController +public class FileController { + private final FileService fileService; + + @Operation(summary = "presigend url 발급") + @PostMapping("/presigned-url") + public ResponseEntity generatePresignedUrl( + @Valid @RequestBody final PresignedUrlRequestDTO requestDTO) { + final PresignedResponseDTO responseDTO = fileService.generatePresignedUrl(requestDTO); + + return ResponseEntity.ok(responseDTO); + } +} \ No newline at end of file diff --git a/src/main/java/com/werp/sero/file/dto/PresignedResponseDTO.java b/src/main/java/com/werp/sero/file/dto/PresignedResponseDTO.java new file mode 100644 index 00000000..f6817073 --- /dev/null +++ b/src/main/java/com/werp/sero/file/dto/PresignedResponseDTO.java @@ -0,0 +1,11 @@ +package com.werp.sero.file.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class PresignedResponseDTO { + private String presignedUrl; + private String s3Url; +} \ No newline at end of file diff --git a/src/main/java/com/werp/sero/file/dto/PresignedUrlRequestDTO.java b/src/main/java/com/werp/sero/file/dto/PresignedUrlRequestDTO.java new file mode 100644 index 00000000..9ee4eb0a --- /dev/null +++ b/src/main/java/com/werp/sero/file/dto/PresignedUrlRequestDTO.java @@ -0,0 +1,15 @@ +package com.werp.sero.file.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; + +@Getter +public class PresignedUrlRequestDTO { + @NotBlank + private String fileName; + + @NotBlank + @Schema(defaultValue = "image/png") + private String contentType; +} \ No newline at end of file diff --git a/src/main/java/com/werp/sero/file/service/FileService.java b/src/main/java/com/werp/sero/file/service/FileService.java new file mode 100644 index 00000000..f5f03216 --- /dev/null +++ b/src/main/java/com/werp/sero/file/service/FileService.java @@ -0,0 +1,8 @@ +package com.werp.sero.file.service; + +import com.werp.sero.file.dto.PresignedResponseDTO; +import com.werp.sero.file.dto.PresignedUrlRequestDTO; + +public interface FileService { + PresignedResponseDTO generatePresignedUrl(final PresignedUrlRequestDTO requestDTO); +} \ No newline at end of file diff --git a/src/main/java/com/werp/sero/file/service/FileServiceImpl.java b/src/main/java/com/werp/sero/file/service/FileServiceImpl.java new file mode 100644 index 00000000..7be1b926 --- /dev/null +++ b/src/main/java/com/werp/sero/file/service/FileServiceImpl.java @@ -0,0 +1,18 @@ +package com.werp.sero.file.service; + +import com.werp.sero.common.file.S3Uploader; +import com.werp.sero.file.dto.PresignedResponseDTO; +import com.werp.sero.file.dto.PresignedUrlRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class FileServiceImpl implements FileService { + private final S3Uploader s3Uploader; + + @Override + public PresignedResponseDTO generatePresignedUrl(final PresignedUrlRequestDTO requestDTO) { + return s3Uploader.createPresignedPutUrl("sero/temp/", requestDTO.getFileName(), requestDTO.getContentType()); + } +} \ No newline at end of file