Skip to content

Commit bf3b625

Browse files
authored
Feat: 파일 관련 기능 유효성 검사 추가 및 S3 버킷 정책 수정
* Infra: 버킷 정책 추가 - 이제 S3 오브젝트를 퍼블릭하게 읽을 수 있도록 허용 * Feat: 파일 업로드 유효성 검사 추가 - EntityType, EntityId에 @notblank 적용 * Feat: EntityValidator 생성 - EntityType, EntityId로 해당 데이터의 존재 유무를 체크 * Refactor: 파일 서비스 리팩토링 - 중복되는 로직 메서드화 -> 중복제거 - entityType, entityId로 데이터가 존재하는지 체크하는 로직 추가 * Feat: 유효성 검사 추가 - 파일 업로드 시, 파일 입력을 필수로 지정 * Feat: CORS 허용 URL 추가 - Catfe 프론트 운영 서버 추가 * Test: 파일 업로드 컨트롤러 테스트 코드 구현 - 파일 업로드 성공한 경우 - 파일 업로드 실패한 경우 - 업로드 시, 파일이 없는 경우 * Fix: 파일 업로드 유효성 검사 수정 - multiparFile 필드 만 @NotNull로 지정 * Fix: Test 환경 S3Mock 수정 - 기존 : withport(8001) -> 8001포트에 고정 - 변경 : 할당 withport(0) -> 사용 가능한 랜덤포트 자동할당 * Fix: 파일 컨트롤러 테스트 수정 문제 - 컨트롤러 테스트에서 S3Mock에 사용하는 포트를 점유하는 문제 발생 -> 서비스 테스트에서 S3Mock을 생성하지 못함 해결 - 컨트롤러 테스트가 끝나면 S3Mock의 포트를 해체 시켜 해당 문제 해결
1 parent 2efc53d commit bf3b625

File tree

10 files changed

+249
-38
lines changed

10 files changed

+249
-38
lines changed

infra/terraform/main.tf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,30 @@ resource "aws_s3_bucket_public_access_block" "public-access" {
359359
restrict_public_buckets = false
360360
}
361361

