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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 count "
+ "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 @@ -7,6 +7,7 @@
import org.juniortown.backend.user.jwt.LoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -77,9 +78,9 @@ 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()
.requestMatchers("/api/auth/login", "/", "/api/auth/signup","/swagger-ui/**","/v3/api-docs/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/posts/details/**", "/api/posts", "/api/posts/home").permitAll()
.requestMatchers("/actuator/health","/actuator/prometheus").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 count "
+ "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.from(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 getCount();
}
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,23 @@
package org.juniortown.backend.post.entity;

import lombok.Getter;

@Getter
public enum Category {
QUESTION(),
STUDY(),
NOTICE(),
SECOND_HAND_MARKET(),
MENTOR();

public static Category from(String value) {
if (value == null) {
throw new IllegalArgumentException("Category는 null일 수 없다.");
}
try {
return Category.valueOf(value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("유효하지 않은 카테고리입니다: " + value);
}
}
}
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(String title, String content) {
this.title = title;
this.content = content;
}

public void addReadCount(Long redisReadCount) {
Expand Down
Loading