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 @@ -4,10 +4,13 @@
import clap.server.adapter.inbound.web.dto.task.request.CreateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.request.UpdateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.response.CreateTaskResponse;
import clap.server.adapter.inbound.web.dto.task.response.UpdateTaskResponse;
import clap.server.application.port.inbound.task.*;
import clap.server.application.port.inbound.task.CreateTaskUsecase;
import clap.server.application.port.inbound.task.UpdateTaskUsecase;
import clap.server.common.annotation.architecture.WebAdapter;
import clap.server.exception.AdapterException;
import clap.server.exception.code.TaskErrorCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
Expand All @@ -21,6 +24,8 @@

import java.util.List;

import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT;


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

@Operation(summary = "작업 수정")
Expand All @@ -49,8 +58,12 @@ public ResponseEntity<CreateTaskResponse> createTask(
public void updateTask(
@PathVariable @NotNull Long taskId,
@RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest,
@Schema(description = "하나의 작업에는 총 5개 이하만 업로드 가능합니다.")
@RequestPart(name = "attachment", required = false) List<MultipartFile> attachments,
@AuthenticationPrincipal SecurityUserDetails userInfo){
@AuthenticationPrincipal SecurityUserDetails userInfo) {
if (attachments != null && attachments.size() > 5) {
throw new AdapterException(TaskErrorCode.FILE_COUNT_EXCEEDED);
}
updateTaskUsecase.updateTask(userInfo.getUserId(), taskId, updateTaskRequest, attachments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package clap.server.adapter.outbound.infrastructure.s3;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.atomic.AtomicInteger;

public class FileIDGenerator {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final AtomicInteger sequence = new AtomicInteger(0);

public static String createFileId() {
LocalDateTime now = LocalDateTime.now();
String date = now.format(formatter);
int seq = sequence.getAndIncrement() % 1000; // 0부터 999까지 순환
return String.format("%s-%03d", date, seq);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import clap.server.application.port.outbound.s3.S3UploadPort;
import clap.server.common.annotation.architecture.InfrastructureAdapter;
import clap.server.config.s3.KakaoS3Config;
import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import clap.server.exception.S3Exception;
import clap.server.exception.code.FileErrorcode;
import lombok.RequiredArgsConstructor;
Expand All @@ -17,20 +17,20 @@
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;

@Slf4j
@InfrastructureAdapter
@RequiredArgsConstructor
public class S3UploadAdapter implements S3UploadPort {

private final KakaoS3Config kakaoS3Config;
private final S3Client s3Client;

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

public String uploadSingleFile(FilePathPolicy filePrefix, MultipartFile file) {
public String uploadSingleFile(FilePathPolicyConstants filePrefix, MultipartFile file) {
try {
Path filePath = getFilePath(file);
String objectKey = createObjectKey(filePrefix.getPath(), file.getOriginalFilename());
Expand Down Expand Up @@ -62,13 +62,9 @@ private void uploadToS3(String filePath, Path path) {
s3Client.putObject(putObjectRequest, path);
}

private String createFileId() {
return UUID.randomUUID().toString();
}

private String createObjectKey(String filepath, String fileName) {
String fileId = createFileId();
return String.format("%s/%s-%s", filepath, fileId , fileName);
String fileId = FileIDGenerator.createFileId();
return String.format("%s/%s-%s", filepath, fileId, fileName);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,7 @@ public class TaskEntity extends BaseTimeEntity {

@Column
private LocalDateTime finishedAt;

@Column(nullable = false)
private int attachmentCount;
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package clap.server.application.port.outbound.s3;

import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

public interface S3UploadPort {
List<String> uploadFiles(FilePathPolicy filePrefix, List<MultipartFile> multipartFiles);
List<String> uploadFiles(FilePathPolicyConstants filePrefix, List<MultipartFile> multipartFiles);

String uploadSingleFile(FilePathPolicy filePrefix, MultipartFile file);
String uploadSingleFile(FilePathPolicyConstants filePrefix, MultipartFile file);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import clap.server.domain.model.task.Comment;
import clap.server.domain.model.task.Task;
import clap.server.domain.model.task.TaskHistory;
import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -85,7 +85,7 @@ public void saveCommentAttachment(Long userId, Long taskId, MultipartFile file)
}

private String saveAttachment(MultipartFile file, Task task, Comment comment) {
String fileUrl = s3UploadPort.uploadSingleFile(FilePathPolicy.TASK_COMMENT, file);
String fileUrl = s3UploadPort.uploadSingleFile(FilePathPolicyConstants.TASK_COMMENT, file);
Attachment attachment = Attachment.createCommentAttachment(task, comment, file.getOriginalFilename(), fileUrl, file.getSize());
commandAttachmentPort.save(attachment);
return file.getOriginalFilename();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import clap.server.application.port.outbound.s3.S3UploadPort;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -25,7 +25,7 @@ class UpdateMemberInfoService implements UpdateMemberInfoUsecase {
@Override
public void updateMemberInfo(Long memberId, UpdateMemberInfoRequest request, MultipartFile profileImage) throws IOException {
Member member = memberService.findActiveMember(memberId);
String profileImageUrl = profileImage != null ? s3UploadPort.uploadSingleFile(FilePathPolicy.MEMBER_IMAGE, profileImage) : null;
String profileImageUrl = profileImage != null ? s3UploadPort.uploadSingleFile(FilePathPolicyConstants.MEMBER_IMAGE, profileImage) : null;
member.updateMemberInfo(request.name(), request.agitNotification(), request.emailNotification(),
request.kakaoWorkNotification(), profileImageUrl);
commandMemberPort.save(member);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import clap.server.application.port.outbound.task.CommandTaskPort;
import clap.server.application.service.webhook.SendNotificationService;
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.task.Attachment;
import clap.server.domain.model.task.Category;
Expand Down Expand Up @@ -43,29 +43,35 @@ public class CreateTaskService implements CreateTaskUsecase {
public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createTaskRequest, List<MultipartFile> files) {
Member member = memberService.findActiveMember(requesterId);
Category category = categoryService.findById(createTaskRequest.categoryId());
Task task = Task.createTask(member, category, createTaskRequest.title(), createTaskRequest.description());

int fileSize = files == null ? 0 : files.size();
Task task = Task.createTask(member, category, createTaskRequest.title(), createTaskRequest.description(), fileSize);
Task savedTask = commandTaskPort.save(task);
savedTask.setInitialProcessorOrder();
commandTaskPort.save(savedTask);

if (files != null) {
saveAttachments(files, savedTask);}
fileSize = saveAttachments(files, savedTask);
}
savedTask.finalSave(fileSize);
commandTaskPort.save(savedTask);

publishNotification(savedTask);
return TaskResponseMapper.toCreateTaskResponse(savedTask);
}

private void saveAttachments(List<MultipartFile> files, Task task) {
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicy.TASK_IMAGE, files);
private int saveAttachments(List<MultipartFile> files, Task task) {
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicyConstants.TASK_FILE, files);
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
commandAttachmentPort.saveAll(attachments);
return fileUrls.size();
}

private void publishNotification(Task task) {
List<Member> reviewers = memberService.findReviewers();
reviewers.forEach(reviewer -> {
boolean isManager = reviewer.getMemberInfo().getRole() == MemberRole.ROLE_MANAGER;
sendNotificationService.sendPushNotification(reviewer, NotificationType.TASK_REQUESTED,
task, null, null, isManager);});
task, null, null, isManager);
});

sendNotificationService.sendAgitNotification(NotificationType.TASK_REQUESTED,
task, null, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.task.*;
import clap.server.domain.policy.attachment.FilePathPolicy;
import clap.server.domain.policy.attachment.FilePathPolicyConstants;
import clap.server.exception.ApplicationException;
import clap.server.exception.code.TaskErrorCode;
import lombok.RequiredArgsConstructor;
Expand All @@ -34,6 +34,7 @@

import java.util.List;

import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_MAX_FILE_COUNT;
import static clap.server.domain.policy.task.TaskPolicyConstants.TASK_UPDATABLE_STATUS;


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

@Override
@Transactional
public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest updateTaskRequest, List<MultipartFile> files) {
public void updateTask(Long requesterId, Long taskId, UpdateTaskRequest request, List<MultipartFile> files) {
memberService.findActiveMember(requesterId);
Category category = categoryService.findById(updateTaskRequest.categoryId());
Category category = categoryService.findById(request.categoryId());
Task task = taskService.findById(taskId);

task.updateTask(requesterId, category, updateTaskRequest.title(), updateTaskRequest.description());
taskService.upsert(task);

if (!updateTaskRequest.attachmentsToDelete().isEmpty()) {
updateAttachments(updateTaskRequest.attachmentsToDelete(), files, task);
int attachmentToAdd = files==null? 0 : files.size();
int attachmentCount = task.getAttachmentCount() - request.attachmentsToDelete().size() + attachmentToAdd;
if (attachmentCount > TASK_MAX_FILE_COUNT) {
throw new ApplicationException(TaskErrorCode.FILE_COUNT_EXCEEDED);
}
if (!request.attachmentsToDelete().isEmpty()) {
updateAttachments(request.attachmentsToDelete(), files, task);
}
task.updateTask(requesterId, category, request.title(), request.description(), attachmentCount);
taskService.upsert(task);
}

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

if(!TASK_UPDATABLE_STATUS.contains(taskStatus)){
if (!TASK_UPDATABLE_STATUS.contains(taskStatus)) {
throw new ApplicationException(TaskErrorCode.TASK_STATUS_NOT_ALLOWED);
}

if(!task.getTaskStatus().equals(taskStatus)){
if (!task.getTaskStatus().equals(taskStatus)) {
task.updateTaskStatus(taskStatus);
Task updateTask = taskService.upsert(task);
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, taskStatus.getDescription(), null,null);
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.STATUS_SWITCHED, task, taskStatus.getDescription(), null, null);
commandTaskHistoryPort.save(taskHistory);

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

task.updateProcessor(processor);
Task updateTask = taskService.upsert(task);
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor,null);
TaskHistory taskHistory = TaskHistory.createTaskHistory(TaskHistoryType.PROCESSOR_CHANGED, task, null, processor, null);
commandTaskHistoryPort.save(taskHistory);

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

private void updateAttachments(List<Long> attachmentIdsToDelete, List<MultipartFile> files, Task task) {
List<Attachment> attachmentsToDelete = validateAndGetAttachments(attachmentIdsToDelete, task);
attachmentsToDelete.forEach(Attachment::softDelete);
attachmentsToDelete.stream()
.peek(Attachment::softDelete)
.forEach(commandAttachmentPort::save);

if (files != null) {
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicy.TASK_IMAGE, files);
List<String> fileUrls = s3UploadPort.uploadFiles(FilePathPolicyConstants.TASK_FILE, files);
List<Attachment> attachments = AttachmentMapper.toTaskAttachments(task, files, fileUrls);
commandAttachmentPort.saveAll(attachments);
}

}

private List<Attachment> validateAndGetAttachments(List<Long> attachmentIdsToDelete, Task task) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import clap.server.domain.model.task.Task;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.CompletableFuture;

Expand Down Expand Up @@ -75,6 +76,7 @@ public void sendPushNotification(Member receiver, NotificationType notificationT
}

@Async("notificationExecutor")
@Transactional
public void sendAgitNotification(NotificationType notificationType,
Task task, String message, String commenterName) {
PushNotificationTemplate pushNotificationTemplate = new PushNotificationTemplate(
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/clap/server/domain/model/task/Task.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import clap.server.domain.model.member.Member;
import clap.server.exception.ApplicationException;
import clap.server.exception.DomainException;
import clap.server.exception.code.TaskErrorCode;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand Down Expand Up @@ -36,32 +35,36 @@ public class Task extends BaseTime {
private Member reviewer;
private LocalDateTime dueDate;
private LocalDateTime finishedAt;
private int attachmentCount;

public static Task createTask(Member member, Category category, String title, String description) {
public static Task createTask(Member member, Category category, String title, String description, int attachmentCount) {
return Task.builder()
.requester(member)
.category(category)
.title(title)
.description(description)
.taskStatus(TaskStatus.REQUESTED)
.taskCode(toTaskCode(category))
.attachmentCount(attachmentCount)
.build();
}

public void updateTask(Long requesterId, Category category, String title, String description) {
public void updateTask(Long requesterId, Category category, String title, String description, int attachmentCount) {
if (!Objects.equals(requesterId, this.requester.getMemberId())) {
throw new ApplicationException(NOT_A_REQUESTER);
}
this.category = category;
this.title = title;
this.description = description;
this.taskCode = toTaskCode(category);
this.attachmentCount = attachmentCount;
}

public void setInitialProcessorOrder() {
public void finalSave(int attachmentCount) {
if (this.processor == null) {
this.processorOrder = this.taskId * DEFAULT_PROCESSOR_ORDER_GAP;
}
this.attachmentCount = attachmentCount;
}

public void updateTaskStatus(TaskStatus status) {
Expand Down
Loading