Skip to content

Commit 0ad86d1

Browse files
committed
feat: 파일 목록 조회 기능 구현 (#30)
1 parent d25fca8 commit 0ad86d1

File tree

5 files changed

+146
-53
lines changed

5 files changed

+146
-53
lines changed

src/main/java/com/dmu/debug_visual/config/SecurityConfig.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public SecurityFilterChain devSecurityFilterChain(HttpSecurity http) throws Exce
6262
.requestMatchers("/api/notifications/**").hasRole("USER")
6363
.requestMatchers("/api/report/**").hasRole("USER")
6464
.requestMatchers("/api/comments/**").hasRole("USER")
65-
.requestMatchers("/api/files/upload").hasRole("USER")
65+
.requestMatchers("/api/files/**").hasRole("USER")
6666
.requestMatchers("/api/collab-rooms").hasRole("USER")
6767

6868
// 3. 나머지 모든 요청은 인증된 사용자만 접근 가능 (ADMIN 경로 포함)
@@ -102,7 +102,7 @@ public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws
102102
.requestMatchers("/api/notifications/**").hasRole("USER")
103103
.requestMatchers("/api/report/**").hasRole("USER")
104104
.requestMatchers("/api/comments/**").hasRole("USER")
105-
.requestMatchers("/api/files/upload").hasRole("USER")
105+
.requestMatchers("/api/files/**").hasRole("USER")
106106
.requestMatchers("/api/collab-rooms").hasRole("USER")
107107

108108

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,48 @@
11
package com.dmu.debug_visual.file_upload;
22

3+
import com.dmu.debug_visual.file_upload.service.S3Uploader;
34
import com.dmu.debug_visual.security.CustomUserDetails;
45
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.media.ArraySchema;
7+
import io.swagger.v3.oas.annotations.media.Content;
8+
import io.swagger.v3.oas.annotations.media.Schema;
9+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
11+
import io.swagger.v3.oas.annotations.tags.Tag;
512
import lombok.RequiredArgsConstructor;
613
import org.springframework.http.HttpStatus;
714
import org.springframework.http.MediaType;
815
import org.springframework.http.ResponseEntity;
916
import org.springframework.security.core.annotation.AuthenticationPrincipal;
10-
import org.springframework.web.bind.annotation.PostMapping;
11-
import org.springframework.web.bind.annotation.RequestMapping;
12-
import org.springframework.web.bind.annotation.RequestParam;
13-
import org.springframework.web.bind.annotation.RestController;
17+
import org.springframework.web.bind.annotation.*;
1418
import org.springframework.web.multipart.MultipartFile;
1519

1620
import java.io.IOException;
21+
import java.util.List;
1722

23+
@Tag(name = "파일 관리 API", description = "S3 파일 업로드 및 사용자별 파일 목록 조회를 제공하는 컨트롤러")
1824
@RestController
1925
@RequiredArgsConstructor
20-
@RequestMapping("/api/files") // 공통되는 URL 경로 설정
26+
@RequestMapping("/api/files")
2127
public class FileController {
2228

2329
private final S3Uploader s3Uploader;
2430

25-
@Operation(summary = "파일 업로드", description = "form-data 형식으로 파일을 업로드합니다.")
31+
@Operation(summary = "파일 업로드", description = "form-data 형식으로 단일 파일을 AWS S3에 업로드하고, 저장된 파일의 URL을 반환합니다. JWT 인증이 필요합니다.")
32+
@ApiResponses({
33+
@ApiResponse(responseCode = "200", description = "업로드 성공",
34+
content = @Content(mediaType = "text/plain", schema = @Schema(type = "string", example = "https://your-bucket.s3.amazonaws.com/.../file.txt"))),
35+
@ApiResponse(responseCode = "401", description = "인증 실패 (JWT 토큰 누락 또는 유효하지 않음)",
36+
content = @Content),
37+
@ApiResponse(responseCode = "500", description = "서버 내부 오류 (파일 업로드 실패)",
38+
content = @Content)
39+
})
2640
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
2741
public ResponseEntity<String> uploadFile(
2842
@RequestParam("file") MultipartFile file,
29-
@AuthenticationPrincipal CustomUserDetails userDetails) { // <-- String 대신 CustomUserDetails로 변경
43+
@AuthenticationPrincipal CustomUserDetails userDetails) {
3044

31-
// userDetails 객체에서 userId를 직접 꺼내서 사용합니다.
32-
String userId = userDetails.getUsername(); // 또는 userDetails.getUser().getUserId()
45+
String userId = userDetails.getUsername();
3346

3447
try {
3548
String fileUrl = s3Uploader.upload(file, userId + "-codes");
@@ -39,4 +52,23 @@ public ResponseEntity<String> uploadFile(
3952
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("파일 업로드에 실패했습니다.");
4053
}
4154
}
55+
56+
@Operation(summary = "사용자 파일 목록 조회", description = "특정 사용자가 업로드한 모든 파일의 목록을 조회합니다. 본인의 파일 목록만 조회할 수 있습니다.")
57+
@ApiResponses({
58+
@ApiResponse(responseCode = "200", description = "조회 성공",
59+
content = @Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = S3FileDTO.class)))),
60+
@ApiResponse(responseCode = "401", description = "인증 실패 (JWT 토큰 누락 또는 유효하지 않음)",
61+
content = @Content),
62+
@ApiResponse(responseCode = "403", description = "접근 권한 없음 (다른 사용자의 파일 목록에 접근 시도)",
63+
content = @Content)
64+
})
65+
@GetMapping("/my")
66+
public ResponseEntity<List<S3FileDTO>> listFilesForUser(
67+
@AuthenticationPrincipal CustomUserDetails userDetails) {
68+
69+
String userId = userDetails.getUsername();
70+
71+
List<S3FileDTO> files = s3Uploader.listFiles(userId);
72+
return ResponseEntity.ok(files);
73+
}
4274
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.dmu.debug_visual.file_upload;
2+
3+
import lombok.Builder;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Getter
9+
@Builder
10+
public class S3FileDTO {
11+
private String fileName; // 원본 파일 이름
12+
private String fileUrl; // S3에 저장된 파일 URL
13+
private LocalDateTime uploadDate; // 업로드 날짜
14+
private Long fileSize; // 파일 크기 (bytes)
15+
}