362+
# S3 접근 정책 추가
363+
resource "aws_s3_bucket_policy" "bucket-policy" {
364+
bucket = aws_s3_bucket.s3_1.id
365+
366+
depends_on = [
367+
aws_s3_bucket_public_access_block.public-access
368+
]
369+
370+
policy = <<POLICY
371+
{
372+
"Version":"2012-10-17",
373+
"Statement":[
374+
{
375+
"Sid":"PublicRead",
376+
"Effect":"Allow",
377+
"Principal": "*",
378+
"Action":["s3:GetObject"],
379+
"Resource":["arn:aws:s3:::${aws_s3_bucket.s3_1.id}/*"]
380+
}
381+
]
382+
}
383+
POLICY
384+
}
385+
362386
# S3 인스턴스 생성
363387
resource "aws_s3_bucket" "s3_1" {
364388
bucket = "team5-s3-1"

src/main/java/com/back/domain/file/controller/FileController.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,26 @@
55
import com.back.domain.file.service.FileService;
66
import com.back.global.common.dto.RsData;
77
import com.back.global.security.user.CustomUserDetails;
8+
import jakarta.validation.Valid;
9+
import jakarta.validation.constraints.NotBlank;
810
import lombok.RequiredArgsConstructor;
911
import org.springframework.http.HttpStatus;
1012
import org.springframework.http.MediaType;
1113
import org.springframework.http.ResponseEntity;
1214
import org.springframework.security.core.annotation.AuthenticationPrincipal;
15+
import org.springframework.validation.annotation.Validated;
1316
import org.springframework.web.bind.annotation.*;
1417

1518
@RestController
1619
@RequiredArgsConstructor
1720
@RequestMapping("/api/file")
21+
@Validated
1822
public class FileController {
1923
private final FileService fileService;
2024

2125
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
2226
public ResponseEntity<RsData<FileUploadResponseDto>> uploadFile(
23-
@ModelAttribute FileUploadRequestDto req,
27+
@ModelAttribute @Valid FileUploadRequestDto req,
2428
@AuthenticationPrincipal CustomUserDetails user
2529
) {
2630
FileUploadResponseDto res = fileService.uploadFile(
@@ -37,8 +41,8 @@ public ResponseEntity<RsData<FileUploadResponseDto>> uploadFile(
3741

3842
@GetMapping(value = "/read")
3943
public ResponseEntity<RsData<FileReadResponseDto>> getFile(
40-
@RequestParam("entityType") EntityType entityType,
41-
@RequestParam("entityId") Long entityId
44+
@RequestParam("entityType") @NotBlank(message = "entityType은 필수입니다.") EntityType entityType,
45+
@RequestParam("entityId") @NotBlank(message = "entityId는 필수입니다.") Long entityId
4246
) {
4347
FileReadResponseDto res = fileService.getFile(entityType, entityId);
4448

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

5054
@PutMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
5155
public ResponseEntity<RsData<Void>> updateFile(
52-
@ModelAttribute FileUpdateRequestDto req,
56+
@ModelAttribute @Valid FileUpdateRequestDto req,
5357
@AuthenticationPrincipal CustomUserDetails user
5458
) {
5559
fileService.updateFile(
@@ -66,8 +70,8 @@ public ResponseEntity<RsData<Void>> updateFile(
6670

6771
@DeleteMapping(value = "/delete")
6872
public ResponseEntity<RsData<Void>> deleteFile(
69-
@RequestParam("entityType") EntityType entityType,
70-
@RequestParam("entityId") Long entityId,
73+
@RequestParam("entityType") @NotBlank(message = "entityType은 필수입니다.") EntityType entityType,
74+
@RequestParam("entityId") @NotBlank(message = "entityId는 필수입니다.") Long entityId,
7175
@AuthenticationPrincipal CustomUserDetails user
7276
) {
7377
fileService.deleteFile(
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.back.domain.file.dto;
22

33
import com.back.domain.file.entity.EntityType;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.NotNull;
46
import lombok.Data;
57
import org.springframework.web.multipart.MultipartFile;
68

79
@Data
810
public class FileUpdateRequestDto {
11+
@NotNull(message = "파일 입력은 필수입니다.")
912
private MultipartFile multipartFile;
13+
1014
private EntityType entityType;
15+
1116
private Long entityId;
12-
}
17+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.back.domain.file.dto;
22

33
import com.back.domain.file.entity.EntityType;
4+
import jakarta.validation.constraints.NotBlank;
45
import jakarta.validation.constraints.NotNull;
56
import lombok.Data;
67
import org.springframework.web.multipart.MultipartFile;
78

89
@Data
910
public class FileUploadRequestDto {
11+
@NotNull(message = "파일 입력은 필수입니다.")
1012
private MultipartFile multipartFile;
13+
1114
private EntityType entityType;
15+
1216
private Long entityId;
1317
}

src/main/java/com/back/domain/file/dto/FileUploadResponseDto.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ public class FileUploadResponseDto {
99
public FileUploadResponseDto(String imageUrl) {
1010
this.imageUrl = imageUrl;
1111
}
12-
}
12+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
package com.back.domain.file.entity;
22

33
public enum EntityType {
4-
POST, COMMENT, CHAT
5-
}
4+
POST, COMMENT
5+
}

src/main/java/com/back/domain/file/service/FileService.java

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.back.domain.file.entity.FileAttachment;
1212
import com.back.domain.file.repository.AttachmentMappingRepository;
1313
import com.back.domain.file.repository.FileAttachmentRepository;
14+
import com.back.domain.file.util.EntityValidator;
1415
import com.back.domain.user.entity.User;
1516
import com.back.domain.user.repository.UserRepository;
1617
import com.back.global.exception.CustomException;
@@ -34,6 +35,7 @@ public class FileService {
3435
private final FileAttachmentRepository fileAttachmentRepository;
3536
private final UserRepository userRepository;
3637
private final AttachmentMappingRepository attachmentMappingRepository;
38+
private final EntityValidator entityValidator;
3739

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

53-
// S3의 저장된 파일의 public URL
55+
// S3의 저장된 파일의 PublicURL
5456
String filePath = s3Upload(storedFileName, multipartFile);
5557

5658
// FileAttachment 정보 저장
@@ -74,13 +76,9 @@ public FileReadResponseDto getFile(
7476
EntityType entityType,
7577
Long entityId
7678
) {
77-
AttachmentMapping attachmentMapping = attachmentMappingRepository
78-
.findByEntityTypeAndEntityId(entityType, entityId)
79-
.orElseThrow(() ->
80-
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
81-
);
79+
FileAttachment fileAttachment = getFileAttachmentOrThrow(entityType, entityId);
8280

83-
String filePath = attachmentMapping.getFileAttachment().getFilePath();
81+
String filePath = fileAttachment.getFilePath();
8482

8583
return new FileReadResponseDto(filePath);
8684
}
@@ -92,18 +90,11 @@ public void updateFile(
9290
Long entityId,
9391
Long userId
9492
) {
95-
AttachmentMapping attachmentMapping = attachmentMappingRepository
96-
.findByEntityTypeAndEntityId(entityType, entityId)
97-
.orElseThrow(() ->
98-
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
99-
);
93+
entityValidator.validate(entityType, entityId);
10094

101-
FileAttachment fileAttachment = attachmentMapping.getFileAttachment();
95+
FileAttachment fileAttachment = getFileAttachmentOrThrow(entityType, entityId);
10296

103-
// 파일 접근 권한 체크
104-
if (fileAttachment.getUser().getId() != userId) {
105-
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
106-
}
97+
checkAccessPermission(fileAttachment, userId);
10798

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

114105
String filePath = s3Upload(newStoredName, multipartFile);
115106

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

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

123113
@Transactional
124114
public void deleteFile(EntityType entityType, Long entityId, Long userId) {
115+
entityValidator.validate(entityType, entityId);
116+
125117
AttachmentMapping attachmentMapping = attachmentMappingRepository
126118
.findByEntityTypeAndEntityId(entityType, entityId)
127119
.orElseThrow(() ->
@@ -130,19 +122,15 @@ public void deleteFile(EntityType entityType, Long entityId, Long userId) {
130122

131123
FileAttachment fileAttachment = attachmentMapping.getFileAttachment();
132124

133-
// 파일 접근 권한 체크
134-
if (fileAttachment.getUser().getId() != userId) {
135-
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
136-
}
125+
checkAccessPermission(fileAttachment, userId);
137126

138-
// S3 오브젝트 삭제
139127
s3Delete(fileAttachment.getStoredName());
140128

141129
// fileAttachment 정보 삭제
142130
fileAttachmentRepository.delete(fileAttachment);
143131
}
144132

145-
// S3에 파일을 업로드 하는 함수
133+
// S3 오브젝트 생성
146134
private String s3Upload(
147135
String storedFileName,
148136
MultipartFile multipartFile
@@ -173,13 +161,31 @@ private String s3Upload(
173161
return filePath;
174162
}
175163

176-
// S3 파일 삭제 함수
164+
// S3 오브젝트 삭제
177165
private void s3Delete(String fileName) {
178166
amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
179167
}
180168

181-
// 파일 이름을 난수화 하기 위한 함수
169+
// 파일 이름을 난수화
182170
private String createFileName(String fileName) {
183171
return UUID.randomUUID().toString().concat(fileName);
184172
}
185-
}
173+
174+
// 파일 접근 권한 체크
175+
private void checkAccessPermission(FileAttachment fileAttachment, Long userId) {
176+
if (fileAttachment.getUser().getId() != userId) {
177+
throw new CustomException(ErrorCode.FILE_ACCESS_DENIED);
178+
}
179+
}
180+
181+
// AttachmentMapping -> fileAttachment 추출
182+
private FileAttachment getFileAttachmentOrThrow(EntityType entityType, Long entityId) {
183+
AttachmentMapping attachmentMapping = attachmentMappingRepository
184+
.findByEntityTypeAndEntityId(entityType, entityId)
185+
.orElseThrow(() ->
186+
new CustomException(ErrorCode.ATTACHMENT_MAPPING_NOT_FOUND)
187+
);
188+
189+
return attachmentMapping.getFileAttachment();
190+
}
191+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.back.domain.file.util;
2+
3+
import com.back.domain.board.comment.repository.CommentRepository;
4+
import com.back.domain.board.post.repository.PostRepository;
5+
import com.back.domain.file.entity.EntityType;
6+
import com.back.global.exception.CustomException;
7+
import com.back.global.exception.ErrorCode;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Component;
10+
11+
/**
12+
* EntityType, EntityId를 통해 매핑되는 데이터 존재 확인
13+
*/
14+
@Component
15+
@RequiredArgsConstructor
16+
public class EntityValidator {
17+
private final PostRepository postRepository;
18+
private final CommentRepository commentRepository;
19+
20+
public void validate(EntityType entityType, Long entityId) {
21+
switch (entityType) {
22+
case POST:
23+
if(!postRepository.existsById(entityId)) throw new CustomException(ErrorCode.POST_NOT_FOUND);
24+
break;
25+
26+
case COMMENT:
27+
if(!commentRepository.existsById(entityId)) throw new CustomException(ErrorCode.COMMENT_NOT_FOUND);
28+
break;
29+
}
30+
}
31+
}

src/main/java/com/back/global/security/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public WebMvcConfigurer corsConfigurer() {
8888
public void addCorsMappings(CorsRegistry registry) {
8989
registry.addMapping("/**")
9090
.allowedOrigins(
91-
"http://localhost:3000" // Next.js 개발 서버
91+
"http://localhost:3000", // Catfe 프론트 개발 서버
92+
"https://www.catfe.com" // Catfe 프론트 운영 서버
9293
)
9394
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
9495
.allowedHeaders("*")

0 commit comments

Comments
 (0)