Skip to content

Commit b03952a

Browse files
authored
Merge pull request #107 from TaskFlow-CLAP/CLAP-113
Clap-113 요청 생성 및 수정에 첨부파일 업로드 로직 추가
2 parents 2912879 + 045583c commit b03952a

30 files changed

+354
-148
lines changed

build.gradle

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ dependencies {
9393
// Spring aop
9494
implementation 'org.springframework.boot:spring-boot-starter-aop'
9595

96+
// S3
97+
implementation platform('software.amazon.awssdk:bom:2.23.7')
98+
implementation 'software.amazon.awssdk:s3'
99+
implementation 'ch.qos.logback:logback-classic:1.4.12'
100+
96101
}
97102

98103
tasks.named('test') {

src/main/java/clap/server/adapter/inbound/web/dto/task/AttachmentRequest.java

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/main/java/clap/server/adapter/inbound/web/dto/task/CreateTaskRequest.java

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,18 @@
44
import jakarta.validation.constraints.NotBlank;
55
import jakarta.validation.constraints.NotNull;
66

7-
import java.util.List;
8-
97

108
@Schema(description = "작업 생성 요청")
119
public record CreateTaskRequest(
1210
@Schema(description = "카테고리 ID")
1311
@NotNull
1412
Long categoryId,
1513

16-
@Schema(description = "메인 카테고리 ID")
17-
@NotNull
18-
Long mainCategoryId,
19-
2014
@Schema(description = "작업 제목")
2115
@NotBlank
2216
String title,
2317

2418
@Schema(description = "작업 설명")
25-
String description,
26-
27-
@Schema(description = "첨부 파일 URL 목록", example = "[\"https://example.com/file1.png\", \"https://example.com/file2.pdf\"]")
28-
List<@NotBlank String> fileUrls
19+
String description
2920
) {
3021
}

src/main/java/clap/server/adapter/inbound/web/dto/task/UpdateTaskRequest.java

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,19 @@
99
@Schema(description = "작업 업데이트 요청")
1010
public record UpdateTaskRequest(
1111

12-
@Schema(description = "작업 ID", example = "123")
13-
@NotNull
14-
Long taskId,
15-
1612
@Schema(description = "카테고리 ID", example = "1")
1713
@NotNull
1814
Long categoryId,
1915

20-
@Schema(description = "메인 카테고리 ID", example = "10")
21-
@NotNull
22-
Long mainCategoryId,
23-
2416
@Schema(description = "작업 제목", example = "업데이트된 제목")
2517
@NotBlank
2618
String title,
2719

2820
@Schema(description = "작업 설명", example = "업데이트된 설명.")
2921
String description,
3022

31-
@Schema(description = "첨부 파일 요청 목록", implementation = AttachmentRequest.class)
32-
List<AttachmentRequest> attachmentRequests
23+
@Schema(description = "삭제할 파일 ID 목록, 없을 경우 emptylist 전송")
24+
@NotNull
25+
List<Long> attachmentsToDelete
3326
) {}
3427

src/main/java/clap/server/adapter/inbound/web/task/ManagementTaskController.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111
import io.swagger.v3.oas.annotations.Operation;
1212
import io.swagger.v3.oas.annotations.tags.Tag;
1313
import jakarta.validation.Valid;
14+
import jakarta.validation.constraints.NotNull;
1415
import lombok.RequiredArgsConstructor;
16+
import org.springframework.http.MediaType;
1517
import org.springframework.http.ResponseEntity;
1618
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1719
import org.springframework.web.bind.annotation.*;
20+
import org.springframework.web.multipart.MultipartFile;
21+
22+
import java.util.List;
1823

1924

2025
@Tag(name = "작업 생성 및 수정")
@@ -28,18 +33,22 @@ public class ManagementTaskController {
2833
private final UpdateTaskUsecase updateTaskUsecase;
2934

3035
@Operation(summary = "작업 요청 생성")
31-
@PostMapping
36+
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
3237
public ResponseEntity<CreateTaskResponse> createTask(
33-
@RequestBody @Valid CreateTaskRequest createTaskRequest,
34-
@AuthenticationPrincipal SecurityUserDetails userInfo){
35-
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest));
38+
@RequestPart(name = "taskInfo") @Valid CreateTaskRequest createTaskRequest,
39+
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
40+
@AuthenticationPrincipal SecurityUserDetails userInfo
41+
){
42+
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest, attachments));
3643
}
3744

