From a16898b0294f2327e3bf783d6bb48e91def4d96a Mon Sep 17 00:00:00 2001 From: Haeyul Date: Mon, 1 Dec 2025 16:19:58 +0900 Subject: [PATCH 1/2] =?UTF-8?q?RQB-8=20DirectoryDto=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EC=82=AC=EC=9A=A9=EC=8B=9C=20=EA=B7=9C=EC=B9=99=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/directory/dto/RequestDirectoryDto.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/hello/cluebackend/application/directory/dto/RequestDirectoryDto.java b/src/main/java/hello/cluebackend/application/directory/dto/RequestDirectoryDto.java index 370c906..4767eb6 100644 --- a/src/main/java/hello/cluebackend/application/directory/dto/RequestDirectoryDto.java +++ b/src/main/java/hello/cluebackend/application/directory/dto/RequestDirectoryDto.java @@ -1,12 +1,20 @@ package hello.cluebackend.application.directory.dto; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; import lombok.Getter; import java.util.UUID; @Getter public class RequestDirectoryDto { + @Nullable private UUID directoryId; + + @NotNull(message="클래스룸 ID는 필수입니다.") private UUID classRoomId; + + @NotBlank(message = "디렉토리 이름은 필수입니다.") private String name; } From 9b2efdc31d5bce382e445db2a4be8cc25ae8af83 Mon Sep 17 00:00:00 2001 From: Haeyul Date: Mon, 1 Dec 2025 20:48:18 +0900 Subject: [PATCH 2/2] =?UTF-8?q?RQB-8=20Resource=EB=A5=BC=20url=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/AssignmentAttachmentDto.java | 9 ++-- .../dto/response/AssignmentDto.java | 11 +---- .../SubmissionAttachmentResponse.java | 9 ++-- .../service/AssignmentCommandService.java | 38 +++++++++++++--- .../domain/file/service/FileService.java | 20 ++++++--- .../service/SubmissionCommandService.java | 44 ++++++++++++++----- .../AssignmentCommandController.java | 30 +++---------- .../SubmissionCommandController.java | 26 +++-------- 8 files changed, 104 insertions(+), 83 deletions(-) diff --git a/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentAttachmentDto.java b/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentAttachmentDto.java index 95b4f57..6de4b07 100644 --- a/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentAttachmentDto.java +++ b/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentAttachmentDto.java @@ -11,16 +11,19 @@ public record AssignmentAttachmentDto( String value, String originalFileName, String contentType, - Long size + Long size, + String downloadUrl ) { - public static AssignmentAttachmentDto from(AssignmentAttachment assignmentAttachment){ + + public static AssignmentAttachmentDto from(AssignmentAttachment assignmentAttachment, String downloadUrl){ return new AssignmentAttachmentDto( assignmentAttachment.getAssignmentAttachmentId(), assignmentAttachment.getType(), assignmentAttachment.getValue(), assignmentAttachment.getOriginalFileName(), assignmentAttachment.getContentType(), - assignmentAttachment.getSize() + assignmentAttachment.getSize(), + downloadUrl ); } } diff --git a/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentDto.java b/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentDto.java index 9ff8b8e..9fb8909 100644 --- a/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentDto.java +++ b/src/main/java/hello/cluebackend/application/assignment/dto/response/AssignmentDto.java @@ -15,16 +15,9 @@ public record AssignmentDto( @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm", timezone = "Asia/Seoul") LocalDateTime startDate, @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm", timezone = "Asia/Seoul") LocalDateTime endDate, String userName, - - // 과제 첨부 데이터 List attachmentDtos ) { - public static AssignmentDto from(Assignment assignment, List assignmentAttachments){ - - List assignmentResponseDtos = assignmentAttachments.stream() - .map(aa -> AssignmentAttachmentDto.from(aa)) - .toList(); - + public static AssignmentDto from(Assignment assignment, List attachmentDtos){ return new AssignmentDto( assignment.getAssignmentId(), assignment.getTitle(), @@ -32,7 +25,7 @@ public static AssignmentDto from(Assignment assignment, List new EntityNotFoundException("해당 첨부 파일을 찾을수 없습니다.")); } - // 과제 단일 조회 public AssignmentDto findById(UUID assignmentId) { Assignment a = findByIdOrThrow(assignmentId); List assignmentAttachments = assignmentAttachmentJpaRepository.findAllByAssignment(a); - return AssignmentDto.from(a, assignmentAttachments); + List attachmentDtos = assignmentAttachments.stream() + .map(attachment -> { + String downloadUrl = null; + if (attachment.getType() == hello.cluebackend.domain.assignment.model.FileType.FILE) { + downloadUrl = fileService.getPresignedDownloadUrl(attachment.getValue()); + } else if (attachment.getType() == hello.cluebackend.domain.assignment.model.FileType.URL) { + downloadUrl = attachment.getValue(); + } + return AssignmentAttachmentDto.from(attachment, downloadUrl); + }) + .toList(); + + return AssignmentDto.from(a, attachmentDtos); } // 사용자가 속한 모든 수업 과제 조회 @@ -80,17 +91,30 @@ public Assignment findByIdOrThrow(UUID assignmentId) { .orElseThrow(() -> new EntityNotFoundException("해당 과제를 찾을수 없습니다.")); } - public Resource downloadAttachment(AssignmentAttachment assignmentAttachment) throws IOException { - String path = assignmentAttachment.getValue(); - return fileService.downloadFile(path); + public String getAttachmentDownloadUrl(UUID userId, UUID attachmentId) { + AssignmentAttachment attachment = findAssignmentAttachmentByIdOrderThrow(userId, attachmentId); + + if (attachment.getType() == hello.cluebackend.domain.assignment.model.FileType.FILE) { + return fileService.getPresignedDownloadUrl(attachment.getValue()); + } + + return attachment.getValue(); } - // 과제 첨부 파일 목록 조회 + // 과제 첨부 파일 목록 조회 (downloadUrl 포함) public List findAttachmentsByAssignmentId(UUID assignmentId) { Assignment assignment = findByIdOrThrow(assignmentId); List attachments = assignmentAttachmentJpaRepository.findAllByAssignment(assignment); return attachments.stream() - .map(AssignmentAttachmentDto::from) + .map(attachment -> { + String downloadUrl = null; + if (attachment.getType() == hello.cluebackend.domain.assignment.model.FileType.FILE) { + downloadUrl = fileService.getPresignedDownloadUrl(attachment.getValue()); + } else if (attachment.getType() == hello.cluebackend.domain.assignment.model.FileType.URL) { + downloadUrl = attachment.getValue(); + } + return AssignmentAttachmentDto.from(attachment, downloadUrl); + }) .toList(); } } diff --git a/src/main/java/hello/cluebackend/domain/file/service/FileService.java b/src/main/java/hello/cluebackend/domain/file/service/FileService.java index 4547967..55325b6 100644 --- a/src/main/java/hello/cluebackend/domain/file/service/FileService.java +++ b/src/main/java/hello/cluebackend/domain/file/service/FileService.java @@ -1,10 +1,10 @@ package hello.cluebackend.domain.file.service; +import com.amazonaws.HttpMethod; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.PutObjectRequest; import com.amazonaws.services.s3.model.S3Object; -import hello.cluebackend.domain.file.exception.S3FileNotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.InputStreamResource; @@ -13,6 +13,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.Date; import java.util.UUID; @Service @@ -23,7 +24,8 @@ public class FileService { @Value("${cloud.aws.s3.bucket}") private String bucket; - // 파일 추가 + private static final int DEFAULT_EXPIRATION_MINUTES = 60; + public String storeFile(MultipartFile file) throws IOException { String originalFilename = file.getOriginalFilename(); String extension = ""; @@ -36,18 +38,26 @@ public String storeFile(MultipartFile file) throws IOException { metadata.setContentLength(file.getSize()); metadata.setContentType(file.getContentType()); - amazonS3Client.putObject(new PutObjectRequest(bucket, storedFileName, file.getInputStream(), metadata)); // 공개 권한 설정 + amazonS3Client.putObject(new PutObjectRequest(bucket, storedFileName, file.getInputStream(), metadata)); return storedFileName; } - // 파일 삭제 public void deleteFile(String storedFileName) { amazonS3Client.deleteObject(bucket, storedFileName); } - // 파일 다운로드 + @Deprecated public Resource downloadFile(String filePath) { S3Object s3Object = amazonS3Client.getObject(bucket, filePath); return new InputStreamResource(s3Object.getObjectContent()); } + + public String getPresignedDownloadUrl(String storedFileName) { + return getPresignedDownloadUrl(storedFileName, DEFAULT_EXPIRATION_MINUTES); + } + + public String getPresignedDownloadUrl(String storedFileName, int expirationMinutes) { + Date expiration = new Date(System.currentTimeMillis() + expirationMinutes * 60 * 1000); + return amazonS3Client.generatePresignedUrl(bucket, storedFileName, expiration, HttpMethod.GET).toString(); + } } diff --git a/src/main/java/hello/cluebackend/domain/submission/service/SubmissionCommandService.java b/src/main/java/hello/cluebackend/domain/submission/service/SubmissionCommandService.java index 3ffc244..ac55c47 100644 --- a/src/main/java/hello/cluebackend/domain/submission/service/SubmissionCommandService.java +++ b/src/main/java/hello/cluebackend/domain/submission/service/SubmissionCommandService.java @@ -37,7 +37,7 @@ public class SubmissionCommandService { private final UserService userService; private final ClassRoomMapper classRoomMapper; - // 과제 전체 조회 및 과제 첨부 파일 조회 + // 과제 전체 조회 및 과제 첨부 파일 조회 (downloadUrl 포함) public List findAllByAssignmentId(UUID userId, UUID classId) { UserEntity user = userService.findById(userId); ClassRoom classRoom = classRoomMapper.fromClassRoomDtoToEntity(classRoomQueryService.findById(userId, classId)); @@ -48,7 +48,10 @@ public List findAllByAssignmentId(UUID userId, UUID classId) .map(submission -> { List submissionAttachmentResponses = submissionAttachmentJpaRepository.findAllBySubmission(submission).stream() - .map(SubmissionAttachmentResponse::from) + .map(attachment -> { + String downloadUrl = getDownloadUrlForAttachment(attachment); + return SubmissionAttachmentResponse.from(attachment, downloadUrl); + }) .toList(); return SubmissionResponse.from(submission, submissionAttachmentResponses); @@ -56,7 +59,7 @@ public List findAllByAssignmentId(UUID userId, UUID classId) .toList(); } - // 과제 제출 단일 조회 + // 과제 제출 단일 조회 (downloadUrl 포함) public SubmissionResponse findByAssignmentId(UUID userId, UUID submissionId) { Submission submission = findByIdOrThrow(submissionId); UserEntity requestUser = userService.findById(userId); @@ -67,7 +70,10 @@ public SubmissionResponse findByAssignmentId(UUID userId, UUID submissionId) { List submissionAttachments = submissionAttachmentJpaRepository.findAllBySubmission(submission); List submissionAttachmentResponses = submissionAttachments.stream() - .map(SubmissionAttachmentResponse::from) + .map(attachment -> { + String downloadUrl = getDownloadUrlForAttachment(attachment); + return SubmissionAttachmentResponse.from(attachment, downloadUrl); + }) .toList(); return SubmissionResponse.from(submission, submissionAttachmentResponses); } @@ -87,7 +93,7 @@ public List checkAssignment(UUID userId, UUID assignmentId) { .toList(); } - // 첨부 파일 혹은 링크 전체 조회 (학생) + // 첨부 파일 혹은 링크 전체 조회 (학생) - downloadUrl 포함 public List findAllAssignmentStudent(UUID userId, UUID submissionId) { Submission submission = findByIdOrThrow(submissionId); UserEntity requestUser = userService.findById(userId); @@ -99,11 +105,14 @@ public List findAllAssignmentStudent(UUID userId, List attachments = submissionAttachmentJpaRepository.findAllBySubmission(submission); return attachments.stream() .filter(sa -> sa.getUser().getUserId().equals(userId)) - .map(sa -> SubmissionAttachmentResponse.from(sa)) + .map(attachment -> { + String downloadUrl = getDownloadUrlForAttachment(attachment); + return SubmissionAttachmentResponse.from(attachment, downloadUrl); + }) .toList(); } - // 첨부 파일 혹은 링크 전체 조회 (선생) + // 첨부 파일 혹은 링크 전체 조회 (선생) - downloadUrl 포함 public List findAllAssignmentTeacher(UUID userId, UUID submissionId) { UserEntity requestUser = userService.findById(userId); if (!requestUser.getRole().equals(Role.TEACHER)) { @@ -113,14 +122,25 @@ public List findAllAssignmentTeacher(UUID userId, Submission submission = findByIdOrThrow(submissionId); List attachments = submissionAttachmentJpaRepository.findAllBySubmission(submission); return attachments.stream() - .map(sa -> SubmissionAttachmentResponse.from(sa)) + .map(attachment -> { + String downloadUrl = getDownloadUrlForAttachment(attachment); + return SubmissionAttachmentResponse.from(attachment, downloadUrl); + }) .toList(); } - // 첨부파일 다운로드 - public Resource downloadAttachment(SubmissionAttachment submissionAttachment) throws IOException { - String path = submissionAttachment.getValue(); - return fileService.downloadFile(path); + // S3 다운로드 URL 생성 함수 + public String getAttachmentDownloadUrl(UUID submissionAttachmentId) { + SubmissionAttachment attachment = findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); + return getDownloadUrlForAttachment(attachment); + } + + // + private String getDownloadUrlForAttachment(SubmissionAttachment attachment) { + if (attachment.getType() == hello.cluebackend.domain.submission.model.FileType.FILE) { + return fileService.getPresignedDownloadUrl(attachment.getValue()); + } + return attachment.getValue(); } public Submission findByIdOrThrow(UUID submissionId) { diff --git a/src/main/java/hello/cluebackend/presentation/api/assignment/AssignmentCommandController.java b/src/main/java/hello/cluebackend/presentation/api/assignment/AssignmentCommandController.java index 427b843..1cc7498 100644 --- a/src/main/java/hello/cluebackend/presentation/api/assignment/AssignmentCommandController.java +++ b/src/main/java/hello/cluebackend/presentation/api/assignment/AssignmentCommandController.java @@ -2,7 +2,6 @@ import hello.cluebackend.domain.assignment.service.AssignmentCommandService; import hello.cluebackend.domain.assignment.model.Assignment; -import hello.cluebackend.domain.assignment.model.AssignmentAttachment; import hello.cluebackend.domain.assignment.exception.AccessDeniedException; import hello.cluebackend.application.assignment.dto.response.AssignmentDto; import hello.cluebackend.application.assignment.dto.response.GetAllAssignmentDto; @@ -10,10 +9,6 @@ import hello.cluebackend.application.user.dto.oauth2.CustomOAuth2User; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.Resource; -import org.springframework.http.ContentDisposition; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; @@ -23,8 +18,6 @@ import hello.cluebackend.application.assignment.dto.response.AssignmentAttachmentDto; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; @@ -86,25 +79,16 @@ public ResponseEntity> getAttachments( return ResponseEntity.ok(attachments); } - // 첨부 파일 다운로드 @GetMapping("/{assignmentAttachmentId}/download") - public ResponseEntity assignmentAttachmentDownload( + public ResponseEntity assignmentAttachmentDownload( @AuthenticationPrincipal CustomOAuth2User customOAuth2User, @PathVariable UUID assignmentAttachmentId - ) throws IOException { - AssignmentAttachment assignmentAttachment = assignmentCommandService.findAssignmentAttachmentByIdOrderThrow(customOAuth2User.getUserId(),assignmentAttachmentId); - Resource resource = assignmentCommandService.downloadAttachment(assignmentAttachment); - - String original = assignmentAttachment.getOriginalFileName(); - String contentType = assignmentAttachment.getContentType(); - MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.ALL; + ) { + String downloadUrl = assignmentCommandService.getAttachmentDownloadUrl( + customOAuth2User.getUserId(), + assignmentAttachmentId + ); - return ResponseEntity.ok() - .contentType(mediaType) - .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment() - .filename(original, StandardCharsets.UTF_8) - .build() - .toString()) - .body(resource); + return ResponseEntity.ok(downloadUrl); } } diff --git a/src/main/java/hello/cluebackend/presentation/api/submission/SubmissionCommandController.java b/src/main/java/hello/cluebackend/presentation/api/submission/SubmissionCommandController.java index c708a68..98e214d 100644 --- a/src/main/java/hello/cluebackend/presentation/api/submission/SubmissionCommandController.java +++ b/src/main/java/hello/cluebackend/presentation/api/submission/SubmissionCommandController.java @@ -3,18 +3,14 @@ import hello.cluebackend.domain.assignment.exception.AccessDeniedException; import hello.cluebackend.domain.classroomuser.service.ClassroomUserService; import hello.cluebackend.domain.submission.service.SubmissionCommandService; -import hello.cluebackend.domain.submission.model.SubmissionAttachment; import hello.cluebackend.application.submission.dto.response.SubmissionCheck; import hello.cluebackend.application.submission.dto.response.SubmissionResponse; import hello.cluebackend.common.annotation.CurrentUser; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.core.io.Resource; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; -import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.UUID; @@ -59,25 +55,13 @@ public ResponseEntity> checkAssignment( return ResponseEntity.ok(assignmentChecks); } - // 첨부파일 다운로드 + // 과제 파일 다운로드 @GetMapping("/{submissionAttachmentId}/download") - public ResponseEntity submissionAttachmentDownload( + public ResponseEntity submissionAttachmentDownload( @CurrentUser UUID userId, @PathVariable UUID submissionAttachmentId - ) throws IOException { - SubmissionAttachment submissionAttachment = submissionCommandService.findSubmissionAttachmentByIdOrThrow(submissionAttachmentId); - Resource resource = submissionCommandService.downloadAttachment(submissionAttachment); - - String original = submissionAttachment.getOriginalFileName(); - String contentType = submissionAttachment.getContentType(); - MediaType mediaType = (contentType != null) ? MediaType.parseMediaType(contentType) : MediaType.ALL; - - return ResponseEntity.ok() - .contentType(mediaType) - .header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment() - .filename(original, StandardCharsets.UTF_8) - .build() - .toString()) - .body(resource); + ) { + String downloadUrl = submissionCommandService.getAttachmentDownloadUrl(submissionAttachmentId); + return ResponseEntity.ok(downloadUrl); } }