Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@
import java.util.List;

import org.juniortown.backend.comment.entity.Comment;
import org.juniortown.backend.post.dto.response.IdCount;
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;

@Repository
public interface CommentRepository extends JpaRepository<Comment, Long> {
List<Comment> findByPostIdOrderByCreatedAtAsc(Long postId);

@Query("SELECT c.post.id AS postId, COUNT(c) AS cnt "
+ "FROM Comment c "
+ "WHERE c.post.id IN :postIds "
+ "GROUP BY c.post.id")
List<IdCount> countByPostIds(@Param("postIds") List<Long> postsIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ public RedisTemplate<String, Boolean> keyCheckRedisTemplate(RedisConnectionFacto
@Bean
public RedisTemplate<String, Long> readCountRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String,Long> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
http
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/api/auth/login", "/", "/api/auth/signup","/swagger-ui/**","/v3/api-docs/**",
"/api/posts/details/**", "/actuator/health","/actuator/prometheus").permitAll()
.requestMatchers(RegexRequestMatcher.regexMatcher("/api/posts/\\d+")).permitAll()
"/api/posts/details/**", "/actuator/health","/actuator/prometheus","/api/posts", "/api/posts/home").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.anyRequest().authenticated());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.juniortown.backend.dummyData;

import org.juniortown.backend.post.entity.Category;
import org.juniortown.backend.post.entity.Post;
import org.juniortown.backend.post.repository.PostRepository;
import org.juniortown.backend.user.entity.User;
Expand Down Expand Up @@ -35,18 +36,19 @@ public void run(String... args) throws Exception {
.orElseThrow(() -> new RuntimeException("User not found"));

// 게시글 생성
Category[] categories = Category.values();
for (int i = 0; i < 25; i++) {
createPost("title" + i,"content" + i, user, categories[i % categories.length]);
}
}

private void createPost(String Dummy_Post_Title, String content, User user, Category category) {
Post testPost1 = Post.builder()
.title("Dummy Post Title")
.content("This is a dummy post content.")
.title(Dummy_Post_Title)
.content(content)
.user(user)
.category(category)
.build();
postRepository.save(testPost1);

Post testPost2 = Post.builder()
.title("Another Dummy Post Title")
.content("This is another dummy post content.")
.user(user)
.build();
postRepository.save(testPost2);
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package org.juniortown.backend.like.repository;

import java.util.List;
import java.util.Optional;

import org.juniortown.backend.like.entity.Like;
import org.juniortown.backend.post.dto.response.IdCount;
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;

@Repository
public interface LikeRepository extends JpaRepository<Like, Long> {
Optional<Like> findByUserIdAndPostId(Long userId, Long postId);
Long countByPostId(Long postId);

@Query("SELECT l.post.id AS postId, COUNT(l) AS cnt "
+ "FROM Like l "
+ "WHERE l.post.id IN :postIds "
+ "GROUP BY l.post.id")
List<IdCount> countByPostIds(@Param("postIds")List<Long>postIds);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package org.juniortown.backend.post.controller;

import java.util.stream.Collectors;

import org.juniortown.backend.post.dto.request.PostCreateRequest;
import org.juniortown.backend.post.dto.request.PostEditRequest;
import org.juniortown.backend.post.dto.response.HomePageResponse;
import org.juniortown.backend.post.dto.response.PageResponse;
import org.juniortown.backend.post.dto.response.PostDetailResponse;
import org.juniortown.backend.post.dto.response.PostWithLikeCount;
import org.juniortown.backend.post.dto.response.PostResponse;
import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;
import org.juniortown.backend.post.entity.Category;
import org.juniortown.backend.post.service.PostService;
import org.juniortown.backend.post.service.ViewCountService;
import org.juniortown.backend.post.service.ReadCountRedisService;
import org.juniortown.backend.user.dto.CustomUserDetails;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
Expand All @@ -23,6 +22,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.Valid;
Expand All @@ -33,7 +33,7 @@
@RequestMapping("/api")
public class PostController {
private final PostService postService;
private final ViewCountService viewCountService;
private final ReadCountRedisService readCountRedisService;
@PostMapping("/posts")
public ResponseEntity<PostResponse> create(@AuthenticationPrincipal CustomUserDetails customUserDetails,
@Valid @RequestBody PostCreateRequest postCreateRequest) {
Expand All @@ -52,20 +52,22 @@ public ResponseEntity<?> delete(@AuthenticationPrincipal CustomUserDetails custo
}

@PatchMapping("/posts/{postId}")
public ResponseEntity<PostResponse> update(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable Long postId, @Valid @RequestBody PostCreateRequest postCreateRequest) {
public ResponseEntity<PostResponse> update(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable Long postId, @Valid @RequestBody PostEditRequest postEditRequest) {
Long userId = customUserDetails.getUserId();
PostResponse response = postService.update(postId, userId, postCreateRequest);
PostResponse response = postService.update(postId, userId, postEditRequest);
// 202가 뭐임?
return ResponseEntity.status(HttpStatus.OK).body(response);
}

// 게시글 목록 조회, 페이지네이션 적용
@GetMapping("/posts/{page}")
@GetMapping("/posts")
public ResponseEntity<PageResponse<PostResponse>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails,
@PathVariable int page
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(defaultValue = "NOTICE") Category category
) {
Long userId = (customUserDetails != null) ? customUserDetails.getUserId() : null;
Page<PostResponse> posts = postService.getPosts(userId, page);
Page<PostResponse> posts = postService.getPosts(userId, page, size, category);
PageResponse<PostResponse> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
Expand All @@ -80,4 +82,12 @@ public ResponseEntity<PostDetailResponse> getPost(@AuthenticationPrincipal Custo
return ResponseEntity.ok(postDetailResponse);
}

@GetMapping("/posts/home")
public ResponseEntity<HomePageResponse> homePagePosts(
@RequestParam(defaultValue = "5") int perCategory
){
HomePageResponse homePageResponse = postService.homePagePosts(perCategory);
return ResponseEntity.ok(homePageResponse);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.juniortown.backend.post.dto.request;

import org.juniortown.backend.post.entity.Category;
import org.juniortown.backend.post.entity.Post;
import org.juniortown.backend.user.entity.User;

Expand All @@ -17,16 +18,20 @@ public class PostCreateRequest {
private String title;
@NotBlank(message = "컨텐츠를 입력해주세요.")
private String content;
@NotBlank(message = "카테고리를 입력해주세요.")
private String category;
@Builder
public PostCreateRequest(String title, String content) {
public PostCreateRequest(String title, String content, String category) {
this.title = title;
this.content = content;
this.category = category;
}

public Post toEntity(User user) {
return Post.builder()
.title(title)
.content(content)
.category(Category.valueOf(category))
.user(user)
.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.juniortown.backend.post.dto;
package org.juniortown.backend.post.dto.request;

import jakarta.validation.constraints.NotBlank;
import lombok.Builder;
Expand All @@ -8,13 +8,13 @@
@Getter
@Builder
@ToString
public class PostEdit {
public class PostEditRequest {
@NotBlank(message = "타이틀을 입력해주세요.")
private String title;
@NotBlank(message = "컨텐츠를 입력해주세요.")
private String content;
@Builder
public PostEdit(String title, String content) {
public PostEditRequest(String title, String content) {
this.title = title;
this.content = content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.juniortown.backend.post.dto.response;

import java.time.LocalDateTime;
import java.util.List;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class HomePageResponse {
List<HomePageSection> sections;
private LocalDateTime createdAt;
@Builder
public HomePageResponse(List<HomePageSection> sections, LocalDateTime createdAt) {
this.sections = sections;
this.createdAt = createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.juniortown.backend.post.dto.response;

import java.util.List;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class HomePageSection {
private String category;
private List<PostForHomePage> items;
@Builder
public HomePageSection(String category, List<PostForHomePage> items) {
this.category = category;
this.items = items;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.juniortown.backend.post.dto.response;

public interface IdCount {
Long getPostId();
Long getCnt();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.juniortown.backend.post.dto.response;

import java.time.LocalDateTime;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class PostForHomePage {
private Long postId;
private String title;
private Long userId;
private String nickname;
private Long likeCount;
private Long commentCount;
private Long readCount;
private LocalDateTime createdAt;
@Builder
public PostForHomePage(Long postId, String title, Long userId, String nickname, Long likeCount, Long commentCount,
Long readCount, LocalDateTime createdAt) {
this.postId = postId;
this.title = title;
this.userId = userId;
this.nickname = nickname;
this.likeCount = likeCount;
this.commentCount = commentCount;
this.readCount = readCount;
this.createdAt = createdAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.juniortown.backend.post.entity;

import lombok.Getter;

@Getter
public enum Category {
QUESTION(),
STUDY(),
NOTICE(),
SECOND_HAND_MARKET(),
MENTOR()
}
16 changes: 12 additions & 4 deletions backend/src/main/java/org/juniortown/backend/post/entity/Post.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@

import org.juniortown.backend.entity.BaseTimeEntity;
import org.juniortown.backend.post.dto.request.PostCreateRequest;
import org.juniortown.backend.post.dto.request.PostEditRequest;
import org.juniortown.backend.user.entity.User;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
Expand Down Expand Up @@ -43,20 +46,25 @@ public class Post extends BaseTimeEntity {
@Column(name = "read_count", nullable = false)
private Long readCount = 0L;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Category category;

@Builder
public Post(String title, String content, User user) {
public Post(String title, String content, User user, Category category) {
this.title = title;
this.content = content;
this.user = user;
this.category = category;
}
Comment on lines +49 to 59
Copy link

@coderabbitai coderabbitai bot Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

카테고리 null 방지 및 인덱스 고려 필요

  • DB 레벨에 nullable=false만으로는 런타임 null 유입을 막기 어렵습니다. 생성자/빌더에서 null 방어를 추가해 주세요.
  • 카테고리별 조회가 핵심이면 DB 인덱스가 필요합니다(카테고리+삭제여부, 정렬 컬럼 조합 포함).

적용 예(선택 1: Lombok @nonnull 사용):

 @Getter
 public class Post extends BaseTimeEntity {
@@
-    @Column(nullable = false)
-    @Enumerated(EnumType.STRING)
-    private Category category;
+    @Column(nullable = false)
+    @Enumerated(EnumType.STRING)
+    private Category category;
@@
-  public Post(String title, String content, User user, Category category) {
+  public Post(String title, String content, User user, @lombok.NonNull Category category) {
     this.title = title;
     this.content = content;
     this.user = user;
     this.category = category;
   }

인덱스 예(파일 범위 밖 변경 예시):

// 클래스 선언 위에 추가
// import jakarta.persistence.Table;
// import jakarta.persistence.Index;
@Table(indexes = {
  @Index(name = "idx_post_category_deleted_created",
         columnList = "category, deleted_at, created_at")
})
🤖 Prompt for AI Agents
backend/src/main/java/org/juniortown/backend/post/entity/Post.java lines 49-59:
add null-checks for category at construction/builder level and declare DB index
for category+deleted_at+created_at; specifically, ensure the Post constructor
(and any Lombok builder) validates category is not null (throw
IllegalArgumentException or use Objects.requireNonNull) and set the field only
after validation, and add a @Table(indexes = {...}) annotation on the entity
class (file-scope change) with an index like "category, deleted_at, created_at"
to support category-based queries and soft-delete filtering.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실무에서는 DTO와 엔티티에서 입력을 이중으로 검증하는구나.. 일단 나는 DTO에서만 막을게

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 더 알아보고 리팩토링 해볼게


public void softDelete(Clock clock) {
this.deletedAt = LocalDateTime.now(clock);
}

public void update(PostCreateRequest postCreateRequest, Clock clock) {
this.title = postCreateRequest.getTitle();
this.content = postCreateRequest.getContent();
public void update(PostEditRequest postEditRequest) {
this.title = postEditRequest.getTitle();
this.content = postEditRequest.getContent();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

엔티티가 API DTO(PostEditRequest)에 의존합니다

엔티티 레이어가 웹 DTO에 의존하면 계층 결합이 심해집니다. 순수 도메인 메서드로 바꾸고 매핑은 Service에서 처리해 주세요.

- public void update(PostEditRequest postEditRequest) {
-   this.title = postEditRequest.getTitle();
-   this.content = postEditRequest.getContent();
- }
+ public void update(String title, String content) {
+   this.title = title;
+   this.content = content;
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void update(PostEditRequest postEditRequest) {
this.title = postEditRequest.getTitle();
this.content = postEditRequest.getContent();
}
public void update(String title, String content) {
this.title = title;
this.content = content;
}
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/post/entity/Post.java around
lines 65-68, the entity currently depends on the web DTO PostEditRequest; change
the entity method to a pure domain method that does not reference web DTOs
(e.g., replace public void update(PostEditRequest postEditRequest) with a
signature like public void update(String title, String content) or a
domain-specific DTO), move mapping from PostEditRequest to the service layer
where you call post.update(postEditRequest.getTitle(),
postEditRequest.getContent()), and update all callers to perform the
DTO-to-domain mapping in the service or controller so the entity layer has no
dependency on web DTO classes.


public void addReadCount(Long redisReadCount) {
Expand Down
Loading