Skip to content
24 changes: 24 additions & 0 deletions infra/terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,30 @@ resource "aws_s3_bucket_public_access_block" "public-access" {
restrict_public_buckets = false
}

# S3 접근 정책 추가
resource "aws_s3_bucket_policy" "bucket-policy" {
bucket = aws_s3_bucket.s3_1.id

depends_on = [
aws_s3_bucket_public_access_block.public-access
]

policy = <<POLICY
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"PublicRead",
"Effect":"Allow",
"Principal": "*",
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::${aws_s3_bucket.s3_1.id}/*"]
}
]
}
POLICY
}

# S3 인스턴스 생성
resource "aws_s3_bucket" "s3_1" {
bucket = "team5-s3-1"
Expand Down
16 changes: 10 additions & 6 deletions src/main/java/com/back/domain/file/controller/FileController.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,26 @@
import com.back.domain.file.service.FileService;
import com.back.global.common.dto.RsData;
import com.back.global.security.user.CustomUserDetails;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/file")
@Validated
public class FileController {
private final FileService fileService;

@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<RsData<FileUploadResponseDto>> uploadFile(
@ModelAttribute FileUploadRequestDto req,
@ModelAttribute @Valid FileUploadRequestDto req,
@AuthenticationPrincipal CustomUserDetails user
) {
FileUploadResponseDto res = fileService.uploadFile(
Expand All @@ -37,8 +41,8 @@ public ResponseEntity<RsData<FileUploadResponseDto>> uploadFile(

@GetMapping(value = "/read")
public ResponseEntity<RsData<FileReadResponseDto>> getFile(
@RequestParam("entityType") EntityType entityType,
@RequestParam("entityId") Long entityId
@RequestParam("entityType") @NotBlank(message = "entityType은 필수입니다.") EntityType entityType,
@RequestParam("entityId") @NotBlank(message = "entityId는 필수입니다.") Long entityId
) {
FileReadResponseDto res = fileService.getFile(entityType, entityId);

Expand All @@ -49,7 +53,7 @@ public ResponseEntity<RsData<FileReadResponseDto>> getFile(

@PutMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<RsData<Void>> updateFile(
@ModelAttribute FileUpdateRequestDto req,
@ModelAttribute @Valid FileUpdateRequestDto req,
@AuthenticationPrincipal CustomUserDetails user
) {
fileService.updateFile(
Expand All @@ -66,8 +70,8 @@ public ResponseEntity<RsData<Void>> updateFile(

@DeleteMapping(value = "/delete")
public ResponseEntity<RsData<Void>> deleteFile(
@RequestParam("entityType") EntityType entityType,
@RequestParam("entityId") Long entityId,
@RequestParam("entityType") @NotBlank(message = "entityType은 필수입니다.") EntityType entityType,
@RequestParam("entityId") @NotBlank(message = "entityId는 필수입니다.") Long entityId,
@AuthenticationPrincipal CustomUserDetails user
) {
fileService.deleteFile(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.back.domain.file.dto;

import com.back.domain.file.entity.EntityType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class FileUpdateRequestDto {
@NotNull(message = "파일 입력은 필수입니다.")
private MultipartFile multipartFile;

private EntityType entityType;

private Long entityId;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.back.domain.file.dto;

import com.back.domain.file.entity.EntityType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;

@Data
public class FileUploadRequestDto {
@NotNull(message = "파일 입력은 필수입니다.")
private MultipartFile multipartFile;

private EntityType entityType;

private Long entityId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ public class FileUploadResponseDto {
public FileUploadResponseDto(String imageUrl) {
this.imageUrl = imageUrl;
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/com/back/domain/file/entity/EntityType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.back.domain.file.entity;

public enum EntityType {
POST, COMMENT, CHAT
}
POST, COMMENT
}
60 changes: 33 additions & 27 deletions src/main/java/com/back/domain/file/service/FileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.back.domain.file.entity.FileAttachment;
import com.back.domain.file.repository.AttachmentMappingRepository;
import com.back.domain.file.repository.FileAttachmentRepository;
import com.back.domain.file.util.EntityValidator;
import com.back.domain.user.entity.User;
import com.back.domain.user.repository.UserRepository;
import com.back.global.exception.CustomException;
Expand All @@ -34,6 +35,7 @@ public class FileService {
private final FileAttachmentRepository fileAttachmentRepository;
private final UserRepository userRepository;
private final AttachmentMappingRepository attachmentMappingRepository;
private final EntityValidator entityValidator;

@Transactional
public FileUploadResponseDto uploadFile(
Expand All @@ -50,7 +52,7 @@ public FileUploadResponseDto uploadFile(
// S3에 저장할 파일 이름
String storedFileName = createFileName(multipartFile.getOriginalFilename());

// S3의 저장된 파일의 public URL
// S3의 저장된 파일의 PublicURL
String filePath = s3Upload(storedFileName, multipartFile);

// FileAttachment 정보 저장
Expand All @@ -74,13 +76,9 @@ public FileReadResponseDto getFile(
EntityType entityType,
Long entityId
) {
AttachmentMapping attachmentMapping = attachmentMappingRepository
.findByEntityTypeAndEntityId(entityType, entityId)
.orElseThrow(() ->
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
);
FileAttachment fileAttachment = getFileAttachmentOrThrow(entityType, entityId);

String filePath = attachmentMapping.getFileAttachment().getFilePath();
String filePath = fileAttachment.getFilePath();

return new FileReadResponseDto(filePath);
}
Expand All @@ -92,18 +90,11 @@ public void updateFile(
Long entityId,
Long userId
) {
AttachmentMapping attachmentMapping = attachmentMappingRepository
.findByEntityTypeAndEntityId(entityType, entityId)
.orElseThrow(() ->
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
);
entityValidator.validate(entityType, entityId);

FileAttachment fileAttachment = attachmentMapping.getFileAttachment();
FileAttachment fileAttachment = getFileAttachmentOrThrow(entityType, entityId);

// 파일 접근 권한 체크
if (fileAttachment.getUser().getId() != userId) {
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
}
checkAccessPermission(fileAttachment, userId);

// 현재 저장된(삭제할) 파일 이름
String oldStoredName = fileAttachment.getStoredName();
Expand All @@ -113,7 +104,6 @@ public void updateFile(

String filePath = s3Upload(newStoredName, multipartFile);

// S3에 기존에 저장된 파일 삭제
s3Delete(oldStoredName);

// fileAttachment 정보 업데이트
Expand All @@ -122,6 +112,8 @@ public void updateFile(

@Transactional
public void deleteFile(EntityType entityType, Long entityId, Long userId) {
entityValidator.validate(entityType, entityId);

AttachmentMapping attachmentMapping = attachmentMappingRepository
.findByEntityTypeAndEntityId(entityType, entityId)
.orElseThrow(() ->
Expand All @@ -130,19 +122,15 @@ public void deleteFile(EntityType entityType, Long entityId, Long userId) {

FileAttachment fileAttachment = attachmentMapping.getFileAttachment();

// 파일 접근 권한 체크
if (fileAttachment.getUser().getId() != userId) {
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
}
checkAccessPermission(fileAttachment, userId);

// S3 오브젝트 삭제
s3Delete(fileAttachment.getStoredName());

// fileAttachment 정보 삭제
fileAttachmentRepository.delete(fileAttachment);
}

// S3에 파일을 업로드 하는 함수
// S3 오브젝트 생성
private String s3Upload(
String storedFileName,
MultipartFile multipartFile
Expand Down Expand Up @@ -173,13 +161,31 @@ private String s3Upload(
return filePath;
}

// S3 파일 삭제 함수
// S3 오브젝트 삭제
private void s3Delete(String fileName) {
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
}

// 파일 이름을 난수화 하기 위한 함수
// 파일 이름을 난수화
private String createFileName(String fileName) {
return UUID.randomUUID().toString().concat(fileName);
}
}

// 파일 접근 권한 체크
private void checkAccessPermission(FileAttachment fileAttachment, Long userId) {
if (fileAttachment.getUser().getId() != userId) {
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
}
}

// AttachmentMapping -> fileAttachment 추출
private FileAttachment getFileAttachmentOrThrow(EntityType entityType, Long entityId) {
AttachmentMapping attachmentMapping = attachmentMappingRepository
.findByEntityTypeAndEntityId(entityType, entityId)
.orElseThrow(() ->
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
);

return attachmentMapping.getFileAttachment();
}
}
31 changes: 31 additions & 0 deletions src/main/java/com/back/domain/file/util/EntityValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.back.domain.file.util;

import com.back.domain.board.comment.repository.CommentRepository;
import com.back.domain.board.post.repository.PostRepository;
import com.back.domain.file.entity.EntityType;
import com.back.global.exception.CustomException;
import com.back.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

/**
* EntityType, EntityId를 통해 매핑되는 데이터 존재 확인
*/
@Component
@RequiredArgsConstructor
public class EntityValidator {
private final PostRepository postRepository;
private final CommentRepository commentRepository;

public void validate(EntityType entityType, Long entityId) {
switch (entityType) {
case POST:
if(!postRepository.existsById(entityId)) throw new CustomException(ErrorCode.POST_NOT_FOUND);
break;

case COMMENT:
if(!commentRepository.existsById(entityId)) throw new CustomException(ErrorCode.COMMENT_NOT_FOUND);
break;
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/back/global/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public WebMvcConfigurer corsConfigurer() {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:3000" // Next.js 개발 서버
"http://localhost:3000", // Catfe 프론트 개발 서버
"https://www.catfe.com" // Catfe 프론트 운영 서버
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowedHeaders("*")
Expand Down
Loading