38-
@Operation(summary = "요청한 작업 수정")
39-
@PatchMapping
45+
@Operation(summary = "작업 수정")
46+
@PatchMapping(value = "/{taskId}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
4047
public ResponseEntity<UpdateTaskResponse> updateTask(
41-
@RequestBody @Valid UpdateTaskRequest updateTaskRequest,
48+
@PathVariable @NotNull Long taskId,
49+
@RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest,
50+
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
4251
@AuthenticationPrincipal SecurityUserDetails userInfo){
43-
return ResponseEntity.ok(updateTaskUsecase.updateTask(userInfo.getUserId(), updateTaskRequest));
52+
return ResponseEntity.ok(updateTaskUsecase.updateTask(userInfo.getUserId(), taskId, updateTaskRequest, attachments));
4453
}
4554
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package clap.server.adapter.outbound.infrastructure.s3;
2+
3+
import clap.server.application.port.outbound.s3.S3UploadPort;
4+
import clap.server.config.s3.KakaoS3Config;
5+
import clap.server.domain.model.task.FilePath;
6+
import clap.server.exception.S3Exception;
7+
import clap.server.exception.code.S3Errorcode;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.stereotype.Service;
11+
import org.springframework.web.multipart.MultipartFile;
12+
import software.amazon.awssdk.services.s3.S3Client;
13+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
14+
15+
import java.io.IOException;
16+
import java.nio.file.Files;
17+
import java.nio.file.Path;
18+
import java.nio.file.StandardCopyOption;
19+
import java.util.List;
20+
import java.util.UUID;
21+
22+
@Slf4j
23+
@Service
24+
@RequiredArgsConstructor
25+
public class S3UploadAdapter implements S3UploadPort {
26+
private final KakaoS3Config kakaoS3Config;
27+
private final S3Client s3Client;
28+
29+
public List<String> uploadFiles(FilePath filePrefix, List<MultipartFile> multipartFiles) {
30+
return multipartFiles.stream().map((file) -> uploadSingleFile(filePrefix, file)).toList();
31+
}
32+
33+
public String uploadSingleFile(FilePath filePrefix, MultipartFile file) {
34+
try {
35+
Path filePath = getFilePath(file);
36+
String objectKey = createObjectKey(filePrefix.getPath(), file.getOriginalFilename());
37+
uploadToS3(objectKey, filePath);
38+
Files.delete(filePath);
39+
return getFileUrl(objectKey);
40+
} catch (IOException e) {
41+
throw new S3Exception(S3Errorcode.FILE_UPLOAD_REQUEST_FAILED);
42+
}
43+
}
44+
45+
private String getFileUrl(String objectKey) {
46+
return kakaoS3Config.getEndpoint() + "/v1/" + kakaoS3Config.getProjectId() + '/' + kakaoS3Config.getBucketName() + '/' + objectKey;
47+
}
48+
49+
private static Path getFilePath(MultipartFile file) throws IOException {
50+
Path path = Files.createTempFile(null,null);
51+
Files.copy(file.getInputStream(),path, StandardCopyOption.REPLACE_EXISTING);
52+
return path;
53+
}
54+
55+
private void uploadToS3(String filePath, Path path) {
56+
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
57+
.bucket(kakaoS3Config.getBucketName())
58+
.key(filePath)
59+
.build();
60+
61+
s3Client.putObject(putObjectRequest, path);
62+
}
63+
64+
private String createFileId() {
65+
return UUID.randomUUID().toString();
66+
}
67+
68+
private String createObjectKey(String filepath, String fileName) {
69+
String fileId = createFileId();
70+
return String.format("%s/%s-%s", filepath, fileId , fileName);
71+
}
72+
73+
}

src/main/java/clap/server/adapter/outbound/persistense/AttachmentPersistenceAdapter.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ public List<Attachment> findAllByTaskIdAndCommentIsNull(final Long taskId) {
4545
.collect(Collectors.toList());
4646
}
4747

48+
public List<Attachment> findAllByTaskIdAndCommentIsNullAndAttachmentId(final Long taskId, final List<Long> attachmentIds) {
49+
List<AttachmentEntity> attachmentEntities = attachmentRepository.findAllByTask_TaskIdAndCommentIsNullAndAttachmentIdIn(taskId, attachmentIds);
50+
return attachmentEntities.stream()
51+
.map(attachmentPersistenceMapper::toDomain)
52+
.collect(Collectors.toList());
53+
}
54+
4855
@Override
4956
public void deleteByIds(List<Long> attachmentIds) {
5057
attachmentRepository.deleteAllByAttachmentIdIn(attachmentIds);

src/main/java/clap/server/adapter/outbound/persistense/entity/task/TaskEntity.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import lombok.experimental.SuperBuilder;
1111

1212
import java.time.LocalDateTime;
13-
import java.util.ArrayList;
14-
import java.util.List;
1513

1614
@Entity
1715
@Table(name = "task")

src/main/java/clap/server/adapter/outbound/persistense/repository/task/AttachmentRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import org.springframework.data.jpa.repository.JpaRepository;
44
import org.springframework.stereotype.Repository;
55

6+
import java.util.Collection;
67
import java.util.List;
78

89
@Repository
910
public interface AttachmentRepository extends JpaRepository<AttachmentEntity, Long> {
1011
List<AttachmentEntity> findAllByTask_TaskIdAndCommentIsNull(Long taskId);
1112
void deleteAllByAttachmentIdIn(List<Long> attachmentIds);
13+
List<AttachmentEntity> findAllByTask_TaskIdAndCommentIsNullAndAttachmentIdIn(Long task_taskId, List<Long> attachmentId);
1214

1315
}

src/main/java/clap/server/application/Task/CreateTaskService.java

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,31 @@
22

33
import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest;
44
import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse;
5-
5+
import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter;
66
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
7+
import clap.server.application.mapper.AttachmentMapper;
78
import clap.server.application.mapper.TaskMapper;
89
import clap.server.application.port.inbound.domain.CategoryService;
910
import clap.server.application.port.inbound.domain.MemberService;
1011
import clap.server.application.port.inbound.task.CreateTaskUsecase;
1112
import clap.server.application.port.outbound.task.CommandAttachmentPort;
1213
import clap.server.application.port.outbound.task.CommandTaskPort;
13-
1414
import clap.server.common.annotation.architecture.ApplicationService;
1515
import clap.server.domain.model.member.Member;
1616
import clap.server.domain.model.notification.Notification;
1717
import clap.server.domain.model.task.Attachment;
1818
import clap.server.domain.model.task.Category;
19+
import clap.server.domain.model.task.FilePath;
1920
import clap.server.domain.model.task.Task;
2021
import lombok.RequiredArgsConstructor;
21-
2222
import org.springframework.context.ApplicationEventPublisher;
2323
import org.springframework.transaction.annotation.Transactional;
24+
import org.springframework.web.multipart.MultipartFile;
2425

2526
import java.util.List;
2627

28+
import static clap.server.domain.model.notification.Notification.createTaskNotification;
29+
2730

2831
@ApplicationService
2932
@RequiredArgsConstructor
@@ -33,37 +36,36 @@ public class CreateTaskService implements CreateTaskUsecase {
3336
private final CategoryService categoryService;
3437
private final CommandTaskPort commandTaskPort;
3538
private final CommandAttachmentPort commandAttachmentPort;
39+
private final S3UploadAdapter s3UploadAdapter;
3640
private final ApplicationEventPublisher applicationEventPublisher;
3741

3842
@Override
3943
@Transactional
40-
public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createTaskRequest) {
44+
public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createTaskRequest, List<MultipartFile> files) {
4145
Member member = memberService.findActiveMember(requesterId);
4246
Category category = categoryService.findById(createTaskRequest.categoryId());
4347
Task task = Task.createTask(member, category, createTaskRequest.title(), createTaskRequest.description());
4448
Task savedTask = commandTaskPort.save(task);
4549

46-
List<Attachment> attachments = Attachment.createAttachments(savedTask, createTaskRequest.fileUrls());
47-
commandAttachmentPort.saveAll(attachments);
48-
50+
saveAttachments(files, savedTask);
51+
publishNotification(savedTask);
52+
return TaskMapper.toCreateTaskResponse(savedTask);
53+
}
4954

50-
// requestDto에 알림 데이터 mapping
55+
private void saveAttachments(List<MultipartFile> files, Task task) {
56+
List<String> fileUrls = s3UploadAdapter.uploadFiles(FilePath.TASK_IMAGE, files);
57+
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
58+
commandAttachmentPort.saveAll(attachments);
59+
}
5160

61+
private void publishNotification(Task task){
5262
List<Member> reviewers = memberService.findReviewers();
5363

54-
5564
// 검토자들 각각에 대한 알림 생성 후 event 발행
5665
for (Member reviewer : reviewers) {
57-
Notification notification = Notification.builder()
58-
.task(savedTask)
59-
.type(NotificationType.TASK_REQUESTED)
60-
.receiver(reviewer)
61-
.message(null)
62-
.build();
63-
// publish event로 event 발행
66+
Notification notification = createTaskNotification(task, reviewer, NotificationType.TASK_REQUESTED);
6467
applicationEventPublisher.publishEvent(notification);
6568
}
66-
67-
return TaskMapper.toCreateTaskResponse(savedTask);
6869
}
69-
}
70+
71+
}

0 commit comments

Comments
 (0)