Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
6f7fdb6
Feat : Comment, News 엔티티 작성.
tlswltjq Sep 19, 2025
6be1cd9
Feat : likes필드 추가
tlswltjq Sep 19, 2025
01a5591
Feat : Video 작성 및 News 와 연관관계 매핑
tlswltjq Sep 19, 2025
75eb051
Feat : 비디오 픽스쳐 메소드 작성
tlswltjq Sep 19, 2025
69583cb
Test : News 도메인 테스트 추가
tlswltjq Sep 19, 2025
76a8e17
Refactor : 생성자를 통해 초기화되도록 수정
tlswltjq Sep 19, 2025
5e5dfcb
Feat : News의 유효성 검증 로직 추가
tlswltjq Sep 19, 2025
74fbd3d
Test : 테스트 추가
tlswltjq Sep 19, 2025
22c8e39
Test : 생성 테스트가 코멘트 리스트 크기를 체크
tlswltjq Sep 19, 2025
e6e9571
Fix : 생성자를 통해 코멘트 리스트를 초기화하도록 수정
tlswltjq Sep 19, 2025
56930e7
Feat : News와 Member가 연관관계를 맺도록 매핑
tlswltjq Sep 19, 2025
9ab5c6b
Feat : Member 픽스쳐 메소드 작성
tlswltjq Sep 19, 2025
5591ed1
Test : Member 연관관계 매핑에 따른 테스트케이스 추가 및 수정
tlswltjq Sep 19, 2025
3dce02e
Feat : News 픽스쳐 메소드 작성
tlswltjq Sep 19, 2025
cbf817f
Feat : Comment 유효성 검증 로직 추가
tlswltjq Sep 19, 2025
4526c33
Test : 테스트 추가
tlswltjq Sep 19, 2025
b73f3ec
Feat : Video 유효성 검증 로직 추가
tlswltjq Sep 19, 2025
a2571e9
Chore : 패키지 위치 변경
tlswltjq Sep 19, 2025
cb6fb24
Test : 테스트 추가
tlswltjq Sep 19, 2025
c3eda5c
Feat : News 도메인 Repository, Service 작성
tlswltjq Sep 19, 2025
7f90734
Feat : Like 엔티티 작성
tlswltjq Sep 19, 2025
05f6696
Feat : 좋아요 집계를 정수가 아닌 Like 엔티티로 변경, 메서드 추가
tlswltjq Sep 19, 2025
2abbb55
Feat : 메서드 추가
tlswltjq Sep 19, 2025
1e758ef
Fix : News에서 추가되도록 수정
tlswltjq Sep 19, 2025
ace6d2d
Fix : News에서 추가되도록 수정
tlswltjq Sep 19, 2025
1e1150d
Chore : 메모
tlswltjq Sep 19, 2025
469bb0d
Test : 테스트 추가
tlswltjq Sep 19, 2025
c61a7d7
Fix : News엔티티를 통해 삭제하도록 수정
tlswltjq Sep 19, 2025
a7e10d2
Test : 테스트 추가
tlswltjq Sep 19, 2025
dafa8c7
Feat : file 도메인 Service, Repository 작성
tlswltjq Sep 19, 2025
f330c67
Feat : comment 도메인 Service, Repository 작성
tlswltjq Sep 19, 2025
d46cc13
Feat : Like 도메인 Service, Repository 작성
tlswltjq Sep 19, 2025
3aa9157
Feat : NewsService 작성
tlswltjq Sep 19, 2025
48abe96
Feat : News로부터 Comment접근 가능하기떄문에 굳이 작성 안해도 될 것 같다, 다만 컨트롤러는 분리하는것이 좋다…
tlswltjq Sep 19, 2025
003d85c
Feat : JpaRepository 상속받도록함
tlswltjq Sep 19, 2025
68b8c59
Feat : S3 사용전 모킹을 위한 MiniO
tlswltjq Sep 19, 2025
6d3e75d
Feat : 파일 다운로드를 위한 설정 및 메서드 작성
tlswltjq Sep 19, 2025
20726d1
Feat : 메타데이터 저장할 메서드 createVideo,
tlswltjq Sep 20, 2025
48e2fc4
Feat : 메서드 추가
tlswltjq Sep 20, 2025
5b8349f
Feat : news 생성 엔드포인트 작성
tlswltjq Sep 20, 2025
49fda98
Feat : 코멘트 컨트롤러 생성
tlswltjq Sep 20, 2025
62d1b39
Feat : 메서드 오버로딩
tlswltjq Sep 21, 2025
9cc02db
Feat : 예외처리
tlswltjq Sep 22, 2025
d11dd01
Feat : HEAD요청을 통해 객체 존재하는지 미리 확인
tlswltjq Sep 22, 2025
f5312af
Test : 테스트 추가
tlswltjq Sep 22, 2025
6e0ca1e
Feat : Like 서비스, 레포지토리 작성
tlswltjq Sep 22, 2025
fcbba80
Feat : News단건, 다건조회 엔드포인트 작성
tlswltjq Sep 22, 2025
dcd4ede
Test : Like 테스트 작성
tlswltjq Sep 22, 2025
92a0e65
Feat : 좋아요기능 작성
tlswltjq Sep 22, 2025
cc542d6
Fix : 타입 수정
tlswltjq Sep 22, 2025
e8afed8
Fix : 타입 수정
tlswltjq Sep 22, 2025
d24fefd
Fix : 타입 수정
tlswltjq Sep 22, 2025
3ca1a80
Test : 테스트 추가
tlswltjq Sep 22, 2025
16435b7
Feat : 반환타입 변경
tlswltjq Sep 22, 2025
5223460
Feat : 좋아요수 반환하는 메서드 추가
tlswltjq Sep 22, 2025
2f9927b
Feat : 좋아요 엔드포인트 작성
tlswltjq Sep 22, 2025
dca4ea2
Feat : PresignedURL 발급 엔드포인트 작성
tlswltjq Sep 22, 2025
0cc0bfc
Refactor : 메서드명 변경
tlswltjq Sep 22, 2025
e995ded
FIX : 반환타입 변경
tlswltjq Sep 22, 2025
a370d78
Feat : 뉴스 수정 엔드포인트 작성
tlswltjq Sep 22, 2025
d7c8dc6
Feat : 뉴스 삭제 엔드포인트 작성
tlswltjq Sep 22, 2025
789bcec
Feat : 커밋 누락
tlswltjq Sep 22, 2025
1f091d5
Feat : Comment 서비스/레포지토리 작성
tlswltjq Sep 22, 2025
05ecb4c
Feat/Test : Comment 엔드포인트, 테스트 추가
tlswltjq Sep 22, 2025
6bbec08
Feat/Test : 수정, 삭제 요청시 삭제하려는 댓글이 뉴스의 것이 맞는지 확인
tlswltjq Sep 22, 2025
5315f04
Chore : work
tlswltjq Sep 22, 2025
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 @@ -43,6 +43,8 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")

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

