Skip to content

Commit 6d74ca4

Browse files
authored
Merge pull request #25 from prgrms-web-devcourse-final-project/feat/3
뉴스, 댓글, 업로드/다운로드용 PresignedURL발급메서드 작성완료
2 parents 4247077 + 5315f04 commit 6d74ca4

37 files changed

+2320
-0
lines changed

back/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ dependencies {
4343
testImplementation("org.springframework.boot:spring-boot-starter-test")
4444
testImplementation("org.springframework.security:spring-security-test")
4545
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
46+
47+
implementation ("software.amazon.awssdk:s3:2.25.0")
4648
}
4749

4850
tasks.withType<Test> {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.back.domain.file.controller;
2+
3+
import com.back.domain.file.service.VideoService;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RequestParam;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import java.net.URL;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
public class VideoController {
16+
17+
private final VideoService videoService;
18+
19+
// 업로드용 Presigned URL
20+
@GetMapping("/videos/upload-url")
21+
public URL getUploadUrl(@RequestParam String fileName) {
22+
return videoService.generateUploadUrl("test-bucket", fileName);
23+
}
24+
25+
// DASH 스트리밍용 URL
26+
@GetMapping("/videos/dash-urls")
27+
public Map<String, URL> getDashUrls(
28+
@RequestParam String mpdFile,
29+
@RequestParam List<String> segmentFiles
30+
) {
31+
return videoService.generateDashUrls("test-bucket", mpdFile, segmentFiles);
32+
}
33+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.back.domain.file.entity;
2+
3+
import com.back.global.jpa.BaseEntity;
4+
import jakarta.persistence.Column;
5+
import jakarta.persistence.Entity;
6+
import lombok.AccessLevel;
7+
import lombok.Builder;
8+
import lombok.Getter;
9+
import lombok.NoArgsConstructor;
10+
import org.hibernate.annotations.JdbcTypeCode;
11+
import org.hibernate.type.SqlTypes;
12+
13+
@Getter
14+
@Entity
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
public class Video extends BaseEntity {
17+
@Column(unique = true)
18+
private String uuid;
19+
20+
@JdbcTypeCode(SqlTypes.JSON)
21+
@Column(name = "transcoding_results")
22+
private String transcodingResults;
23+
24+
private String originalPath;
25+
26+
private Integer views;
27+
28+
private String originalFileName;
29+
30+
private Integer duration;
31+
32+
private Long fileSize;
33+
34+
@Builder(access = AccessLevel.PRIVATE)
35+
private Video(String uuid, String transcodingResults, String originalPath, Integer views, String originalFileName, Integer duration, Long fileSize) {
36+
this.uuid = uuid;
37+
this.transcodingResults = transcodingResults;
38+
this.originalPath = originalPath;
39+
this.views = views;
40+
this.originalFileName = originalFileName;
41+
this.duration = duration;
42+
this.fileSize = fileSize;
43+
}
44+
45+
public static Video create(String uuid, String transcodingResults, String originalPath, String originalFileName, Integer duration, Long fileSize) {
46+
if (uuid == null || uuid.isBlank()) {
47+
throw new IllegalArgumentException("uuid cannot be null or empty");
48+
}
49+
if (originalPath == null || originalPath.isBlank()) {
50+
throw new IllegalArgumentException("originalPath cannot be null or empty");
51+
}
52+
if (originalFileName == null || originalFileName.isBlank()) {
53+
throw new IllegalArgumentException("originalFileName cannot be null or empty");
54+
}
55+
56+
return Video.builder()
57+
.uuid(uuid)
58+
.transcodingResults(transcodingResults)
59+
.originalPath(originalPath)
60+
.views(0)
61+
.originalFileName(originalFileName)
62+
.duration(duration)
63+
.fileSize(fileSize)
64+
.build();
65+
}
66+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.back.domain.file.repository;
2+
3+
import com.back.domain.file.entity.Video;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
import java.util.Optional;
8+
9+
@Repository
10+
public interface VideoRepository extends JpaRepository<Video, Integer> {
11+
Optional<Video> findByUuid(String uuid);
12+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.back.domain.file.service;
2+
3+
import com.back.domain.file.entity.Video;
4+
import com.back.domain.file.repository.VideoRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.stereotype.Service;
7+
import software.amazon.awssdk.services.s3.S3Client;
8+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
9+
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
10+
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
11+
import software.amazon.awssdk.services.s3.model.S3Exception;
12+
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
13+
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
14+
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
15+
16+
import java.net.URL;
17+
import java.time.Duration;
18+
import java.util.List;
19+
import java.util.Map;
20+
import java.util.UUID;
21+
import java.util.stream.Collectors;
22+
23+
@Service
24+
@RequiredArgsConstructor
25+
public class VideoService {
26+
private final VideoRepository videoRepository;
27+
private final S3Presigner presigner;
28+
private final S3Client s3Client;
29+
30+
public Video createVideo(String transcodingStatus, String originalPath, String originalFilename, Integer duration, Long fileSize) {
31+
String uuid = UUID.randomUUID().toString();
32+
Video video = Video.create(uuid, transcodingStatus, originalPath, originalFilename, duration, fileSize);
33+
return videoRepository.save(video);
34+
}
35+
36+
public Video getNewsByUuid(String uuid) {
37+
return videoRepository.findByUuid(uuid)
38+
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 비디오입니다."));
39+
}
40+
41+
//HeadObjectRequest 고려
42+
public URL generateUploadUrl(String bucket, String objectKey) {
43+
if(!isExist(bucket, objectKey)){
44+
throw new RuntimeException("요청한 파일이 존재하지 않습니다: " + objectKey);
45+
}
46+
47+
PutObjectRequest request = PutObjectRequest.builder()
48+
.bucket(bucket)
49+
.key(objectKey)
50+
.build();
51+
52+
PresignedPutObjectRequest presignedRequest =
53+
presigner.presignPutObject(builder -> builder
54+
.signatureDuration(Duration.ofMinutes(30))
55+
.putObjectRequest(request));
56+
57+
URL url = presignedRequest.url();
58+
if (url == null) {
59+
throw new RuntimeException("Presigned URL 생성 실패");
60+
}
61+
62+
return url;
63+
}
64+
65+
public URL generateDownloadUrl(String bucket, String objectKey) {
66+
if(!isExist(bucket, objectKey)){
67+
throw new RuntimeException("요청한 파일이 존재하지 않습니다: " + objectKey);
68+
}
69+
70+
GetObjectRequest request = GetObjectRequest.builder()
71+
.bucket(bucket)
72+
.key(objectKey)
73+
.build();
74+
75+
PresignedGetObjectRequest presignedRequest =
76+
presigner.presignGetObject(builder -> builder
77+
.signatureDuration(Duration.ofHours(1))
78+
.getObjectRequest(request));
79+
80+
URL url = presignedRequest.url();
81+
if (url == null) {
82+
throw new RuntimeException("Presigned URL 생성 실패");
83+
}
84+
85+
return url;
86+
}
87+
88+
// DASH용 인덱스 + 세그먼트 URL 발급
89+
public Map<String, URL> generateDashUrls(String bucket, String mpdFile, List<String> segmentFiles) {
90+
// MPD 파일 URL
91+
URL mpdUrl = generateDownloadUrl(bucket, mpdFile);
92+
93+
// 각 세그먼트 파일 URL
94+
Map<String, URL> segmentUrls = segmentFiles.stream()
95+
.collect(Collectors.toMap(f -> f, f -> generateDownloadUrl(bucket, f)));
96+
97+
// MPD 포함 합쳐서 반환
98+
segmentUrls.put("mpd", mpdUrl);
99+
return segmentUrls;
100+
}
101+
102+
public boolean isExist(String bucket, String objectKey) {
103+
try {
104+
HeadObjectRequest headRequest = HeadObjectRequest.builder()
105+
.bucket(bucket)
106+
.key(objectKey)
107+
.build();
108+
109+
s3Client.headObject(headRequest);
110+
return true;
111+
} catch (S3Exception e) {
112+
return false;
113+
}
114+
}
115+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package com.back.domain.news.comment.controller;
2+
3+
import com.back.domain.member.member.entity.Member;
4+
import com.back.domain.news.comment.dto.CommentCreateRequest;
5+
import com.back.domain.news.comment.dto.CommentResponse;
6+
import com.back.domain.news.comment.dto.CommentUpdateRequest;
7+
import com.back.domain.news.comment.entity.Comment;
8+
import com.back.domain.news.comment.service.CommentService;
9+
import com.back.domain.news.news.entity.News;
10+
import com.back.domain.news.news.service.NewsService;
11+
import com.back.global.rq.Rq;
12+
import com.back.global.rsData.RsData;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.web.bind.annotation.*;
15+
16+
import java.util.List;
17+
import java.util.stream.Collectors;
18+
19+
@RestController
20+
@RequestMapping("/news/{newsId}/comment")
21+
@RequiredArgsConstructor
22+
public class CommentController {
23+
private final NewsService newsService;
24+
private final CommentService commentService;
25+
private final Rq rq;
26+
27+
@GetMapping
28+
public RsData<List<CommentResponse>> getComments(@PathVariable Long newsId) {
29+
News news = newsService.getNewsById(newsId);
30+
List<Comment> comments = commentService.getComments(news);
31+
List<CommentResponse> commentResponses = comments.stream()
32+
.map(CommentResponse::new)
33+
.collect(Collectors.toList());
34+
return new RsData<>("200", "댓글 목록 불러오기 완료", commentResponses);
35+
}
36+
37+
@PostMapping
38+
public RsData<CommentResponse> createComment(@PathVariable Long newsId, @RequestBody CommentCreateRequest request) {
39+
Member member = rq.getActor();
40+
if (member == null) {
41+
return new RsData<>("401", "로그인이 필요합니다.");
42+
}
43+
News news = newsService.getNewsById(newsId);
44+
Comment comment = commentService.createComment(member, news, request.content());
45+
CommentResponse commentResponse = new CommentResponse(comment);
46+
return new RsData<>("201", "댓글 생성 완료", commentResponse);
47+
}
48+
49+
@PutMapping("/{commentId}")
50+
public RsData<CommentResponse> updateComment(@PathVariable Long newsId, @PathVariable Long commentId, @RequestBody CommentUpdateRequest request) {
51+
Member member = rq.getActor();
52+
if (member == null) {
53+
return new RsData<>("401", "로그인이 필요합니다.");
54+
}
55+
News news = newsService.getNewsById(newsId);
56+
Comment updatedComment = commentService.updateComment(member, news, commentId, request.content());
57+
CommentResponse commentResponse = new CommentResponse(updatedComment);
58+
return new RsData<>("200", "댓글 수정 완료", commentResponse);
59+
}
60+
61+
@DeleteMapping("/{commentId}")
62+
public RsData<Void> deleteComment(@PathVariable Long newsId, @PathVariable Long commentId) {
63+
Member member = rq.getActor();
64+
if (member == null) {
65+
return new RsData<>("401", "로그인이 필요합니다.");
66+
}
67+
News news = newsService.getNewsById(newsId);
68+
commentService.deleteComment(member, news, commentId);
69+
return new RsData<>("200", "댓글 삭제 완료");
70+
}
71+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.back.domain.news.comment.dto;
2+
3+
public record CommentCreateRequest(String content) {
4+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.back.domain.news.comment.dto;
2+
3+
import com.back.domain.news.comment.entity.Comment;
4+
import lombok.Getter;
5+
6+
import java.time.LocalDateTime;
7+
8+
@Getter
9+
public class CommentResponse {
10+
private final Long id;
11+
private final String content;
12+
private final String author;
13+
private final LocalDateTime createdDate;
14+
private final LocalDateTime modifiedDate;
15+
16+
public CommentResponse(Comment comment) {
17+
this.id = comment.getId();
18+
this.content = comment.getContent();
19+
this.author = comment.getMember().getName();
20+
this.createdDate = comment.getCreateDate();
21+
this.modifiedDate = comment.getModifyDate();
22+
}
23+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.back.domain.news.comment.dto;
2+
3+
public record CommentUpdateRequest(String content) {
4+
}

0 commit comments

Comments
 (0)