Skip to content

Commit

Permalink
Merge pull request #17 from Leets-Official/feature-#6
Browse files Browse the repository at this point in the history
#6 Feat: 게시글 구현
jj0526 authored Jul 18, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents 135d1cd + 337e186 commit c493a33
Showing 22 changed files with 663 additions and 9 deletions.
8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -56,6 +56,9 @@ dependencies {

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'

// AWS Spring Cloud
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
}

tasks.named('test') {
24 changes: 24 additions & 0 deletions src/main/java/leets/weeth/domain/file/entity/File.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package leets.weeth.domain.file.entity;

import jakarta.persistence.*;
import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Getter
public class File {
public File(String url) {
this.url = url;
}

@Id //엔티티의 대푯값 지정
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "file_id")
private Long id;

private String url;


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package leets.weeth.domain.file.repository;

import leets.weeth.domain.file.entity.File;
import org.springframework.data.jpa.repository.JpaRepository;

public interface FileRepository extends JpaRepository<File, Long> {

}
71 changes: 71 additions & 0 deletions src/main/java/leets/weeth/domain/file/service/FileService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package leets.weeth.domain.file.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.PutObjectRequest;
import leets.weeth.domain.file.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import leets.weeth.domain.file.entity.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@Slf4j
@Service
@RequiredArgsConstructor
public class FileService {

private final FileRepository fileRepository;

@Value("${cloud.aws.s3.bucket}")
private String bucketName;

private final AmazonS3 s3Client;

public List<File> uploadFiles(List<MultipartFile> files) {
// 다중 업로드 && 리스트 ","을 기준으로 하나의 문자열 반환
// files 갯수 0 이면 반환 ""
if(files == null || files.isEmpty())
return List.of();

List<File> results = new ArrayList<>();

for (MultipartFile file : files) {
java.io.File fileObj = convertMultiPartFileToFile(file);
String originalFilename = file.getOriginalFilename();
String extension = getFileExtension(originalFilename);
String fileName = UUID.randomUUID() + "." + extension;

log.info("uploadFile fileName: {}", fileName);
s3Client.putObject(new PutObjectRequest(bucketName, fileName, fileObj));
fileObj.delete();
File newFile = new File(s3Client.getUrl(bucketName, fileName).toString());


fileRepository.save(newFile);
results.add(newFile);
}
return results;
}

private java.io.File convertMultiPartFileToFile(MultipartFile file) {
java.io.File convertedFile = new java.io.File(file.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(convertedFile)) {
fos.write(file.getBytes());
} catch (IOException e) {
log.error("Error converting multipartFile to file", e);
}
return convertedFile;
}

private static String getFileExtension(String originalFileName) {
return originalFileName.substring(originalFileName.lastIndexOf(".") + 1);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package leets.weeth.domain.post.controller;

import leets.weeth.domain.post.dto.RequestCommentDTO;
import leets.weeth.domain.post.dto.ResponseCommentDTO;
import leets.weeth.domain.post.service.CommentService;
import leets.weeth.global.common.response.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/articles/{articleId}/comments")
public class CommentController {
@Autowired
private CommentService commentService;
@PostMapping()
public CommonResponse<String> create(@PathVariable Long articleId, @RequestBody RequestCommentDTO dto,
@AuthenticationPrincipal User user){
commentService.create(user.getUsername(), articleId, dto);
return CommonResponse.createSuccess();
}
@GetMapping()
public CommonResponse<List<ResponseCommentDTO>> show(@PathVariable Long articleId){
List<ResponseCommentDTO> comments = commentService.comments(articleId);
return CommonResponse.createSuccess(comments);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package leets.weeth.domain.post.controller;

import leets.weeth.domain.post.dto.RequestPostDTO;
import leets.weeth.domain.post.dto.ResponsePostDTO;
import leets.weeth.domain.post.service.PostService;
import leets.weeth.global.auth.annotation.CurrentUser;
import leets.weeth.global.common.error.exception.custom.InvalidAccessException;
import leets.weeth.global.common.response.CommonResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;

@RestController
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostController {
@Autowired
private final PostService postService;
@PostMapping(value = {"/{postId}",""})
public CommonResponse<String> createOrUpdate(@RequestPart(value = "requestPostDTO") RequestPostDTO requestPostDTO,
@RequestPart(value = "files", required = false) List<MultipartFile> files,
@CurrentUser Long userId, @PathVariable(required = false) Long postId) throws InvalidAccessException {

postService.create(userId, requestPostDTO, files, postId);
return CommonResponse.createSuccess();
}

@GetMapping("")
public CommonResponse<List<ResponsePostDTO>> findAllPosts(){
List<ResponsePostDTO> posts = postService.findAllPosts();
return CommonResponse.createSuccess(posts);
}

@GetMapping("/myPosts")
public CommonResponse<List<ResponsePostDTO>> showMyPost(@CurrentUser Long userId){
List<ResponsePostDTO> myPost = postService.myPosts(userId);
return CommonResponse.createSuccess(myPost);
}

@GetMapping("/{postId}")
public CommonResponse<ResponsePostDTO> showPost(@PathVariable Long postId){
ResponsePostDTO newPost = postService.show(postId);
return CommonResponse.createSuccess(newPost);
}

@DeleteMapping("/{postId}")
public CommonResponse<String> delete(@PathVariable Long postId, @CurrentUser Long userId) throws InvalidAccessException {
postService.delete(postId, userId);
return CommonResponse.createSuccess();
}

}
18 changes: 18 additions & 0 deletions src/main/java/leets/weeth/domain/post/dto/RequestCommentDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package leets.weeth.domain.post.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class RequestCommentDTO {

@NotBlank
private String content;

}
16 changes: 16 additions & 0 deletions src/main/java/leets/weeth/domain/post/dto/RequestPostDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package leets.weeth.domain.post.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.*;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
@Setter
public class RequestPostDTO {
@NotBlank
private String title;
@NotBlank
private String content;
}
25 changes: 25 additions & 0 deletions src/main/java/leets/weeth/domain/post/dto/ResponseCommentDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package leets.weeth.domain.post.dto;

import jakarta.validation.constraints.NotBlank;
import leets.weeth.domain.post.entity.Comment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.time.LocalDateTime;

@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class ResponseCommentDTO {
private Long id;
@NotBlank
private String content;
private LocalDateTime time;

public static ResponseCommentDTO createResponseCommentDto(Comment comment) {
return new ResponseCommentDTO(comment.getId(), comment.getContent(),
comment.getTime());
}
}
37 changes: 37 additions & 0 deletions src/main/java/leets/weeth/domain/post/dto/ResponsePostDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package leets.weeth.domain.post.dto;

import jakarta.validation.constraints.NotBlank;
import leets.weeth.domain.file.entity.File;
import leets.weeth.domain.post.entity.Post;
import lombok.*;
import java.time.LocalDateTime;
import java.util.List;

@AllArgsConstructor
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@ToString
public class ResponsePostDTO {

private Long id;
@NotBlank
private String name;
@NotBlank
private String title;
@NotBlank
private String content;
private LocalDateTime time;
private List<File> fileUrls;

public static ResponsePostDTO createResponsePostDTO(Post post) {
return ResponsePostDTO.builder()
.id(post.getId())
.name(post.getUser().getName())
.title(post.getTitle())
.content(post.getContent())
.time(post.getTime())
.fileUrls(post.getFileUrls())
.build();
}
}
59 changes: 59 additions & 0 deletions src/main/java/leets/weeth/domain/post/entity/Comment.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package leets.weeth.domain.post.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import leets.weeth.domain.post.dto.RequestCommentDTO;
import leets.weeth.domain.user.entity.User;
import leets.weeth.global.common.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.time.LocalDateTime;


@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Getter
@Table
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long id;

@ManyToOne
@JoinColumn(name="post_id")
private Post post;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@NotEmpty
@Column
private String content;
LocalDateTime time;
public static Comment createComment(RequestCommentDTO dto, Post post, User user){

Comment newComment = new Comment(
null,
post,
user,
dto.getContent(),
null
);
return newComment;
}

@PrePersist
@PreUpdate
public void setTime() {
this.time = this.getModifiedAt() == null ? this.getCreatedAt() : this.getModifiedAt();
}


}
66 changes: 66 additions & 0 deletions src/main/java/leets/weeth/domain/post/entity/Post.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package leets.weeth.domain.post.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import leets.weeth.domain.file.entity.File;
import leets.weeth.domain.post.dto.RequestPostDTO;
import leets.weeth.domain.user.entity.User;
import leets.weeth.global.common.entity.BaseEntity;
import lombok.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
@Entity
@Getter
@Table
public class Post extends BaseEntity {
@Id //엔티티의 대푯값 지정
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "post_id")
private Long id;

@ManyToOne
@JoinColumn(name = "user_id")
private User user;

@NotEmpty
private String title;

@NotEmpty
private String content;

LocalDateTime time;
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<File> fileUrls = new ArrayList<>();

public static Post createPost(RequestPostDTO dto, User user, List<File> urls){

return Post.builder()
.id(null)
.user(user)
.title(dto.getTitle())
.content(dto.getContent())
.time(null)
.fileUrls(urls)
.build();
}

public void updatePost(RequestPostDTO dto, List<File> newUrls) {
this.title = dto.getTitle();
this.content = dto.getContent();
this.fileUrls.clear(); // 기존 파일 제거
this.fileUrls.addAll(newUrls); // 새로운 url 추가
}

@PrePersist
@PreUpdate
public void setTime() {
this.time = this.getModifiedAt() == null ? this.getCreatedAt() : this.getModifiedAt();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package leets.weeth.domain.post.repository;

import leets.weeth.domain.post.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface CommentRepository extends JpaRepository<Comment, Long> {

@Query(value = "SELECT * FROM comment WHERE post_id = :postId", nativeQuery = true)
List<Comment> findByPostId(Long postId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package leets.weeth.domain.post.repository;

import leets.weeth.domain.post.entity.Post;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.ArrayList;
import java.util.List;

public interface PostRepository extends JpaRepository<Post, Long> {
ArrayList<Post> findAll();
List<Post> findByUserId(Long userId, Sort sort);

}
43 changes: 43 additions & 0 deletions src/main/java/leets/weeth/domain/post/service/CommentService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package leets.weeth.domain.post.service;

import jakarta.persistence.EntityNotFoundException;
import leets.weeth.domain.post.dto.RequestCommentDTO;
import leets.weeth.domain.post.dto.ResponseCommentDTO;
import leets.weeth.domain.post.entity.Comment;
import leets.weeth.domain.post.entity.Post;
import leets.weeth.domain.post.repository.CommentRepository;
import leets.weeth.domain.post.repository.PostRepository;
import leets.weeth.domain.user.entity.User;
import leets.weeth.domain.user.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

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

@Service
public class CommentService {
@Autowired
private PostRepository postRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private CommentRepository commentRepository;

public List<ResponseCommentDTO> comments(Long articleId) {
return commentRepository.findByPostId(articleId)
.stream()
.map(ResponseCommentDTO::createResponseCommentDto)
.collect(Collectors.toList());
}

public void create(String email, Long articleId, RequestCommentDTO requestCommentDTO) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("failed to add post! no such user"));
Post targetPost = postRepository.findById(articleId).orElseThrow(()->new EntityNotFoundException("failed to add post! no such post"));

Comment newComment = Comment.createComment(requestCommentDTO, targetPost, user);
commentRepository.save(newComment);
}
}
104 changes: 104 additions & 0 deletions src/main/java/leets/weeth/domain/post/service/PostService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package leets.weeth.domain.post.service;

import jakarta.transaction.Transactional;
import leets.weeth.domain.file.entity.File;
import leets.weeth.domain.file.service.FileService;
import leets.weeth.domain.post.dto.RequestPostDTO;
import leets.weeth.domain.post.dto.ResponsePostDTO;
import leets.weeth.domain.post.entity.Post;
import leets.weeth.domain.post.repository.PostRepository;
import leets.weeth.domain.user.entity.User;
import leets.weeth.domain.user.repository.UserRepository;
import leets.weeth.global.common.error.exception.custom.InvalidAccessException;
import leets.weeth.global.common.error.exception.custom.PostNotFoundException;
import leets.weeth.global.common.error.exception.custom.UserNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Slf4j
@Service //서비스 객체 생성
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final FileService fileService;

//모든 게시물 가져오기
public List<ResponsePostDTO> findAllPosts() {
// 모든 게시물을 id에 대해 오름차순으로 조회
List<Post> posts = postRepository.findAll(Sort.by(Sort.Direction.ASC, "id"));
return posts.stream()
.map(ResponsePostDTO::createResponsePostDTO)
.collect(Collectors.toList());
}

// 특정 postId의 게시물만 조회
public ResponsePostDTO show(Long postId) {
Post target = postRepository.findById(postId)
.orElseThrow(PostNotFoundException::new);

return ResponsePostDTO.createResponsePostDTO(target);
}

// 특정 유저(본인)의 게시물만 조회
public List<ResponsePostDTO> myPosts(Long userId){
// 특정 유저의 모든 게시물을 오름차순으로 조회
List<Post> myPosts = postRepository.findByUserId(userId, Sort.by(Sort.Direction.ASC, "id"));

// Post 리스트를 ResponsePostDTO 리스트로 변환
return myPosts.stream()
.map(ResponsePostDTO::createResponsePostDTO) // Post -> ResponsePostDTO 변환
.collect(Collectors.toList());
}

@Transactional
public void create(Long userId, RequestPostDTO requestPostDTO, List<MultipartFile> files, Long postId) throws InvalidAccessException {
// 사용자가 존재하지 않는 경우
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
//
List<File> fileUrls;
Post newPost;
if(postId!=null){
Post targetPost = postRepository.findById(postId).orElse(null);
// 대상 게시물이 존재하지 않는 경우
if (targetPost==null){
throw new PostNotFoundException();
}
// 게시글을 수정하려는 유저가 원래의 게시글 작성자와 다를 경우
if(!(targetPost.getUser().getId() == userId)){
throw new InvalidAccessException();
}
// 파일 첨부
fileUrls = fileService.uploadFiles(files);
newPost = postRepository.findById(postId).orElse(null);
// 게시물 수정
newPost.updatePost(requestPostDTO, fileUrls);
}
else {
// 파일 첨부
fileUrls = fileService.uploadFiles(files);
// 게시물 생성
newPost = Post.createPost(requestPostDTO, user, fileUrls);
}
postRepository.save(newPost);
}

@Transactional
public void delete(Long postId, Long userId) throws InvalidAccessException {
// 대상 게시물이 존재하지 않는 경우
Post deleted = postRepository.findById(postId)
.orElseThrow(PostNotFoundException::new);
// 게시글을 수정하려는 유저가 원래의 게시글 작성자와 다를 경우
if(!(deleted.getUser().getId() == userId)){
throw new InvalidAccessException();
}
postRepository.delete(deleted);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package leets.weeth.global.common.error.exception.custom;

import jakarta.persistence.EntityNotFoundException;

public class PostNotFoundException extends EntityNotFoundException {
public PostNotFoundException() {
super("존재하지 않는 게시물입니다.");
}
}
32 changes: 32 additions & 0 deletions src/main/java/leets/weeth/global/config/AwsS3Config.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package leets.weeth.global.config;

import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@RequiredArgsConstructor
public class AwsS3Config {

@Value("${cloud.aws.credentials.access-key}")
private String accessKey;

@Value("${cloud.aws.credentials.secret-key}")
private String accessSecret;
@Value("${cloud.aws.region.static}")
private String region;

@Bean
public AmazonS3 s3Client() {
AWSCredentials credentials = new BasicAWSCredentials(accessKey, accessSecret);
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.withRegion(region).build();
}
}
18 changes: 18 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
@@ -11,6 +11,11 @@ spring:
dialect: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: update
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
resolve-lazily: true

# MySQL 유저 생성 및 스키마 생성 터미널 명령어
# 터미널 혹은 프로그램으로 루트 사용자 접속 후,
@@ -27,3 +32,16 @@ weeth:
refresh:
expiration: ${REFRESH_EXP}
header: ${REFRESH_HEAD}

cloud:
aws:
s3:
bucket: ${S3_BUCKET}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
region:
static: ap-northeast-2
auto: false
stack:
auto: false
14 changes: 13 additions & 1 deletion src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -19,4 +19,16 @@ weeth:
header: ${ACCESS_HEAD}
refresh:
expiration: ${REFRESH_EXP}
header: ${REFRESH_HEAD}
header: ${REFRESH_HEAD}
cloud:
aws:
s3:
bucket: ${S3_BUCKET}
credentials:
access-key: ${S3_ACCESS_KEY}
secret-key: ${S3_SECRET_KEY}
region:
static: ap-northeast-2
auto: false
stack:
auto: false
5 changes: 5 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
spring:
profiles:
active: local
servlet:
multipart:
enabled: true
max-file-size: 10MB
max-request-size: 10MB

springdoc:
swagger-ui:

0 comments on commit c493a33

Please sign in to comment.