diff --git a/src/main/java/itstime/reflog/comment/controller/CommentController.java b/src/main/java/itstime/reflog/comment/controller/CommentController.java new file mode 100644 index 0000000..831d073 --- /dev/null +++ b/src/main/java/itstime/reflog/comment/controller/CommentController.java @@ -0,0 +1,129 @@ +package itstime.reflog.comment.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import itstime.reflog.comment.dto.CommentDto; +import itstime.reflog.comment.service.CommentService; +import itstime.reflog.common.CommonApiResponse; +import itstime.reflog.common.annotation.UserId; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "COMMENT API", description = "댓글에 대한 API입니다.") +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/comments") +public class CommentController { + + private final CommentService commentService; + + @Operation( + summary = "댓글 생성 API", + description = "새로운 댓글을 생성합니다. AccessToken 필요.", + responses = { + @ApiResponse( + responseCode = "200", + description = "댓글 생성 성공" + ), + @ApiResponse( + responseCode = "404", + description = "해당 회원을 찾을 수 없음" + ), + @ApiResponse( + responseCode = "500", + description = "서버 에러" + ) + } + ) + @PostMapping("/{communityId}") + public ResponseEntity> createComment( + @PathVariable Long communityId, + @UserId String memberId, + @RequestBody CommentDto.CommentSaveOrUpdateRequest dto + ) { + commentService.createComment(communityId, memberId, dto); + return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); + } + + @Operation( + summary = "댓글 수정 API", + description = "댓글을 수정합니다. AccessToken 필요.", + responses = { + @ApiResponse( + responseCode = "200", + description = "댓글 수정 성공" + ), + @ApiResponse( + responseCode = "404", + description = "해당 회원을 찾을 수 없음" + ), + @ApiResponse( + responseCode = "500", + description = "서버 에러" + ) + } + ) + @PatchMapping("/{commentId}") + public ResponseEntity> updateComment( + @PathVariable Long commentId, + @RequestBody CommentDto.CommentSaveOrUpdateRequest dto + ) { + commentService.updateComment(commentId, dto); + return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); + } + + @Operation( + summary = "댓글 삭제 API", + description = "댓글을 삭제합니다. AccessToken 필요.", + responses = { + @ApiResponse( + responseCode = "200", + description = "댓글 삭제 성공" + ), + @ApiResponse( + responseCode = "404", + description = "해당 회원을 찾을 수 없음" + ), + @ApiResponse( + responseCode = "500", + description = "서버 에러" + ) + } + ) + @DeleteMapping("/{commentId}") + public ResponseEntity> deleteComment( + @PathVariable Long commentId + ) { + commentService.deleteComment(commentId); + return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); + } + + @Operation( + summary = "댓글 좋아요 API", + description = "댓글에 좋아요를 합니다. AccessToken 필요.", + responses = { + @ApiResponse( + responseCode = "200", + description = "댓글 좋아요 성공" + ), + @ApiResponse( + responseCode = "404", + description = "해당 회원을 찾을 수 없음" + ), + @ApiResponse( + responseCode = "500", + description = "서버 에러" + ) + } + ) + @PostMapping("/like/{commentId}") + public ResponseEntity> toggleCommentLike( + @PathVariable Long commentId, + @UserId String memberId + ) { + commentService.toggleCommentLike(commentId, memberId); + return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); + } +} diff --git a/src/main/java/itstime/reflog/comment/domain/Comment.java b/src/main/java/itstime/reflog/comment/domain/Comment.java new file mode 100644 index 0000000..ffaad26 --- /dev/null +++ b/src/main/java/itstime/reflog/comment/domain/Comment.java @@ -0,0 +1,55 @@ +package itstime.reflog.comment.domain; + +import itstime.reflog.comment.dto.CommentDto; +import itstime.reflog.commentLike.domain.CommentLike; +import itstime.reflog.community.domain.Community; +import itstime.reflog.member.domain.Member; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class Comment { + + @Id + @Column(name = "comment_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "commnunity_id", nullable = false) + private Community community; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @OneToMany(mappedBy = "comment", cascade = CascadeType.ALL, orphanRemoval = true) + private List likes = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "parent_id") + private Comment parent; + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) + private List children = new ArrayList<>(); + + private LocalDateTime createdAt; // 생성일 + + private LocalDateTime updatedAt; // 수정일 + + public void update(CommentDto.CommentSaveOrUpdateRequest dto) { + this.content = dto.getContent(); + this.updatedAt = LocalDateTime.now(); + } +} diff --git a/src/main/java/itstime/reflog/comment/dto/CommentDto.java b/src/main/java/itstime/reflog/comment/dto/CommentDto.java new file mode 100644 index 0000000..bfdae13 --- /dev/null +++ b/src/main/java/itstime/reflog/comment/dto/CommentDto.java @@ -0,0 +1,30 @@ +package itstime.reflog.comment.dto; + +import jakarta.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +public class CommentDto { + + @Getter + @AllArgsConstructor + public static class CommentSaveOrUpdateRequest { + + @NotBlank(message = "댓글은 비어 있을 수 없습니다.") + private String content; + + private Long parentId; + } + + @Getter + @AllArgsConstructor + public static class CommentResponse { + private Long commentId; + private String name; + private String content; + private Long parentId; + private LocalDateTime createdAt; + } +} diff --git a/src/main/java/itstime/reflog/comment/repository/CommentRepository.java b/src/main/java/itstime/reflog/comment/repository/CommentRepository.java new file mode 100644 index 0000000..17d7074 --- /dev/null +++ b/src/main/java/itstime/reflog/comment/repository/CommentRepository.java @@ -0,0 +1,13 @@ +package itstime.reflog.comment.repository; + +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.community.domain.Community; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CommentRepository extends JpaRepository { + + // 커뮤니티 모든 댓글 조회 + List findAllByCommunityOrderByCreatedAtDesc(Community community); +} diff --git a/src/main/java/itstime/reflog/comment/service/CommentService.java b/src/main/java/itstime/reflog/comment/service/CommentService.java new file mode 100644 index 0000000..4dab1fe --- /dev/null +++ b/src/main/java/itstime/reflog/comment/service/CommentService.java @@ -0,0 +1,111 @@ +package itstime.reflog.comment.service; + +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.comment.dto.CommentDto; +import itstime.reflog.comment.repository.CommentRepository; +import itstime.reflog.commentLike.domain.CommentLike; +import itstime.reflog.commentLike.repository.CommentLikeRepository; +import itstime.reflog.common.code.status.ErrorStatus; +import itstime.reflog.common.exception.GeneralException; +import itstime.reflog.community.domain.Community; +import itstime.reflog.community.repository.CommunityRepository; +import itstime.reflog.member.domain.Member; +import itstime.reflog.member.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class CommentService { + + private final MemberRepository memberRepository; + private final CommunityRepository communityRepository; + private final CommentRepository commentRepository; + private final CommentLikeRepository commentLikeRepository; + + @Transactional + public void createComment(Long communityId, String memberId, CommentDto.CommentSaveOrUpdateRequest dto) { + // 1. 멤버 조회 + Member member = memberRepository.findByUuid(UUID.fromString(memberId)) + .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND)); + + // 2. 커뮤니티 조회 + Community community = communityRepository.findById(communityId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); + + // 3. parent 댓글 확인 + Comment parentComment = null; + if (dto.getParentId() != null) { + parentComment = commentRepository.findById(dto.getParentId()) + .orElseThrow(() -> new GeneralException(ErrorStatus._PARENT_COMMENT_NOT_FOUND)); + } + + // 4. 댓글 생성 + Comment comment = Comment.builder() + .content(dto.getContent()) + .community(community) + .member(member) + .parent(parentComment) + .createdAt(LocalDateTime.now()) + .updatedAt(LocalDateTime.now()) + .build(); + + // 5. 댓글 저장 + commentRepository.save(comment); + } + + @Transactional + public void updateComment(Long commentId, CommentDto.CommentSaveOrUpdateRequest dto) { + // 1. 댓글 조회 + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMENT_NOT_FOUND)); + + // 2. 댓글 업데이트 + comment.update(dto); + } + + @Transactional + public void deleteComment(Long commentId) { + // 1. 댓글 조회 + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMENT_NOT_FOUND)); + + // 2. 댓글 삭제 + commentRepository.delete(comment); + } + + @Transactional + public void toggleCommentLike(Long commentId, String memberId) { + // 1. 댓글 조회 + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMENT_NOT_FOUND)); + + // 2. 멤버 조회 + Member member = memberRepository.findByUuid(UUID.fromString(memberId)) + .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND)); + + // 3. 좋아요 상태 확인 + Optional optionalCommentLike = commentLikeRepository.findByCommentAndMember(comment, member); + + if (optionalCommentLike.isPresent()) { + // 좋아요가 이미 존재하면 상태 토글 + CommentLike commentLike = optionalCommentLike.get(); + commentLike.update(!commentLike.isLiked()); + } else { + // 좋아요가 없으면 새로 생성 + CommentLike newLike = CommentLike.builder() + .comment(comment) + .member(member) + .isLiked(true) + .build(); + + commentLikeRepository.save(newLike); + } + + } +} diff --git a/src/main/java/itstime/reflog/commentLike/domain/CommentLike.java b/src/main/java/itstime/reflog/commentLike/domain/CommentLike.java new file mode 100644 index 0000000..50a574c --- /dev/null +++ b/src/main/java/itstime/reflog/commentLike/domain/CommentLike.java @@ -0,0 +1,34 @@ +package itstime.reflog.commentLike.domain; + +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.member.domain.Member; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class CommentLike { + + @Id + @Column(name = "commentlike_id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "comment_id", nullable = false) + private Comment comment; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + private Member member; + + @Column(nullable = false) + private boolean isLiked; + + public void update(boolean isLiked) { + this.isLiked = isLiked; + } +} diff --git a/src/main/java/itstime/reflog/commentLike/repository/CommentLikeRepository.java b/src/main/java/itstime/reflog/commentLike/repository/CommentLikeRepository.java new file mode 100644 index 0000000..e1f8945 --- /dev/null +++ b/src/main/java/itstime/reflog/commentLike/repository/CommentLikeRepository.java @@ -0,0 +1,12 @@ +package itstime.reflog.commentLike.repository; + +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.commentLike.domain.CommentLike; +import itstime.reflog.member.domain.Member; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface CommentLikeRepository extends JpaRepository { + Optional findByCommentAndMember(Comment comment, Member member); +} diff --git a/src/main/java/itstime/reflog/common/annotation/UserId.java b/src/main/java/itstime/reflog/common/annotation/UserId.java new file mode 100644 index 0000000..618247f --- /dev/null +++ b/src/main/java/itstime/reflog/common/annotation/UserId.java @@ -0,0 +1,9 @@ +package itstime.reflog.common.annotation; + +import java.lang.annotation.*; + +@Target(ElementType.PARAMETER) // 파라미터에서 사용 +@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지 +@Documented +public @interface UserId { +} diff --git a/src/main/java/itstime/reflog/common/code/status/ErrorStatus.java b/src/main/java/itstime/reflog/common/code/status/ErrorStatus.java index cdf37e9..bf0db56 100644 --- a/src/main/java/itstime/reflog/common/code/status/ErrorStatus.java +++ b/src/main/java/itstime/reflog/common/code/status/ErrorStatus.java @@ -57,8 +57,11 @@ public enum ErrorStatus implements BaseErrorCode { _S3_INVALID_URL(HttpStatus.BAD_REQUEST, "S3_FILE_400", "S3 파일 URL이 잘못되었거나 존재하지 않습니다."), // Community 관련 에러 - _COMMUNITY_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMUNITY404", "해당 커뮤니티 게시글을 찾을 수 없습니다."); + _COMMUNITY_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMUNITY404", "해당 커뮤니티 게시글을 찾을 수 없습니다."), + // comment 관련 에러 + _COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT404", "해당 댓글을 찾을 수 없습니다."), + _PARENT_COMMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMENT404", "해당 부모 댓글을 찾을 수 없습니다."); private final HttpStatus httpStatus; private final String code; diff --git a/src/main/java/itstime/reflog/common/resolver/UserIdArgumentResolver.java b/src/main/java/itstime/reflog/common/resolver/UserIdArgumentResolver.java new file mode 100644 index 0000000..2c17ac1 --- /dev/null +++ b/src/main/java/itstime/reflog/common/resolver/UserIdArgumentResolver.java @@ -0,0 +1,47 @@ +package itstime.reflog.common.resolver; + +import itstime.reflog.util.JwtUtil; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import jakarta.servlet.http.HttpServletRequest; + +@Component +@RequiredArgsConstructor +public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { + + private final JwtUtil jwtUtil; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + // @UserId 애노테이션이 붙은 파라미터만 처리 + return parameter.hasParameterAnnotation(itstime.reflog.common.annotation.UserId.class); + } + + @Override + public Object resolveArgument(@NotNull MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + // HTTP 요청에서 Authorization 헤더 추출 + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + String authorizationHeader = request.getHeader("Authorization"); + + if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { + throw new IllegalArgumentException("유효하지 않은 Authorization 헤더입니다."); + } + + // 토큰에서 userId 추출 + String token = jwtUtil.getTokenFromHeader(authorizationHeader); + String memberId = jwtUtil.getUserIdFromToken(token); + + // UUID 타입으로 변환하여 반환 + return memberId; + } +} diff --git a/src/main/java/itstime/reflog/community/controller/CommunityController.java b/src/main/java/itstime/reflog/community/controller/CommunityController.java index c6d7fb0..5f62ed0 100644 --- a/src/main/java/itstime/reflog/community/controller/CommunityController.java +++ b/src/main/java/itstime/reflog/community/controller/CommunityController.java @@ -4,15 +4,7 @@ import java.util.UUID; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; @@ -82,6 +74,33 @@ public ResponseEntity> createCommunity( return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); } + @Operation( + summary = "커뮤니티 상세조회 API", + description = "커뮤니티 게시글 상세조회 API입니다. AccessToken 필요.", + responses = { + @ApiResponse( + responseCode = "200", + description = "커뮤니티 게시글 상세조회 성공" + ), + @ApiResponse( + responseCode = "404", + description = "해당 회원을 찾을 수 없음" + ), + @ApiResponse( + responseCode = "500", + description = "서버 에러" + ) + } + ) + @GetMapping("/{communityId}") + public ResponseEntity> getCommunity( + @PathVariable Long communityId) { + CommunityDto.CommunityResponse communityResponse = communityService.getCommunity(communityId); + return ResponseEntity.ok(CommonApiResponse.onSuccess(communityResponse)); + } + + + @Operation( summary = "커뮤니티 수정 API", description = "커뮤니티 게시글 수정 API입니다. 마찬가지로 게시글 수정할 때 파일을 업로드하게 되면 커뮤니티 파일 임시 생성 API에서 받은 url값을 fileUrls에 전달해주시면 돼요. AccessToken 필요.", @@ -187,6 +206,3 @@ public ResponseEntity responses = communityService.getSearchedCommunity(title); return ResponseEntity.ok(CommonApiResponse.onSuccess(responses)); } - - -} diff --git a/src/main/java/itstime/reflog/community/domain/Community.java b/src/main/java/itstime/reflog/community/domain/Community.java index bb830d5..8355e21 100644 --- a/src/main/java/itstime/reflog/community/domain/Community.java +++ b/src/main/java/itstime/reflog/community/domain/Community.java @@ -1,8 +1,10 @@ package itstime.reflog.community.domain; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import itstime.reflog.comment.domain.Comment; import itstime.reflog.member.domain.Member; import jakarta.persistence.CascadeType; import jakarta.persistence.CollectionTable; @@ -14,7 +16,6 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; -import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import lombok.AccessLevel; @@ -54,6 +55,9 @@ public class Community { @JoinColumn(name = "member_id", nullable = false) private Member member; + @OneToMany(mappedBy = "community", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY) + private List comments = new ArrayList<>(); + private LocalDateTime createdAt; // 생성일 private LocalDateTime updatedAt; // 수정일 diff --git a/src/main/java/itstime/reflog/community/dto/CommunityDto.java b/src/main/java/itstime/reflog/community/dto/CommunityDto.java index e42e2f1..6d3bdf6 100644 --- a/src/main/java/itstime/reflog/community/dto/CommunityDto.java +++ b/src/main/java/itstime/reflog/community/dto/CommunityDto.java @@ -1,14 +1,12 @@ package itstime.reflog.community.dto; -import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; +import itstime.reflog.comment.dto.CommentDto; import itstime.reflog.community.domain.Community; -import itstime.reflog.member.domain.Member; import itstime.reflog.retrospect.domain.Retrospect; -import itstime.reflog.retrospect.domain.StudyType; import lombok.*; public class CommunityDto { @@ -23,6 +21,12 @@ public static class CommunitySaveOrUpdateRequest { private List fileUrls; } + @Getter + @AllArgsConstructor + public static class CommunityResponse { + private List commentList; + } + //카테고리 별 필터링 api dto @Getter @Builder diff --git a/src/main/java/itstime/reflog/community/service/CommunityService.java b/src/main/java/itstime/reflog/community/service/CommunityService.java index 7b52f64..10ad52a 100644 --- a/src/main/java/itstime/reflog/community/service/CommunityService.java +++ b/src/main/java/itstime/reflog/community/service/CommunityService.java @@ -1,9 +1,13 @@ package itstime.reflog.community.service; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.comment.dto.CommentDto; +import itstime.reflog.comment.repository.CommentRepository; import itstime.reflog.mypage.domain.MyPage; import itstime.reflog.mypage.repository.MyPageRepository; import itstime.reflog.retrospect.domain.Retrospect; @@ -28,159 +32,185 @@ @Service @RequiredArgsConstructor public class CommunityService { - private final CommunityRepository communityRepository; - private final UploadedFileRepository uploadedFileRepository; - private final AmazonS3Manager amazonS3Manager; - private final MemberRepository memberRepository; - private final MyPageRepository myPageRepository; - private final RetrospectRepository retrospectRepository; - - @Transactional - public void createCommunity(Long memberId, CommunityDto.CommunitySaveOrUpdateRequest dto) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND)); - - Community community = Community.builder() - .title(dto.getTitle()) - .content(dto.getContent()) - .postTypes(dto.getPostTypes()) - .learningTypes(dto.getLearningTypes()) - .member(member) - .createdAt(LocalDateTime.now()) - .build(); - - dto.getFileUrls().forEach(tempUrl -> { - try { - // S3 파일 이동 - String fileKey = amazonS3Manager.extractFileKeyFromUrl(tempUrl); - String newKey = fileKey.replace("temporary/", "community/"); - amazonS3Manager.moveFile(fileKey, newKey); - - // DB에 파일 정보 저장 - UploadedFile uploadedFile = UploadedFile.builder() - .fileName(newKey.substring(newKey.lastIndexOf("/") + 1)) - .fileUrl(amazonS3Manager.getFileUrl(newKey)) - .community(community) - .build(); - uploadedFileRepository.save(uploadedFile); - } catch (AmazonS3Exception e) { - throw new GeneralException(ErrorStatus._S3_INVALID_URL); - } catch (Exception e) { - // 기타 예상치 못한 오류 처리 - throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); - } - }); - - communityRepository.save(community); - } - - @Transactional - public void updateCommunity(Long communityId, CommunityDto.CommunitySaveOrUpdateRequest dto) { - // 1. Community 조회 - Community community = communityRepository.findById(communityId) - .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); - - // 2. 기존 파일 제거 및 새 파일 처리 - List existingFiles = uploadedFileRepository.findByCommunityId(communityId); - existingFiles.forEach(file -> uploadedFileRepository.delete(file)); - - // 새 파일 처리 - dto.getFileUrls().forEach(tempUrl -> { - try { - // S3 파일 이동 - String fileKey = amazonS3Manager.extractFileKeyFromUrl(tempUrl); - String newKey = fileKey.replace("temporary/", "community/"); - amazonS3Manager.moveFile(fileKey, newKey); - - // 새 파일 정보 저장 - UploadedFile uploadedFile = UploadedFile.builder() - .fileName(newKey.substring(newKey.lastIndexOf("/") + 1)) - .fileUrl(amazonS3Manager.getFileUrl(newKey)) - .community(community) - .build(); - uploadedFileRepository.save(uploadedFile); - } catch (AmazonS3Exception e) { - throw new GeneralException(ErrorStatus._S3_INVALID_URL); - } catch (Exception e) { - throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); - } - }); - - // 3. Community 업데이트 - community.update(dto.getTitle(), dto.getContent(), dto.getPostTypes(), dto.getLearningTypes()); - - // 4. 저장 - communityRepository.save(community); - } - - @Transactional - public void deleteCommunity(Long communityId) { - // 1. Community 조회 - Community community = communityRepository.findById(communityId) - .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); - - // 2. 관련된 파일 삭제 - List files = uploadedFileRepository.findByCommunityId(communityId); - files.forEach(file -> { - try { - // S3에서 파일 삭제 (fileUrl 바로 사용) - amazonS3Manager.deleteImage(file.getFileUrl()); - } catch (AmazonS3Exception e) { - throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); - } - }); - - // 3. DB에서 파일 정보 삭제 - uploadedFileRepository.deleteAll(files); - - // 4. Community 삭제 - communityRepository.delete(community); - } - //커뮤니티 게시글 필터링 - @Transactional - public List getFilteredCommunity(List postTypes, List learningTypes) { - - List communities; - - //학습유형에 기타가 있는 경우 - if (learningTypes != null && learningTypes.contains("기타")) { - String typePrefix = "기타:%"; - String remainingLearningType = learningTypes.stream() - .filter(type -> !"기타".equals(type)) - .findFirst() - .orElse(null); //나머지 유형이 없는 경우 null - - communities = communityRepository.findCommunitiesByLearningTypePrefix(postTypes, typePrefix, remainingLearningType); - } else { - communities = communityRepository.findByLearningTypesAndPostTypes(postTypes, learningTypes); - } - - //커뮤니티 response형태로 반환 - List responses = communities.stream() - .map(community -> { - String nickname = myPageRepository.findByMember(community.getMember()) - .map(MyPage::getNickname) - .orElse("닉네임 없음"); - return CommunityDto.CombinedCategoryResponse.fromCommunity(community, nickname); - }) - .collect(Collectors.toList()); - - //글 유형에 회고일지가 있는 경우 - if (postTypes != null && postTypes.contains("회고일지")) { - List retrospects = retrospectRepository.findByVisibilityIsTrue(); - List retrospectResponses = retrospects.stream() - .map(retrospect -> { - String nickname = myPageRepository.findByMember(retrospect.getMember()) - .map(MyPage::getNickname) - .orElse("닉네임 없음"); - return CommunityDto.CombinedCategoryResponse.fromRetrospect(retrospect, nickname); - }) - .collect(Collectors.toList()); - responses.addAll(retrospectResponses); // 두 리스트 합치기(회고일지, 커뮤니티) - } - - return responses; - } + private final CommunityRepository communityRepository; + private final UploadedFileRepository uploadedFileRepository; + private final AmazonS3Manager amazonS3Manager; + private final MemberRepository memberRepository; + private final MyPageRepository myPageRepository; + private final RetrospectRepository retrospectRepository; + private final CommentRepository commentRepository; + + @Transactional + public void createCommunity(Long memberId, CommunityDto.CommunitySaveOrUpdateRequest dto) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND)); + + Community community = Community.builder() + .title(dto.getTitle()) + .content(dto.getContent()) + .postTypes(dto.getPostTypes()) + .learningTypes(dto.getLearningTypes()) + .member(member) + .createdAt(LocalDateTime.now()) + .build(); + + dto.getFileUrls().forEach(tempUrl -> { + try { + // S3 파일 이동 + String fileKey = amazonS3Manager.extractFileKeyFromUrl(tempUrl); + String newKey = fileKey.replace("temporary/", "community/"); + amazonS3Manager.moveFile(fileKey, newKey); + + // DB에 파일 정보 저장 + UploadedFile uploadedFile = UploadedFile.builder() + .fileName(newKey.substring(newKey.lastIndexOf("/") + 1)) + .fileUrl(amazonS3Manager.getFileUrl(newKey)) + .community(community) + .build(); + uploadedFileRepository.save(uploadedFile); + } catch (AmazonS3Exception e) { + throw new GeneralException(ErrorStatus._S3_INVALID_URL); + } catch (Exception e) { + // 기타 예상치 못한 오류 처리 + throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); + } + }); + + communityRepository.save(community); + } + + @Transactional + public CommunityDto.CommunityResponse getCommunity(Long communityId) { + // 1. 커뮤니티 조회 + Community community = communityRepository.findById(communityId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); + + // 2. 댓글 조회 + List comments = commentRepository.findAllByCommunityOrderByCreatedAtDesc(community); + + // 3. 댓글 정리 + List commentResponseList = new ArrayList<>(); + for (Comment comment : comments) { + commentResponseList.add(0, + new CommentDto.CommentResponse( + comment.getId(), + comment.getMember().getName(), + comment.getContent(), + comment.getParent() != null ? comment.getParent().getId() : null, + comment.getCreatedAt())); + } + + return new CommunityDto.CommunityResponse(commentResponseList); + } + + @Transactional + public void updateCommunity(Long communityId, CommunityDto.CommunitySaveOrUpdateRequest dto) { + // 1. Community 조회 + Community community = communityRepository.findById(communityId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); + + // 2. 기존 파일 제거 및 새 파일 처리 + List existingFiles = uploadedFileRepository.findByCommunityId(communityId); + existingFiles.forEach(file -> uploadedFileRepository.delete(file)); + + // 새 파일 처리 + dto.getFileUrls().forEach(tempUrl -> { + try { + // S3 파일 이동 + String fileKey = amazonS3Manager.extractFileKeyFromUrl(tempUrl); + String newKey = fileKey.replace("temporary/", "community/"); + amazonS3Manager.moveFile(fileKey, newKey); + + // 새 파일 정보 저장 + UploadedFile uploadedFile = UploadedFile.builder() + .fileName(newKey.substring(newKey.lastIndexOf("/") + 1)) + .fileUrl(amazonS3Manager.getFileUrl(newKey)) + .community(community) + .build(); + uploadedFileRepository.save(uploadedFile); + } catch (AmazonS3Exception e) { + throw new GeneralException(ErrorStatus._S3_INVALID_URL); + } catch (Exception e) { + throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); + } + }); + + // 3. Community 업데이트 + community.update(dto.getTitle(), dto.getContent(), dto.getPostTypes(), dto.getLearningTypes()); + + // 4. 저장 + communityRepository.save(community); + } + + @Transactional + public void deleteCommunity(Long communityId) { + // 1. Community 조회 + Community community = communityRepository.findById(communityId) + .orElseThrow(() -> new GeneralException(ErrorStatus._COMMUNITY_NOT_FOUND)); + + // 2. 관련된 파일 삭제 + List files = uploadedFileRepository.findByCommunityId(communityId); + files.forEach(file -> { + try { + // S3에서 파일 삭제 (fileUrl 바로 사용) + amazonS3Manager.deleteImage(file.getFileUrl()); + } catch (AmazonS3Exception e) { + throw new GeneralException(ErrorStatus._S3_FILE_OPERATION_FAILED); + } + }); + + // 3. DB에서 파일 정보 삭제 + uploadedFileRepository.deleteAll(files); + + // 4. Community 삭제 + communityRepository.delete(community); + } + + //커뮤니티 게시글 필터링 + @Transactional + public List getFilteredCommunity(List postTypes, List learningTypes) { + + List communities; + + //학습유형에 기타가 있는 경우 + if (learningTypes != null && learningTypes.contains("기타")) { + String typePrefix = "기타:%"; + String remainingLearningType = learningTypes.stream() + .filter(type -> !"기타".equals(type)) + .findFirst() + .orElse(null); //나머지 유형이 없는 경우 null + + communities = communityRepository.findCommunitiesByLearningTypePrefix(postTypes, typePrefix, remainingLearningType); + } else { + communities = communityRepository.findByLearningTypesAndPostTypes(postTypes, learningTypes); + } + + //커뮤니티 response형태로 반환 + List responses = communities.stream() + .map(community -> { + String nickname = myPageRepository.findByMember(community.getMember()) + .map(MyPage::getNickname) + .orElse("닉네임 없음"); + return CommunityDto.CombinedCategoryResponse.fromCommunity(community, nickname); + }) + .collect(Collectors.toList()); + + //글 유형에 회고일지가 있는 경우 + if (postTypes != null && postTypes.contains("회고일지")) { + List retrospects = retrospectRepository.findByVisibilityIsTrue(); + List retrospectResponses = retrospects.stream() + .map(retrospect -> { + String nickname = myPageRepository.findByMember(retrospect.getMember()) + .map(MyPage::getNickname) + .orElse("닉네임 없음"); + return CommunityDto.CombinedCategoryResponse.fromRetrospect(retrospect, nickname); + }) + .collect(Collectors.toList()); + responses.addAll(retrospectResponses); // 두 리스트 합치기(회고일지, 커뮤니티) + } + + return responses; + } //커뮤니티 게시물 검색 @Transactional diff --git a/src/main/java/itstime/reflog/config/SecurityConfig.java b/src/main/java/itstime/reflog/config/SecurityConfig.java index 683e9b4..7136587 100644 --- a/src/main/java/itstime/reflog/config/SecurityConfig.java +++ b/src/main/java/itstime/reflog/config/SecurityConfig.java @@ -45,7 +45,7 @@ public CorsConfigurationSource corsConfigurationSource() { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(requests -> requests - .requestMatchers("/test", "/swagger-ui/index.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/v3/api-docs", "/api/v1/token/**", "/api/v1/todo/**", "/api/v1/learn/**", "/api/v1/plan/**", "/api/v1/weekly-analysis/**","/api/v1/monthly-analysis/**","/api/v1/retrospect/**","/api/v1/mypage/**","/api/v1/notifications/**","/api/v1/communities/**").permitAll() + .requestMatchers("/test", "/swagger-ui/index.html", "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/v3/api-docs", "/api/v1/token/**", "/api/v1/todo/**", "/api/v1/learn/**", "/api/v1/plan/**", "/api/v1/weekly-analysis/**","/api/v1/monthly-analysis/**","/api/v1/retrospect/**","/api/v1/mypage/**","/api/v1/notifications/**","/api/v1/communities/**", "/api/v1/comments/**").permitAll() .anyRequest().authenticated() // 나머지 URL은 인증 필요 ) // .addFilterBefore(new TokenAuthenticationFilter(jwtTokenProvider()), UsernamePasswordAuthenticationFilter.class) diff --git a/src/main/java/itstime/reflog/config/WebConfig.java b/src/main/java/itstime/reflog/config/WebConfig.java new file mode 100644 index 0000000..24f9835 --- /dev/null +++ b/src/main/java/itstime/reflog/config/WebConfig.java @@ -0,0 +1,21 @@ +package itstime.reflog.config; + +import itstime.reflog.common.resolver.UserIdArgumentResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final UserIdArgumentResolver userIdArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(userIdArgumentResolver); // 커스텀 Argument Resolver 등록 + } +} diff --git a/src/main/java/itstime/reflog/member/domain/Member.java b/src/main/java/itstime/reflog/member/domain/Member.java index e16d717..04e166a 100644 --- a/src/main/java/itstime/reflog/member/domain/Member.java +++ b/src/main/java/itstime/reflog/member/domain/Member.java @@ -2,6 +2,8 @@ import java.util.List; +import itstime.reflog.comment.domain.Comment; +import itstime.reflog.commentLike.domain.CommentLike; import itstime.reflog.community.domain.Community; import itstime.reflog.goal.domain.DailyGoal; import itstime.reflog.mypage.domain.MyPage; @@ -57,6 +59,12 @@ public class Member { @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) private List communities; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List comments; + + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL) + private List commentLikes; + @OneToOne(mappedBy = "member", cascade = CascadeType.ALL) private MyPage myPage; diff --git a/src/main/java/itstime/reflog/schedule/controller/ScheduleController.java b/src/main/java/itstime/reflog/schedule/controller/ScheduleController.java index 9e89252..f7a74a6 100644 --- a/src/main/java/itstime/reflog/schedule/controller/ScheduleController.java +++ b/src/main/java/itstime/reflog/schedule/controller/ScheduleController.java @@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import itstime.reflog.common.CommonApiResponse; +import itstime.reflog.common.annotation.UserId; import itstime.reflog.schedule.dto.ScheduleDto; import itstime.reflog.schedule.service.ScheduleService; import jakarta.validation.Valid; @@ -44,11 +45,10 @@ public class ScheduleController { ) @PostMapping("/schedule") public ResponseEntity> createSchedule( -// @RequestParam Long memberId, - @RequestHeader("Authorization") String authorizationHeader, + @UserId String memberId, @RequestBody ScheduleDto.ScheduleSaveOrUpdateRequest dto ) { - scheduleService.createSchedule(authorizationHeader, dto); + scheduleService.createSchedule(memberId, dto); return ResponseEntity.ok(CommonApiResponse.onSuccess(null)); } diff --git a/src/main/java/itstime/reflog/schedule/service/ScheduleService.java b/src/main/java/itstime/reflog/schedule/service/ScheduleService.java index 80a7c11..448ee5f 100644 --- a/src/main/java/itstime/reflog/schedule/service/ScheduleService.java +++ b/src/main/java/itstime/reflog/schedule/service/ScheduleService.java @@ -4,12 +4,9 @@ import itstime.reflog.common.exception.GeneralException; import itstime.reflog.member.domain.Member; import itstime.reflog.member.repository.MemberRepository; -import itstime.reflog.oauth.token.exception.TokenErrorResult; -import itstime.reflog.oauth.token.exception.TokenException; import itstime.reflog.schedule.domain.Schedule; import itstime.reflog.schedule.dto.ScheduleDto; import itstime.reflog.schedule.repository.ScheduleRepository; -import itstime.reflog.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -23,22 +20,10 @@ public class ScheduleService { private final ScheduleRepository scheduleRepository; private final MemberRepository memberRepository; - private final JwtUtil jwtUtil; @Transactional - public void createSchedule(String authorizationHeader, ScheduleDto.ScheduleSaveOrUpdateRequest dto) { -// Member member = memberRepository.findById(memberId) -// .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND)); - if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { - throw new TokenException(TokenErrorResult.INVALID_TOKEN); - } - - String accessToken = jwtUtil.getTokenFromHeader(authorizationHeader); - if (jwtUtil.isTokenExpired(accessToken)) { - throw new TokenException(TokenErrorResult.INVALID_TOKEN); - } - - String memberId = jwtUtil.getUserIdFromToken(accessToken); + public void createSchedule(String memberId, ScheduleDto.ScheduleSaveOrUpdateRequest dto) { + Member member = memberRepository.findByUuid(UUID.fromString(memberId)) .orElseThrow(() -> new GeneralException(ErrorStatus._MEMBER_NOT_FOUND));