src/main/java/com/dmu/debug_visual/file_upload/S3Uploader.java

Lines changed: 0 additions & 42 deletions
This file was deleted.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.dmu.debug_visual.file_upload.service;
2+
3+
import com.dmu.debug_visual.file_upload.S3FileDTO;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.stereotype.Service;
7+
import org.springframework.web.multipart.MultipartFile;
8+
import software.amazon.awssdk.core.sync.RequestBody;
9+
import software.amazon.awssdk.services.s3.S3Client;
10+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response;
11+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
12+
import software.amazon.awssdk.services.s3.model.S3Object;
13+
14+
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request;
15+
16+
import java.time.LocalDateTime;
17+
import java.time.ZoneId;
18+
import java.util.List;
19+
import java.util.stream.Collectors;
20+
import java.io.IOException;
21+
import java.util.UUID;
22+
23+
24+
@Service
25+
@RequiredArgsConstructor
26+
public class S3Uploader {
27+
28+
private final S3Client s3Client;
29+
30+
@Value("${spring.cloud.aws.s3.bucket}")
31+
private String bucket;
32+
33+
public String upload(MultipartFile file, String dirName) throws IOException {
34+
// 1. 고유한 파일 이름 생성
35+
String originalFilename = file.getOriginalFilename();
36+
String uniqueFileName = dirName + "/" + UUID.randomUUID().toString() + "_" + originalFilename;
37+
38+
// 2. S3에 업로드할 요청 객체 생성
39+
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
40+
.bucket(bucket)
41+
.key(uniqueFileName)
42+
.contentType(file.getContentType())
43+
.contentLength(file.getSize())
44+
.build();
45+
46+
// 3. 파일의 InputStream을 RequestBody로 만들어 S3에 업로드
47+
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize()));
48+
49+
// 4. 업로드된 파일의 URL 반환
50+
return s3Client.utilities().getUrl(builder -> builder.bucket(bucket).key(uniqueFileName)).toString();
51+
}
52+
53+
/**
54+
* 특정 사용자의 모든 파일 목록을 S3에서 조회합니다.
55+
* @param userId 조회할 사용자의 ID
56+
* @return 파일 정보 DTO 리스트
57+
*/
58+
public List<S3FileDTO> listFiles(String userId) {
59+
// 1. 조회할 폴더(prefix)를 지정합니다. (예: "test-codes/")
60+
String prefix = userId + "-codes/";
61+
62+
// 2. S3 객체 목록을 요청하기 위한 객체를 생성합니다.
63+
ListObjectsV2Request listObjectsV2Request = ListObjectsV2Request.builder()
64+
.bucket(bucket)
65+
.prefix(prefix)
66+
.build();
67+
68+
// 3. S3 클라이언트로 객체 목록을 조회합니다.
69+
ListObjectsV2Response response = s3Client.listObjectsV2(listObjectsV2Request);
70+
List<S3Object> s3Objects = response.contents();
71+
72+
// 4. 조회된 S3Object 목록을 우리가 만든 S3FileDTO 리스트로 변환합니다.
73+
return s3Objects.stream()
74+
.map(s3Object -> {
75+
String fullKey = s3Object.key();
76+
// "test-codes/UUID_원본파일이름" 에서 "원본파일이름"만 추출
77+
String originalFileName = fullKey.substring(fullKey.indexOf('_') + 1);
78+
79+
return S3FileDTO.builder()
80+
.fileName(originalFileName)
81+
.fileUrl(s3Client.utilities().getUrl(builder -> builder.bucket(bucket).key(fullKey)).toString())
82+
.uploadDate(LocalDateTime.ofInstant(s3Object.lastModified(), ZoneId.systemDefault()))
83+
.fileSize(s3Object.size())
84+
.build();
85+
})
86+
.collect(Collectors.toList());
87+
}
88+
}

0 commit comments

Comments
 (0)