From d1118070dd2317462252a1d65c858ebfd9290091 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 19:19:27 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feature:=20post=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20-=20=EC=9E=91=EC=84=B1=20-=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20-=20=EC=88=98=EC=A0=95=20-=20=EB=8B=A8=EA=B1=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20-=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20(=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EC=85=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../server/merchant/entity/Merchant.java | 2 +- .../repository/MerchantRepository.java | 9 ++ .../post/controller/PostController.java | 104 ++++++++++++++ .../payper/server/post/dto/PostRequest.java | 36 +++++ .../payper/server/post/dto/PostResponse.java | 72 ++++++++++ .../payper/server/post/dto/PostSortType.java | 21 +++ .../com/payper/server/post/entity/Post.java | 21 ++- .../post/repository/PostRepository.java | 40 ++++++ .../server/post/service/PostService.java | 130 ++++++++++++++++++ .../user/repository/UserRepository.java | 9 ++ 11 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/payper/server/merchant/repository/MerchantRepository.java create mode 100644 src/main/java/com/payper/server/post/controller/PostController.java create mode 100644 src/main/java/com/payper/server/post/dto/PostRequest.java create mode 100644 src/main/java/com/payper/server/post/dto/PostResponse.java create mode 100644 src/main/java/com/payper/server/post/dto/PostSortType.java create mode 100644 src/main/java/com/payper/server/post/repository/PostRepository.java create mode 100644 src/main/java/com/payper/server/post/service/PostService.java create mode 100644 src/main/java/com/payper/server/user/repository/UserRepository.java diff --git a/build.gradle b/build.gradle index 95a3340..6cb5b93 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-webmvc' //implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' // lombok compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/payper/server/merchant/entity/Merchant.java b/src/main/java/com/payper/server/merchant/entity/Merchant.java index 7278424..56980f1 100644 --- a/src/main/java/com/payper/server/merchant/entity/Merchant.java +++ b/src/main/java/com/payper/server/merchant/entity/Merchant.java @@ -10,7 +10,7 @@ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Merchant { +public class Merchant { // TODO 가맹점이 삭제되면 해당 가맹점에 등록된 post는?? @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java b/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java new file mode 100644 index 0000000..b51d22e --- /dev/null +++ b/src/main/java/com/payper/server/merchant/repository/MerchantRepository.java @@ -0,0 +1,9 @@ +package com.payper.server.merchant.repository; + +import com.payper.server.merchant.entity.Merchant; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MerchantRepository extends JpaRepository { +} diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java new file mode 100644 index 0000000..db9b2eb --- /dev/null +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -0,0 +1,104 @@ +package com.payper.server.post.controller; + +import com.payper.server.post.dto.PostRequest; +import com.payper.server.post.dto.PostResponse; +import com.payper.server.post.dto.PostSortType; +import com.payper.server.post.entity.PostType; +import com.payper.server.post.service.PostService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/posts") +@RequiredArgsConstructor +public class PostController { + private final PostService postService; + + /** + * 게시글 작성 + * 가맹점에 대해 글을 작성함 + * 가맹점 리스트에서 가맹점을 선택해서 해당 가맹점의 id를 넘겨 받음 + * TODO 가맹점이 없을 때는 어떻게 해야할까? + * + * 가입된 사용자만 글을 작성할 수 있음 + */ + @PostMapping("/merchants/{merchantId}") // TODO: 흠 RESTFUL한 URL은 아닌 것 같음, posts가 뒤로 가는 게 맞는 것 같음 + public ResponseEntity createPost( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long merchantId, + @RequestBody @Valid PostRequest.CreatePost request + ) { + Long postId = postService.createPost(1L, merchantId, request); + return ResponseEntity.status(201).body(postId); + } + + /** + * 게시글 수정 + * 작성자만 수정 가능 + */ + @PutMapping("/{postId}") + public ResponseEntity updatePost( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long postId, + @RequestBody @Valid PostRequest.UpdatePost request + ) { + postService.updatePost(1L, postId, request); + return ResponseEntity.ok().build(); + } + + /** + * 게시글 삭제 + * 작성자만 삭제 가능 + * TODO 댓글도 같이 soft delete + */ + @DeleteMapping("/{postId}") + public ResponseEntity deletePost( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long postId + ) { + postService.deletePost(1L, postId); + return ResponseEntity.ok().build(); + } + + /** + * 단일 게시글 조회 + * + * 삭제되지 않은 글만 조회함 + */ + @GetMapping("/{postId}") + public ResponseEntity getPostDetail(@PathVariable Long postId) { + PostResponse.postDetail response = postService.getPostDetail(postId); + return ResponseEntity.ok(response); + } + + /** + * 게시글 리스트 조회 + * + * 필터링 조건 + * 가맹점, postType + * + * 정렬 조건 + * 생성 순, 댓글 수, 좋아요 수, 조회 수 + * + * 페이지네이션 + */ + @GetMapping() + public ResponseEntity> getPosts( + @RequestParam(required = false) Long merchantId, + @RequestParam(required = false) PostType type, + @RequestParam(defaultValue = "POSTING_DATE") PostSortType sort, + @RequestParam(defaultValue = "DESC") Sort.Direction direction, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Pageable pageable = PageRequest.of(page, size, sort.toSort(direction)); + Page response = postService.getPosts(merchantId, type, pageable); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/dto/PostRequest.java b/src/main/java/com/payper/server/post/dto/PostRequest.java new file mode 100644 index 0000000..1b87611 --- /dev/null +++ b/src/main/java/com/payper/server/post/dto/PostRequest.java @@ -0,0 +1,36 @@ +package com.payper.server.post.dto; + +import com.payper.server.post.entity.PostType; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public class PostRequest { + + /** + * 게시글 작성 DTO + */ + public record CreatePost( + @NotNull(message = "게시글의 타입을 선택해주세요.") + PostType type, + + @NotBlank(message = "제목을 적어주세요.") + String title, + + @NotBlank(message = "내용을 적어주세요.") + @Size(max = 5500000, message = "내용은 500만자 이내로 적어주세요.") + String content + ) {} + + /** + * 게시글 수정 DTO + */ + public record UpdatePost( + @NotBlank(message = "제목을 적어주세요.") + String title, + + @NotBlank(message = "내용을 적어주세요.") + @Size(max = 5500000, message = "내용은 500만자 이내로 적어주세요.") + String content + ) {} +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/dto/PostResponse.java b/src/main/java/com/payper/server/post/dto/PostResponse.java new file mode 100644 index 0000000..569faef --- /dev/null +++ b/src/main/java/com/payper/server/post/dto/PostResponse.java @@ -0,0 +1,72 @@ +package com.payper.server.post.dto; + +import com.payper.server.post.entity.Post; +import com.payper.server.post.entity.PostType; + +import java.time.LocalDateTime; + +public class PostResponse { + + /** + * 게시글 단일 조회 DTO + */ + public record postDetail( + Long id, + String authorName, /* 작성자 이름 */ + String merchantName, /* 가맹점명 */ + PostType type, /* 게시글 타입 */ + String title, /* 제목 */ + String content, /* 내용 */ + long commentCount, /* 댓글 수 */ + long viewCount, /* 조회 수 */ + long likeCount, /* 좋아요 수 */ + LocalDateTime createdAt, /* 글 작성 시간 */ + LocalDateTime updatedAt /* 글 수정 시간 */ + ) { + public static postDetail from(Post post) { + return new postDetail( + post.getId(), + post.getAuthor().getName(), + post.getMerchant().getName(), + post.getType(), + post.getTitle(), + post.getContent(), + post.getCommentCount(), + post.getViewCount(), + post.getLikeCount(), + post.getCreatedAt(), + post.getUpdatedAt() + ); + } + } + + /** + * 게시글 리스트 조회 DTO + */ + public record postList( + Long id, + String authorName, /* 작성자 이름 */ + String merchantName, /* 가맹점명 */ + PostType type, /* 게시글 타입 */ + String title, /* 제목 */ + long commentCount, /* 댓글 수 */ + long viewCount, /* 조회 수 */ + long likeCount, /* 좋아요 수 */ + LocalDateTime createdAt /* 글 작성 시간 */ + + ) { + public static postList from(Post post) { + return new postList( + post.getId(), + post.getAuthor().getName(), + post.getMerchant().getName(), + post.getType(), + post.getTitle(), + post.getCommentCount(), + post.getViewCount(), + post.getLikeCount(), + post.getCreatedAt() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/dto/PostSortType.java b/src/main/java/com/payper/server/post/dto/PostSortType.java new file mode 100644 index 0000000..cd58692 --- /dev/null +++ b/src/main/java/com/payper/server/post/dto/PostSortType.java @@ -0,0 +1,21 @@ +package com.payper.server.post.dto; + +import org.springframework.data.domain.Sort; + +public enum PostSortType { + + POSTING_DATE("createdAt"), + COMMENT_COUNT("commentCount"), + LIKE_COUNT("likeCount"), + VIEW_COUNT("viewCount"); + + private final String property; + + PostSortType(String property) { + this.property = property; + } + + public Sort toSort(Sort.Direction direction) { + return Sort.by(direction, property); + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/entity/Post.java b/src/main/java/com/payper/server/post/entity/Post.java index fa4d115..56eac08 100644 --- a/src/main/java/com/payper/server/post/entity/Post.java +++ b/src/main/java/com/payper/server/post/entity/Post.java @@ -121,8 +121,12 @@ public void decreaseCommentCount() { * soft delete */ public void delete() { + if (this.isDeleted) { // 멱등성 고려 처리 + return; + } + this.isDeleted = true; - this.deletedAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + this.deletedAt = LocalDateTime.now(); } /** @@ -132,4 +136,19 @@ public void inactivate() { this.isInactive = true; this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); } + + public static Post create(User author, Merchant merchant, PostType type, String title, String content) { + return Post.builder() + .author(author) + .merchant(merchant) + .type(type) + .title(title) + .content(content) + .build(); + } + + public void update(String title, String content) { + this.title = title; + this.content = content; + } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/repository/PostRepository.java b/src/main/java/com/payper/server/post/repository/PostRepository.java new file mode 100644 index 0000000..0976fb5 --- /dev/null +++ b/src/main/java/com/payper/server/post/repository/PostRepository.java @@ -0,0 +1,40 @@ +package com.payper.server.post.repository; + +import com.payper.server.post.entity.Post; +import com.payper.server.post.entity.PostType; +import org.springframework.data.domain.Page; +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 org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PostRepository extends JpaRepository { + + Optional findByIdAndIsDeletedFalse(Long id); + + @Query( + value = """ + select p from Post p + join fetch p.author + join fetch p.merchant + where p.isDeleted = false + and (:merchantId IS NULL or p.merchant.id = :merchantId) + and (:type IS NULL or p.type = :type) + """, + countQuery = """ + select count(p) from Post p + where p.isDeleted = false + and (:merchantId IS NULL or p.merchant.id = :merchantId) + and (:type IS NULL or p.type = :type) + """ + ) + Page findActivePostsByCondition( + @Param("merchantId") Long merchantId, + @Param("type") PostType type, + Pageable pageable + ); +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java new file mode 100644 index 0000000..3f6675c --- /dev/null +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -0,0 +1,130 @@ +package com.payper.server.post.service; + +import com.payper.server.comment.repository.CommentRepository; +import com.payper.server.merchant.entity.Merchant; +import com.payper.server.merchant.repository.MerchantRepository; +import com.payper.server.post.dto.PostRequest; +import com.payper.server.post.dto.PostResponse; +import com.payper.server.post.entity.Post; +import com.payper.server.post.entity.PostType; +import com.payper.server.post.repository.PostRepository; +import com.payper.server.user.entity.User; +import com.payper.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PostService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final MerchantRepository merchantRepository; + private final CommentRepository commentRepository; + + /** + * 게시글 작성 + */ + @Transactional + public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost request) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> { + log.error("사용자 조회 실패 - userId: {}", userId); + return new RuntimeException(); + }); + + // 가맹점 조회 + Merchant merchant = merchantRepository.findById(merchantId) + .orElseThrow(() -> { + log.error("가맹점 조회 실패 - merchantId: {}", merchantId); + return new RuntimeException(); + }); + + // 게시글 생성 + Post post = Post.create(user, merchant, request.type(), request.title(), request.content()); + postRepository.save(post); + + log.info("게시글 생성 완료 - postId: {}, userId: {}, merchantId: {}", post.getId(), userId, merchantId); + return post.getId(); + } + + /** + * 게시글 수정 + */ + @Transactional + public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) { + // 게시글 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> { + log.error("게시글 조회 실패 - postId: {}", postId); + return new RuntimeException(); + }); + + // 게시글 수정 권한 조회 + if(!userId.equals(post.getAuthor().getId())) { + throw new RuntimeException(); + } + + // 게시글 수정 + post.update(request.title(), request.content()); + + log.info("게시글 수정 완료 - postId: {}", post.getId()); + } + + /** + * 게시글 삭제 + */ + @Transactional + public void deletePost(Long userId, Long postId) { + // 게시글 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> { + log.error("게시글 조회 실패 - postId: {}", postId); + return new RuntimeException(); + }); + + // 게시글 삭제 권한 조회 + if(!userId.equals(post.getAuthor().getId())) { + throw new RuntimeException(); + } + + // 게시글 삭제 + post.delete(); + log.info("게시글 삭제 완료 - postId: {}", post.getId()); + + // TODO 댓글 삭제 (대댓글도 삭제) +// commentRepository.softDeleteByPostId(postId); + } + + /** + * 게시글 단일 조회 + */ + @Transactional(readOnly = true) + public PostResponse.postDetail getPostDetail(Long postId) { + // 게시글 조회 + Post post = postRepository.findByIdAndIsDeletedFalse(postId) + .orElseThrow(() -> { + log.error("게시글 조회 실패 - postId: {}", postId); + return new RuntimeException(); + }); + + return PostResponse.postDetail.from(post); + } + + /** + * 게시글 리스트 조회 + */ + @Transactional(readOnly = true) + public Page getPosts(Long merchantId, PostType type, Pageable pageable) { + Page posts = postRepository.findActivePostsByCondition(merchantId, type, pageable); + + log.info("post 조회 완료 {}", posts); + return posts.map(PostResponse.postList::from); + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/user/repository/UserRepository.java b/src/main/java/com/payper/server/user/repository/UserRepository.java new file mode 100644 index 0000000..d9d099c --- /dev/null +++ b/src/main/java/com/payper/server/user/repository/UserRepository.java @@ -0,0 +1,9 @@ +package com.payper.server.user.repository; + +import com.payper.server.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { +} From bfede9c84ec42de2970ddb9f6361b5628bd88fb6 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 19:31:19 +0900 Subject: [PATCH 02/15] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/payper/server/post/service/PostService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index 3f6675c..86860d8 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -1,6 +1,5 @@ package com.payper.server.post.service; -import com.payper.server.comment.repository.CommentRepository; import com.payper.server.merchant.entity.Merchant; import com.payper.server.merchant.repository.MerchantRepository; import com.payper.server.post.dto.PostRequest; @@ -25,8 +24,7 @@ public class PostService { private final UserRepository userRepository; private final PostRepository postRepository; private final MerchantRepository merchantRepository; - private final CommentRepository commentRepository; - + /** * 게시글 작성 */ From b314e0d9e59a4c63e3e32272e20fd0750c7334a4 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 21:42:54 +0900 Subject: [PATCH 03/15] =?UTF-8?q?refactor:=20gemini=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/post/controller/PostController.java | 10 +++++----- .../com/payper/server/post/dto/PostResponse.java | 12 ++++++------ .../com/payper/server/post/service/PostService.java | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index db9b2eb..4b06d75 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -63,7 +63,7 @@ public ResponseEntity deletePost( @PathVariable Long postId ) { postService.deletePost(1L, postId); - return ResponseEntity.ok().build(); + return ResponseEntity.noContent().build(); } /** @@ -72,8 +72,8 @@ public ResponseEntity deletePost( * 삭제되지 않은 글만 조회함 */ @GetMapping("/{postId}") - public ResponseEntity getPostDetail(@PathVariable Long postId) { - PostResponse.postDetail response = postService.getPostDetail(postId); + public ResponseEntity getPostDetail(@PathVariable Long postId) { + PostResponse.PostDetail response = postService.getPostDetail(postId); return ResponseEntity.ok(response); } @@ -89,7 +89,7 @@ public ResponseEntity getPostDetail(@PathVariable Long * 페이지네이션 */ @GetMapping() - public ResponseEntity> getPosts( + public ResponseEntity> getPosts( @RequestParam(required = false) Long merchantId, @RequestParam(required = false) PostType type, @RequestParam(defaultValue = "POSTING_DATE") PostSortType sort, @@ -98,7 +98,7 @@ public ResponseEntity> getPosts( @RequestParam(defaultValue = "10") int size ) { Pageable pageable = PageRequest.of(page, size, sort.toSort(direction)); - Page response = postService.getPosts(merchantId, type, pageable); + Page response = postService.getPosts(merchantId, type, pageable); return ResponseEntity.ok(response); } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/dto/PostResponse.java b/src/main/java/com/payper/server/post/dto/PostResponse.java index 569faef..acffc28 100644 --- a/src/main/java/com/payper/server/post/dto/PostResponse.java +++ b/src/main/java/com/payper/server/post/dto/PostResponse.java @@ -10,7 +10,7 @@ public class PostResponse { /** * 게시글 단일 조회 DTO */ - public record postDetail( + public record PostDetail( Long id, String authorName, /* 작성자 이름 */ String merchantName, /* 가맹점명 */ @@ -23,8 +23,8 @@ public record postDetail( LocalDateTime createdAt, /* 글 작성 시간 */ LocalDateTime updatedAt /* 글 수정 시간 */ ) { - public static postDetail from(Post post) { - return new postDetail( + public static PostDetail from(Post post) { + return new PostDetail( post.getId(), post.getAuthor().getName(), post.getMerchant().getName(), @@ -43,7 +43,7 @@ public static postDetail from(Post post) { /** * 게시글 리스트 조회 DTO */ - public record postList( + public record PostList( Long id, String authorName, /* 작성자 이름 */ String merchantName, /* 가맹점명 */ @@ -55,8 +55,8 @@ public record postList( LocalDateTime createdAt /* 글 작성 시간 */ ) { - public static postList from(Post post) { - return new postList( + public static PostList from(Post post) { + return new PostList( post.getId(), post.getAuthor().getName(), post.getMerchant().getName(), diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index 86860d8..15ff3bc 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -104,7 +104,7 @@ public void deletePost(Long userId, Long postId) { * 게시글 단일 조회 */ @Transactional(readOnly = true) - public PostResponse.postDetail getPostDetail(Long postId) { + public PostResponse.PostDetail getPostDetail(Long postId) { // 게시글 조회 Post post = postRepository.findByIdAndIsDeletedFalse(postId) .orElseThrow(() -> { @@ -112,17 +112,17 @@ public PostResponse.postDetail getPostDetail(Long postId) { return new RuntimeException(); }); - return PostResponse.postDetail.from(post); + return PostResponse.PostDetail.from(post); } /** * 게시글 리스트 조회 */ @Transactional(readOnly = true) - public Page getPosts(Long merchantId, PostType type, Pageable pageable) { + public Page getPosts(Long merchantId, PostType type, Pageable pageable) { Page posts = postRepository.findActivePostsByCondition(merchantId, type, pageable); log.info("post 조회 완료 {}", posts); - return posts.map(PostResponse.postList::from); + return posts.map(PostResponse.PostList::from); } } \ No newline at end of file From cf775e9c7b1f59fcbdce8624fc71d22fdde83aa1 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 22:22:20 +0900 Subject: [PATCH 04/15] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/response/ErrorCode.java | 13 +++++++- .../server/post/service/PostService.java | 31 ++++++------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/payper/server/global/response/ErrorCode.java b/src/main/java/com/payper/server/global/response/ErrorCode.java index 38ef100..ea1d4f2 100644 --- a/src/main/java/com/payper/server/global/response/ErrorCode.java +++ b/src/main/java/com/payper/server/global/response/ErrorCode.java @@ -12,8 +12,19 @@ public enum ErrorCode { UNAUTHORIZED("GEN-002", HttpStatus.UNAUTHORIZED, "Unauthorized"), NOT_FOUND("GEN-003", HttpStatus.NOT_FOUND, "Not Found"), CONFLICT("GEN-004", HttpStatus.CONFLICT, "Conflict"), - INTERNAL_SERVER_ERROR("GEN-005", HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error"); + INTERNAL_SERVER_ERROR("GEN-005", HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error"), + // USER + USER_NOT_FOUND("USER-001", HttpStatus.NOT_FOUND, "User Not Found"), + NOT_POSTING_USER("USER-002", HttpStatus.FORBIDDEN, "Not Posting User"), + + // POST + POST_NOT_FOUND("POST-001", HttpStatus.NOT_FOUND, "Post Not Found"), + + // MERCHANT + MERCHANT_NOT_FOUND("MERCHANT-001", HttpStatus.NOT_FOUND, "Merchant Not Found") + + ; private final String code; private final HttpStatus status; diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index 15ff3bc..a414bbd 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -1,5 +1,7 @@ package com.payper.server.post.service; +import com.payper.server.global.exception.ApiException; +import com.payper.server.global.response.ErrorCode; import com.payper.server.merchant.entity.Merchant; import com.payper.server.merchant.repository.MerchantRepository; import com.payper.server.post.dto.PostRequest; @@ -32,17 +34,11 @@ public class PostService { public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost request) { // 사용자 조회 User user = userRepository.findById(userId) - .orElseThrow(() -> { - log.error("사용자 조회 실패 - userId: {}", userId); - return new RuntimeException(); - }); + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); // 가맹점 조회 Merchant merchant = merchantRepository.findById(merchantId) - .orElseThrow(() -> { - log.error("가맹점 조회 실패 - merchantId: {}", merchantId); - return new RuntimeException(); - }); + .orElseThrow(() -> new ApiException(ErrorCode.MERCHANT_NOT_FOUND)); // 게시글 생성 Post post = Post.create(user, merchant, request.type(), request.title(), request.content()); @@ -59,14 +55,11 @@ public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost requ public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) { // 게시글 조회 Post post = postRepository.findById(postId) - .orElseThrow(() -> { - log.error("게시글 조회 실패 - postId: {}", postId); - return new RuntimeException(); - }); + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 게시글 수정 권한 조회 if(!userId.equals(post.getAuthor().getId())) { - throw new RuntimeException(); + throw new ApiException(ErrorCode.NOT_POSTING_USER); } // 게시글 수정 @@ -82,14 +75,11 @@ public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) public void deletePost(Long userId, Long postId) { // 게시글 조회 Post post = postRepository.findById(postId) - .orElseThrow(() -> { - log.error("게시글 조회 실패 - postId: {}", postId); - return new RuntimeException(); - }); + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 게시글 삭제 권한 조회 if(!userId.equals(post.getAuthor().getId())) { - throw new RuntimeException(); + throw new ApiException(ErrorCode.NOT_POSTING_USER); } // 게시글 삭제 @@ -107,10 +97,7 @@ public void deletePost(Long userId, Long postId) { public PostResponse.PostDetail getPostDetail(Long postId) { // 게시글 조회 Post post = postRepository.findByIdAndIsDeletedFalse(postId) - .orElseThrow(() -> { - log.error("게시글 조회 실패 - postId: {}", postId); - return new RuntimeException(); - }); + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); return PostResponse.PostDetail.from(post); } From 19c409310388afe4386609e1a52877a85cea0288 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 22:37:42 +0900 Subject: [PATCH 05/15] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/global/response/ApiResponse.java | 9 ++++++++ .../post/controller/PostController.java | 21 ++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/payper/server/global/response/ApiResponse.java b/src/main/java/com/payper/server/global/response/ApiResponse.java index 9526e46..1ccefa0 100644 --- a/src/main/java/com/payper/server/global/response/ApiResponse.java +++ b/src/main/java/com/payper/server/global/response/ApiResponse.java @@ -12,6 +12,15 @@ public class ApiResponse { private final T data; private final ExceptionDto error; + public static ApiResponse ok() { + return ApiResponse + .builder() + .status(HttpStatus.OK.value()) + .data(null) + .error(null) + .build(); + } + public static ApiResponse ok(@Nullable T data) { return ApiResponse .builder() diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 4b06d75..0f3b37c 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -1,5 +1,6 @@ package com.payper.server.post.controller; +import com.payper.server.global.response.ApiResponse; import com.payper.server.post.dto.PostRequest; import com.payper.server.post.dto.PostResponse; import com.payper.server.post.dto.PostSortType; @@ -29,13 +30,13 @@ public class PostController { * 가입된 사용자만 글을 작성할 수 있음 */ @PostMapping("/merchants/{merchantId}") // TODO: 흠 RESTFUL한 URL은 아닌 것 같음, posts가 뒤로 가는 게 맞는 것 같음 - public ResponseEntity createPost( + public ResponseEntity> createPost( // TODO @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long merchantId, @RequestBody @Valid PostRequest.CreatePost request ) { Long postId = postService.createPost(1L, merchantId, request); - return ResponseEntity.status(201).body(postId); + return ResponseEntity.status(201).body(ApiResponse.created(postId)); } /** @@ -43,13 +44,13 @@ public ResponseEntity createPost( * 작성자만 수정 가능 */ @PutMapping("/{postId}") - public ResponseEntity updatePost( + public ResponseEntity> updatePost( // TODO @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId, @RequestBody @Valid PostRequest.UpdatePost request ) { postService.updatePost(1L, postId, request); - return ResponseEntity.ok().build(); + return ResponseEntity.ok(ApiResponse.ok()); } /** @@ -58,12 +59,12 @@ public ResponseEntity updatePost( * TODO 댓글도 같이 soft delete */ @DeleteMapping("/{postId}") - public ResponseEntity deletePost( + public ResponseEntity> deletePost( // TODO @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId ) { postService.deletePost(1L, postId); - return ResponseEntity.noContent().build(); + return ResponseEntity.ok(ApiResponse.ok()); } /** @@ -72,9 +73,9 @@ public ResponseEntity deletePost( * 삭제되지 않은 글만 조회함 */ @GetMapping("/{postId}") - public ResponseEntity getPostDetail(@PathVariable Long postId) { + public ResponseEntity> getPostDetail(@PathVariable Long postId) { PostResponse.PostDetail response = postService.getPostDetail(postId); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.ok(response)); } /** @@ -89,7 +90,7 @@ public ResponseEntity getPostDetail(@PathVariable Long * 페이지네이션 */ @GetMapping() - public ResponseEntity> getPosts( + public ResponseEntity>> getPosts( @RequestParam(required = false) Long merchantId, @RequestParam(required = false) PostType type, @RequestParam(defaultValue = "POSTING_DATE") PostSortType sort, @@ -99,6 +100,6 @@ public ResponseEntity> getPosts( ) { Pageable pageable = PageRequest.of(page, size, sort.toSort(direction)); Page response = postService.getPosts(merchantId, type, pageable); - return ResponseEntity.ok(response); + return ResponseEntity.ok(ApiResponse.ok(response)); } } \ No newline at end of file From 34264532d27c751e0173baff4fd6385d0a95a8b6 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 27 Jan 2026 22:39:10 +0900 Subject: [PATCH 06/15] =?UTF-8?q?fix:=20todo=20task=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/payper/server/merchant/entity/Merchant.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/payper/server/merchant/entity/Merchant.java b/src/main/java/com/payper/server/merchant/entity/Merchant.java index 56980f1..7278424 100644 --- a/src/main/java/com/payper/server/merchant/entity/Merchant.java +++ b/src/main/java/com/payper/server/merchant/entity/Merchant.java @@ -10,7 +10,7 @@ @Getter @AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Merchant { // TODO 가맹점이 삭제되면 해당 가맹점에 등록된 post는?? +public class Merchant { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) From e4666065553e34f5bc7252d0ae9b9b27941ae1bf Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Wed, 28 Jan 2026 06:55:56 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feature:=20comment=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84=20-=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20-=20=EB=8C=93=EA=B8=80=20=EC=88=98=EC=A0=95=20-=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C=20-=20=EB=82=B4=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C(=EB=AC=B4=ED=95=9C=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4)=20-=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 93 +++++++++++ .../server/comment/dto/CommentRequest.java | 29 ++++ .../server/comment/dto/CommentResponse.java | 72 +++++++++ .../payper/server/comment/entity/Comment.java | 28 +++- .../comment/repository/CommentRepository.java | 70 ++++++++ .../comment/service/CommentService.java | 153 ++++++++++++++++++ .../server/global/response/ErrorCode.java | 8 +- .../post/controller/PostController.java | 3 +- .../com/payper/server/post/entity/Post.java | 13 ++ .../post/repository/PostRepository.java | 2 + .../server/post/service/PostService.java | 8 +- 11 files changed, 472 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/payper/server/comment/controller/CommentController.java create mode 100644 src/main/java/com/payper/server/comment/dto/CommentRequest.java create mode 100644 src/main/java/com/payper/server/comment/dto/CommentResponse.java create mode 100644 src/main/java/com/payper/server/comment/repository/CommentRepository.java create mode 100644 src/main/java/com/payper/server/comment/service/CommentService.java diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java new file mode 100644 index 0000000..0a54de6 --- /dev/null +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -0,0 +1,93 @@ +package com.payper.server.comment.controller; + +import com.payper.server.comment.dto.CommentRequest; +import com.payper.server.comment.dto.CommentResponse; +import com.payper.server.comment.service.CommentService; +import com.payper.server.global.response.ApiResponse; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/v1/comments") +@RequiredArgsConstructor +public class CommentController { + private final CommentService commentService; + + /** + * 댓글 작성 + * is inactive = false, is deleted = false 상태의 post에만 댓글을 작성할 수 있음 + * + * 부모 댓글이 삭제되어도 대댓글 작성 허용 + */ + @PostMapping("/posts/{postId}") + public ResponseEntity> createComment( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long postId, + @RequestBody @Valid CommentRequest.CreateComment request + ) { + Long commentId = commentService.createComment(1L, postId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(commentId)); + } + + /** + * 댓글 수정 + * 작성자만 수정 가능 + */ + @PutMapping("/{commentId}") + public ResponseEntity> updateComment( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long commentId, + @RequestBody @Valid CommentRequest.UpdateComment request + ) { + commentService.updateComment(1L, commentId, request); + return ResponseEntity.ok(ApiResponse.ok()); + } + + /** + * 댓글 삭제 + * 작성자만 삭제 가능 + * + * 자식 댓글은 삭제하지 않음 + */ + @DeleteMapping("/{commentId}") + public ResponseEntity> deleteComment( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long commentId) { + + commentService.deleteComment(1L, commentId); + return ResponseEntity.ok(ApiResponse.ok()); + } + + /** + * 내가 쓴 댓글 조회 + * + * 무한 스크롤 방식 + * + * 정렬: 최신 순 + */ + @GetMapping("/me") + public ResponseEntity> getMyComments( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "20") int size + ) { + CommentResponse.MyCommentList response = commentService.getMyComments(1L, cursorId, size); + return ResponseEntity.ok(ApiResponse.ok(response)); + } + + /** + * 게시글 댓글 조회 + * + * TODO 무한 스크롤 + */ + @GetMapping("/posts/{postId}") + public ResponseEntity>> getPostComments(@PathVariable Long postId) { + List response = commentService.getPostComments(postId); + return ResponseEntity.ok(ApiResponse.ok(response)); + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/comment/dto/CommentRequest.java b/src/main/java/com/payper/server/comment/dto/CommentRequest.java new file mode 100644 index 0000000..0b193ad --- /dev/null +++ b/src/main/java/com/payper/server/comment/dto/CommentRequest.java @@ -0,0 +1,29 @@ +package com.payper.server.comment.dto; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class CommentRequest { + + /** + * 댓글 작성 DTO + */ + public record CreateComment( + @NotBlank(message = "댓글을 적어주세요.") + @Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.") + String content, + + @Nullable + Long parentCommentId + ) {} + + /** + * 댓글 수정 DTO + */ + public record UpdateComment( + @NotBlank(message = "댓글을 적어주세요.") + @Size(max = 21800, message = "댓글은 21,800자 이하여야 합니다.") + String content + ) {} +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/comment/dto/CommentResponse.java b/src/main/java/com/payper/server/comment/dto/CommentResponse.java new file mode 100644 index 0000000..fbedef1 --- /dev/null +++ b/src/main/java/com/payper/server/comment/dto/CommentResponse.java @@ -0,0 +1,72 @@ +package com.payper.server.comment.dto; + +import com.payper.server.comment.entity.Comment; + +import java.time.LocalDateTime; +import java.util.List; + +public class CommentResponse { + + /** + * Post에 달린 Comment Item + */ + public record CommentItem( + Long id, + String userName, + Long parentCommentId, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + public static CommentItem from(Comment comment) { + return new CommentItem( + comment.getId(), + comment.getUser().getName(), + comment.getParentComment() != null ? comment.getParentComment().getId() : null, + comment.isDeleted() ? "[삭제된 댓글입니다]" : comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + ); + } + } + + /** + * 내가 작성한 Comment 리스트 + */ + public record MyCommentList( + List comments, + Long nextCursor, + boolean hasNext + ) { + public static MyCommentList from(List comments, Long nextCursor, boolean hasNext) { + return new MyCommentList( + comments.stream() + .map(CommentResponse.MyCommentItem::from) + .toList(), + nextCursor, + hasNext + ); + } + } + + /** + * 내가 작성한 Comment Item + */ + public record MyCommentItem( + Long id, + Long postId, + String content, + LocalDateTime createdAt, + LocalDateTime updatedAt + ) { + public static MyCommentItem from(Comment comment) { + return new MyCommentItem( + comment.getId(), + comment.getPost().getId(), + comment.getContent(), + comment.getCreatedAt(), + comment.getUpdatedAt() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/comment/entity/Comment.java b/src/main/java/com/payper/server/comment/entity/Comment.java index 3894252..4194ba0 100644 --- a/src/main/java/com/payper/server/comment/entity/Comment.java +++ b/src/main/java/com/payper/server/comment/entity/Comment.java @@ -71,7 +71,33 @@ public class Comment extends BaseTimeEntity { * soft delete */ public void delete() { + if (this.isDeleted) { // 멱등성 고려 + return; + } + this.isDeleted = true; - this.deletedAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + this.deletedAt = LocalDateTime.now(); + this.post.decreaseCommentCount(); + } + + /** + * 댓글 생성 + */ + public static Comment create(Post post, User user, Comment parentComment, String content) { + return Comment.builder() + .post(post) + .user(user) + .parentComment(parentComment) + .content(content) + .isDeleted(false) + .deletedAt(null) + .build(); + } + + /** + * 댓글 수정 + */ + public void update(String content) { + this.content = content; } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java new file mode 100644 index 0000000..155bb9c --- /dev/null +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -0,0 +1,70 @@ +package com.payper.server.comment.repository; + +import com.payper.server.comment.entity.Comment; +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 org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface CommentRepository extends JpaRepository { + + Optional findByIdAndIsDeletedFalse(Long id); + + @Query(""" + select c from Comment c + where c.user.id = :userId + and c.isDeleted = false + order by c.createdAt desc, c.id desc + """) + List findFirstMyCommentPage(@Param("userId") Long userId, Pageable pageable); + + @Query(""" + select c from Comment c + where c.user.id = :userId + and c.isDeleted = false + and ( + c.createdAt < :createdAt + or (c.createdAt = :createdAt and c.id < :cursorId) + ) + order by c.createdAt desc, c.id desc + """) + List findNextMyCommentPage( + @Param("userId") Long userId, + @Param("cursorId") Long cursorId, + @Param("createdAt") LocalDateTime createdAt, + Pageable pageable + ); + + @Query(""" + select c from Comment c + join fetch c.user + where c.post.id = :postId + and( + c.isDeleted = false + or exists ( + select 1 + from Comment child + where child.parentComment = c + and child.isDeleted = false + ) + ) + ORDER BY COALESCE(c.parentComment.id, c.id), c.createdAt, c.id + """) + List findVisibleCommentsByPostId(Long postId); + +// @Modifying +// @Query(""" +// update Comment c +// set c.isDeleted = true, +// c.deletedAt = CURRENT_TIMESTAMP +// where c.post.id = :postId +// and c.isDeleted = false +// """) +// void softDeleteByPostId(Long postId); +} diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java new file mode 100644 index 0000000..9ae91a9 --- /dev/null +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -0,0 +1,153 @@ +package com.payper.server.comment.service; + +import com.payper.server.comment.dto.CommentRequest; +import com.payper.server.comment.dto.CommentResponse; +import com.payper.server.comment.entity.Comment; +import com.payper.server.comment.repository.CommentRepository; +import com.payper.server.global.exception.ApiException; +import com.payper.server.global.response.ErrorCode; +import com.payper.server.post.entity.Post; +import com.payper.server.post.repository.PostRepository; +import com.payper.server.user.entity.User; +import com.payper.server.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CommentService { + + private final UserRepository userRepository; + private final PostRepository postRepository; + private final CommentRepository commentRepository; + + /** + * 댓글 작성 + */ + @Transactional + public Long createComment(Long userId, Long postId, CommentRequest.CreateComment request) { + // 사용자 조회 + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApiException(ErrorCode.USER_NOT_FOUND)); + + // 게시글 조회 + Post post = postRepository.findById(postId) + .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); + + // 삭제 또는 비활성화된 post에는 댓글 작성 불가 + if (!post.isCommentable()) { + throw new ApiException(ErrorCode.POST_NOT_COMMENTABLE); + } + + Comment parentComment = request.parentCommentId() != null + ? getValidatedParentComment(request.parentCommentId(), postId) + : null; + + // 댓글 생성 + Comment comment = Comment.create(post, user, parentComment, request.content()); + commentRepository.save(comment); + + post.increaseCommentCount(); + + log.info("댓글 생성 완료 - commentId: {}, userId: {}, postId: {}", comment.getId(), userId, postId); + return comment.getId(); + } + + // 요청 body로 받은 parent comment id 검증 + private Comment getValidatedParentComment(Long parentCommentId, Long postId) { + Comment parentComment = commentRepository.findById(parentCommentId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + if (!parentComment.getPost().getId().equals(postId)) { + throw new ApiException(ErrorCode.INVALID_PARENT_COMMENT); + } + + return parentComment; + } + + /** + * 댓글 수정 + */ + @Transactional + public void updateComment(Long userId, Long commentId, CommentRequest.UpdateComment request) { + // 댓글 조회 + Comment comment = commentRepository.findByIdAndIsDeletedFalse(commentId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 댓글 수정 권한 조회 + if(!userId.equals(comment.getUser().getId())) { + throw new ApiException(ErrorCode.NOT_COMMENT_AUTHOR); + } + + // 댓글 수정 + comment.update(request.content()); + + log.info("댓글 수정 완료 - commentId: {}", comment.getId()); + } + + /** + * 댓글 삭제 + */ + @Transactional + public void deleteComment(Long userId, Long commentId) { + // 댓글 조회 + Comment comment = commentRepository.findById(commentId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + // 댓글 삭제 권한 조회 + if(!userId.equals(comment.getUser().getId())) { + throw new ApiException(ErrorCode.NOT_COMMENT_AUTHOR); + } + + comment.delete(); + } + + /** + * 내가 작성한 댓글 조회 + */ + @Transactional(readOnly = true) + public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, int size) { + Pageable pageable = PageRequest.of(0, size); + + List comments; + + if (cursorId == null) { // 첫 요청 + comments = commentRepository.findFirstMyCommentPage(userId, pageable); + } else { // 첫 요청이 아닌 경우 + Comment lastComment = commentRepository.findById(cursorId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + comments = commentRepository.findNextMyCommentPage(userId, cursorId, lastComment.getCreatedAt(), pageable); + } + + boolean hasNext = comments.size() == size; + Long nextCursor = hasNext ? comments.get(comments.size()-1).getId() : null; + + return CommentResponse.MyCommentList.from(comments, nextCursor, hasNext); + } + + /** + * 게시글 댓글 조회 + */ + @Transactional(readOnly = true) + public List getPostComments(Long postId) { + + // 게시글 존재 및 삭제 여부 확인 + if(!postRepository.existsByIdAndIsDeletedFalse(postId)) { + throw new ApiException(ErrorCode.POST_NOT_FOUND); + } + + List comments = commentRepository.findVisibleCommentsByPostId(postId); + + return comments.stream() + .map(CommentResponse.CommentItem::from) + .toList(); + } +} diff --git a/src/main/java/com/payper/server/global/response/ErrorCode.java b/src/main/java/com/payper/server/global/response/ErrorCode.java index ea1d4f2..ae2c69d 100644 --- a/src/main/java/com/payper/server/global/response/ErrorCode.java +++ b/src/main/java/com/payper/server/global/response/ErrorCode.java @@ -16,10 +16,16 @@ public enum ErrorCode { // USER USER_NOT_FOUND("USER-001", HttpStatus.NOT_FOUND, "User Not Found"), - NOT_POSTING_USER("USER-002", HttpStatus.FORBIDDEN, "Not Posting User"), // POST POST_NOT_FOUND("POST-001", HttpStatus.NOT_FOUND, "Post Not Found"), + NOT_POST_AUTHOR("POST-002", HttpStatus.FORBIDDEN, "Not Post Author"), + + // COMMENT + COMMENT_NOT_FOUND("COMMENT-001", HttpStatus.NOT_FOUND, "Comment Not Found"), + NOT_COMMENT_AUTHOR("COMMENT-002", HttpStatus.FORBIDDEN, "Not Comment Author"), + POST_NOT_COMMENTABLE("COMMENT-003", HttpStatus.BAD_REQUEST, "Cannot comment on deleted or inactive post"), + INVALID_PARENT_COMMENT("COMMENT-004", HttpStatus.BAD_REQUEST, "Invalid parent comment"), // MERCHANT MERCHANT_NOT_FOUND("MERCHANT-001", HttpStatus.NOT_FOUND, "Merchant Not Found") diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 0f3b37c..860f9f6 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -12,6 +12,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -36,7 +37,7 @@ public ResponseEntity> createPost( @RequestBody @Valid PostRequest.CreatePost request ) { Long postId = postService.createPost(1L, merchantId, request); - return ResponseEntity.status(201).body(ApiResponse.created(postId)); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(postId)); } /** diff --git a/src/main/java/com/payper/server/post/entity/Post.java b/src/main/java/com/payper/server/post/entity/Post.java index 56eac08..e8716bb 100644 --- a/src/main/java/com/payper/server/post/entity/Post.java +++ b/src/main/java/com/payper/server/post/entity/Post.java @@ -137,6 +137,9 @@ public void inactivate() { this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); } + /** + * 게시글 생성 + */ public static Post create(User author, Merchant merchant, PostType type, String title, String content) { return Post.builder() .author(author) @@ -147,8 +150,18 @@ public static Post create(User author, Merchant merchant, PostType type, String .build(); } + /** + * 게시글 수정 + */ public void update(String title, String content) { this.title = title; this.content = content; } + + /** + * 댓글을 달 수 있는 게시글인지 체크 + */ + public boolean isCommentable() { + return !this.isDeleted && !this.isInactive; + } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/repository/PostRepository.java b/src/main/java/com/payper/server/post/repository/PostRepository.java index 0976fb5..1fd7568 100644 --- a/src/main/java/com/payper/server/post/repository/PostRepository.java +++ b/src/main/java/com/payper/server/post/repository/PostRepository.java @@ -16,6 +16,8 @@ public interface PostRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); + boolean existsByIdAndIsDeletedFalse(Long id); + @Query( value = """ select p from Post p diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index a414bbd..44e1b63 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -53,13 +53,13 @@ public Long createPost(Long userId, Long merchantId, PostRequest.CreatePost requ */ @Transactional public void updatePost(Long userId, Long postId, PostRequest.UpdatePost request) { - // 게시글 조회 - Post post = postRepository.findById(postId) + // 게시글 조회 및 삭제 여부 체크 + Post post = postRepository.findByIdAndIsDeletedFalse(postId) .orElseThrow(() -> new ApiException(ErrorCode.POST_NOT_FOUND)); // 게시글 수정 권한 조회 if(!userId.equals(post.getAuthor().getId())) { - throw new ApiException(ErrorCode.NOT_POSTING_USER); + throw new ApiException(ErrorCode.NOT_POST_AUTHOR); } // 게시글 수정 @@ -79,7 +79,7 @@ public void deletePost(Long userId, Long postId) { // 게시글 삭제 권한 조회 if(!userId.equals(post.getAuthor().getId())) { - throw new ApiException(ErrorCode.NOT_POSTING_USER); + throw new ApiException(ErrorCode.NOT_POST_AUTHOR); } // 게시글 삭제 From b75fb7d81063164f14b4498ae37d2d4d48b76aa9 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Thu, 29 Jan 2026 14:02:44 +0900 Subject: [PATCH 08/15] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/MerchantController.java | 34 +++++++++++++++++++ .../post/controller/PostController.java | 18 ---------- 2 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/main/java/com/payper/server/merchant/controller/MerchantController.java diff --git a/src/main/java/com/payper/server/merchant/controller/MerchantController.java b/src/main/java/com/payper/server/merchant/controller/MerchantController.java new file mode 100644 index 0000000..29def83 --- /dev/null +++ b/src/main/java/com/payper/server/merchant/controller/MerchantController.java @@ -0,0 +1,34 @@ +package com.payper.server.merchant.controller; + +import com.payper.server.global.response.ApiResponse; +import com.payper.server.post.dto.PostRequest; +import com.payper.server.post.service.PostService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/merchants") +@RequiredArgsConstructor +public class MerchantController { + private final PostService postService; + + /** + * 게시글 작성 + * 가맹점에 대해 글을 작성함 + * 가맹점 리스트에서 가맹점을 선택해서 해당 가맹점의 id를 넘겨 받음 + * TODO 가맹점이 없을 때는 어떻게 해야할까? + * + * 가입된 사용자만 글을 작성할 수 있음 + */ + @PostMapping("/{merchantId}/posts") + public ResponseEntity> createPost( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long merchantId, + @RequestBody @Valid PostRequest.CreatePost request + ) { + Long postId = postService.createPost(1L, merchantId, request); + return ResponseEntity.status(201).body(ApiResponse.created(postId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 0f3b37c..58ca877 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -21,24 +21,6 @@ public class PostController { private final PostService postService; - /** - * 게시글 작성 - * 가맹점에 대해 글을 작성함 - * 가맹점 리스트에서 가맹점을 선택해서 해당 가맹점의 id를 넘겨 받음 - * TODO 가맹점이 없을 때는 어떻게 해야할까? - * - * 가입된 사용자만 글을 작성할 수 있음 - */ - @PostMapping("/merchants/{merchantId}") // TODO: 흠 RESTFUL한 URL은 아닌 것 같음, posts가 뒤로 가는 게 맞는 것 같음 - public ResponseEntity> createPost( - // TODO @AuthenticationPrincipal CustomUserDetails user, - @PathVariable Long merchantId, - @RequestBody @Valid PostRequest.CreatePost request - ) { - Long postId = postService.createPost(1L, merchantId, request); - return ResponseEntity.status(201).body(ApiResponse.created(postId)); - } - /** * 게시글 수정 * 작성자만 수정 가능 From a9a3d2d72fd9db0f65ee4a4baec1ad4444629061 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Sat, 31 Jan 2026 16:03:09 +0900 Subject: [PATCH 09/15] =?UTF-8?q?fix:=20TODO=20task=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/payper/server/comment/controller/CommentController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java index 0a54de6..b890e73 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentController.java +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -83,7 +83,7 @@ public ResponseEntity> getMyComments( /** * 게시글 댓글 조회 * - * TODO 무한 스크롤 + * TODO 부모 댓글은 페이지네이션, 대댓글은 전체 조회 */ @GetMapping("/posts/{postId}") public ResponseEntity>> getPostComments(@PathVariable Long postId) { From 85dd99749683717527d312b433b8be76a382988a Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Mon, 2 Feb 2026 01:40:46 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feature:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=EC=8B=9C=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/repository/CommentRepository.java | 19 ++++++++++--------- .../comment/service/CommentService.java | 14 ++++++++++++++ .../post/controller/PostController.java | 1 - .../com/payper/server/post/entity/Post.java | 5 +++-- .../post/repository/PostRepository.java | 12 ++++++++++++ .../server/post/service/PostService.java | 10 ++++++++-- 6 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java index 155bb9c..b37bdd3 100644 --- a/src/main/java/com/payper/server/comment/repository/CommentRepository.java +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -3,6 +3,7 @@ import com.payper.server.comment.entity.Comment; import org.springframework.data.domain.Pageable; 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; @@ -58,13 +59,13 @@ ORDER BY COALESCE(c.parentComment.id, c.id), c.createdAt, c.id """) List findVisibleCommentsByPostId(Long postId); -// @Modifying -// @Query(""" -// update Comment c -// set c.isDeleted = true, -// c.deletedAt = CURRENT_TIMESTAMP -// where c.post.id = :postId -// and c.isDeleted = false -// """) -// void softDeleteByPostId(Long postId); + @Modifying + @Query(""" + update Comment c + set c.isDeleted = true, + c.deletedAt = :now + where c.post.id = :postId + and c.isDeleted = false + """) + long softDeleteByPostId(@Param("postId") Long postId, @Param("now") LocalDateTime now); } diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java index 9ae91a9..1a80793 100644 --- a/src/main/java/com/payper/server/comment/service/CommentService.java +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -17,8 +17,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.List; +import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; + @Slf4j @Service @RequiredArgsConstructor @@ -150,4 +153,15 @@ public List getPostComments(Long postId) { .map(CommentResponse.CommentItem::from) .toList(); } + + /** + * 게시글 삭제 시 댓글 삭제(soft delete) + */ + @Transactional(propagation = REQUIRES_NEW) + public void softDeleteByPostId(Long postId) { + long deletedCount = commentRepository.softDeleteByPostId(postId, LocalDateTime.now()); + if (deletedCount > 0) { + postRepository.decreaseCommentCount(postId, deletedCount); + } + } } diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 58ca877..6f160b2 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -38,7 +38,6 @@ public ResponseEntity> updatePost( /** * 게시글 삭제 * 작성자만 삭제 가능 - * TODO 댓글도 같이 soft delete */ @DeleteMapping("/{postId}") public ResponseEntity> deletePost( diff --git a/src/main/java/com/payper/server/post/entity/Post.java b/src/main/java/com/payper/server/post/entity/Post.java index e8716bb..413e925 100644 --- a/src/main/java/com/payper/server/post/entity/Post.java +++ b/src/main/java/com/payper/server/post/entity/Post.java @@ -110,10 +110,10 @@ public void increaseCommentCount() { } /** - * 댓글 삭제 + * 댓글 감소 */ public void decreaseCommentCount() { - this.commentCount--; + this.commentCount = Math.max(0, this.commentCount - 1); } /** @@ -127,6 +127,7 @@ public void delete() { this.isDeleted = true; this.deletedAt = LocalDateTime.now(); + this.commentCount = 0; } /** diff --git a/src/main/java/com/payper/server/post/repository/PostRepository.java b/src/main/java/com/payper/server/post/repository/PostRepository.java index 1fd7568..9e438f0 100644 --- a/src/main/java/com/payper/server/post/repository/PostRepository.java +++ b/src/main/java/com/payper/server/post/repository/PostRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; 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; @@ -18,6 +19,17 @@ public interface PostRepository extends JpaRepository { boolean existsByIdAndIsDeletedFalse(Long id); + @Modifying + @Query(""" + update Post p + set p.commentCount = case + when p.commentCount < :count then 0 + else p.commentCount - :count + end + where p.id = :postId + """) + void decreaseCommentCount(@Param("postId") Long postId, @Param("count") long count); + @Query( value = """ select p from Post p diff --git a/src/main/java/com/payper/server/post/service/PostService.java b/src/main/java/com/payper/server/post/service/PostService.java index 44e1b63..e48064e 100644 --- a/src/main/java/com/payper/server/post/service/PostService.java +++ b/src/main/java/com/payper/server/post/service/PostService.java @@ -1,5 +1,6 @@ package com.payper.server.post.service; +import com.payper.server.comment.service.CommentService; import com.payper.server.global.exception.ApiException; import com.payper.server.global.response.ErrorCode; import com.payper.server.merchant.entity.Merchant; @@ -26,6 +27,7 @@ public class PostService { private final UserRepository userRepository; private final PostRepository postRepository; private final MerchantRepository merchantRepository; + private final CommentService commentService; /** * 게시글 작성 @@ -86,8 +88,12 @@ public void deletePost(Long userId, Long postId) { post.delete(); log.info("게시글 삭제 완료 - postId: {}", post.getId()); - // TODO 댓글 삭제 (대댓글도 삭제) -// commentRepository.softDeleteByPostId(postId); + // 댓글 삭제 - 댓글 삭제에 실패해도 게시물 삭제는 진행되어야 하므로 try-catch로 묶음 + try { + commentService.softDeleteByPostId(postId); + } catch (Exception e) { + log.error("댓글 삭제 실패 postId={}", postId); + } } /** From 397e29f8e6e31f820f4a4767838c7f4392aa96be Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Mon, 2 Feb 2026 22:56:23 +0900 Subject: [PATCH 11/15] =?UTF-8?q?refactor:=20=EC=BB=A4=EC=84=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EB=AC=B4=ED=95=9C=20=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20-=20Slice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/comment/repository/CommentRepository.java | 5 +++-- .../payper/server/comment/service/CommentService.java | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java index b37bdd3..adf190c 100644 --- a/src/main/java/com/payper/server/comment/repository/CommentRepository.java +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -2,6 +2,7 @@ import com.payper.server.comment.entity.Comment; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -23,7 +24,7 @@ public interface CommentRepository extends JpaRepository { and c.isDeleted = false order by c.createdAt desc, c.id desc """) - List findFirstMyCommentPage(@Param("userId") Long userId, Pageable pageable); + Slice findFirstMyCommentPage(@Param("userId") Long userId, Pageable pageable); @Query(""" select c from Comment c @@ -35,7 +36,7 @@ public interface CommentRepository extends JpaRepository { ) order by c.createdAt desc, c.id desc """) - List findNextMyCommentPage( + Slice findNextMyCommentPage( @Param("userId") Long userId, @Param("cursorId") Long cursorId, @Param("createdAt") LocalDateTime createdAt, diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java index 1a80793..5788371 100644 --- a/src/main/java/com/payper/server/comment/service/CommentService.java +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -119,7 +120,7 @@ public void deleteComment(Long userId, Long commentId) { public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, int size) { Pageable pageable = PageRequest.of(0, size); - List comments; + Slice comments; if (cursorId == null) { // 첫 요청 comments = commentRepository.findFirstMyCommentPage(userId, pageable); @@ -130,10 +131,10 @@ public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, i comments = commentRepository.findNextMyCommentPage(userId, cursorId, lastComment.getCreatedAt(), pageable); } - boolean hasNext = comments.size() == size; - Long nextCursor = hasNext ? comments.get(comments.size()-1).getId() : null; + Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + log.info("다음 조회 ID는 {}습니다. {}", comments.hasNext() ? "있" : "없", nextCursor); - return CommentResponse.MyCommentList.from(comments, nextCursor, hasNext); + return CommentResponse.MyCommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } /** From 7443ad205a5a5e6ed34df48e8773e64a1a62bfa9 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 3 Feb 2026 15:39:07 +0900 Subject: [PATCH 12/15] =?UTF-8?q?refactor:=20post=20=EB=B3=84=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20-=20=EB=B6=80=EB=AA=A8=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=EB=A1=9C=201=EC=B0=A8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=95=20-=20=EB=B6=80=EB=AA=A8=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=202=EC=B0=A8=20=ED=8E=98=EC=9D=B4=EC=A7=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 26 +++++-- .../server/comment/dto/CommentResponse.java | 21 +++++- .../comment/repository/CommentRepository.java | 70 +++++++++++++++++++ .../comment/service/CommentService.java | 46 ++++++++++-- 4 files changed, 151 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java index b890e73..05093e6 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentController.java +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -10,8 +10,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequestMapping("/api/v1/comments") @RequiredArgsConstructor @@ -83,11 +81,29 @@ public ResponseEntity> getMyComments( /** * 게시글 댓글 조회 * - * TODO 부모 댓글은 페이지네이션, 대댓글은 전체 조회 + * 부모 댓글로 페이지네이션 + * 주의) 부모 댓글이 삭제되어도 자식 댓글이 남아있으면 [삭제된 댓글입니다]로 제공 */ @GetMapping("/posts/{postId}") - public ResponseEntity>> getPostComments(@PathVariable Long postId) { - List response = commentService.getPostComments(postId); + public ResponseEntity> getPostComments( + @PathVariable Long postId, + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "20") int size + ) { + CommentResponse.CommentList response = commentService.getPostComments(postId, cursorId, size); + return ResponseEntity.ok(ApiResponse.ok(response)); + } + + /** + * 자식 댓글 조회 + */ + @GetMapping("/{parentId}/replies") + public ResponseEntity> getReplies( + @PathVariable Long parentId, + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "20") int size + ) { + CommentResponse.CommentList response = commentService.getReplies(parentId, cursorId, size); return ResponseEntity.ok(ApiResponse.ok(response)); } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/comment/dto/CommentResponse.java b/src/main/java/com/payper/server/comment/dto/CommentResponse.java index fbedef1..3a83871 100644 --- a/src/main/java/com/payper/server/comment/dto/CommentResponse.java +++ b/src/main/java/com/payper/server/comment/dto/CommentResponse.java @@ -8,7 +8,26 @@ public class CommentResponse { /** - * Post에 달린 Comment Item + * Post에 달린 Comment 리스트 + */ + public record CommentList( + List comments, + Long nextCursor, + boolean hasNext + ) { + public static CommentList from(List comments, Long nextCursor, boolean hasNext) { + return new CommentList( + comments.stream() + .map(CommentResponse.CommentItem::from) + .toList(), + nextCursor, + hasNext + ); + } + } + + /** + * Comment Item */ public record CommentItem( Long id, diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java index adf190c..a9c5dbb 100644 --- a/src/main/java/com/payper/server/comment/repository/CommentRepository.java +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -18,6 +18,76 @@ public interface CommentRepository extends JpaRepository { Optional findByIdAndIsDeletedFalse(Long id); + @Query(""" + select c from Comment c + join fetch c.user + where c.post.id = :postId + and c.parentComment is null + and ( + c.isDeleted = false + or exists ( + select 1 from Comment child + where child.parentComment = c + and child.isDeleted = false + ) + ) + order by c.createdAt asc, c.id asc + """) + Slice findParent(@Param("postId") Long postId, Pageable pageable); + + @Query(""" + select c from Comment c + join fetch c.user + where c.post.id = :postId + and c.parentComment is null + and ( + c.isDeleted = false + or exists ( + select 1 from Comment child + where child.parentComment = c + and child.isDeleted = false + ) + ) + and ( + c.createdAt > :createdAt + or (c.createdAt = :createdAt and c.id > :cursorId) + ) + order by c.createdAt asc, c.id asc + """) + Slice findParentNext( + @Param("postId") Long postId, + @Param("cursorId") Long cursorId, + @Param("createdAt") LocalDateTime createdAt, + Pageable pageable + ); + + @Query(""" + select c from Comment c + join fetch c.user + where c.parentComment.id = :parentId + and c.isDeleted = false + order by c.createdAt asc, c.id asc + """) + Slice findReply(@Param("parentId") Long parentId, Pageable pageable); + + @Query(""" + select c from Comment c + join fetch c.user + where c.parentComment.id = :parentId + and c.isDeleted = false + and ( + c.createdAt > :createdAt + or (c.createdAt = :createdAt and c.id > :cursorId) + ) + order by c.createdAt asc, c.id asc + """) + Slice findReplyNext( + @Param("parentId") Long parentId, + @Param("cursorId") Long cursorId, + @Param("createdAt") LocalDateTime createdAt, + Pageable pageable + ); + @Query(""" select c from Comment c where c.user.id = :userId diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java index 5788371..00c8d0d 100644 --- a/src/main/java/com/payper/server/comment/service/CommentService.java +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -19,7 +19,6 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.List; import static org.springframework.transaction.annotation.Propagation.REQUIRES_NEW; @@ -141,18 +140,53 @@ public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, i * 게시글 댓글 조회 */ @Transactional(readOnly = true) - public List getPostComments(Long postId) { + public CommentResponse.CommentList getPostComments(Long postId, Long cursorId, int size) { // 게시글 존재 및 삭제 여부 확인 if(!postRepository.existsByIdAndIsDeletedFalse(postId)) { throw new ApiException(ErrorCode.POST_NOT_FOUND); } - List comments = commentRepository.findVisibleCommentsByPostId(postId); + Pageable pageable = PageRequest.of(0, size); + + Slice comments; + + if (cursorId == null) { + comments = commentRepository.findParent(postId, pageable); + } else { + Comment lastComment = commentRepository.findById(cursorId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + comments = commentRepository.findParentNext(postId, cursorId, lastComment.getCreatedAt(), pageable); + } + + Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + return CommentResponse.CommentList.from(comments.getContent(), nextCursor, comments.hasNext()); + } + + @Transactional(readOnly = true) + public CommentResponse.CommentList getReplies(Long parentId, Long cursorId, int size) { - return comments.stream() - .map(CommentResponse.CommentItem::from) - .toList(); + // 부모 댓글 존재 여부 확인 + if(!commentRepository.existsById(parentId)) { + throw new ApiException(ErrorCode.COMMENT_NOT_FOUND); + } + + Pageable pageable = PageRequest.of(0, size); + + Slice comments; + + if (cursorId == null) { + comments = commentRepository.findReply(parentId, pageable); + } else { + Comment lastComment = commentRepository.findById(cursorId) + .orElseThrow(() -> new ApiException(ErrorCode.COMMENT_NOT_FOUND)); + + comments = commentRepository.findReplyNext(parentId, cursorId, lastComment.getCreatedAt(), pageable); + } + + Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; + return CommentResponse.CommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } /** From 1f75da02e3d7d01ea2618f98ef031ed3856d39f4 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 3 Feb 2026 16:00:34 +0900 Subject: [PATCH 13/15] =?UTF-8?q?refactor:=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/repository/CommentRepository.java | 18 ------------------ .../server/comment/service/CommentService.java | 2 -- .../com/payper/server/post/entity/Post.java | 3 ++- 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/payper/server/comment/repository/CommentRepository.java b/src/main/java/com/payper/server/comment/repository/CommentRepository.java index a9c5dbb..5b41cc6 100644 --- a/src/main/java/com/payper/server/comment/repository/CommentRepository.java +++ b/src/main/java/com/payper/server/comment/repository/CommentRepository.java @@ -10,7 +10,6 @@ import org.springframework.stereotype.Repository; import java.time.LocalDateTime; -import java.util.List; import java.util.Optional; @Repository @@ -113,23 +112,6 @@ Slice findNextMyCommentPage( Pageable pageable ); - @Query(""" - select c from Comment c - join fetch c.user - where c.post.id = :postId - and( - c.isDeleted = false - or exists ( - select 1 - from Comment child - where child.parentComment = c - and child.isDeleted = false - ) - ) - ORDER BY COALESCE(c.parentComment.id, c.id), c.createdAt, c.id - """) - List findVisibleCommentsByPostId(Long postId); - @Modifying @Query(""" update Comment c diff --git a/src/main/java/com/payper/server/comment/service/CommentService.java b/src/main/java/com/payper/server/comment/service/CommentService.java index 00c8d0d..0a977d8 100644 --- a/src/main/java/com/payper/server/comment/service/CommentService.java +++ b/src/main/java/com/payper/server/comment/service/CommentService.java @@ -131,8 +131,6 @@ public CommentResponse.MyCommentList getMyComments(Long userId, Long cursorId, i } Long nextCursor = comments.hasNext() ? comments.getContent().get(comments.getContent().size()-1).getId() : null; - log.info("다음 조회 ID는 {}습니다. {}", comments.hasNext() ? "있" : "없", nextCursor); - return CommentResponse.MyCommentList.from(comments.getContent(), nextCursor, comments.hasNext()); } diff --git a/src/main/java/com/payper/server/post/entity/Post.java b/src/main/java/com/payper/server/post/entity/Post.java index 413e925..604abaa 100644 --- a/src/main/java/com/payper/server/post/entity/Post.java +++ b/src/main/java/com/payper/server/post/entity/Post.java @@ -135,7 +135,8 @@ public void delete() { */ public void inactivate() { this.isInactive = true; - this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); +// this.inactiveAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + this.inactiveAt = LocalDateTime.now(); } /** From be31ce75e77e94d72e0bd062926991371c87fb07 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Tue, 3 Feb 2026 16:05:16 +0900 Subject: [PATCH 14/15] =?UTF-8?q?refactor:=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 33 ----------------- .../post/controller/PostController.java | 37 +++++++++++++++++++ 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java index 05093e6..34820b7 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentController.java +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -6,7 +6,6 @@ import com.payper.server.global.response.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -16,22 +15,6 @@ public class CommentController { private final CommentService commentService; - /** - * 댓글 작성 - * is inactive = false, is deleted = false 상태의 post에만 댓글을 작성할 수 있음 - * - * 부모 댓글이 삭제되어도 대댓글 작성 허용 - */ - @PostMapping("/posts/{postId}") - public ResponseEntity> createComment( - // TODO @AuthenticationPrincipal CustomUserDetails user, - @PathVariable Long postId, - @RequestBody @Valid CommentRequest.CreateComment request - ) { - Long commentId = commentService.createComment(1L, postId, request); - return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(commentId)); - } - /** * 댓글 수정 * 작성자만 수정 가능 @@ -78,22 +61,6 @@ public ResponseEntity> getMyComments( return ResponseEntity.ok(ApiResponse.ok(response)); } - /** - * 게시글 댓글 조회 - * - * 부모 댓글로 페이지네이션 - * 주의) 부모 댓글이 삭제되어도 자식 댓글이 남아있으면 [삭제된 댓글입니다]로 제공 - */ - @GetMapping("/posts/{postId}") - public ResponseEntity> getPostComments( - @PathVariable Long postId, - @RequestParam(required = false) Long cursorId, - @RequestParam(defaultValue = "20") int size - ) { - CommentResponse.CommentList response = commentService.getPostComments(postId, cursorId, size); - return ResponseEntity.ok(ApiResponse.ok(response)); - } - /** * 자식 댓글 조회 */ diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index 6f160b2..a04e58c 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -1,5 +1,8 @@ package com.payper.server.post.controller; +import com.payper.server.comment.dto.CommentRequest; +import com.payper.server.comment.dto.CommentResponse; +import com.payper.server.comment.service.CommentService; import com.payper.server.global.response.ApiResponse; import com.payper.server.post.dto.PostRequest; import com.payper.server.post.dto.PostResponse; @@ -12,6 +15,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -20,6 +24,7 @@ @RequiredArgsConstructor public class PostController { private final PostService postService; + private final CommentService commentService; /** * 게시글 수정 @@ -83,4 +88,36 @@ public ResponseEntity>> getPosts( Page response = postService.getPosts(merchantId, type, pageable); return ResponseEntity.ok(ApiResponse.ok(response)); } + + /** + * 댓글 작성 + * is inactive = false, is deleted = false 상태의 post에만 댓글을 작성할 수 있음 + * + * 부모 댓글이 삭제되어도 대댓글 작성 허용 + */ + @PostMapping("/{postId}/comments") + public ResponseEntity> createComment( + // TODO @AuthenticationPrincipal CustomUserDetails user, + @PathVariable Long postId, + @RequestBody @Valid CommentRequest.CreateComment request + ) { + Long commentId = commentService.createComment(1L, postId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(commentId)); + } + + /** + * 게시글 댓글 조회 + * + * 부모 댓글로 페이지네이션 + * 주의) 부모 댓글이 삭제되어도 자식 댓글이 남아있으면 [삭제된 댓글입니다]로 제공 + */ + @GetMapping("/{postId}/comments") + public ResponseEntity> getPostComments( + @PathVariable Long postId, + @RequestParam(required = false) Long cursorId, + @RequestParam(defaultValue = "20") int size + ) { + CommentResponse.CommentList response = commentService.getPostComments(postId, cursorId, size); + return ResponseEntity.ok(ApiResponse.ok(response)); + } } \ No newline at end of file From 211579f8726be4b95697c04ca47df3f3f6c803e0 Mon Sep 17 00:00:00 2001 From: seoyeon2001 Date: Wed, 4 Feb 2026 02:36:02 +0900 Subject: [PATCH 15/15] =?UTF-8?q?refactor:=20=ED=95=98=EB=93=9C=20?= =?UTF-8?q?=EC=BD=94=EB=94=A9=20user=20ID=20->=20@AuthenticationPrincipal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../comment/controller/CommentController.java | 14 +++++++------ .../controller/MerchantController.java | 6 ++++-- .../post/controller/PostController.java | 14 +++++++------ .../server/security/CustomUserDetails.java | 4 ++++ .../server/security/SecurityConfig.java | 20 +++++++++++++++++-- 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/payper/server/comment/controller/CommentController.java b/src/main/java/com/payper/server/comment/controller/CommentController.java index 34820b7..fed250f 100644 --- a/src/main/java/com/payper/server/comment/controller/CommentController.java +++ b/src/main/java/com/payper/server/comment/controller/CommentController.java @@ -4,9 +4,11 @@ import com.payper.server.comment.dto.CommentResponse; import com.payper.server.comment.service.CommentService; import com.payper.server.global.response.ApiResponse; +import com.payper.server.security.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -21,11 +23,11 @@ public class CommentController { */ @PutMapping("/{commentId}") public ResponseEntity> updateComment( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long commentId, @RequestBody @Valid CommentRequest.UpdateComment request ) { - commentService.updateComment(1L, commentId, request); + commentService.updateComment(user.getId(), commentId, request); return ResponseEntity.ok(ApiResponse.ok()); } @@ -37,10 +39,10 @@ public ResponseEntity> updateComment( */ @DeleteMapping("/{commentId}") public ResponseEntity> deleteComment( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long commentId) { - commentService.deleteComment(1L, commentId); + commentService.deleteComment(user.getId(), commentId); return ResponseEntity.ok(ApiResponse.ok()); } @@ -53,11 +55,11 @@ public ResponseEntity> deleteComment( */ @GetMapping("/me") public ResponseEntity> getMyComments( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @RequestParam(required = false) Long cursorId, @RequestParam(defaultValue = "20") int size ) { - CommentResponse.MyCommentList response = commentService.getMyComments(1L, cursorId, size); + CommentResponse.MyCommentList response = commentService.getMyComments(user.getId(), cursorId, size); return ResponseEntity.ok(ApiResponse.ok(response)); } diff --git a/src/main/java/com/payper/server/merchant/controller/MerchantController.java b/src/main/java/com/payper/server/merchant/controller/MerchantController.java index 29def83..565f1ed 100644 --- a/src/main/java/com/payper/server/merchant/controller/MerchantController.java +++ b/src/main/java/com/payper/server/merchant/controller/MerchantController.java @@ -3,9 +3,11 @@ import com.payper.server.global.response.ApiResponse; import com.payper.server.post.dto.PostRequest; import com.payper.server.post.service.PostService; +import com.payper.server.security.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -24,11 +26,11 @@ public class MerchantController { */ @PostMapping("/{merchantId}/posts") public ResponseEntity> createPost( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long merchantId, @RequestBody @Valid PostRequest.CreatePost request ) { - Long postId = postService.createPost(1L, merchantId, request); + Long postId = postService.createPost(user.getId(), merchantId, request); return ResponseEntity.status(201).body(ApiResponse.created(postId)); } } \ No newline at end of file diff --git a/src/main/java/com/payper/server/post/controller/PostController.java b/src/main/java/com/payper/server/post/controller/PostController.java index a04e58c..61e6504 100644 --- a/src/main/java/com/payper/server/post/controller/PostController.java +++ b/src/main/java/com/payper/server/post/controller/PostController.java @@ -9,6 +9,7 @@ import com.payper.server.post.dto.PostSortType; import com.payper.server.post.entity.PostType; import com.payper.server.post.service.PostService; +import com.payper.server.security.CustomUserDetails; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; @@ -17,6 +18,7 @@ import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; @RestController @@ -32,11 +34,11 @@ public class PostController { */ @PutMapping("/{postId}") public ResponseEntity> updatePost( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId, @RequestBody @Valid PostRequest.UpdatePost request ) { - postService.updatePost(1L, postId, request); + postService.updatePost(user.getId(), postId, request); return ResponseEntity.ok(ApiResponse.ok()); } @@ -46,10 +48,10 @@ public ResponseEntity> updatePost( */ @DeleteMapping("/{postId}") public ResponseEntity> deletePost( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId ) { - postService.deletePost(1L, postId); + postService.deletePost(user.getId(), postId); return ResponseEntity.ok(ApiResponse.ok()); } @@ -97,11 +99,11 @@ public ResponseEntity>> getPosts( */ @PostMapping("/{postId}/comments") public ResponseEntity> createComment( - // TODO @AuthenticationPrincipal CustomUserDetails user, + @AuthenticationPrincipal CustomUserDetails user, @PathVariable Long postId, @RequestBody @Valid CommentRequest.CreateComment request ) { - Long commentId = commentService.createComment(1L, postId, request); + Long commentId = commentService.createComment(user.getId(), postId, request); return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.created(commentId)); } diff --git a/src/main/java/com/payper/server/security/CustomUserDetails.java b/src/main/java/com/payper/server/security/CustomUserDetails.java index 36aa84c..0137f18 100644 --- a/src/main/java/com/payper/server/security/CustomUserDetails.java +++ b/src/main/java/com/payper/server/security/CustomUserDetails.java @@ -33,6 +33,10 @@ public String getUsername() { return user.getId().toString(); } + public Long getId() { + return user.getId(); + } + @Override public boolean isEnabled() { return user.isActive(); diff --git a/src/main/java/com/payper/server/security/SecurityConfig.java b/src/main/java/com/payper/server/security/SecurityConfig.java index 828b7b7..e228dd1 100644 --- a/src/main/java/com/payper/server/security/SecurityConfig.java +++ b/src/main/java/com/payper/server/security/SecurityConfig.java @@ -48,11 +48,27 @@ void init() { requestMatcher.matcher(HttpMethod.GET, "/swagger-ui/**"), requestMatcher.matcher(HttpMethod.GET, "/v3/api-docs/**"), requestMatcher.matcher(HttpMethod.GET, "/favicon.ico"), - requestMatcher.matcher("/auth/**") + requestMatcher.matcher("/auth/**"), + requestMatcher.matcher(HttpMethod.GET, "/api/v1/posts/**"), + requestMatcher.matcher(HttpMethod.GET, "/api/v1/comments/*/replies") ); + // 인증이 필요한 요청 authenticatedRequestMatcher = new OrRequestMatcher( //requestMatcher.matcher("/**"), - requestMatcher.matcher(HttpMethod.GET, "/me") + requestMatcher.matcher(HttpMethod.GET, "/me"), + + // 댓글 관련 + requestMatcher.matcher(HttpMethod.PUT, "/api/v1/comments/**"), + requestMatcher.matcher(HttpMethod.DELETE, "/api/v1/comments/**"), + requestMatcher.matcher(HttpMethod.GET, "/api/v1/comments/me"), + + // 게시물 관련 + requestMatcher.matcher(HttpMethod.POST, "/api/v1/posts/**"), + requestMatcher.matcher(HttpMethod.PUT, "/api/v1/posts/**"), + requestMatcher.matcher(HttpMethod.DELETE, "/api/v1/posts/**"), + + // 가맹점 관련 + requestMatcher.matcher(HttpMethod.POST, "/api/v1/merchants/**") ); adminRequestMatcher = new OrRequestMatcher( requestMatcher.matcher(HttpMethod.GET, "/admin/**")