Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0c2f408
CLAP-113 Config: s3 의존성 추가
joowojr Jan 26, 2025
38f6647
CLAP-113 Config: kakao s3 config 추가
joowojr Jan 26, 2025
da181de
CLAP-113 Feat: s3 파일 업로드 서비스 구현
joowojr Jan 26, 2025
13530bc
CLAP-113 Refactor: mapper 통합
joowojr Jan 26, 2025
5da90ae
CLAP-113 Refactor: 요청 생성 및 수정 기능에 파일 업로드 로직 추가
joowojr Jan 26, 2025
84a8131
CLAP-113 Feat: Attachment 도메인 로직 구현
joowojr Jan 26, 2025
ddbcd28
CLAP-113 Feat: Notification 도메인 로직 구현
joowojr Jan 26, 2025
465ddc1
CLAP-113 Feat: multifile과 file url을 Attachment로 매핑하는 매퍼 메서드 구현
joowojr Jan 26, 2025
efa00b3
CLAP-113 Feat: 첨부파일 업로드 기획 변경에 따른 요청 생성/수정 dto 스펙 수정
joowojr Jan 26, 2025
66cb611
CLAP-113 Feat: 첨부파일 업로드 멀티파트 추가
joowojr Jan 26, 2025
845f1eb
CLAP-113 Feat: 해당 task에 해당하는 첨부파일 id인지 검증하는 로직 추가
joowojr Jan 26, 2025
69dac4d
CLAP-113 Refactor: 메서드 리팩토링
joowojr Jan 26, 2025
cf7315d
CLAP-113 Refactor: 유효성 검증 메서드 분리
joowojr Jan 26, 2025
a4ba2f8
CLAP-113 Remove: s3presigner 삭제
joowojr Jan 26, 2025
d3baac2
CLAP-113 Rename: 헥사고날 아키텍처 및 명명 규칙에 맞게 파일 수정
joowojr Jan 26, 2025
25bc069
Merge branch 'develop' into CLAP-113
joowojr Jan 26, 2025
4f218b8
CLAP-113 Refactor: attachment 조회 시 comment null 조건 추가
joowojr Jan 26, 2025
045583c
CLAP-113 Fix: 오타 수정
joowojr Jan 26, 2025
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
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ dependencies {
// Spring aop
implementation 'org.springframework.boot:spring-boot-starter-aop'

// S3
implementation platform('software.amazon.awssdk:bom:2.23.7')
implementation 'software.amazon.awssdk:s3'
implementation 'ch.qos.logback:logback-classic:1.4.12'

}

tasks.named('test') {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,18 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

import java.util.List;


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

@Schema(description = "메인 카테고리 ID")
@NotNull
Long mainCategoryId,

@Schema(description = "작업 제목")
@NotBlank
String title,

@Schema(description = "작업 설명")
String description,

@Schema(description = "첨부 파일 URL 목록", example = "[\"https://example.com/file1.png\", \"https://example.com/file2.pdf\"]")
List<@NotBlank String> fileUrls
String description
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,19 @@
@Schema(description = "작업 업데이트 요청")
public record UpdateTaskRequest(

@Schema(description = "작업 ID", example = "123")
@NotNull
Long taskId,

@Schema(description = "카테고리 ID", example = "1")
@NotNull
Long categoryId,

@Schema(description = "메인 카테고리 ID", example = "10")
@NotNull
Long mainCategoryId,

@Schema(description = "작업 제목", example = "업데이트된 제목")
@NotBlank
String title,

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

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

Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;


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

@Operation(summary = "작업 요청 생성")
@PostMapping
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<CreateTaskResponse> createTask(
@RequestBody @Valid CreateTaskRequest createTaskRequest,
@AuthenticationPrincipal SecurityUserDetails userInfo){
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest));
@RequestPart(name = "taskInfo") @Valid CreateTaskRequest createTaskRequest,
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
@AuthenticationPrincipal SecurityUserDetails userInfo
){
return ResponseEntity.ok(createTaskUsecase.createTask(userInfo.getUserId(), createTaskRequest, attachments));
}

