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
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public class ErrorMessage {
final public static String HASHTAG_NOT_FOUND = "해쉬태그가 존재하지 않습니다.";
final public static String POST_NOT_FOUND = "게시글이 존재하지 않습니다.";
final public static String ALREADY_EXISTS_CATEGORY = "이미 존재하는 카테고리 이름입니다.";
final public static String INVALID_PASSWORD = "비밀번호가 일치하지 않습니다";
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.starchive.springapp.category.exception.CategoryNotFoundException;
import com.starchive.springapp.global.dto.ErrorResult;
import com.starchive.springapp.hashtag.exception.HashTagNotFoundException;
import com.starchive.springapp.post.exception.InvalidPasswordException;
import com.starchive.springapp.post.exception.PostNotFoundException;
import java.util.HashMap;
import java.util.Map;
Expand Down Expand Up @@ -47,4 +48,10 @@ public ResponseEntity<ErrorResult> handleRuntimeException(RuntimeException ex) {
ErrorResult errorResult = new ErrorResult(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage());
return ResponseEntity.internalServerError().body(errorResult);
}

@ExceptionHandler(InvalidPasswordException.class)
public ResponseEntity<ErrorResult> handleInvalidPasswordException(InvalidPasswordException ex) {
ErrorResult errorResult = new ErrorResult(HttpStatus.BAD_REQUEST, ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResult);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ public interface PostImageRepository extends JpaRepository<PostImage, Long> {
void deleteByIds(@Param("ids") List<Long> ids);

List<PostImage> findManyByIdIn(@Param("ids") List<Long> ids);

@Query("select pi from PostImage pi where pi.post.id = :postId")
List<PostImage> findAllByPostId(@Param("postId")Long postId);

@Modifying
@Query("update PostImage pi set pi.post.id = :postId where pi.imagePath in :imageUrls")
int bulkUpdatePostId(@Param("imageUrls") List<String> imageUrls,@Param("postId")Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,18 @@ public void setPost(List<Long> imageIds, Post post) {
});
}

public void setPostByImagePath(List<String> imagePaths, Post post) {
postImageRepository.bulkUpdatePostId(imagePaths, post.getId());
}

@Scheduled(cron = "0 0 2 * * ?")
public void deleteOldOrphanedPostImages() {
LocalDateTime cutoffDate = LocalDateTime.now().minusDays(1); // 하루 전

List<PostImage> oldOrphanedPostImages = postImageRepository.findOldOrphanedPostImages(cutoffDate);
oldOrphanedPostImages.forEach(postImage -> {
s3Service.deleteObject(extractKeyFromUrl(postImage.getImagePath()));
postImageRepository.delete(postImage);
log.info("Deleted old orphaned PostImages: {}", postImage.getImagePath());
});

}

private String extractKeyFromUrl(String url) {
URI uri = URI.create(url);
return uri.getPath().substring(1); // 첫 번째 '/' 제거
List<String> urls = oldOrphanedPostImages.stream().map(postImage -> postImage.getImagePath()).toList();
s3Service.deleteObjects(urls);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@
import com.starchive.springapp.post.dto.PostCreateRequest;
import com.starchive.springapp.post.dto.PostDto;
import com.starchive.springapp.post.dto.PostListResponse;
import com.starchive.springapp.post.dto.PostUpdateRequest;
import com.starchive.springapp.post.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Null;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

@RequiredArgsConstructor
@RestController
Expand Down Expand Up @@ -50,4 +47,18 @@ public ResponseEntity<PostDto> findPost(@PathVariable("postId") Long postId) {
PostDto postDto = postService.findOne(postId);
return ResponseEntity.ok(postDto);
}

@PutMapping("/post")
@Operation(summary = "게시글 수정")
public ResponseEntity<PostDto> update(@Valid @RequestBody PostUpdateRequest request) {
PostDto postDto = postService.update(request);
return ResponseEntity.ok(postDto);
}

@DeleteMapping("/post/{postId}")
@Operation(summary = "게시글 삭제")
public ResponseEntity<Null> delete(@Valid @PathVariable("postId") Long postId) {
postService.delete(postId);
return ResponseEntity.status(204).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.starchive.springapp.category.domain.Category;
import com.starchive.springapp.post.dto.PostCreateRequest;
import com.starchive.springapp.post.dto.PostUpdateRequest;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
Expand Down Expand Up @@ -63,4 +64,11 @@ public static Post of(PostCreateRequest request, Category category) {
return post;
}

public void update(PostUpdateRequest request, Category category) {
title = request.getTitle();
content = request.getContent();
author = request.getAuthor();
this.category = category;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.starchive.springapp.post.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PostUpdateRequest {
@NotNull
@Schema(description = "게시글 id",example = "1")
private Long id;

@NotEmpty
@Size(max = 64)
@Schema(description = "게시글 제목", example = "게시글 제목 예시")
private String title;

@NotEmpty
@Schema(description = "게시글 내용", example = "게시글 내용 예시")
private String content;

@NotEmpty
@Size(max = 32)
@Schema(description = "작성자 이름", example = "홍길동")
private String author;

@NotEmpty
@Size(max = 128)
@Schema(description = "비밀번호", example = "1234")
private String password;

@NotNull
@Schema(description = "카테고리 ID", example = "1")
private Long categoryId;

@Nullable
@Schema(description = "해쉬 태그 ID", example = "[1,2,3]")
private List<Long> hashTagIds;

@Nullable
@Schema(description = "첨부 이미지 ID", example = "[1,2]")
private List<Long> imageIds;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.starchive.springapp.post.exception;

import static com.starchive.springapp.global.ErrorMessage.INVALID_PASSWORD;

public class InvalidPasswordException extends RuntimeException {
public InvalidPasswordException() {
super(INVALID_PASSWORD);
}
public InvalidPasswordException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,23 @@
import com.starchive.springapp.category.service.CategoryService;
import com.starchive.springapp.hashtag.dto.HashTagResponse;
import com.starchive.springapp.hashtag.service.HashTagService;
import com.starchive.springapp.image.domain.PostImage;
import com.starchive.springapp.image.repository.PostImageRepository;
import com.starchive.springapp.image.service.PostImageService;
import com.starchive.springapp.post.domain.Post;
import com.starchive.springapp.post.dto.PostCreateRequest;
import com.starchive.springapp.post.dto.PostDto;
import com.starchive.springapp.post.dto.PostListResponse;
import com.starchive.springapp.post.dto.PostSimpleDto;
import com.starchive.springapp.post.dto.*;
import com.starchive.springapp.post.exception.InvalidPasswordException;
import com.starchive.springapp.post.exception.PostNotFoundException;
import com.starchive.springapp.post.repository.PostRepository;
import com.starchive.springapp.posthashtag.domain.PostHashTag;
import com.starchive.springapp.posthashtag.service.PostHashTagService;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.starchive.springapp.s3.S3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
Expand All @@ -32,6 +37,8 @@ public class PostService {
private final CategoryService categoryService;
private final PostImageService postImageService;
private final HashTagService hashTagService;
private final PostImageRepository postImageRepository;
private final S3Service s3Service;

public void createPost(PostCreateRequest request) {
Category category = categoryService.findOne(request.getCategoryId());
Expand Down Expand Up @@ -82,6 +89,98 @@ public PostListResponse findPosts(Long categoryId, Long hashTagId, int pageNum,
return PostListResponse.from(dtoPage);
}

public PostDto update(PostUpdateRequest request) {
Post post = postRepository.findById(request.getId()).orElseThrow(PostNotFoundException::new);

if(request.getPassword() != post.getPassword()) {
throw new InvalidPasswordException();
}

Category category = categoryService.findOne(request.getCategoryId());

post.update(request, category);

HashSet<Long> hashTagIds = new HashSet<>();
postHashTagService.findManyByPost(post.getId()).stream().forEach(postHashTag -> {hashTagIds.add(postHashTag.getHashTag().getId());});

updatePostHashTags(request, hashTagIds, post);

updateImages(request, post);

List<HashTagResponse> hashTagResponses = hashTagService.findManyByPost(post.getId());
return PostDto.of(post, hashTagResponses);
}

public void delete(Long postId) {
Post post = postRepository.findById(postId).orElseThrow(PostNotFoundException::new);

List<PostImage> postImages = postImageRepository.findAllByPostId(post.getId());

List<String> urls = postImages.stream().map(PostImage::getImagePath).toList();
s3Service.deleteObjects(urls);

postImageRepository.deleteAll(postImages);

postHashTagService.deleteManyByPostId(post.getId());

postRepository.delete(post);
}

private void updateImages(PostUpdateRequest postUpdateRequest, Post post){
String content = postUpdateRequest.getContent();
HashSet<String> imageUrlsToUpdate = new HashSet<>();

imageUrlsToUpdate.addAll(extractImageUrls(content));

List<PostImage> postImages = postImageRepository.findAllByPostId(postUpdateRequest.getId());

List<PostImage> postImageToDelete = new ArrayList<>();
for (PostImage postImage : postImages) {
if (imageUrlsToUpdate.contains(postImage.getImagePath())) {
imageUrlsToUpdate.remove(postImage.getImagePath());
continue;
}
postImageToDelete.add(postImage);
}

List<String> urls = postImageToDelete.stream().map(PostImage::getImagePath).toList();
s3Service.deleteObjects(urls);
postImageRepository.deleteAll(postImageToDelete);
postImageService.setPostByImagePath(new ArrayList<>(imageUrlsToUpdate),post);
}

private List<String> extractImageUrls(String markdownContent) {
List<String> imageUrls = new ArrayList<>();

// 정규식 패턴 정의
String regex = "!\\[.*?\\]\\((.*?)\\)";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(markdownContent);

// 매칭된 이미지 URL 추출
while (matcher.find()) {
imageUrls.add(matcher.group(1));
}

return imageUrls;
}

private void updatePostHashTags(PostUpdateRequest request, HashSet<Long> hashTagIds, Post post) {
List<Long> hashTagIdsToUpdate = new ArrayList<>();
if(request.getHashTagIds() != null) {
for (Long updatedHashTagId : request.getHashTagIds()) {
if(hashTagIds.contains(updatedHashTagId)) {
hashTagIds.remove(updatedHashTagId);
continue;
}
hashTagIdsToUpdate.add(updatedHashTagId);
}
}

postHashTagService.storePostHashTag(hashTagIdsToUpdate, post);
postHashTagService.deleteManyByHashTagIds(hashTagIds);
}

private List<Long> extractCategoryIds(Category category) {
List<Long> categoryIds = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
package com.starchive.springapp.posthashtag.repository;

import com.starchive.springapp.post.domain.Post;
import com.starchive.springapp.posthashtag.domain.PostHashTag;

import java.util.HashSet;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface PostHashTagRepository extends JpaRepository<PostHashTag, Long> {

@Query("select ph from PostHashTag ph WHERE ph.hashTag.id = :hashTagId")
List<PostHashTag> findAllByHashTagId(@Param("hashTagId") Long hasTagId);

List<PostHashTag> post(Post post);

@Query("select ph from PostHashTag ph where ph.post.id = :postId")
List<PostHashTag> findAllByPostId(@Param("postId") Long postId);

@Modifying
@Query("DELETE FROM PostHashTag p WHERE p.hashTag.id = :hashTagId")
@Query("DELETE FROM PostHashTag ph WHERE ph.hashTag.id = :hashTagId")
void deleteAllByHashTagId(@Param("hashTagId") Long hasTagId);

@Query("select p from PostHashTag p WHERE p.hashTag.id = :hashTagId")
List<PostHashTag> findAllByHashTagId(@Param("hashTagId") Long hasTagId);
@Modifying
@Query("delete from PostHashTag ph where ph.hashTag.id in :hashTagIds")
void deleteAllByHashTagIds(@Param("hashTagIds") HashSet<Long> hashTagIds);

@Modifying
@Query("delete from PostHashTag ph where ph.post.id = :postId")
void deleteAllByPostId(@Param("postId") Long postId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.starchive.springapp.posthashtag.domain.PostHashTag;
import com.starchive.springapp.posthashtag.repository.PostHashTagRepository;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -38,4 +39,16 @@ public void storePostHashTag(List<Long> hashTagsIds, Post post) {
public List<PostHashTag> findManyByHashTag(Long hashTagId) {
return postHashTagRepository.findAllByHashTagId(hashTagId);
}

public List<PostHashTag> findManyByPost(Long postId) {
return postHashTagRepository.findAllByPostId(postId);
}

public void deleteManyByHashTagIds(HashSet<Long> hashTagIds) {
postHashTagRepository.deleteAllByHashTagIds(hashTagIds);
}

public void deleteManyByPostId(Long postId) {
postHashTagRepository.deleteAllByPostId(postId);
}
}
Loading