Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions back/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

implementation ("software.amazon.awssdk:s3:2.25.0")

implementation ("org.springframework.kafka:spring-kafka")
}

tasks.withType<Test> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.back.domain.file.video.dto.service.PresignedUrlResponse;
import com.back.domain.file.video.service.FileManager;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
Expand All @@ -15,16 +16,18 @@ public class VideoController {
private final FileManager fileManager;

@GetMapping("/videos/upload")
@Operation(summary="업로드용 URL 요청", description="파일 업로드를 위한 Presigned URL을 발급받습니다.")
public RsData<UploadUrlGetResponse> getUploadUrl() {
PresignedUrlResponse uploadUrl = fileManager.getUploadUrl();
UploadUrlGetResponse response = new UploadUrlGetResponse(uploadUrl.url(), uploadUrl.expiresAt());
UploadUrlGetResponse response = new UploadUrlGetResponse(uploadUrl.url().toString(), uploadUrl.expiresAt());
return new RsData<>("200", "업로드용 URL 요청완료", response);
}

@GetMapping("/videos/download")
@Operation(summary="다운로드용 URL 요청", description="파일 다운로드를 위한 Presigned URL을 발급받습니다.")
public RsData<UploadUrlGetResponse> getDownloadUrls(@RequestParam String objectKey) {
PresignedUrlResponse downloadUrl = fileManager.getDownloadUrl(objectKey);
UploadUrlGetResponse response = new UploadUrlGetResponse(downloadUrl.url(), downloadUrl.expiresAt());
UploadUrlGetResponse response = new UploadUrlGetResponse(downloadUrl.url().toString(), downloadUrl.expiresAt());
return new RsData<>("200", "다운로드용 URL 요청완료", response);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.back.domain.file.video.dto.controller;

import java.net.URL;
import java.time.LocalDateTime;

public record UploadUrlGetResponse(
URL url,
String url,
LocalDateTime expiresAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@ public class Video extends BaseEntity {

private Integer duration;

private Long fileSize;

@Builder(access = AccessLevel.PRIVATE)
private Video(String uuid, String status, String path, Integer duration, Long fileSize) {
private Video(String uuid, String status, String path, Integer duration) {
this.uuid = uuid;
this.status = status;
this.path = path;
this.duration = duration;
this.fileSize = fileSize;
}

public static Video create(String uuid, String status, String path, Integer duration, Long fileSize) {
public static Video create(String uuid, String status, String path, Integer duration) {
if (uuid == null || uuid.isBlank()) {
throw new IllegalArgumentException("uuid cannot be null or empty");
}
Expand All @@ -48,7 +45,6 @@ public static Video create(String uuid, String status, String path, Integer dura
.status(status)
.path(path)
.duration(duration)
.fileSize(fileSize)
.build();
}
public void updateStatus(String status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,13 @@ public PresignedUrlResponse getDownloadUrl(String objectKey) {
LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(expires);
return new PresignedUrlResponse(url, expiresAt);
}

//TODO : 테스트 작성필요
public void updateVideoStatus(String videoId, String status) {
try {
videoService.updateStatus(videoId, status);
} catch (Exception e) {
videoService.createVideo(videoId, status, "/", 0);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
public class VideoService {
private final VideoRepository videoRepository;

public Video createVideo(String uuid, String status, String path, Integer duration, Long fileSize) {
Video video = Video.create(uuid, status, path, duration, fileSize);
public Video createVideo(String uuid, String status, String path, Integer duration) {
Video video = Video.create(uuid, status, path, duration);
return videoRepository.save(video);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public RsData<NewsCreateResponse> createNews(@RequestBody NewsCreateRequest requ
@Operation(summary = "뉴스 단건 조회", description = "특정 ID의 뉴스를 읽어옵니다.")
public RsData<NewsGetResponse> getNews(@PathVariable("newsId") Long newsId) {
News news = newsService.getNewsById(newsId);
newsService.incrementViews(news);
NewsGetResponse response = new NewsGetResponse(news);
return new RsData<>("200", "뉴스 읽어오기 완료", response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,19 @@ public class News extends BaseEntity {
@OneToOne
private Video video;
private String content;
private Integer views;
@OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NewsComment> newsComment;
@OneToMany(mappedBy = "news", cascade = CascadeType.ALL, orphanRemoval = true)
private List<NewsLike> newsLikes = new ArrayList<>();

@Builder(access = AccessLevel.PRIVATE)
private News(Member member, String title, Video video, String content, List<NewsComment> newsComment, List<NewsLike> newsLikes) {
private News(Member member, String title, Video video, String content, Integer views, List<NewsComment> newsComment, List<NewsLike> newsLikes) {
this.member = member;
this.title = title;
this.video = video;
this.content = content;
this.views = views;
this.newsComment = newsComment;
this.newsLikes = newsLikes;
}
Expand All @@ -57,6 +59,7 @@ public static News create(Member member, String title, Video video, String conte
.title(title)
.video(video)
.content(content)
.views(0)
.newsComment(new ArrayList<>())
.newsLikes(new ArrayList<>())
.build();
Expand All @@ -80,6 +83,10 @@ public void unLike(NewsLike newsLike) {
this.newsLikes.remove(newsLike);
}

public void incrementViews() {
this.views += 1;
}

public void addComment(NewsComment newsComment) {
if (newsComment == null) {
throw new IllegalArgumentException("Comment cannot be null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,20 @@ public News getNewsById(Long id) {

public News updateNews(Member member, News news, String title, Video video, String content) {
if (!(member.getRole() == Member.Role.ADMIN)) {
throw new ServiceException("403","수정 권한이 없습니다.");
throw new ServiceException("403", "수정 권한이 없습니다.");
}
news.update(title, video, content);
return newsRepository.save(news);
}

public News incrementViews(News news) {
news.incrementViews();
return newsRepository.save(news);
}

public void deleteNews(Member member, News news) {
if (!(member.getRole() == Member.Role.ADMIN)) {
throw new ServiceException("403","수정 권한이 없습니다.");
throw new ServiceException("403", "수정 권한이 없습니다.");
}
newsRepository.delete(news);
}
Expand Down
38 changes: 38 additions & 0 deletions back/src/main/java/com/back/global/kafka/KafkaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.back.global.kafka;

import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.annotation.EnableKafka;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import java.util.HashMap;
import java.util.Map;

@EnableKafka
@Configuration
public class KafkaConfig {

@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> props = new HashMap<>();

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "spring-boot-group");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

return new DefaultKafkaConsumerFactory<>(props);
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
31 changes: 31 additions & 0 deletions back/src/main/java/com/back/global/kafka/KafkaConsumer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.back.global.kafka;

import com.back.domain.file.video.service.FileManager;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class KafkaConsumer {
private final FileManager fileManager;
private final ObjectMapper mapper;

@KafkaListener(topics = "transcoding-status")
public void consume(String transcodingStatusMessage) {

try {
JsonNode rootNode = mapper.readTree(transcodingStatusMessage);

String uuid = rootNode.get("key").asText();
String status = rootNode.get("qualities").toString();
//현재 duration이 0인 문제가 있음 영상 길이가 필요한게 아니라면 제거할 예정
fileManager.updateVideoStatus(uuid, status);

} catch (Exception e) {
e.printStackTrace();
}
}
}
4 changes: 4 additions & 0 deletions back/src/main/java/com/back/global/kafka/KafkaProducer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.back.global.kafka;

public class KafkaProducer {
}
16 changes: 7 additions & 9 deletions back/src/test/java/com/back/domain/file/entity/VideoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@ void videoCreationTest() {
String status = "{\"status\":\"done\"}";
String originalPath = "/videos/sample.mp4";
Integer duration = 120;
Long fileSize = 1024L;

Video video = Video.create(uuid, status, originalPath, duration, fileSize);
Video video = Video.create(uuid, status, originalPath, duration);

assertThat(video).isNotNull();
assertThat(video.getUuid()).isEqualTo(uuid);
assertThat(video.getStatus()).isEqualTo(status);
assertThat(video.getPath()).isEqualTo(originalPath);
assertThat(video.getDuration()).isEqualTo(duration);
assertThat(video.getFileSize()).isEqualTo(fileSize);
}

@Test
Expand All @@ -34,13 +32,13 @@ void videoCreationTestWithInvalidUuid() {
String originalPath = "/videos/sample.mp4";

try {
Video.create(null, status, originalPath, 100, 1000L);
Video.create(null, status, originalPath, 100);
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}

try {
Video.create("", status, originalPath, 100, 1000L);
Video.create("", status, originalPath, 100);
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
Expand All @@ -53,13 +51,13 @@ void videoCreationTestWithInvalidOriginalPath() {
String status = "{}";

try {
Video.create(uuid, status, null, 100, 1000L);
Video.create(uuid, status, null, 100);
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}

try {
Video.create(uuid, status, "", 100, 1000L);
Video.create(uuid, status, "", 100);
} catch (Exception e) {
assertThat(e).isInstanceOf(IllegalArgumentException.class);
}
Expand All @@ -74,7 +72,7 @@ void videoUpdateStatusTest() {
Integer duration = 120;
Long fileSize = 1024L;

Video video = Video.create(uuid, status, originalPath, duration, fileSize);
Video video = Video.create(uuid, status, originalPath, duration);
assertThat(video.getStatus()).isEqualTo(status);

String newStatus = "{\"status\":\"done\"}";
Expand All @@ -91,7 +89,7 @@ void videoUpdateStatusTestWithInvalidStatus() {
Integer duration = 120;
Long fileSize = 1024L;

Video video = Video.create(uuid, status, originalPath, duration, fileSize);
Video video = Video.create(uuid, status, originalPath, duration);

try {
video.updateStatus(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class VideoServiceTest {
private VideoService videoService;

@Test
@DisplayName("transcodingResults, originalPath, originalFileName, duration, fileSize로 Video 객체 생성")
@DisplayName("transcodingResults, originalPath, originalFileName, duration로 Video 객체 생성")
void videoCreationTest() {
String uuid = UUID.randomUUID().toString();
String transcodingResults = """
Expand All @@ -51,18 +51,16 @@ void videoCreationTest() {
""";
String originalPath = "/videos/sample.mp4";
Integer duration = 120;
Long fileSize = 1024L;

Video video = VideoFixture.create(uuid, transcodingResults, originalPath, duration, fileSize);
Video video = VideoFixture.create(uuid, transcodingResults, originalPath, duration);
when(videoRepository.save(any(Video.class))).thenReturn(video);

Video createdVideo = videoService.createVideo(uuid, transcodingResults, originalPath, duration, fileSize);
Video createdVideo = videoService.createVideo(uuid, transcodingResults, originalPath, duration);
assertThat(createdVideo).isNotNull();
assertThat(createdVideo.getUuid()).isEqualTo(uuid);
assertThat(createdVideo.getStatus()).isEqualTo(transcodingResults);
assertThat(createdVideo.getPath()).isEqualTo(originalPath);
assertThat(createdVideo.getDuration()).isEqualTo(duration);
assertThat(createdVideo.getFileSize()).isEqualTo(fileSize);
}

@Test
Expand Down
18 changes: 18 additions & 0 deletions back/src/test/java/com/back/domain/news/news/entity/NewsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,22 @@ void removeNullCommentTest() {

assertThat(news.getNewsComment().size()).isEqualTo(0);
}

@Test
@DisplayName("뉴스 조회수를 증가시킬 수 있다.")
void incrementViewsTest() {
Member member = MemberFixture.createDefault();
String title = "Sample News Title";
String content = "This is a sample news content.";
Video video = VideoFixture.createDefault();
News news = News.create(member, title, video, content);

assertThat(news.getViews()).isEqualTo(0);

news.incrementViews();
assertThat(news.getViews()).isEqualTo(1);

news.incrementViews();
assertThat(news.getViews()).isEqualTo(2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -175,4 +175,21 @@ void deleteNews_NoPermission_ThrowsException() {
});
verify(newsRepository, never()).delete(any(News.class));
}

@Test
@DisplayName("뉴스 조회수 증가 성공")
void incrementViews_Success() {
// given
News news = NewsFixture.createDefault();
int initialViews = news.getViews();
when(newsRepository.save(any(News.class))).thenReturn(news);

// when
News updatedNews = newsService.incrementViews(news);

// then
assertThat(updatedNews).isNotNull();
assertThat(updatedNews.getViews()).isEqualTo(initialViews + 1);
verify(newsRepository, times(1)).save(news);
}
}
Loading