@Operation(summary = "요청한 작업 수정")
@PatchMapping
@Operation(summary = "작업 수정")
@PatchMapping(value = "/{taskId}", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public ResponseEntity<UpdateTaskResponse> updateTask(
@RequestBody @Valid UpdateTaskRequest updateTaskRequest,
@PathVariable @NotNull Long taskId,
@RequestPart(name = "taskInfo") @Valid UpdateTaskRequest updateTaskRequest,
@RequestPart(name = "attachment") @NotNull List<MultipartFile> attachments,
@AuthenticationPrincipal SecurityUserDetails userInfo){
return ResponseEntity.ok(updateTaskUsecase.updateTask(userInfo.getUserId(), updateTaskRequest));
return ResponseEntity.ok(updateTaskUsecase.updateTask(userInfo.getUserId(), taskId, updateTaskRequest, attachments));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package clap.server.adapter.outbound.infrastructure.s3;

import clap.server.application.port.outbound.s3.S3UploadPort;
import clap.server.config.s3.KakaoS3Config;
import clap.server.domain.model.task.FilePath;
import clap.server.exception.S3Exception;
import clap.server.exception.code.S3Errorcode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class S3UploadAdapter implements S3UploadPort {
private final KakaoS3Config kakaoS3Config;
private final S3Client s3Client;

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

public String uploadSingleFile(FilePath filePrefix, MultipartFile file) {
try {
Path filePath = getFilePath(file);
String objectKey = createObjectKey(filePrefix.getPath(), file.getOriginalFilename());
uploadToS3(objectKey, filePath);
Files.delete(filePath);
return getFileUrl(objectKey);
} catch (IOException e) {
throw new S3Exception(S3Errorcode.FILE_UPLOAD_REQUEST_FAILED);
}
}

private String getFileUrl(String objectKey) {
return kakaoS3Config.getEndpoint() + "/v1/" + kakaoS3Config.getProjectId() + '/' + kakaoS3Config.getBucketName() + '/' + objectKey;
}

private static Path getFilePath(MultipartFile file) throws IOException {
Path path = Files.createTempFile(null,null);
Files.copy(file.getInputStream(),path, StandardCopyOption.REPLACE_EXISTING);
return path;
}

private void uploadToS3(String filePath, Path path) {
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(kakaoS3Config.getBucketName())
.key(filePath)
.build();

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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ public List<Attachment> findAllByTaskIdAndCommentIsNull(final Long taskId) {
.collect(Collectors.toList());
}

public List<Attachment> findAllByTaskIdAndCommentIsNullAndAttachmentId(final Long taskId, final List<Long> attachmentIds) {
List<AttachmentEntity> attachmentEntities = attachmentRepository.findAllByTask_TaskIdAndCommentIsNullAndAttachmentIdIn(taskId, attachmentIds);
return attachmentEntities.stream()
.map(attachmentPersistenceMapper::toDomain)
.collect(Collectors.toList());
}

@Override
public void deleteByIds(List<Long> attachmentIds) {
attachmentRepository.deleteAllByAttachmentIdIn(attachmentIds);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@
import lombok.experimental.SuperBuilder;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "task")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Collection;
import java.util.List;

@Repository
public interface AttachmentRepository extends JpaRepository<AttachmentEntity, Long> {
List<AttachmentEntity> findAllByTask_TaskIdAndCommentIsNull(Long taskId);
void deleteAllByAttachmentIdIn(List<Long> attachmentIds);
List<AttachmentEntity> findAllByTask_TaskIdAndCommentIsNullAndAttachmentIdIn(Long task_taskId, List<Long> attachmentId);

}
40 changes: 21 additions & 19 deletions src/main/java/clap/server/application/Task/CreateTaskService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,31 @@

import clap.server.adapter.inbound.web.dto.task.CreateTaskRequest;
import clap.server.adapter.inbound.web.dto.task.CreateTaskResponse;

import clap.server.adapter.outbound.infrastructure.s3.S3UploadAdapter;
import clap.server.adapter.outbound.persistense.entity.notification.constant.NotificationType;
import clap.server.application.mapper.AttachmentMapper;
import clap.server.application.mapper.TaskMapper;
import clap.server.application.port.inbound.domain.CategoryService;
import clap.server.application.port.inbound.domain.MemberService;
import clap.server.application.port.inbound.task.CreateTaskUsecase;
import clap.server.application.port.outbound.task.CommandAttachmentPort;
import clap.server.application.port.outbound.task.CommandTaskPort;

import clap.server.common.annotation.architecture.ApplicationService;
import clap.server.domain.model.member.Member;
import clap.server.domain.model.notification.Notification;
import clap.server.domain.model.task.Attachment;
import clap.server.domain.model.task.Category;
import clap.server.domain.model.task.FilePath;
import clap.server.domain.model.task.Task;
import lombok.RequiredArgsConstructor;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

import static clap.server.domain.model.notification.Notification.createTaskNotification;


@ApplicationService
@RequiredArgsConstructor
Expand All @@ -33,37 +36,36 @@ public class CreateTaskService implements CreateTaskUsecase {
private final CategoryService categoryService;
private final CommandTaskPort commandTaskPort;
private final CommandAttachmentPort commandAttachmentPort;
private final S3UploadAdapter s3UploadAdapter;
private final ApplicationEventPublisher applicationEventPublisher;

@Override
@Transactional
public CreateTaskResponse createTask(Long requesterId, CreateTaskRequest createTaskRequest) {
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());
Task savedTask = commandTaskPort.save(task);

List<Attachment> attachments = Attachment.createAttachments(savedTask, createTaskRequest.fileUrls());
commandAttachmentPort.saveAll(attachments);

saveAttachments(files, savedTask);
publishNotification(savedTask);
return TaskMapper.toCreateTaskResponse(savedTask);
}

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

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


// 검토자들 각각에 대한 알림 생성 후 event 발행
for (Member reviewer : reviewers) {
Notification notification = Notification.builder()
.task(savedTask)
.type(NotificationType.TASK_REQUESTED)
.receiver(reviewer)
.message(null)
.build();
// publish event로 event 발행
Notification notification = createTaskNotification(task, reviewer, NotificationType.TASK_REQUESTED);
applicationEventPublisher.publishEvent(notification);
}

return TaskMapper.toCreateTaskResponse(savedTask);
}
}

}
Loading
Loading