tasks.withType<Test> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.file.controller;

import com.back.domain.file.service.VideoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.net.URL;
import java.util.List;
import java.util.Map;

@RestController
@RequiredArgsConstructor
public class VideoController {

private final VideoService videoService;

// 업로드용 Presigned URL
@GetMapping("/videos/upload-url")
public URL getUploadUrl(@RequestParam String fileName) {
return videoService.generateUploadUrl("test-bucket", fileName);
}

// DASH 스트리밍용 URL
@GetMapping("/videos/dash-urls")
public Map<String, URL> getDashUrls(
@RequestParam String mpdFile,
@RequestParam List<String> segmentFiles
) {
return videoService.generateDashUrls("test-bucket", mpdFile, segmentFiles);
}
}
66 changes: 66 additions & 0 deletions back/src/main/java/com/back/domain/file/entity/Video.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.back.domain.file.entity;

import com.back.global.jpa.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Video extends BaseEntity {
@Column(unique = true)
private String uuid;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "transcoding_results")
private String transcodingResults;

private String originalPath;

private Integer views;

private String originalFileName;

private Integer duration;

private Long fileSize;

@Builder(access = AccessLevel.PRIVATE)
private Video(String uuid, String transcodingResults, String originalPath, Integer views, String originalFileName, Integer duration, Long fileSize) {
this.uuid = uuid;
this.transcodingResults = transcodingResults;
this.originalPath = originalPath;
this.views = views;
this.originalFileName = originalFileName;
this.duration = duration;
this.fileSize = fileSize;
}

