diff --git a/src/main/java/com/tiki/server/common/entity/Position.java b/src/main/java/com/tiki/server/common/entity/Position.java index 4148eda9..c9d25f38 100644 --- a/src/main/java/com/tiki/server/common/entity/Position.java +++ b/src/main/java/com/tiki/server/common/entity/Position.java @@ -1,5 +1,14 @@ package com.tiki.server.common.entity; +import lombok.Getter; + +@Getter public enum Position { - ADMIN, EXECUTIVE, MEMBER, + ADMIN(1), EXECUTIVE(2), MEMBER(3); + + private final int authorization; + + Position(int authorization) { + this.authorization = authorization; + } } diff --git a/src/main/java/com/tiki/server/common/support/UriGenerator.java b/src/main/java/com/tiki/server/common/support/UriGenerator.java new file mode 100644 index 00000000..4c34d24e --- /dev/null +++ b/src/main/java/com/tiki/server/common/support/UriGenerator.java @@ -0,0 +1,26 @@ +package com.tiki.server.common.support; + +import java.net.URI; + +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@Component +public class UriGenerator { + + public static URI getUri(String path, long id) { + return ServletUriComponentsBuilder + .fromCurrentRequest() + .path(path + id) + .buildAndExpand() + .toUri(); + } + + public static URI getUri(String path) { + return ServletUriComponentsBuilder + .fromCurrentRequest() + .path(path) + .buildAndExpand() + .toUri(); + } +} diff --git a/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java b/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java new file mode 100644 index 00000000..2b27c951 --- /dev/null +++ b/src/main/java/com/tiki/server/document/adapter/DocumentSaver.java @@ -0,0 +1,18 @@ +package com.tiki.server.document.adapter; + +import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.document.entity.Document; +import com.tiki.server.document.repository.DocumentRepository; + +import lombok.RequiredArgsConstructor; + +@RepositoryAdapter +@RequiredArgsConstructor +public class DocumentSaver { + + private final DocumentRepository documentRepository; + + public void save(Document document) { + documentRepository.save(document); + } +} diff --git a/src/main/java/com/tiki/server/document/entity/DeletedDocument.java b/src/main/java/com/tiki/server/document/entity/DeletedDocument.java new file mode 100644 index 00000000..dcf7da07 --- /dev/null +++ b/src/main/java/com/tiki/server/document/entity/DeletedDocument.java @@ -0,0 +1,43 @@ +package com.tiki.server.document.entity; + +import static jakarta.persistence.GenerationType.IDENTITY; + +import java.time.LocalDate; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class DeletedDocument { + + @Id + @GeneratedValue(strategy = IDENTITY) + @Column(name = "deleted_document_id") + private Long id; + + private String fileName; + + private String fileUrl; + + @Column(name = "block_id") + private long timeBlockId; + + private LocalDate deletedDate; + + @Builder + public static DeletedDocument of(String fileName, String fileUrl, long timeBlockId, LocalDate deletedDate) { + return DeletedDocument.builder() + .fileName(fileName) + .fileUrl(fileUrl) + .timeBlockId(timeBlockId) + .deletedDate(deletedDate) + .build(); + } +} diff --git a/src/main/java/com/tiki/server/document/entity/Document.java b/src/main/java/com/tiki/server/document/entity/Document.java index 91e13950..1c9de49b 100644 --- a/src/main/java/com/tiki/server/document/entity/Document.java +++ b/src/main/java/com/tiki/server/document/entity/Document.java @@ -1,28 +1,29 @@ package com.tiki.server.document.entity; -import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; - -import java.time.LocalDate; -import java.time.LocalDateTime; +import static lombok.AccessLevel.PRIVATE; +import static lombok.AccessLevel.PROTECTED; import com.tiki.server.common.entity.BaseTime; import com.tiki.server.timeblock.entity.TimeBlock; import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor +@Builder(access = PRIVATE) +@AllArgsConstructor(access = PRIVATE) +@NoArgsConstructor(access = PROTECTED) public class Document extends BaseTime { @Id @@ -30,14 +31,19 @@ public class Document extends BaseTime { @Column(name = "document_id") private Long id; + private String fileName; + private String fileUrl; @ManyToOne(fetch = LAZY) @JoinColumn(name = "block_id") private TimeBlock timeBlock; - @Enumerated(value = STRING) - private DocumentStatus status; - - private LocalDate deletedDate; + public static Document of(String fileName, String fileUrl, TimeBlock timeBlock) { + return Document.builder() + .fileName(fileName) + .fileUrl(fileUrl) + .timeBlock(timeBlock) + .build(); + } } diff --git a/src/main/java/com/tiki/server/document/entity/DocumentStatus.java b/src/main/java/com/tiki/server/document/entity/DocumentStatus.java deleted file mode 100644 index a55db05b..00000000 --- a/src/main/java/com/tiki/server/document/entity/DocumentStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.tiki.server.document.entity; - -public enum DocumentStatus { - BASIC, DELETED -} diff --git a/src/main/java/com/tiki/server/external/constant/ExternalConstant.java b/src/main/java/com/tiki/server/external/constant/ExternalConstant.java new file mode 100644 index 00000000..0c0fbd32 --- /dev/null +++ b/src/main/java/com/tiki/server/external/constant/ExternalConstant.java @@ -0,0 +1,7 @@ +package com.tiki.server.external.constant; + +public class ExternalConstant { + + public static final Long PRE_SIGNED_URL_EXPIRE_MINUTE = 10L; + public static final String FILE_SAVE_PREFIX = "file/"; +} diff --git a/src/main/java/com/tiki/server/external/controller/S3Controller.java b/src/main/java/com/tiki/server/external/controller/S3Controller.java new file mode 100644 index 00000000..993651d5 --- /dev/null +++ b/src/main/java/com/tiki/server/external/controller/S3Controller.java @@ -0,0 +1,32 @@ +package com.tiki.server.external.controller; + +import static com.tiki.server.common.dto.SuccessResponse.*; +import static com.tiki.server.external.message.SuccessMessage.PRESIGNED_URL_GET_SUCCESS; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.tiki.server.common.dto.SuccessResponse; +import com.tiki.server.external.dto.request.PreSignedUrlRequest; +import com.tiki.server.external.dto.response.PreSignedUrlResponse; +import com.tiki.server.external.util.S3Service; + +import lombok.RequiredArgsConstructor; +import lombok.val; + +@RestController +@RequestMapping("api/v1/file") +@RequiredArgsConstructor +public class S3Controller { + + private final S3Service s3Service; + + @GetMapping("/upload") + public ResponseEntity> getPreSignedUrl(@RequestBody PreSignedUrlRequest request) { + val response = s3Service.getUploadPreSignedUrl(request); + return ResponseEntity.ok(success(PRESIGNED_URL_GET_SUCCESS.getMessage(), response)); + } +} diff --git a/src/main/java/com/tiki/server/external/dto/request/PreSignedUrlRequest.java b/src/main/java/com/tiki/server/external/dto/request/PreSignedUrlRequest.java new file mode 100644 index 00000000..5b631ce9 --- /dev/null +++ b/src/main/java/com/tiki/server/external/dto/request/PreSignedUrlRequest.java @@ -0,0 +1,8 @@ +package com.tiki.server.external.dto.request; + +import lombok.NonNull; + +public record PreSignedUrlRequest( + @NonNull String fileFormat +) { +} diff --git a/src/main/java/com/tiki/server/external/dto/response/PreSignedUrlResponse.java b/src/main/java/com/tiki/server/external/dto/response/PreSignedUrlResponse.java new file mode 100644 index 00000000..683ab43f --- /dev/null +++ b/src/main/java/com/tiki/server/external/dto/response/PreSignedUrlResponse.java @@ -0,0 +1,20 @@ +package com.tiki.server.external.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.Builder; +import lombok.NonNull; + +@Builder(access = PRIVATE) +public record PreSignedUrlResponse( + @NonNull String fileName, + @NonNull String url +) { + + public static PreSignedUrlResponse of(String fileName, String url) { + return PreSignedUrlResponse.builder() + .fileName(fileName) + .url(url) + .build(); + } +} diff --git a/src/main/java/com/tiki/server/external/message/ErrorCode.java b/src/main/java/com/tiki/server/external/message/ErrorCode.java index 782cba3e..778f8e12 100644 --- a/src/main/java/com/tiki/server/external/message/ErrorCode.java +++ b/src/main/java/com/tiki/server/external/message/ErrorCode.java @@ -1,6 +1,7 @@ package com.tiki.server.external.message; import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR; import org.springframework.http.HttpStatus; @@ -11,8 +12,9 @@ @AllArgsConstructor public enum ErrorCode { - /* 400 BAD_REQUEST : 잘못된 요청 */ - INVALID_FILE_SIZE(BAD_REQUEST, "파일의 용량은 30MB를 초과할 수 없습니다."); + /* 500 INTERNAL_SERVER_ERROR : 서버 에러 */ + PRESIGNED_URL_GET_ERROR(INTERNAL_SERVER_ERROR, "S3 PRESIGNED URL 불러오기 실패"), + FILE_DELETE_ERROR(INTERNAL_SERVER_ERROR, "S3 버킷의 파일 삭제 실패"); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/tiki/server/external/message/SuccessMessage.java b/src/main/java/com/tiki/server/external/message/SuccessMessage.java new file mode 100644 index 00000000..9a16e4b8 --- /dev/null +++ b/src/main/java/com/tiki/server/external/message/SuccessMessage.java @@ -0,0 +1,13 @@ +package com.tiki.server.external.message; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SuccessMessage { + + PRESIGNED_URL_GET_SUCCESS("S3 PRESIGNED URL 불러오기 성공"); + + private final String message; +} diff --git a/src/main/java/com/tiki/server/external/util/S3Service.java b/src/main/java/com/tiki/server/external/util/S3Service.java index c8e98e9e..72b260ff 100644 --- a/src/main/java/com/tiki/server/external/util/S3Service.java +++ b/src/main/java/com/tiki/server/external/util/S3Service.java @@ -1,8 +1,11 @@ package com.tiki.server.external.util; +import static com.tiki.server.external.constant.ExternalConstant.FILE_SAVE_PREFIX; +import static com.tiki.server.external.constant.ExternalConstant.PRE_SIGNED_URL_EXPIRE_MINUTE; import static com.tiki.server.external.message.ErrorCode.*; import java.io.IOException; +import java.time.Duration; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; @@ -10,6 +13,8 @@ import org.springframework.web.multipart.MultipartFile; import com.tiki.server.external.config.AWSConfig; +import com.tiki.server.external.dto.request.PreSignedUrlRequest; +import com.tiki.server.external.dto.response.PreSignedUrlResponse; import com.tiki.server.external.exception.ExternalException; import com.tiki.server.external.message.ErrorCode; @@ -18,16 +23,13 @@ import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; @Component @RequiredArgsConstructor public class S3Service { - private static final Long MAX_FILE_SIZE = 30 * 1024 * 1024L; - private static final String FILE_NAME_START_POINT = "."; - private static final String CONTENT_DISPOSITION = "inline"; - private static final String DELIMITER = "/"; - private final AWSConfig awsConfig; @Value("${aws-property.bucket}") @@ -36,49 +38,48 @@ public class S3Service { @Value("${aws-property.s3-url}") private String s3URL; - public String uploadFile(String directoryPath, MultipartFile file) { - validateFileSize(file); + public PreSignedUrlResponse getUploadPreSignedUrl(PreSignedUrlRequest request) { try { - val key = directoryPath + DELIMITER + generateFileName(file); - val s3Client = awsConfig.getS3Client(); - val request = createRequest(key, file.getContentType()); - val requestBody = RequestBody.fromBytes(file.getBytes()); - s3Client.putObject(request, requestBody); - return s3URL + key; - } catch (IOException exception) { - throw new IllegalArgumentException(); + val fileName = generateFileName(request.fileFormat()); + val key = FILE_SAVE_PREFIX + fileName; + val preSigner = awsConfig.getS3PreSigner(); + val putObjectRequest = createPutObjectRequest(key); + val putObjectPresignRequest = createPutObjectPresignRequest(putObjectRequest); + val url = preSigner.presignPutObject(putObjectPresignRequest).url().toString(); + return PreSignedUrlResponse.of(fileName, url); + } catch (RuntimeException e) { + throw new ExternalException(PRESIGNED_URL_GET_ERROR); } } public void deleteFile(String key) throws IOException { - val s3Client = awsConfig.getS3Client(); - s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> - builder.bucket(bucket) - .key(key) - .build() - ); + try { + val s3Client = awsConfig.getS3Client(); + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucket) + .key(key) + .build() + ); + } catch (RuntimeException e) { + throw new ExternalException(FILE_DELETE_ERROR); + } } - private PutObjectRequest createRequest(String key, String contentType) { + private PutObjectRequest createPutObjectRequest(String key) { return PutObjectRequest.builder() .bucket(bucket) .key(key) - .contentType(contentType) - .contentDisposition(CONTENT_DISPOSITION) .build(); } - private String generateFileName(MultipartFile file) { - return UUID.randomUUID() + getFileFormat(file.getName()); - } - - private String getFileFormat(String fileName) { - return fileName.substring(fileName.lastIndexOf(FILE_NAME_START_POINT)); + private PutObjectPresignRequest createPutObjectPresignRequest(PutObjectRequest putObjectRequest) { + return PutObjectPresignRequest.builder() + .signatureDuration(Duration.ofMinutes(PRE_SIGNED_URL_EXPIRE_MINUTE)) + .putObjectRequest(putObjectRequest) + .build(); } - private void validateFileSize(MultipartFile file) { - if (file.getSize() > MAX_FILE_SIZE) { - throw new ExternalException(INVALID_FILE_SIZE); - } + private String generateFileName(String fileFormat) { + return UUID.randomUUID() + fileFormat; } } diff --git a/src/main/java/com/tiki/server/member/adapter/MemberFinder.java b/src/main/java/com/tiki/server/member/adapter/MemberFinder.java index 7371615b..70e06408 100644 --- a/src/main/java/com/tiki/server/member/adapter/MemberFinder.java +++ b/src/main/java/com/tiki/server/member/adapter/MemberFinder.java @@ -1,7 +1,14 @@ package com.tiki.server.member.adapter; +import static com.tiki.server.member.message.ErrorCode.INVALID_MEMBER; +import static com.tiki.server.team.message.ErrorCode.INVALID_TEAM; + import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.member.entity.Member; +import com.tiki.server.member.exception.MemberException; import com.tiki.server.member.repository.MemberRepository; +import com.tiki.server.team.entity.Team; +import com.tiki.server.team.exception.TeamException; import lombok.RequiredArgsConstructor; @@ -10,4 +17,9 @@ public class MemberFinder { private final MemberRepository memberRepository; + + public Member findById(long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new MemberException(INVALID_MEMBER)); + } } diff --git a/src/main/java/com/tiki/server/memberteammanager/adapter/MemberTeamManagerFinder.java b/src/main/java/com/tiki/server/memberteammanager/adapter/MemberTeamManagerFinder.java index 70ad2a7b..e9a5f438 100644 --- a/src/main/java/com/tiki/server/memberteammanager/adapter/MemberTeamManagerFinder.java +++ b/src/main/java/com/tiki/server/memberteammanager/adapter/MemberTeamManagerFinder.java @@ -1,6 +1,10 @@ package com.tiki.server.memberteammanager.adapter; +import static com.tiki.server.memberteammanager.message.ErrorCode.INVALID_MEMBER_TEAM_MANAGER; + import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.memberteammanager.entity.MemberTeamManager; +import com.tiki.server.memberteammanager.exception.MemberTeamManagerException; import com.tiki.server.memberteammanager.repository.MemberTeamManagerRepository; import lombok.RequiredArgsConstructor; @@ -10,4 +14,9 @@ public class MemberTeamManagerFinder { private final MemberTeamManagerRepository teamManagerRepository; + + public MemberTeamManager findByMemberIdAndTeamId(long memberId, long teamId) { + return teamManagerRepository.findByMemberIdAndTeamId(memberId, teamId) + .orElseThrow(() -> new MemberTeamManagerException(INVALID_MEMBER_TEAM_MANAGER)); + } } diff --git a/src/main/java/com/tiki/server/memberteammanager/message/ErrorCode.java b/src/main/java/com/tiki/server/memberteammanager/message/ErrorCode.java index deca373d..079ecc6f 100644 --- a/src/main/java/com/tiki/server/memberteammanager/message/ErrorCode.java +++ b/src/main/java/com/tiki/server/memberteammanager/message/ErrorCode.java @@ -12,7 +12,7 @@ public enum ErrorCode { /* 404 NOT_FOUND : 자원을 찾을 수 없음 */ - TEMP(NOT_FOUND, "컴파일 에러 방지용 에러입니다."); + INVALID_MEMBER_TEAM_MANAGER(NOT_FOUND, "팀에 존재하지 않는 회원입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/tiki/server/memberteammanager/repository/MemberTeamManagerRepository.java b/src/main/java/com/tiki/server/memberteammanager/repository/MemberTeamManagerRepository.java index 8ac15d84..58cf4be2 100644 --- a/src/main/java/com/tiki/server/memberteammanager/repository/MemberTeamManagerRepository.java +++ b/src/main/java/com/tiki/server/memberteammanager/repository/MemberTeamManagerRepository.java @@ -1,8 +1,11 @@ package com.tiki.server.memberteammanager.repository; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import com.tiki.server.memberteammanager.entity.MemberTeamManager; public interface MemberTeamManagerRepository extends JpaRepository { + Optional findByMemberIdAndTeamId(Long memberId, Long teamId); } diff --git a/src/main/java/com/tiki/server/team/adapter/TeamFinder.java b/src/main/java/com/tiki/server/team/adapter/TeamFinder.java index 0c88eab6..bd4abf33 100644 --- a/src/main/java/com/tiki/server/team/adapter/TeamFinder.java +++ b/src/main/java/com/tiki/server/team/adapter/TeamFinder.java @@ -1,6 +1,10 @@ package com.tiki.server.team.adapter; +import static com.tiki.server.team.message.ErrorCode.INVALID_TEAM; + import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.team.entity.Team; +import com.tiki.server.team.exception.TeamException; import com.tiki.server.team.repository.TeamRepository; import lombok.RequiredArgsConstructor; @@ -10,4 +14,9 @@ public class TeamFinder { private final TeamRepository teamRepository; + + public Team findById(long teamId) { + return teamRepository.findById(teamId) + .orElseThrow(() -> new TeamException(INVALID_TEAM)); + } } diff --git a/src/main/java/com/tiki/server/team/controller/TeamController.java b/src/main/java/com/tiki/server/team/controller/TeamController.java index 113e80c9..e3df8838 100644 --- a/src/main/java/com/tiki/server/team/controller/TeamController.java +++ b/src/main/java/com/tiki/server/team/controller/TeamController.java @@ -1,7 +1,5 @@ package com.tiki.server.team.controller; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; diff --git a/src/main/java/com/tiki/server/team/entity/TeamType.java b/src/main/java/com/tiki/server/team/entity/Group.java similarity index 72% rename from src/main/java/com/tiki/server/team/entity/TeamType.java rename to src/main/java/com/tiki/server/team/entity/Group.java index 118df65b..c0f718b0 100644 --- a/src/main/java/com/tiki/server/team/entity/TeamType.java +++ b/src/main/java/com/tiki/server/team/entity/Group.java @@ -1,5 +1,5 @@ package com.tiki.server.team.entity; -public enum TeamType { +public enum Group { ALLIANCE, UNIVERSITY } diff --git a/src/main/java/com/tiki/server/team/entity/Team.java b/src/main/java/com/tiki/server/team/entity/Team.java index bcaf60c0..d90efe07 100644 --- a/src/main/java/com/tiki/server/team/entity/Team.java +++ b/src/main/java/com/tiki/server/team/entity/Team.java @@ -31,7 +31,7 @@ public class Team extends BaseTime { private Category category; @Enumerated(value = STRING) - private TeamType teamType; + private Group group; private String imageUrl; diff --git a/src/main/java/com/tiki/server/timeblock/adapter/TimeBlockSaver.java b/src/main/java/com/tiki/server/timeblock/adapter/TimeBlockSaver.java new file mode 100644 index 00000000..f72bda9b --- /dev/null +++ b/src/main/java/com/tiki/server/timeblock/adapter/TimeBlockSaver.java @@ -0,0 +1,18 @@ +package com.tiki.server.timeblock.adapter; + +import com.tiki.server.common.support.RepositoryAdapter; +import com.tiki.server.timeblock.entity.TimeBlock; +import com.tiki.server.timeblock.repository.TimeBlockRepository; + +import lombok.RequiredArgsConstructor; + +@RepositoryAdapter +@RequiredArgsConstructor +public class TimeBlockSaver { + + private final TimeBlockRepository timeBlockRepository; + + public TimeBlock save(TimeBlock timeBlock) { + return timeBlockRepository.save(timeBlock); + } +} diff --git a/src/main/java/com/tiki/server/timeblock/constant/TimeBlockConstant.java b/src/main/java/com/tiki/server/timeblock/constant/TimeBlockConstant.java new file mode 100644 index 00000000..8ea4fa9d --- /dev/null +++ b/src/main/java/com/tiki/server/timeblock/constant/TimeBlockConstant.java @@ -0,0 +1,7 @@ +package com.tiki.server.timeblock.constant; + +public class TimeBlockConstant { + + public static final String EXECUTIVE = "executive"; + public static final String MEMBER = "member"; +} diff --git a/src/main/java/com/tiki/server/timeblock/controller/TimeBlockController.java b/src/main/java/com/tiki/server/timeblock/controller/TimeBlockController.java index b5a24987..fb7ed866 100644 --- a/src/main/java/com/tiki/server/timeblock/controller/TimeBlockController.java +++ b/src/main/java/com/tiki/server/timeblock/controller/TimeBlockController.java @@ -1,11 +1,26 @@ package com.tiki.server.timeblock.controller; +import static com.tiki.server.common.dto.SuccessResponse.*; +import static com.tiki.server.timeblock.message.SuccessMessage.SUCCESS_CREATE_TIME_BLOCK; + +import java.security.Principal; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.tiki.server.common.dto.SuccessResponse; +import com.tiki.server.common.support.UriGenerator; +import com.tiki.server.timeblock.dto.request.TimeBlockCreationRequest; +import com.tiki.server.timeblock.dto.response.TimeBlockCreationResponse; import com.tiki.server.timeblock.service.TimeBlockService; import lombok.RequiredArgsConstructor; +import lombok.val; @RestController @RequiredArgsConstructor @@ -13,4 +28,18 @@ public class TimeBlockController { private final TimeBlockService timeBlockService; + + @PostMapping("/team/{teamId}/time-block") + public ResponseEntity> createTimeBlock( + Principal principal, + @PathVariable("teamId") long teamId, + @RequestParam String type, + @RequestBody TimeBlockCreationRequest request + ) { + val memberId = Long.parseLong(principal.getName()); + val response = timeBlockService.createTimeBlock(memberId, teamId, type, request); + return ResponseEntity.created( + UriGenerator.getUri("/api/v1/time-blocks/team/" + teamId + "/time-block") + ).body(success(SUCCESS_CREATE_TIME_BLOCK.getMessage(), response)); + } } diff --git a/src/main/java/com/tiki/server/timeblock/dto/request/TimeBlockCreationRequest.java b/src/main/java/com/tiki/server/timeblock/dto/request/TimeBlockCreationRequest.java new file mode 100644 index 00000000..16bea0be --- /dev/null +++ b/src/main/java/com/tiki/server/timeblock/dto/request/TimeBlockCreationRequest.java @@ -0,0 +1,15 @@ +package com.tiki.server.timeblock.dto.request; + +import java.time.LocalDate; +import java.util.Map; + +import lombok.NonNull; + +public record TimeBlockCreationRequest( + @NonNull String name, + @NonNull String color, + @NonNull LocalDate startDate, + @NonNull LocalDate endDate, + Map files +) { +} diff --git a/src/main/java/com/tiki/server/timeblock/dto/response/TimeBlockCreationResponse.java b/src/main/java/com/tiki/server/timeblock/dto/response/TimeBlockCreationResponse.java new file mode 100644 index 00000000..24a1bb7e --- /dev/null +++ b/src/main/java/com/tiki/server/timeblock/dto/response/TimeBlockCreationResponse.java @@ -0,0 +1,17 @@ +package com.tiki.server.timeblock.dto.response; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.Builder; + +@Builder(access = PRIVATE) +public record TimeBlockCreationResponse( + long timeBlockId +) { + + public static TimeBlockCreationResponse of(long timeBlockId) { + return TimeBlockCreationResponse.builder() + .timeBlockId(timeBlockId) + .build(); + } +} diff --git a/src/main/java/com/tiki/server/timeblock/entity/TimeBlock.java b/src/main/java/com/tiki/server/timeblock/entity/TimeBlock.java index 57622663..4ab18e37 100644 --- a/src/main/java/com/tiki/server/timeblock/entity/TimeBlock.java +++ b/src/main/java/com/tiki/server/timeblock/entity/TimeBlock.java @@ -3,12 +3,15 @@ import static jakarta.persistence.EnumType.STRING; import static jakarta.persistence.FetchType.LAZY; import static jakarta.persistence.GenerationType.IDENTITY; +import static lombok.AccessLevel.PRIVATE; +import static lombok.AccessLevel.PROTECTED; import java.time.LocalDate; import com.tiki.server.common.entity.BaseTime; import com.tiki.server.common.entity.Position; import com.tiki.server.team.entity.Team; +import com.tiki.server.timeblock.dto.request.TimeBlockCreationRequest; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -17,12 +20,16 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter -@NoArgsConstructor +@Builder(access = PRIVATE) +@AllArgsConstructor(access = PRIVATE) +@NoArgsConstructor(access = PROTECTED) public class TimeBlock extends BaseTime { @Id @@ -30,18 +37,29 @@ public class TimeBlock extends BaseTime { @Column(name = "block_id") private Long id; - @ManyToOne(fetch = LAZY) - @JoinColumn(name = "team_id") - private Team team; + private String name; private String color; - private String name; - @Enumerated(value = STRING) private Position accessiblePosition; private LocalDate startDate; private LocalDate endDate; + + @ManyToOne(fetch = LAZY) + @JoinColumn(name = "team_id") + private Team team; + + public static TimeBlock of(Team team, Position accessiblePosition, TimeBlockCreationRequest request) { + return TimeBlock.builder() + .name(request.name()) + .color(request.color()) + .accessiblePosition(accessiblePosition) + .startDate(request.startDate()) + .endDate(request.endDate()) + .team(team) + .build(); + } } diff --git a/src/main/java/com/tiki/server/timeblock/message/ErrorCode.java b/src/main/java/com/tiki/server/timeblock/message/ErrorCode.java index 6c062220..c3252820 100644 --- a/src/main/java/com/tiki/server/timeblock/message/ErrorCode.java +++ b/src/main/java/com/tiki/server/timeblock/message/ErrorCode.java @@ -1,6 +1,7 @@ package com.tiki.server.timeblock.message; -import static org.springframework.http.HttpStatus.NOT_FOUND; +import static org.springframework.http.HttpStatus.BAD_REQUEST; +import static org.springframework.http.HttpStatus.FORBIDDEN; import org.springframework.http.HttpStatus; @@ -11,8 +12,11 @@ @AllArgsConstructor public enum ErrorCode { - /* 404 NOT_FOUND : 자원을 찾을 수 없음 */ - TEMP(NOT_FOUND, "컴파일 에러 방지용 에러입니다."); + /* 400 BAD_REQUEST : 잘못된 요청 */ + INVALID_TYPE(BAD_REQUEST, "유효한 타입이 아닙니다."), + + /* 403 FORBIDDEN : 권한 없음 */ + INVALID_AUTHORIZATION(FORBIDDEN, "타임블록에 대한 권한이 없습니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/com/tiki/server/timeblock/message/SuccessMessage.java b/src/main/java/com/tiki/server/timeblock/message/SuccessMessage.java index 8c74d2c5..44f1bcaa 100644 --- a/src/main/java/com/tiki/server/timeblock/message/SuccessMessage.java +++ b/src/main/java/com/tiki/server/timeblock/message/SuccessMessage.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor public enum SuccessMessage { - TEMP("컴파일 에러 방지용"); + SUCCESS_CREATE_TIME_BLOCK("타임 블록 생성 성공"); private final String message; } diff --git a/src/main/java/com/tiki/server/timeblock/service/TimeBlockService.java b/src/main/java/com/tiki/server/timeblock/service/TimeBlockService.java index c4447358..fb360327 100644 --- a/src/main/java/com/tiki/server/timeblock/service/TimeBlockService.java +++ b/src/main/java/com/tiki/server/timeblock/service/TimeBlockService.java @@ -1,12 +1,84 @@ package com.tiki.server.timeblock.service; +import static com.tiki.server.timeblock.message.ErrorCode.INVALID_AUTHORIZATION; +import static com.tiki.server.timeblock.message.ErrorCode.INVALID_TYPE; +import static com.tiki.server.timeblock.constant.TimeBlockConstant.EXECUTIVE; +import static com.tiki.server.timeblock.constant.TimeBlockConstant.MEMBER; + +import java.util.Map; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.tiki.server.common.entity.Position; +import com.tiki.server.document.adapter.DocumentSaver; +import com.tiki.server.document.entity.Document; +import com.tiki.server.memberteammanager.adapter.MemberTeamManagerFinder; +import com.tiki.server.team.adapter.TeamFinder; +import com.tiki.server.team.entity.Team; +import com.tiki.server.timeblock.adapter.TimeBlockSaver; +import com.tiki.server.timeblock.dto.request.TimeBlockCreationRequest; +import com.tiki.server.timeblock.dto.response.TimeBlockCreationResponse; +import com.tiki.server.timeblock.entity.TimeBlock; +import com.tiki.server.timeblock.exception.TimeBlockException; + import lombok.RequiredArgsConstructor; +import lombok.val; @Service @RequiredArgsConstructor @Transactional(readOnly = true) public class TimeBlockService { + + private final TeamFinder teamFinder; + private final MemberTeamManagerFinder memberTeamManagerFinder; + private final TimeBlockSaver timeBlockSaver; + private final DocumentSaver documentSaver; + + @Transactional + public TimeBlockCreationResponse createTimeBlock( + long memberId, + long teamId, + String type, + TimeBlockCreationRequest request + ) { + val team = teamFinder.findById(teamId); + val position = memberTeamManagerFinder.findByMemberIdAndTeamId(memberId, teamId).getPosition(); + return switch (type) { + case EXECUTIVE -> createTimeBlockByType(team, Position.EXECUTIVE, position, request); + case MEMBER -> createTimeBlockByType(team, Position.MEMBER, position, request); + default -> throw new TimeBlockException(INVALID_TYPE); + }; + } + + private TimeBlockCreationResponse createTimeBlockByType( + Team team, + Position accessiblePosition, + Position memberPosition, + TimeBlockCreationRequest request + ) { + checkMemberAccessible(accessiblePosition, memberPosition); + val timeBlock = createTimeBlock(team, accessiblePosition, request); + val timeBlockId = timeBlockSaver.save(timeBlock).getId(); + saveDocuments(request.files(), timeBlock); + return TimeBlockCreationResponse.of(timeBlockId); + } + + private void checkMemberAccessible(Position accessiblePosition, Position memberPosition) { + if (accessiblePosition.getAuthorization() < memberPosition.getAuthorization()) { + throw new TimeBlockException(INVALID_AUTHORIZATION); + } + } + + private TimeBlock createTimeBlock(Team team, Position accessiblePosition, TimeBlockCreationRequest request) { + return TimeBlock.of(team, accessiblePosition, request); + } + + private void saveDocuments(Map files, TimeBlock timeBlock) { + files.forEach((fileName, fileUrl) -> documentSaver.save(createDocument(fileName, fileUrl, timeBlock))); + } + + private Document createDocument(String fileName, String fileUrl, TimeBlock timeBlock) { + return Document.of(fileName, fileUrl, timeBlock); + } }