Skip to content

Commit 0d285a8

Browse files
authored
Merge pull request #398 from TaskFlow-CLAP/CLAP-317
CLAP-317 파일 용량 및 개수 제한 추가 및 파일 저장 이름에 연월일시 정보 추가
2 parents dfa3b8f + d5d4a27 commit 0d285a8

File tree

17 files changed

+119
-54
lines changed

17 files changed

+119
-54
lines changed

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
import clap.server.adapter.inbound.web.dto.task.request.CreateTaskRequest;
55
import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskRequest;
66
import clap.server.adapter.inbound.web.dto.task.response.CreateTaskResponse;
7-
import clap.server.adapter.inbound.web.dto.task.response.UpdateTaskResponse;
8-
import clap.server.application.port.inbound.task.*;
7+
import clap.server.application.port.inbound.task.CreateTaskUsecase;
8+
import clap.server.application.port.inbound.task.UpdateTaskUsecase;
99
import clap.server.common.annotation.architecture.WebAdapter;
10+
import clap.server.exception.AdapterException;
11+
import clap.server.exception.code.TaskErrorCode;
1012
import io.swagger.v3.oas.annotations.Operation;
13+
import io.swagger.v3.oas.annotations.media.Schema;
1114
import io.swagger.v3.oas.annotations.tags.Tag;
1215
import jakarta.validation.Valid;
1316
import jakarta.validation.constraints.NotNull;
@@ -21,6 +24,8 @@
2124

2225
import java.util.List;
2326

27+
import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT;
28+
2429

2530
@Tag(name = "02. Task [생성/수정]", description = "작업 생성/수정 API")
2631
@WebAdapter
@@ -37,10 +42,14 @@ public class ManagementTaskController {
3742
@Secured({"ROLE_MANAGER", "ROLE_USER"})
3843
public ResponseEntity<CreateTaskResponse> createTask(
3944
@RequestPart(name = "taskInfo") @Valid CreateTaskRequest createTaskRequest,
45+
@Schema(description = "파일은 5개 이하만 업로드 가능합니다.")
4046
@RequestPart(name = "attachment", required = false) List<MultipartFile> attachments,
4147
@AuthenticationPrincipal SecurityUserDetails userInfo
42-
){
43-
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest, attachments));
48+
) {
49+
if (attachments != null && attachments.size() > TASK_MAX_FILE_COUNT) {
50+
throw new AdapterException(TaskErrorCode.FILE_COUNT_EXCEEDED);
51+
}
52+
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest, attachments));
4453
}
4554

