diff --git a/src/main/java/com/example/spot/api/code/status/ErrorStatus.java b/src/main/java/com/example/spot/api/code/status/ErrorStatus.java index ce11e694..36acd1e6 100644 --- a/src/main/java/com/example/spot/api/code/status/ErrorStatus.java +++ b/src/main/java/com/example/spot/api/code/status/ErrorStatus.java @@ -97,6 +97,7 @@ public enum ErrorStatus implements BaseErrorCode { _STUDY_POST_COMMENT_NULL(HttpStatus.BAD_REQUEST, "POST4016", "댓글 아이디가 입력되지 않았습니다."), _STUDY_POST_COMMENT_REACTIOM_ID_NULL(HttpStatus.BAD_REQUEST, "POST4017", "댓글 반응 아이디가 입력되지 않았습니다."), _STUDY_POST_COMMENT_REACTION_NOT_FOUND(HttpStatus.BAD_REQUEST, "POST4018", "댓글 반응이 존재하지 않습니다."), + _STUDY_POST_UPDATE_INVALID(HttpStatus.FORBIDDEN, "POST4019", "게시글 작성자만 변경 가능합니다."), //스터디 일정 관련 에러 _STUDY_SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "SCHEDULE4001", "스터디 일정을 찾을 수 없습니다."), diff --git a/src/main/java/com/example/spot/domain/study/StudyPost.java b/src/main/java/com/example/spot/domain/study/StudyPost.java index b58d5397..57946a27 100644 --- a/src/main/java/com/example/spot/domain/study/StudyPost.java +++ b/src/main/java/com/example/spot/domain/study/StudyPost.java @@ -5,6 +5,7 @@ import com.example.spot.domain.enums.Theme; import com.example.spot.domain.mapping.StudyLikedPost; import com.example.spot.domain.mapping.StudyPostImage; +import com.example.spot.web.dto.memberstudy.request.StudyPostRequestDTO; import jakarta.persistence.*; import java.time.LocalDateTime; @@ -135,4 +136,20 @@ public void minusLikeNum() { public void addStudyPostReport(StudyPostReport studyPostReport) { studyPostReports.add(studyPostReport); } + + public void updatePost(StudyPostRequestDTO.PostDTO requestDTO) { + isAnnouncement = requestDTO.getIsAnnouncement(); + theme = requestDTO.getTheme(); + title = requestDTO.getTitle(); + content = requestDTO.getContent(); + + if (isAnnouncement) { + announcedAt = LocalDateTime.now(); + } else { + announcedAt = null; + } + + member.updateStudyPost(this); + study.updateStudyPost(this); + } } diff --git a/src/main/java/com/example/spot/repository/StudyPostImageRepository.java b/src/main/java/com/example/spot/repository/StudyPostImageRepository.java index a12b7be7..e7b6f56a 100644 --- a/src/main/java/com/example/spot/repository/StudyPostImageRepository.java +++ b/src/main/java/com/example/spot/repository/StudyPostImageRepository.java @@ -2,9 +2,17 @@ import com.example.spot.domain.mapping.StudyPostImage; 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; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; @Repository public interface StudyPostImageRepository extends JpaRepository { - void deleteAllByStudyPostId(Long studyPostId); + + @Modifying + @Transactional + @Query("DELETE FROM StudyPostImage spi WHERE spi.studyPost.id = :studyPostId") + void deleteAllByStudyPostId(@Param("studyPostId") Long studyPostId); } diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostCommandService.java b/src/main/java/com/example/spot/service/studypost/StudyPostCommandService.java index 52ae1994..7780a442 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostCommandService.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostCommandService.java @@ -10,6 +10,9 @@ public interface StudyPostCommandService { // 스터디 게시글 생성 StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestDTO.PostDTO postRequestDTO); + // 스터디 게시글 편집 + StudyPostResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StudyPostRequestDTO.PostDTO postDTO); + // 스터디 게시글 삭제 StudyPostResDTO.PostPreviewDTO deletePost(Long studyId, Long postId); diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java b/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java index 503682e8..eb13ee2b 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostCommandServiceImpl.java @@ -148,6 +148,53 @@ public StudyPostResDTO.PostPreviewDTO createPost(Long studyId, StudyPostRequestD return StudyPostResDTO.PostPreviewDTO.toDTO(studyPost); } + @Override + public StudyPostResDTO.PostPreviewDTO updatePost(Long studyId, Long postId, StudyPostRequestDTO.PostDTO postDTO) { + + Long memberId = SecurityUtils.getCurrentUserId(); + SecurityUtils.verifyUserId(memberId); + + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new MemberHandler(ErrorStatus._MEMBER_NOT_FOUND)); + Study study = studyRepository.findById(studyId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_NOT_FOUND)); + StudyPost studyPost = studyPostRepository.findById(postId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_NOT_FOUND)); + + // 로그인한 회원이 스터디 회원인지 확인 + MemberStudy memberStudy = memberStudyRepository.findByMemberIdAndStudyIdAndStatus(memberId, studyId, ApplicationStatus.APPROVED) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_MEMBER_NOT_FOUND)); + + // 로그인한 회원이 게시글 작성자인지 확인 + studyPostRepository.findByIdAndMemberId(postId, memberId) + .orElseThrow(() -> new StudyHandler(ErrorStatus._STUDY_POST_UPDATE_INVALID)); + + // 스터디장만 공지 가능 + if (!memberStudy.getIsOwned() && postDTO.getIsAnnouncement()) { + throw new StudyHandler(ErrorStatus._STUDY_POST_ANNOUNCEMENT_INVALID); + } + + // 스터디 게시글 업데이트 + studyPost.updatePost(postDTO); + studyPost = studyPostRepository.save(studyPost); + + // 기존 게시글 이미지 삭제 + studyPostImageRepository.deleteAllByStudyPostId(postId); + + if (postDTO.getImages() != null && !postDTO.getImages().isEmpty()) { + ImageResponse.ImageUploadResponse imageUploadResponse = s3ImageService.uploadImages(postDTO.getImages()); + for (ImageResponse.Images imageDTO : imageUploadResponse.getImageUrls()) { + String imageUrl = imageDTO.getImageUrl(); + StudyPostImage studyPostImage = new StudyPostImage(imageUrl); + studyPost.addImage(studyPostImage); // image id가 저장되지 않음 + studyPostImage = studyPostImageRepository.save(studyPostImage); + studyPost.updateImage(studyPostImage); // image id 저장 + } + } + + return StudyPostResDTO.PostPreviewDTO.toDTO(studyPost); + } + /** * 스터디 내부 게시판에 작성된 게시글을 삭제합니다. * @param studyId 게시글을 삭제할 타겟 스터디의 아이디를 입력 받습니다. diff --git a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java index 0ee04353..647a9bb3 100644 --- a/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java +++ b/src/main/java/com/example/spot/service/studypost/StudyPostQueryServiceImpl.java @@ -128,8 +128,8 @@ public StudyPostResDTO.PostDetailDTO getPost(Long studyId, Long postId) { Integer commentNum = studyPostCommentRepository.findAllByStudyPostId(postId).size(); boolean isLiked = studyLikedPostRepository.existsByMemberIdAndStudyPostId(memberId, studyPost.getId()); - - return StudyPostResDTO.PostDetailDTO.toDTO(studyPost, commentNum, isLiked); + boolean isWriter = studyPost.getMember().getId().equals(memberId); + return StudyPostResDTO.PostDetailDTO.toDTO(studyPost, commentNum, isLiked, isWriter); } /* ----------------------------- 스터디 게시글 댓글 관련 API ------------------------------------- */ diff --git a/src/main/java/com/example/spot/web/controller/StudyPostController.java b/src/main/java/com/example/spot/web/controller/StudyPostController.java index dcfc2dcb..f4a3b8e0 100644 --- a/src/main/java/com/example/spot/web/controller/StudyPostController.java +++ b/src/main/java/com/example/spot/web/controller/StudyPostController.java @@ -61,6 +61,26 @@ public ApiResponse createPost( return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_CREATED, postPreviewDTO); } + @Tag(name= "스터디 게시글") + @Operation(summary = "[스터디 게시글] 게시글 편집하기", description = """ + 로그인한 회원이 참여하는 특정 스터디에서 작성한 게시글을 편집합니다. + 수정사항은 study_post db에 반영됩니다. + """) + @Parameter(name = "studyId", description = "스터디의 id를 입력합니다.", required = true) + @Parameter(name = "postId", description = "편집할 스터디 게시글의 id를 입력합니다.", required = true) + @PatchMapping(value = "/studies/{studyId}/posts/{postId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public ApiResponse updatePost( + @PathVariable @ExistStudy Long studyId, + @PathVariable @ExistStudyPost Long postId, + @ModelAttribute(name= "post") @Valid StudyPostRequestDTO.PostDTO postDTO + ) { + if (postDTO.getImages() == null) { + postDTO.initImages(); + } + StudyPostResDTO.PostPreviewDTO postPreviewDTO = studyPostCommandService.updatePost(studyId, postId, postDTO); + return ApiResponse.onSuccess(SuccessStatus._STUDY_POST_UPDATED, postPreviewDTO); + } + @Tag(name = "스터디 게시글") @Operation(summary = "[스터디 게시글] 게시글 삭제하기", description = """ ## [스터디 게시글] 로그인한 회원이 참여하는 특정 스터디에서 작성한 게시글을 삭제합니다. diff --git a/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostResDTO.java b/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostResDTO.java index 1d891068..f6ec2274 100644 --- a/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostResDTO.java +++ b/src/main/java/com/example/spot/web/dto/memberstudy/response/StudyPostResDTO.java @@ -99,9 +99,10 @@ public static class PostDetailDTO { private final Integer hitNum; private final Integer commentNum; private final Boolean isLiked; + private final Boolean isWriter; private final List studyPostImages; - public static PostDetailDTO toDTO(StudyPost studyPost, Integer commentNum, boolean isLiked) { + public static PostDetailDTO toDTO(StudyPost studyPost, Integer commentNum, boolean isLiked, boolean isWriter) { return PostDetailDTO.builder() .member(PostMemberDTO.toDTO(studyPost.getMember())) .postId(studyPost.getId()) @@ -114,6 +115,7 @@ public static PostDetailDTO toDTO(StudyPost studyPost, Integer commentNum, boole .hitNum(studyPost.getHitNum()) .commentNum(commentNum) .isLiked(isLiked) + .isWriter(isWriter) .studyPostImages(studyPost.getImages().stream() .map(ImageDTO::toDTO) .toList())