public static Video create(String uuid, String transcodingResults, String originalPath, String originalFileName, Integer duration, Long fileSize) {
if (uuid == null || uuid.isBlank()) {
throw new IllegalArgumentException("uuid cannot be null or empty");
}
if (originalPath == null || originalPath.isBlank()) {
throw new IllegalArgumentException("originalPath cannot be null or empty");
}
if (originalFileName == null || originalFileName.isBlank()) {
throw new IllegalArgumentException("originalFileName cannot be null or empty");
}

return Video.builder()
.uuid(uuid)
.transcodingResults(transcodingResults)
.originalPath(originalPath)
.views(0)
.originalFileName(originalFileName)
.duration(duration)
.fileSize(fileSize)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.back.domain.file.repository;

import com.back.domain.file.entity.Video;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface VideoRepository extends JpaRepository<Video, Integer> {
Optional<Video> findByUuid(String uuid);
}
115 changes: 115 additions & 0 deletions back/src/main/java/com/back/domain/file/service/VideoService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.back.domain.file.service;

import com.back.domain.file.entity.Video;
import com.back.domain.file.repository.VideoRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.HeadObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;

import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class VideoService {
private final VideoRepository videoRepository;
private final S3Presigner presigner;
private final S3Client s3Client;

public Video createVideo(String transcodingStatus, String originalPath, String originalFilename, Integer duration, Long fileSize) {
String uuid = UUID.randomUUID().toString();
Video video = Video.create(uuid, transcodingStatus, originalPath, originalFilename, duration, fileSize);
return videoRepository.save(video);
}

public Video getNewsByUuid(String uuid) {
return videoRepository.findByUuid(uuid)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 비디오입니다."));
}

//HeadObjectRequest 고려
public URL generateUploadUrl(String bucket, String objectKey) {
if(!isExist(bucket, objectKey)){
throw new RuntimeException("요청한 파일이 존재하지 않습니다: " + objectKey);
}

PutObjectRequest request = PutObjectRequest.builder()
.bucket(bucket)
.key(objectKey)
.build();

PresignedPutObjectRequest presignedRequest =
presigner.presignPutObject(builder -> builder
.signatureDuration(Duration.ofMinutes(30))
.putObjectRequest(request));

URL url = presignedRequest.url();
if (url == null) {
throw new RuntimeException("Presigned URL 생성 실패");
}

return url;
}

public URL generateDownloadUrl(String bucket, String objectKey) {
if(!isExist(bucket, objectKey)){
throw new RuntimeException("요청한 파일이 존재하지 않습니다: " + objectKey);
}

GetObjectRequest request = GetObjectRequest.builder()
.bucket(bucket)
.key(objectKey)
.build();

PresignedGetObjectRequest presignedRequest =
presigner.presignGetObject(builder -> builder
.signatureDuration(Duration.ofHours(1))
.getObjectRequest(request));

URL url = presignedRequest.url();
if (url == null) {
throw new RuntimeException("Presigned URL 생성 실패");
}

return url;
}

// DASH용 인덱스 + 세그먼트 URL 발급
public Map<String, URL> generateDashUrls(String bucket, String mpdFile, List<String> segmentFiles) {
// MPD 파일 URL
URL mpdUrl = generateDownloadUrl(bucket, mpdFile);

// 각 세그먼트 파일 URL
Map<String, URL> segmentUrls = segmentFiles.stream()
.collect(Collectors.toMap(f -> f, f -> generateDownloadUrl(bucket, f)));

// MPD 포함 합쳐서 반환
segmentUrls.put("mpd", mpdUrl);
return segmentUrls;
}

public boolean isExist(String bucket, String objectKey) {
try {
HeadObjectRequest headRequest = HeadObjectRequest.builder()
.bucket(bucket)
.key(objectKey)
.build();

s3Client.headObject(headRequest);
return true;
} catch (S3Exception e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.back.domain.news.comment.controller;

import com.back.domain.member.member.entity.Member;
import com.back.domain.news.comment.dto.CommentCreateRequest;
import com.back.domain.news.comment.dto.CommentResponse;
import com.back.domain.news.comment.dto.CommentUpdateRequest;
import com.back.domain.news.comment.entity.Comment;
import com.back.domain.news.comment.service.CommentService;
import com.back.domain.news.news.entity.News;
import com.back.domain.news.news.service.NewsService;
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/news/{newsId}/comment")
@RequiredArgsConstructor
public class CommentController {
private final NewsService newsService;
private final CommentService commentService;
private final Rq rq;

@GetMapping
public RsData<List<CommentResponse>> getComments(@PathVariable Long newsId) {
News news = newsService.getNewsById(newsId);
List<Comment> comments = commentService.getComments(news);
List<CommentResponse> commentResponses = comments.stream()
.map(CommentResponse::new)
.collect(Collectors.toList());
return new RsData<>("200", "댓글 목록 불러오기 완료", commentResponses);
}

@PostMapping
public RsData<CommentResponse> createComment(@PathVariable Long newsId, @RequestBody CommentCreateRequest request) {
Member member = rq.getActor();
if (member == null) {
return new RsData<>("401", "로그인이 필요합니다.");
}
News news = newsService.getNewsById(newsId);
Comment comment = commentService.createComment(member, news, request.content());
CommentResponse commentResponse = new CommentResponse(comment);
return new RsData<>("201", "댓글 생성 완료", commentResponse);
}

@PutMapping("/{commentId}")
public RsData<CommentResponse> updateComment(@PathVariable Long newsId, @PathVariable Long commentId, @RequestBody CommentUpdateRequest request) {
Member member = rq.getActor();
if (member == null) {
return new RsData<>("401", "로그인이 필요합니다.");
}
News news = newsService.getNewsById(newsId);
Comment updatedComment = commentService.updateComment(member, news, commentId, request.content());
CommentResponse commentResponse = new CommentResponse(updatedComment);
return new RsData<>("200", "댓글 수정 완료", commentResponse);
}

@DeleteMapping("/{commentId}")
public RsData<Void> deleteComment(@PathVariable Long newsId, @PathVariable Long commentId) {
Member member = rq.getActor();
if (member == null) {
return new RsData<>("401", "로그인이 필요합니다.");
}
News news = newsService.getNewsById(newsId);
commentService.deleteComment(member, news, commentId);
return new RsData<>("200", "댓글 삭제 완료");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.back.domain.news.comment.dto;

public record CommentCreateRequest(String content) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.back.domain.news.comment.dto;

import com.back.domain.news.comment.entity.Comment;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class CommentResponse {
private final Long id;
private final String content;
private final String author;
private final LocalDateTime createdDate;
private final LocalDateTime modifiedDate;

public CommentResponse(Comment comment) {
this.id = comment.getId();
this.content = comment.getContent();
this.author = comment.getMember().getName();
this.createdDate = comment.getCreateDate();
this.modifiedDate = comment.getModifyDate();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.back.domain.news.comment.dto;

public record CommentUpdateRequest(String content) {
}
Loading