4655
@Operation(summary = "작업 수정")
@@ -49,8 +58,12 @@ public ResponseEntity<CreateTaskResponse> createTask(
4958
public void updateTask(
5059
@PathVariable @NotNull Long taskId,
5160
@RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest,
61+
@Schema(description = "하나의 작업에는 총 5개 이하만 업로드 가능합니다.")
5262
@RequestPart(name = "attachment", required = false) List<MultipartFile> attachments,
53-
@AuthenticationPrincipal SecurityUserDetails userInfo){
63+
@AuthenticationPrincipal SecurityUserDetails userInfo) {
64+
if (attachments != null && attachments.size() > 5) {
65+
throw new AdapterException(TaskErrorCode.FILE_COUNT_EXCEEDED);
66+
}
5467
updateTaskUsecase.updateTask(userInfo.getUserId(), taskId, updateTaskRequest, attachments);
5568
}
5669
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package clap.server.adapter.outbound.infrastructure.s3;
2+
3+
import java.time.LocalDateTime;
4+
import java.time.format.DateTimeFormatter;
5+
import java.util.concurrent.atomic.AtomicInteger;
6+
7+
public class FileIDGenerator {
8+
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
9+
private static final AtomicInteger sequence = new AtomicInteger(0);
10+
11+
public static String createFileId() {
12+
LocalDateTime now = LocalDateTime.now();
13+
String date = now.format(formatter);
14+
int seq = sequence.getAndIncrement() % 1000; // 0부터 999까지 순환
15+
return String.format("%s-%03d", date, seq);
16+
}
17+
}

src/main/java/clap/server/adapter/outbound/infrastructure/s3/S3UploadAdapter.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import clap.server.application.port.outbound.s3.S3UploadPort;
44
import clap.server.common.annotation.architecture.InfrastructureAdapter;
55
import clap.server.config.s3.KakaoS3Config;
6-
import clap.server.domain.policy.attachment.FilePathPolicy;
6+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
77
import clap.server.exception.S3Exception;
88
import clap.server.exception.code.FileErrorcode;
99
import lombok.RequiredArgsConstructor;
@@ -17,20 +17,20 @@
1717
import java.nio.file.Path;
1818
import java.nio.file.StandardCopyOption;
1919
import java.util.List;
20-
import java.util.UUID;
2120

2221
@Slf4j
2322
@InfrastructureAdapter
2423
@RequiredArgsConstructor
2524
public class S3UploadAdapter implements S3UploadPort {
25+
2626
private final KakaoS3Config kakaoS3Config;
2727
private final S3Client s3Client;
2828

29-
public List<String> uploadFiles(FilePathPolicy filePrefix, List<MultipartFile> multipartFiles) {
29+
public List<String> uploadFiles(FilePathPolicyConstants filePrefix, List<MultipartFile> multipartFiles) {
3030
return multipartFiles.stream().map((file) -> uploadSingleFile(filePrefix, file)).toList();
3131
}
3232

33-
public String uploadSingleFile(FilePathPolicy filePrefix, MultipartFile file) {
33+
public String uploadSingleFile(FilePathPolicyConstants filePrefix, MultipartFile file) {
3434
try {
3535
Path filePath = getFilePath(file);
3636
String objectKey = createObjectKey(filePrefix.getPath(), file.getOriginalFilename());
@@ -62,13 +62,9 @@ private void uploadToS3(String filePath, Path path) {
6262
s3Client.putObject(putObjectRequest, path);
6363
}
6464

65-
private String createFileId() {
66-
return UUID.randomUUID().toString();
67-
}
68-
6965
private String createObjectKey(String filepath, String fileName) {
70-
String fileId = createFileId();
71-
return String.format("%s/%s-%s", filepath, fileId , fileName);
66+
String fileId = FileIDGenerator.createFileId();
67+
return String.format("%s/%s-%s", filepath, fileId, fileName);
7268
}
7369

7470
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,7 @@ public class TaskEntity extends BaseTimeEntity {
6565

6666
@Column
6767
private LocalDateTime finishedAt;
68+
69+
@Column(nullable = false)
70+
private int attachmentCount;
6871
}
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
package clap.server.application.port.outbound.s3;
22

3-
import clap.server.domain.policy.attachment.FilePathPolicy;
3+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
44
import org.springframework.web.multipart.MultipartFile;
55

66
import java.util.List;
77

88
public interface S3UploadPort {
9-
List<String> uploadFiles(FilePathPolicy filePrefix, List<MultipartFile> multipartFiles);
9+
List<String> uploadFiles(FilePathPolicyConstants filePrefix, List<MultipartFile> multipartFiles);
1010

11-
String uploadSingleFile(FilePathPolicy filePrefix, MultipartFile file);
11+
String uploadSingleFile(FilePathPolicyConstants filePrefix, MultipartFile file);
1212
}

src/main/java/clap/server/application/service/history/PostCommentService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import clap.server.domain.model.task.Comment;
2020
import clap.server.domain.model.task.Task;
2121
import clap.server.domain.model.task.TaskHistory;
22-
import clap.server.domain.policy.attachment.FilePathPolicy;
22+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
2323
import lombok.RequiredArgsConstructor;
2424
import org.springframework.transaction.annotation.Transactional;
2525
import org.springframework.web.multipart.MultipartFile;
@@ -85,7 +85,7 @@ public void saveCommentAttachment(Long userId, Long taskId, MultipartFile file)
8585
}
8686

8787
private String saveAttachment(MultipartFile file, Task task, Comment comment) {
88-
String fileUrl = s3UploadPort.uploadSingleFile(FilePathPolicy.TASK_COMMENT, file);
88+
String fileUrl = s3UploadPort.uploadSingleFile(FilePathPolicyConstants.TASK_COMMENT, file);
8989
Attachment attachment = Attachment.createCommentAttachment(task, comment, file.getOriginalFilename(), fileUrl, file.getSize());
9090
commandAttachmentPort.save(attachment);
9191
return file.getOriginalFilename();

src/main/java/clap/server/application/service/member/UpdateMemberInfoService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import clap.server.application.port.outbound.s3.S3UploadPort;
88
import clap.server.common.annotation.architecture.ApplicationService;
99
import clap.server.domain.model.member.Member;
10-
import clap.server.domain.policy.attachment.FilePathPolicy;
10+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
1111
import lombok.RequiredArgsConstructor;
1212
import org.springframework.transaction.annotation.Transactional;
1313
import org.springframework.web.multipart.MultipartFile;
@@ -25,7 +25,7 @@ class UpdateMemberInfoService implements UpdateMemberInfoUsecase {
2525
@Override
2626
public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, MultipartFile profileImage) throws IOException {
2727
Member member = memberService.findActiveMember(memberId);
28-
String profileImageUrl = profileImage != null ? s3UploadPort.uploadSingleFile(FilePathPolicy.MEMBER_IMAGE, profileImage) : null;
28+
String profileImageUrl = profileImage != null ? s3UploadPort.uploadSingleFile(FilePathPolicyConstants.MEMBER_IMAGE, profileImage) : null;
2929
member.updateMemberInfo(request.name(), request.agitNotification(), request.emailNotification(),
3030
request.kakaoWorkNotification(), profileImageUrl);
3131
commandMemberPort.save(member);

src/main/java/clap/server/application/service/task/CreateTaskService.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import clap.server.application.port.outbound.task.CommandTaskPort;
1616
import clap.server.application.service.webhook.SendNotificationService;
1717
import clap.server.common.annotation.architecture.ApplicationService;
18-
import clap.server.domain.policy.attachment.FilePathPolicy;
18+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
1919
import clap.server.domain.model.member.Member;
2020
import clap.server.domain.model.task.Attachment;
2121
import clap.server.domain.model.task.Category;
@@ -43,29 +43,35 @@ public class CreateTaskService implements CreateTaskUsecase {
4343
public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createTaskRequest, List<MultipartFile> files) {
4444
Member member = memberService.findActiveMember(requesterId);
4545
Category category = categoryService.findById(createTaskRequest.categoryId());
46-
Task task = Task.createTask(member, category, createTaskRequest.title(), createTaskRequest.description());
46+
47+
int fileSize = files == null ? 0 : files.size();
48+
Task task = Task.createTask(member, category, createTaskRequest.title(), createTaskRequest.description(), fileSize);
4749
Task savedTask = commandTaskPort.save(task);
48-
savedTask.setInitialProcessorOrder();
49-
commandTaskPort.save(savedTask);
5050

5151
if (files != null) {
52-
saveAttachments(files, savedTask);}
52+
fileSize = saveAttachments(files, savedTask);
53+
}
54+
savedTask.finalSave(fileSize);
55+
commandTaskPort.save(savedTask);
56+
5357
publishNotification(savedTask);
5458
return TaskResponseMapper.toCreateTaskResponse(savedTask);
5559
}
5660

57-
private void saveAttachments(List<MultipartFile> files, Task task) {
58-
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicy.TASK_IMAGE, files);
61+
private int saveAttachments(List<MultipartFile> files, Task task) {
62+
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicyConstants.TASK_FILE, files);
5963
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
6064
commandAttachmentPort.saveAll(attachments);
65+
return fileUrls.size();
6166
}
6267

6368
private void publishNotification(Task task) {
6469
List<Member> reviewers = memberService.findReviewers();
6570
reviewers.forEach(reviewer -> {
6671
boolean isManager = reviewer.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER;
6772
sendNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED,
68-
task, null, null, isManager);});
73+
task, null, null, isManager);
74+
});
6975

7076
sendNotificationService.sendAgitNotification(NotificationType.TASK_REQUESTED,
7177
task, null, null);

src/main/java/clap/server/application/service/task/UpdateTaskService.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
import clap.server.common.annotation.architecture.ApplicationService;
2525
import clap.server.domain.model.member.Member;
2626
import clap.server.domain.model.task.*;
27-
import clap.server.domain.policy.attachment.FilePathPolicy;
27+
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
2828
import clap.server.exception.ApplicationException;
2929
import clap.server.exception.code.TaskErrorCode;
3030
import lombok.RequiredArgsConstructor;
@@ -34,6 +34,7 @@
3434

3535
import java.util.List;
3636

37+
import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT;
3738
import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_UPDATABLE_STATUS;
3839

3940

@@ -55,17 +56,21 @@ public class UpdateTaskService implements UpdateTaskUsecase, UpdateTaskStatusUse
5556

5657
@Override
5758
@Transactional
58-
public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest updateTaskRequest, List<MultipartFile> files) {
59+
public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest request, List<MultipartFile> files) {
5960
memberService.findActiveMember(requesterId);
60-
Category category = categoryService.findById(updateTaskRequest.categoryId());
61+
Category category = categoryService.findById(request.categoryId());
6162
Task task = taskService.findById(taskId);
6263

63-
task.updateTask(requesterId, category, updateTaskRequest.title(), updateTaskRequest.description());
64-
taskService.upsert(task);
65-
66-
if (!updateTaskRequest.attachmentsToDelete().isEmpty()) {
67-
updateAttachments(updateTaskRequest.attachmentsToDelete(), files, task);
64+
int attachmentToAdd = files==null? 0 : files.size();
65+
int attachmentCount = task.getAttachmentCount() - request.attachmentsToDelete().size() + attachmentToAdd;
66+
if (attachmentCount > TASK_MAX_FILE_COUNT) {
67+
throw new ApplicationException(TaskErrorCode.FILE_COUNT_EXCEEDED);
68+
}
69+
if (!request.attachmentsToDelete().isEmpty()) {
70+
updateAttachments(request.attachmentsToDelete(), files, task);
6871
}
72+
task.updateTask(requesterId, category, request.title(), request.description(), attachmentCount);
73+
taskService.upsert(task);
6974
}
7075

7176
@Override
@@ -75,14 +80,14 @@ public void updateTaskStatus(Long memberId, Long taskId, TaskStatus taskStatus)
7580
memberService.findReviewer(memberId);
7681
Task task = taskService.findById(taskId);
7782

78-
if(!TASK_UPDATABLE_STATUS.contains(taskStatus)){
83+
if (!TASK_UPDATABLE_STATUS.contains(taskStatus)) {
7984
throw new ApplicationException(TaskErrorCode.TASK_STATUS_NOT_ALLOWED);
8085
}
8186

82-
if(!task.getTaskStatus().equals(taskStatus)){
87+
if (!task.getTaskStatus().equals(taskStatus)) {
8388
task.updateTaskStatus(taskStatus);
8489
Task updateTask = taskService.upsert(task);
85-
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, taskStatus.getDescription(), null,null);
90+
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, taskStatus.getDescription(), null, null);
8691
commandTaskHistoryPort.save(taskHistory);
8792

8893
List<Member> receivers = List.of(task.getRequester(), task.getProcessor());
@@ -101,7 +106,7 @@ public void updateTaskProcessor(Long taskId, Long userId, UpdateTaskProcessorReq
101106

102107
task.updateProcessor(processor);
103108
Task updateTask = taskService.upsert(task);
104-
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor,null);
109+
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor, null);
105110
commandTaskHistoryPort.save(taskHistory);
106111

107112
List<Member> receivers = List.of(updateTask.getRequester(), updateTask.getProcessor());
@@ -122,13 +127,16 @@ public void updateTaskLabel(Long taskId, Long userId, UpdateTaskLabelRequest req
122127

123128
private void updateAttachments(List<Long> attachmentIdsToDelete, List<MultipartFile> files, Task task) {
124129
List<Attachment> attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task);
125-
attachmentsToDelete.forEach(Attachment::softDelete);
130+
attachmentsToDelete.stream()
131+
.peek(Attachment::softDelete)
132+
.forEach(commandAttachmentPort::save);
126133

127134
if (files != null) {
128-
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicy.TASK_IMAGE, files);
135+
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicyConstants.TASK_FILE, files);
129136
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
130137
commandAttachmentPort.saveAll(attachments);
131138
}
139+
132140
}
133141

134142
private List<Attachment> validateAndGetAttachments(List<Long> attachmentIdsToDelete, Task task) {

src/main/java/clap/server/application/service/webhook/SendNotificationService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import lombok.RequiredArgsConstructor;
1111
import org.springframework.beans.factory.annotation.Value;
1212
import org.springframework.scheduling.annotation.Async;
13+
import org.springframework.transaction.annotation.Transactional;
1314

1415
import java.util.concurrent.CompletableFuture;
1516

@@ -85,6 +86,7 @@ public void sendPushNotification(Member receiver, NotificationType notificationT
8586
}
8687

8788
@Async("notificationExecutor")
89+
@Transactional
8890
public void sendAgitNotification(NotificationType notificationType,
8991
Task task, String message, String commenterName) {
9092
PushNotificationTemplate pushNotificationTemplate = new PushNotificationTemplate(

0 commit comments

Comments
 (0)