diff --git a/build.gradle b/build.gradle index f20bf86..8ccca67 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,10 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + //s3 + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + //test testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/src/main/java/com/gaplog/server/domain/comment/application/CommentService.java b/src/main/java/com/gaplog/server/domain/comment/application/CommentService.java index 6e07729..3396d13 100644 --- a/src/main/java/com/gaplog/server/domain/comment/application/CommentService.java +++ b/src/main/java/com/gaplog/server/domain/comment/application/CommentService.java @@ -7,6 +7,7 @@ import com.gaplog.server.domain.comment.dto.response.CommentLikeUpdateResponse; import com.gaplog.server.domain.comment.dto.response.CommentResponse; import com.gaplog.server.domain.comment.dto.response.CommentUpdateResponse; +import com.gaplog.server.domain.post.application.PostService; import com.gaplog.server.domain.post.dao.PostRepository; import com.gaplog.server.domain.post.domain.Post; import com.gaplog.server.domain.user.dao.UserRepository; @@ -28,6 +29,7 @@ public class CommentService { private final PostRepository postRepository; private final UserRepository userRepository; private final CommentLikeRepository commentLikeRepository; + private final PostService postService; @Transactional public CommentResponse createComment(Long postId, Long userId, String text, Long parentId) { @@ -40,6 +42,8 @@ public CommentResponse createComment(Long postId, Long userId, String text, Long Comment newComment = Comment.of(post, user, text, parentId); + postService.PostCommentCountUpdate(postId, true); + commentRepository.save(newComment); return CommentResponse.of(newComment); } @@ -99,6 +103,7 @@ public void deleteComment(Long commentId) { //case 2 & case 3의 자식 댓글 setDeleted comment.setDeleted(true); commentRepository.save(comment); + postService.PostCommentCountUpdate(comment.getPost().getId(),false); } } diff --git a/src/main/java/com/gaplog/server/domain/post/api/FileUploadController.java b/src/main/java/com/gaplog/server/domain/post/api/FileUploadController.java deleted file mode 100644 index 6ac5450..0000000 --- a/src/main/java/com/gaplog/server/domain/post/api/FileUploadController.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.gaplog.server.domain.post.api; - -import com.gaplog.server.domain.post.application.FileStorageService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; - -@RestController -@RequestMapping("/api/files") -@RequiredArgsConstructor -public class FileUploadController { - - private final FileStorageService fileStorageService; - - @PostMapping("/upload") - public ResponseEntity uploadFile(@RequestParam("file") MultipartFile file) { - String fileUrl = fileStorageService.storeFile(file); - return ResponseEntity.ok(fileUrl); - } -} diff --git a/src/main/java/com/gaplog/server/domain/post/api/PostApi.java b/src/main/java/com/gaplog/server/domain/post/api/PostApi.java index a273c25..04f3ac5 100644 --- a/src/main/java/com/gaplog/server/domain/post/api/PostApi.java +++ b/src/main/java/com/gaplog/server/domain/post/api/PostApi.java @@ -1,6 +1,7 @@ package com.gaplog.server.domain.post.api; +import com.gaplog.server.domain.post.application.PostImageService; import com.gaplog.server.domain.post.application.PostService; import com.gaplog.server.domain.post.domain.Post; import com.gaplog.server.domain.post.dto.request.*; @@ -8,19 +9,34 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import java.io.IOException; import java.util.List; @RestController @RequestMapping("/api/posts") -@RequiredArgsConstructor @Tag(name = "post API", description = "post API") public class PostApi { private final PostService postService; + private final PostImageService postImageService; + @Autowired + public PostApi(PostService postService, PostImageService postImageService) { + this.postService = postService; + this.postImageService = postImageService; + } + + @PostMapping("/upload") + public String upload(MultipartFile image) throws IOException { + String imageUrl = postImageService.upload(image); + //return "\"이미지\"/"; + return imageUrl; + } @PutMapping("/") @Operation(summary = "게시글 작성", description = "게시글을 작성합니다.") @@ -76,11 +92,33 @@ public ResponseEntity getSelectedPost(@PathVariable ("post return ResponseEntity.ok(response); } - @GetMapping("/") - @Operation(summary = "메인 페이지 게시글 조회", description = "메인 페이지에 뜨는 게시글의 정보를 얻습니다.") - public ResponseEntity> getMainPost() { + @GetMapping("/recent") + @Operation(summary = "메인 페이지 최근 게시글 조회", description = "메인 페이지에 뜨는 최근 게시글의 정보를 얻습니다.") + public ResponseEntity> getMainRecentPost() { + try { + List posts = postService.getMainRecentPostInfo(); + return new ResponseEntity<>(posts, HttpStatus.OK); + } catch (RuntimeException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping("/hot") + @Operation(summary = "메인 페이지 인기 게시글 조회", description = "메인 페이지에 뜨는 인기 게시글의 정보를 얻습니다.") + public ResponseEntity> getMainHotPost() { + try { + List posts = postService.getMainHotPostInfo(); + return new ResponseEntity<>(posts, HttpStatus.OK); + } catch (RuntimeException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping("/follow/{user_id}") + @Operation(summary = "메인 페이지 팔로우하는 유저 게시글 조회", description = "메인 페이지에 뜨는 팔로우하는 유저 게시글의 정보를 얻습니다.") + public ResponseEntity> getMainFollowPost(@PathVariable("user_id") Long userId) { try { - List posts = postService.getMainPostInfo(); + List posts = postService.getMainFollowPostInfo(userId); return new ResponseEntity<>(posts, HttpStatus.OK); } catch (RuntimeException e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); diff --git a/src/main/java/com/gaplog/server/domain/post/application/FileStorageService.java b/src/main/java/com/gaplog/server/domain/post/application/FileStorageService.java deleted file mode 100644 index f5414ff..0000000 --- a/src/main/java/com/gaplog/server/domain/post/application/FileStorageService.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.gaplog.server.domain.post.application; - -import com.gaplog.server.domain.post.domain.FileStorageProperties; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.UUID; - -@Service -public class FileStorageService { - - private final Path fileStorageLocation; - - @Autowired - public FileStorageService(FileStorageProperties fileStorageProperties) { - this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir()).toAbsolutePath().normalize(); - try { - Files.createDirectories(this.fileStorageLocation); - } catch (Exception ex) { - throw new RuntimeException("Could not create the directory where the uploaded files will be stored.", ex); - } - } - - public String storeFile(MultipartFile file) { - String fileName = StringUtils.cleanPath(UUID.randomUUID().toString() + "_" + file.getOriginalFilename()); - - try { - if (fileName.contains("..")) { - throw new RuntimeException("Sorry! Filename contains invalid path sequence " + fileName); - } - - Path targetLocation = this.fileStorageLocation.resolve(fileName); - Files.copy(file.getInputStream(), targetLocation, StandardCopyOption.REPLACE_EXISTING); - - return "/uploads/" + fileName; - } catch (IOException ex) { - throw new RuntimeException("Could not store file " + fileName + ". Please try again!", ex); - } - } -} - diff --git a/src/main/java/com/gaplog/server/domain/post/application/PostImageService.java b/src/main/java/com/gaplog/server/domain/post/application/PostImageService.java new file mode 100644 index 0000000..39277ec --- /dev/null +++ b/src/main/java/com/gaplog/server/domain/post/application/PostImageService.java @@ -0,0 +1,44 @@ +package com.gaplog.server.domain.post.application; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.ObjectMetadata; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Service +public class PostImageService { + private final AmazonS3Client s3Client; + + @Autowired + public PostImageService(AmazonS3Client s3Client) { + this.s3Client = s3Client; + } + + @Value("${s3.bucket}") + private String bucket; + + public String upload(MultipartFile image) throws IOException { + String originalFileName = image.getOriginalFilename(); + String fileName = changeFileName(originalFileName); + + ObjectMetadata metadata = new ObjectMetadata(); + metadata.setContentType(image.getContentType()); + metadata.setContentLength(image.getSize()); + + s3Client.putObject(bucket, fileName, image.getInputStream(), metadata); + + return s3Client.getUrl(bucket, fileName).toString(); + } + + private String changeFileName(String originalFileName) { + /* 업로드할 파일의 이름을 변경하는 로직 */ + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + return originalFileName + "_" + LocalDateTime.now().format(formatter); + } +} diff --git a/src/main/java/com/gaplog/server/domain/post/application/PostService.java b/src/main/java/com/gaplog/server/domain/post/application/PostService.java index a1499c5..f09d70f 100644 --- a/src/main/java/com/gaplog/server/domain/post/application/PostService.java +++ b/src/main/java/com/gaplog/server/domain/post/application/PostService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -108,7 +109,7 @@ public void deletePost(Long postId) { //조회 기능 //메인화면 게시글 조회 @Transactional - public List getMainPostInfo() { + public List getMainRecentPostInfo() { List posts = postRepository.findTop20ByOrderByCreatedAtDesc(PageRequest.of(0, 20)); // Post 엔티티를 PostResponse DTO로 변환 @@ -117,6 +118,29 @@ public List getMainPostInfo() { .collect(Collectors.toList()); } + @Transactional + public List getMainHotPostInfo() { + List posts = postRepository.findAllByOrderByLikeCountDesc(PageRequest.of(0, 20)); + + // Post 엔티티를 PostResponse DTO로 변환 + return posts.stream() + .map(MainPostResponse::of) + .collect(Collectors.toList()); + } + + @Transactional + public List getMainFollowPostInfo(Long userId) { + List posts = postRepository.findLatestPostsByFollowedUsers(userId); + + // 게시물들을 최신순으로 정렬하고 상위 20개만 반환 + return posts.stream() + .sorted(Comparator.comparing(Post::getCreatedAt).reversed()) + .limit(20) + .map(MainPostResponse::of) + .collect(Collectors.toList()); + + } + //특정 게시물(선택된 게시물) 조회 @Transactional public SelectedPostResponse getSelectedPostInfo(Long postId) { @@ -227,4 +251,20 @@ public PostScrapUpdateResponse PostScrapUpdate(Long postId, PostScrapUpdateReque return PostScrapUpdateResponse.of(updatedPost); } + @Transactional + public void PostCommentCountUpdate(Long postId, Boolean bool) { + Post post = postRepository.findById(postId).orElseThrow(() + ->new IllegalArgumentException("Post not found: " + postId)); + if(bool){ + post.increaseCommentCount(); + } + else{ + post.decreaseCommentCount(); + } + + postRepository.save(post); + + return; + } + } diff --git a/src/main/java/com/gaplog/server/domain/post/config/S3Config.java b/src/main/java/com/gaplog/server/domain/post/config/S3Config.java new file mode 100644 index 0000000..228dd56 --- /dev/null +++ b/src/main/java/com/gaplog/server/domain/post/config/S3Config.java @@ -0,0 +1,32 @@ +package com.gaplog.server.domain.post.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class S3Config { + + @Value("${s3.credentials.access-key}") + private String accessKey; + + @Value("${s3.credentials.secret-key}") + private String secretKey; + + @Value("${s3.credentials.region}") + private String region; + + @Bean + public AmazonS3Client s3Client(){ + BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + + return (AmazonS3Client) AmazonS3Client.builder() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) + .build(); + } + +} diff --git a/src/main/java/com/gaplog/server/domain/post/dao/PostRepository.java b/src/main/java/com/gaplog/server/domain/post/dao/PostRepository.java index 0fb74bb..abbe90d 100644 --- a/src/main/java/com/gaplog/server/domain/post/dao/PostRepository.java +++ b/src/main/java/com/gaplog/server/domain/post/dao/PostRepository.java @@ -4,6 +4,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; @@ -16,6 +17,11 @@ public interface PostRepository extends JpaRepository { @Query("SELECT p FROM Post p ORDER BY p.createdAt DESC") List findTop20ByOrderByCreatedAtDesc(Pageable pageable); List findByTitleContainingOrContentContaining(String titleKeyword, String contentKeyword); + List findAllByOrderByLikeCountDesc(Pageable pageable); + @Query("SELECT p FROM Post p WHERE p.user IN " + + "(SELECT ur.followee FROM UserRelationships ur WHERE ur.follower.id = :userId) " + + "AND p.createdAt = (SELECT MAX(p2.createdAt) FROM Post p2 WHERE p2.user = p.user)") + List findLatestPostsByFollowedUsers(@Param("userId") Long userId); // 특정 유저가 작성한 게시물 조회 List findByUserId(Long userId); } diff --git a/src/main/java/com/gaplog/server/domain/post/domain/FileStorageProperties.java b/src/main/java/com/gaplog/server/domain/post/domain/FileStorageProperties.java deleted file mode 100644 index 683eade..0000000 --- a/src/main/java/com/gaplog/server/domain/post/domain/FileStorageProperties.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.gaplog.server.domain.post.domain; - -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; -import org.springframework.stereotype.Component; - -@Component -@Configuration -@ConfigurationProperties(prefix = "file") -public class FileStorageProperties { - - private String uploadDir; - - public String getUploadDir() { - return uploadDir; - } - - public void setUploadDir(String uploadDir) { - this.uploadDir = uploadDir; - } -} diff --git a/src/main/java/com/gaplog/server/domain/post/domain/Post.java b/src/main/java/com/gaplog/server/domain/post/domain/Post.java index ea731bb..d004557 100644 --- a/src/main/java/com/gaplog/server/domain/post/domain/Post.java +++ b/src/main/java/com/gaplog/server/domain/post/domain/Post.java @@ -54,6 +54,9 @@ public class Post { @Column private int scrapCount; + @Column + private int commentCount; + @Column private boolean isPrivate; @@ -100,6 +103,10 @@ public void increaseScrapCount() { this.scrapCount++; } + public void increaseCommentCount() { + this.commentCount++; + } + public void decreaseLikeCount() { this.likeCount--; } @@ -112,14 +119,14 @@ public void decreaseScrapCount() { this.scrapCount--; } + public void decreaseCommentCount() { this.commentCount--; } + public void updateIsPrivate() { this.isPrivate = true; } - public void updateIsDeleted() { - this.isDeleted = true; - } + public void updateIsDeleted() { this.isDeleted = true; } @Builder public Post(String title, String content, Category category, String thumbnailUrl, User user) { @@ -131,6 +138,7 @@ public Post(String title, String content, Category category, String thumbnailUrl this.likeCount = 0; this.SeriousnessCount = 0; this.scrapCount = 0; + this.commentCount = 0; this.isPrivate = false; this.isDeleted = false; this.version = 0L; diff --git a/src/main/java/com/gaplog/server/domain/post/dto/response/MainPostResponse.java b/src/main/java/com/gaplog/server/domain/post/dto/response/MainPostResponse.java index 89e5597..561374d 100644 --- a/src/main/java/com/gaplog/server/domain/post/dto/response/MainPostResponse.java +++ b/src/main/java/com/gaplog/server/domain/post/dto/response/MainPostResponse.java @@ -22,6 +22,8 @@ public class MainPostResponse { private LocalDateTime updatedAt; private int likeCount; private int jinjiCount; + private int scrapCount; + private int commentCount; public static MainPostResponse of(Post post) { return MainPostResponse.builder() @@ -30,6 +32,8 @@ public static MainPostResponse of(Post post) { .thumbnailUrl(post.getThumbnailUrl()) .likeCount(post.getLikeCount()) .jinjiCount(post.getSeriousnessCount()) + .scrapCount(post.getScrapCount()) + .commentCount(post.getCommentCount()) .createdAt(LocalDateTime.parse(post.getCreatedAt().toString())) .updatedAt(LocalDateTime.parse(post.getUpdatedAt().toString())) .build(); diff --git a/src/main/java/com/gaplog/server/domain/post/dto/response/SelectedPostResponse.java b/src/main/java/com/gaplog/server/domain/post/dto/response/SelectedPostResponse.java index 3ac7a41..06ca433 100644 --- a/src/main/java/com/gaplog/server/domain/post/dto/response/SelectedPostResponse.java +++ b/src/main/java/com/gaplog/server/domain/post/dto/response/SelectedPostResponse.java @@ -22,6 +22,8 @@ public class SelectedPostResponse { private LocalDateTime updatedAt; private int likeCount; private int jinjiCount; + private int scrapCount; + private int commentCount; public static SelectedPostResponse of(Post post) { return SelectedPostResponse.builder() @@ -30,6 +32,8 @@ public static SelectedPostResponse of(Post post) { .content(post.getContent()) .likeCount(post.getLikeCount()) .jinjiCount(post.getSeriousnessCount()) + .scrapCount(post.getScrapCount()) + .commentCount(post.getCommentCount()) .createdAt(LocalDateTime.parse(post.getCreatedAt().toString())) .updatedAt(LocalDateTime.parse(post.getUpdatedAt().toString())) .build(); diff --git a/src/main/java/com/gaplog/server/domain/user/application/SeriousnessService.java b/src/main/java/com/gaplog/server/domain/user/application/SeriousnessService.java index aa06988..af4262e 100644 --- a/src/main/java/com/gaplog/server/domain/user/application/SeriousnessService.java +++ b/src/main/java/com/gaplog/server/domain/user/application/SeriousnessService.java @@ -26,7 +26,7 @@ public class SeriousnessService { @Transactional public void updateSeriousnessTier(Long userId){ // 유저의 총 진지버튼 수 - int totalSeriousnessCount = postRepository.findById(userId).stream().mapToInt(Post::getJinjiCount).sum(); + int totalSeriousnessCount = postRepository.findById(userId).stream().mapToInt(Post::getSeriousnessCount).sum(); // 진지 티어 계산 Seriousness seriousness = seriousnessRepository.findByUserId(userId) .orElseGet(()-> new Seriousness(userRepository.findById(userId).orElseThrow())); @@ -48,7 +48,7 @@ public List getSeriousnessField(Long userId){ .map(entry -> { LocalDate date = entry.getKey(); List postsOnDate = entry.getValue(); - int seriousnessCount = postsOnDate.stream().mapToInt(Post::getJinjiCount).sum(); + int seriousnessCount = postsOnDate.stream().mapToInt(Post::getSeriousnessCount).sum(); int postCount = postsOnDate.size(); return new SeriousnessFieldResponse(date, seriousnessCount, postCount); }) diff --git a/src/main/java/com/gaplog/server/domain/user/dto/response/PostScrapResponse.java b/src/main/java/com/gaplog/server/domain/user/dto/response/PostScrapResponse.java index 53d51cd..7ce6f0a 100644 --- a/src/main/java/com/gaplog/server/domain/user/dto/response/PostScrapResponse.java +++ b/src/main/java/com/gaplog/server/domain/user/dto/response/PostScrapResponse.java @@ -32,7 +32,7 @@ public static PostScrapResponse of(Long scrapId, Post post) { .createdAt(post.getCreatedAt()) .updatedAt(post.getUpdatedAt()) .likeCount(post.getLikeCount()) - .seriousnessCount(post.getJinjiCount()) + .seriousnessCount(post.getSeriousnessCount()) .build(); } }