Skip to content

Feat#9 좋아요기능#10

Merged
Doncham merged 5 commits intomainfrom
feat#9--좋아요기능
Jul 8, 2025

Hidden character warning

The head ref may contain hidden characters: "feat#9--\uc88b\uc544\uc694\uae30\ub2a5"
Merged

Feat#9 좋아요기능#10
Doncham merged 5 commits intomainfrom
feat#9--좋아요기능

Conversation

@Doncham
Copy link
Owner

@Doncham Doncham commented Jul 8, 2025

Summary by CodeRabbit

  • 신규 기능

    • 게시글에 좋아요 기능이 추가되었습니다. 게시글 목록에서 하트 버튼을 눌러 좋아요를 누르거나 취소할 수 있으며, 각 게시글의 좋아요 개수와 내 좋아요 여부가 표시됩니다.
  • 버그 수정

    • 게시글 목록 조회 시 로그인하지 않은 사용자는 알림 후 로그인 페이지로 이동합니다.
  • 테스트

    • 좋아요 및 좋아요 취소 기능에 대한 단위 테스트와 통합 테스트가 추가되었습니다.
    • 게시글 목록에서 좋아요 개수와 상태가 올바르게 반영되는지 검증하는 테스트가 추가되었습니다.
  • 문서화

    • 좋아요 관련 API 응답 구조 및 게시글 목록 응답에 좋아요 정보가 포함되도록 문서가 보완되었습니다.

Doncham added 4 commits July 3, 2025 21:37
페이지 조회를 통해 좋아요 개수도 확인 가능
좋아요 테스트, 좋아요 취소 테스트 작성
조회하는 유저가 좋아요를 눌렀다면 isLiked 속성을 true로 해서 프론트에서 다르게 처리 하도록 함
@Doncham Doncham self-assigned this Jul 8, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 8, 2025

Walkthrough

게시글 좋아요 기능이 백엔드와 프론트엔드에 새롭게 추가되었습니다. 백엔드는 좋아요 엔티티, 컨트롤러, 서비스, 예외, 저장소, DTO, 그리고 좋아요 수/상태를 포함하는 게시글 조회 기능을 구현했습니다. 프론트엔드는 게시글 목록에서 좋아요 버튼 및 카운트 표시와 토글 기능을 추가했습니다.

Changes

파일/그룹 변경 요약
.../like/controller/LikeController.java
.../like/service/LikeService.java
.../like/dto/response/LikeResponse.java
.../like/entity/Like.java
.../like/exception/LikeFailureException.java
.../like/repository/LikeRepository.java
좋아요 REST 컨트롤러, 서비스, DTO, 엔티티, 예외, 저장소 신규 추가.
.../post/controller/PostController.java
.../post/service/PostService.java
.../post/dto/response/PostWithLikeCount.java
.../post/dto/response/PostWithLikeCountProjection.java
.../post/repository/PostRepository.java
게시글 목록 조회에 좋아요 수 및 상태 포함하도록 수정 및 관련 Projection/DTO/쿼리 추가.
.../test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
.../test/java/org/juniortown/backend/like/service/LikeServiceTest.java
좋아요 컨트롤러 및 서비스 단위/통합 테스트 신규 추가.
.../test/java/org/juniortown/backend/controller/PostControllerPagingTest.java 게시글 페이지네이션 테스트에 좋아요 관련 시나리오 및 데이터 추가.
.../test/java/org/juniortown/backend/post/service/PostServiceTest.java 게시글 서비스 테스트가 좋아요 Projection 기반으로 동작하도록 수정.
.../test/java/org/juniortown/backend/post/entity/PostTest.java 게시글 수정 시 updatedAt 관련 검증 일부 주석 처리.
frontend/src/pages/posts/PostListPage.jsx 게시글 목록에 좋아요 버튼, 카운트, 토글 핸들러 추가 및 로그인 체크 강화.

Sequence Diagram(s)

sequenceDiagram
actor User
participant Frontend as "PostListPage.jsx"
participant Backend as "LikeController"
participant Service as "LikeService"
participant Repo as "LikeRepository"
participant DB as "Database"

User->>Frontend: 게시글 목록 조회
Frontend->>Backend: GET /api/posts?page=n (JWT 포함)
Backend->>Service: getPosts(userId, page)
Service->>Repo: findAllWithLikeCount(userId, pageable)
Repo->>DB: 게시글+좋아요 수/상태 쿼리
DB-->>Repo: 결과 반환
Repo-->>Service: Projection 반환
Service-->>Backend: Projection 반환
Backend-->>Frontend: 게시글+좋아요 수/상태 반환
Frontend-->>User: 목록+좋아요 표시

User->>Frontend: 좋아요 버튼 클릭
Frontend->>Backend: POST /api/posts/likes/{postId} (JWT 포함)
Backend->>Service: likePost(userId, postId)
Service->>Repo: findByUserIdAndPostId(userId, postId)
alt 좋아요 없음
    Service->>Repo: save(Like)
else 이미 좋아요
    Service->>Repo: deleteById(likeId)
end
Service-->>Backend: LikeResponse 반환
Backend-->>Frontend: LikeResponse 반환
Frontend-->>User: UI 업데이트(하트/카운트)
Loading

Suggested labels

🌠 feat

Poem

🐇
오늘은 하트가 폴짝!
게시글마다 사랑이 콩닥콩닥
버튼 한 번에 좋아요 쏙,
숫자도 올라가니 기분이 뿅!
토글로 켜고 끄는 이 마음,
주니어타운에 봄바람이 불어요~ 💖

✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

LGTM 👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (8)
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java (1)

30-31: 생성자 주석 개선

주석이 Korean과 English이 혼재되어 있습니다. 일관성을 위해 Korean으로 통일하는 것이 좋겠습니다.

-	// 생성자 기반 DTO 매핑
+	// 프로젝션 기반 DTO 매핑을 위한 생성자
backend/src/main/java/org/juniortown/backend/like/controller/LikeController.java (2)

9-9: 불필요한 import 제거

DeleteMapping import가 사용되지 않습니다.

-import org.springframework.web.bind.annotation.DeleteMapping;

22-27: API 설계 검토 제안

현재 POST 엔드포인트만 제공하고 있는데, REST API 설계 관점에서 좋아요 추가/제거를 별도 엔드포인트로 분리하는 것을 고려해보세요.

예를 들어:

  • POST /api/posts/{postId}/likes - 좋아요 추가
  • DELETE /api/posts/{postId}/likes - 좋아요 제거

또는 현재와 같은 토글 방식을 유지한다면 PUT 메소드가 더 적절할 수 있습니다.

backend/src/main/java/org/juniortown/backend/like/entity/Like.java (1)

12-12: 불필요한 import 제거 필요

OneToOne 어노테이션이 import되었지만 사용되지 않습니다.

-import jakarta.persistence.OneToOne;
frontend/src/pages/posts/PostListPage.jsx (1)

149-164: 접근성 개선 제안

좋아요 버튼의 접근성을 개선할 수 있습니다.

-                        {post.isLiked ? '❤️' : '🤍'}
+                        {post.isLiked ? '♥' : '♡'}

이모지 대신 유니코드 하트 문자를 사용하면 스크린 리더에서 더 일관된 경험을 제공할 수 있습니다.

backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1)

5-5: 불필요한 import 제거

PostWithLikeCount 클래스가 import되었지만 사용되지 않습니다.

-import org.juniortown.backend.post.dto.response.PostWithLikeCount;
backend/src/test/java/org/juniortown/backend/like/service/LikeServiceTest.java (2)

9-9: 불필요한 import 제거 필요

사용하지 않는 import가 있습니다:

  • 9번째 줄: org.assertj.core.api.Assertions (3번째 줄에서 static import로 이미 가져옴)
  • 24번째 줄: org.springframework.security.core.parameters.P (코드에서 사용되지 않음)
-import org.assertj.core.api.Assertions;
-import org.springframework.security.core.parameters.P;

Also applies to: 24-24


84-101: 예외 테스트 개선 제안

예외 메시지가 한국어와 영어가 혼재되어 있어 일관성이 떨어집니다. 또한 실제 서비스에서 발생할 수 있는 예외 상황을 더 현실적으로 시뮬레이션할 수 있습니다.

-		when(likeRepository.save(any(Like.class)))
-			.thenThrow((new RuntimeException("게시글 삭제됐는데? DB 예외 펑!")));
+		when(likeRepository.save(any(Like.class)))
+			.thenThrow(new RuntimeException("Database constraint violation"));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d61f71 and 509a1f5.

📒 Files selected for processing (18)
  • backend/src/main/java/org/juniortown/backend/controller/ExceptionController.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/controller/LikeController.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/dto/response/LikeResponse.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/entity/Like.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/exception/LikeFailureException.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/service/LikeService.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (2 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (9 hunks)
  • backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/like/service/LikeServiceTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/post/entity/PostTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (4 hunks)
  • frontend/src/pages/posts/PostListPage.jsx (4 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
backend/src/test/java/org/juniortown/backend/like/service/LikeServiceTest.java (1)
backend/src/main/java/org/juniortown/backend/like/exception/LikeFailureException.java (1)
  • LikeFailureException (8-17)
🔇 Additional comments (21)
backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1)

1-12: 잘 구현된 리포지토리 인터페이스입니다.

Spring Data JPA 규칙을 잘 따르고 있으며, Optional<Like> 반환 타입 사용이 적절합니다.

참고사항: @Repository 애너테이션은 Spring Data JPA 인터페이스에서 불필요하지만 명시적으로 표현하는 것도 좋은 방법입니다.

backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1)

5-14: 잘 설계된 프로젝션 인터페이스입니다.

Spring Data JPA 프로젝션 규칙을 잘 따르고 있으며, 좋아요 기능에 필요한 모든 필드를 포함하고 있습니다. Boolean 래퍼 타입 사용도 적절합니다.

backend/src/main/java/org/juniortown/backend/like/exception/LikeFailureException.java (1)

8-17: 잘 구현된 커스텀 예외 클래스입니다.

CustomException을 적절히 확장하고 있으며, 정적 메시지와 원인 예외를 포함한 생성자 구현이 좋습니다. HTTP 500 상태 코드도 적절합니다.

backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)

75-75: getPosts 호출 검증 완료

서비스 메소드 시그니처 변경에 맞춰 아래 모든 호출부가 userId 인자를 포함하도록 업데이트되어 있습니다.

  • backend/src/main/java/org/juniortown/backend/post/controller/PostController.java:62
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java:214, 244
backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (2)

202-217: 테스트 코드 업데이트 잘 반영됨

서비스 레이어의 변경사항이 테스트 코드에 정확히 반영되었습니다. PostWithLikeCountProjection 사용과 새로운 repository 메소드 호출이 적절히 모킹되었습니다.


227-230: 테스트 검증 로직 적절함

새로운 projection 타입을 사용한 테스트 검증 로직이 적절합니다.

backend/src/main/java/org/juniortown/backend/like/dto/response/LikeResponse.java (1)

7-19: 잘 구현된 DTO 클래스

Lombok 어노테이션을 적절히 사용하고 필드 구성이 명확합니다. 좋아요 응답 데이터를 담기에 적절한 구조입니다.

backend/src/test/java/org/juniortown/backend/like/service/LikeServiceTest.java (3)

27-43: 테스트 클래스 설정이 적절합니다

MockitoExtension을 사용한 모킹 설정과 @activeprofiles("test") 설정이 잘 구성되어 있습니다.


45-63: 좋아요 성공 테스트 로직 검증

테스트 로직이 올바르게 구현되었습니다. 모킹 설정과 검증 단계가 적절하게 구성되어 있습니다.


65-82: 좋아요 취소 테스트 로직 검증

기존 좋아요가 있는 상태에서 취소하는 시나리오를 잘 테스트하고 있습니다.

backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (6)

34-39: 테스트 클래스 설정이 적절합니다

SpringBootTest를 사용한 통합 테스트 설정이 올바르게 구성되어 있습니다. @transactional 어노테이션으로 테스트 간 격리도 보장됩니다.


56-64: 테스트 데이터 초기화 로직 검증

@beforeeach에서 좋아요 데이터를 초기화하고 테스트 게시글을 생성하는 로직이 적절합니다.


65-91: JWT 인증 설정 검증

사용자 생성, 로그인, JWT 토큰 발급 과정이 올바르게 구현되어 있습니다.


93-107: 좋아요 성공 테스트 검증

HTTP 요청과 응답 검증이 적절하게 구현되어 있습니다. 상태 코드와 응답 본문의 각 필드를 모두 검증하고 있습니다.


109-128: 좋아요 취소 테스트 검증

사전에 좋아요를 생성하고 취소하는 시나리오를 잘 테스트하고 있습니다.


130-143: 예외 처리 테스트 검증

존재하지 않는 게시글에 대한 좋아요 시도 시 예외 처리를 잘 테스트하고 있습니다.

backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (5)

12-14: 좋아요 기능 통합을 위한 의존성 추가

좋아요 기능 테스트를 위해 필요한 의존성들이 적절히 추가되었습니다.

Also applies to: 55-61


65-67: 테스트 데이터 개선

두 명의 테스트 사용자 생성과 POST_COUNT 상수 사용으로 테스트 데이터 관리가 개선되었습니다.

Also applies to: 69-69


85-108: 사용자 설정 로직 개선

두 명의 테스트 사용자를 생성하여 더 포괄적인 좋아요 테스트가 가능하도록 설정이 개선되었습니다.


209-249: 좋아요 기능 통합 테스트 검증

게시글 목록 조회 시 좋아요 수가 올바르게 집계되고 반환되는지 검증하는 테스트가 잘 구현되어 있습니다.


251-295: 좋아요 취소 기능 통합 테스트 검증

LikeService를 통해 좋아요를 취소하고 목록에서 좋아요 수가 올바르게 업데이트되는지 검증하는 테스트가 적절하게 구현되어 있습니다.

Comment on lines 54 to 56
/*assertEquals(LocalDateTime.ofInstant(
Instant.parse("2025-06-20T10:15:30Z"), ZoneOffset.UTC),
post.getUpdatedAt());
post.getUpdatedAt());*/
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

주석 처리된 테스트 어설션을 수정하거나 제거하세요.

updatedAt 필드에 대한 어설션이 주석 처리되어 있습니다. 이는 테스트가 불안정하거나 예상대로 동작하지 않음을 의미할 수 있습니다. 다음 중 하나를 선택하세요:

  1. 어설션을 완전히 제거하고 테스트 목적을 명확히 하기
  2. 더 안정적인 방식으로 updatedAt 검증 로직 구현하기
-		/*assertEquals(LocalDateTime.ofInstant(
-			Instant.parse("2025-06-20T10:15:30Z"), ZoneOffset.UTC),
-			post.getUpdatedAt());*/
+		assertEquals(LocalDateTime.ofInstant(
+			Instant.parse("2025-06-20T10:15:30Z"), ZoneOffset.UTC),
+			post.getUpdatedAt());

또는 어설션이 불필요하다면 완전히 제거하세요.

📝 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
/*assertEquals(LocalDateTime.ofInstant(
Instant.parse("2025-06-20T10:15:30Z"), ZoneOffset.UTC),
post.getUpdatedAt());
post.getUpdatedAt());*/
assertEquals(LocalDateTime.ofInstant(
Instant.parse("2025-06-20T10:15:30Z"), ZoneOffset.UTC),
post.getUpdatedAt());
🤖 Prompt for AI Agents
In backend/src/test/java/org/juniortown/backend/post/entity/PostTest.java around
lines 54 to 56, the assertion for the updatedAt field is commented out,
indicating instability or incorrect behavior. Either remove this assertion
entirely if it is unnecessary for the test's purpose, or rewrite it using a more
reliable approach to verify updatedAt, such as comparing with a mocked or fixed
timestamp or allowing a time range to account for execution delays.

Comment on lines +20 to 28
public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
// 이거 null 체크 어지럽네
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

likeCount 필드 초기화 누락

Post 객체를 받는 생성자에서 likeCount 필드가 초기화되지 않아 null 값이 될 수 있습니다. 이는 NullPointerException을 발생시킬 수 있습니다.

다음 수정을 제안합니다:

 public PostWithLikeCount(Post post) {
 	this.id = post.getId();
 	this.title = post.getTitle();
 	this.username = post.getUser().getName();
 	this.userId = post.getUser().getId();
+	this.likeCount = 0L; // 기본값 설정
 	this.createdAt = post.getCreatedAt();
 	this.updatedAt = post.getUpdatedAt();
 	this.deletedAt = post.getDeletedAt();
 }
📝 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 PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
// 이거 null 체크 어지럽네
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
public PostWithLikeCount(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.username = post.getUser().getName();
this.userId = post.getUser().getId();
this.likeCount = 0L; // 기본값 설정
this.createdAt = post.getCreatedAt();
this.updatedAt = post.getUpdatedAt();
this.deletedAt = post.getDeletedAt();
}
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java
around lines 20 to 28, the constructor that takes a Post object does not
initialize the likeCount field, which can lead to null values and potential
NullPointerExceptions. To fix this, retrieve the like count from the Post object
(e.g., post.getLikeCount()) and assign it to the likeCount field within the
constructor.

Copy link
Owner Author

Choose a reason for hiding this comment

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

이 코드는 사실상 사용을 하지 않음. JPA가 인터페이스 기반 프로젝션을 통해 구현체를 직접 주입해주기 때문에 삭제해도 무방할듯

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@Doncham 맞습니다! 인터페이스 기반 프로젝션 방식이 훨씬 깔끔하네요.

확인해보겠습니다:

인터페이스 기반 프로젝션을 사용하면 JPA가 런타임에 구현체를 생성해주므로 별도의 DTO 클래스가 필요 없죠. 더 간결하고 효율적인 접근 방식입니다.


🏁 Script executed:

#!/bin/bash
# PostWithLikeCount 클래스의 사용처 확인
echo "=== PostWithLikeCount 클래스 사용처 검색 ==="
rg "PostWithLikeCount" --type java -A 2 -B 2

echo -e "\n=== PostWithLikeCountProjection 인터페이스 사용처 검색 ==="
rg "PostWithLikeCountProjection" --type java -A 2 -B 2

Length of output: 22763


@Doncham
확인 결과 PostWithLikeCount 클래스는 실제 로직에서 참조되지 않고, PostService·PostController에서 import만 남아 있으며 구현은 전부 PostWithLikeCountProjection을 사용하고 있습니다. 안전하게 삭제해도 무방하겠습니다. (불필요한 import·주석도 함께 정리 권장) 👍🏻


✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines 63 to 88
const handleLike = async (postId, currentIsLiked, currentLikeCount) => {
try {
const token = localStorage.getItem('jwt');
const response = await axios.post(`/api/posts/likes/${postId}`, null, {
headers: { 'Authorization': `${token}` },
});
const { isLiked } = response.data;

// posts 배열에서 해당 post의 isLiked, likeCount 업데이트
setPosts((prevPosts) =>
prevPosts.map((post) =>
post.id === postId
? {
...post,
isLiked: isLiked,
likeCount: isLiked
? currentLikeCount + 1
: Math.max(0, currentLikeCount - 1),
}
: post
)
);
} catch (err) {
alert('좋아요 처리에 실패했습니다.');
}
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

좋아요 기능 구현이 잘 되었지만 개선사항 제안

기본적인 좋아요 기능은 잘 구현되었습니다. 다음과 같은 개선사항을 제안합니다:

  1. 토큰 존재 여부 검증 필요
  2. 좋아요 처리 중 로딩 상태 표시
  3. 더 구체적인 에러 처리
 const handleLike = async (postId, currentIsLiked, currentLikeCount) => {
+  const token = localStorage.getItem('jwt');
+  if (!token) {
+    alert('로그인이 필요합니다.');
+    return;
+  }
+
+  // 로딩 상태 관리를 위한 상태 추가 필요
   try {
-    const token = localStorage.getItem('jwt');
     const response = await axios.post(`/api/posts/likes/${postId}`, null, {
       headers: { 'Authorization': `${token}` },
     });
     // ... 나머지 코드
   } catch (err) {
-    alert('좋아요 처리에 실패했습니다.');
+    if (err.response?.status === 401) {
+      alert('로그인이 필요합니다.');
+    } else {
+      alert('좋아요 처리에 실패했습니다.');
+    }
   }
 };
📝 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
const handleLike = async (postId, currentIsLiked, currentLikeCount) => {
try {
const token = localStorage.getItem('jwt');
const response = await axios.post(`/api/posts/likes/${postId}`, null, {
headers: { 'Authorization': `${token}` },
});
const { isLiked } = response.data;
// posts 배열에서 해당 post의 isLiked, likeCount 업데이트
setPosts((prevPosts) =>
prevPosts.map((post) =>
post.id === postId
? {
...post,
isLiked: isLiked,
likeCount: isLiked
? currentLikeCount + 1
: Math.max(0, currentLikeCount - 1),
}
: post
)
);
} catch (err) {
alert('좋아요 처리에 실패했습니다.');
}
};
const handleLike = async (postId, currentIsLiked, currentLikeCount) => {
const token = localStorage.getItem('jwt');
if (!token) {
alert('로그인이 필요합니다.');
return;
}
// 로딩 상태 관리를 위한 상태 추가 필요
try {
const response = await axios.post(`/api/posts/likes/${postId}`, null, {
headers: { 'Authorization': `${token}` },
});
const { isLiked } = response.data;
// posts 배열에서 해당 post의 isLiked, likeCount 업데이트
setPosts((prevPosts) =>
prevPosts.map((post) =>
post.id === postId
? {
...post,
isLiked: isLiked,
likeCount: isLiked
? currentLikeCount + 1
: Math.max(0, currentLikeCount - 1),
}
: post
)
);
} catch (err) {
if (err.response?.status === 401) {
alert('로그인이 필요합니다.');
} else {
alert('좋아요 처리에 실패했습니다.');
}
}
};
🤖 Prompt for AI Agents
In frontend/src/pages/posts/PostListPage.jsx around lines 63 to 88, the
handleLike function lacks token existence validation, loading state management,
and detailed error handling. First, check if the token exists before making the
API call and handle the case when it is missing. Second, introduce a loading
state to indicate the like action is in progress and disable repeated clicks.
Third, improve error handling by capturing and displaying specific error
messages instead of a generic alert.

Comment on lines +58 to 65
public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
Long userId = customUserDetails.getUserId();
// null체크

Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

인증 사용자 null 체크 구현 필요

주석에서 언급된 null 체크를 실제로 구현해야 합니다.

 public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
+  if (customUserDetails == null) {
+    throw new IllegalArgumentException("인증된 사용자 정보가 필요합니다.");
+  }
   Long userId = customUserDetails.getUserId();
-  // null체크
+  if (userId == null) {
+    throw new IllegalArgumentException("사용자 ID가 없습니다.");
+  }
📝 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 ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(@AuthenticationPrincipal CustomUserDetails customUserDetails, @PathVariable int page) {
Long userId = customUserDetails.getUserId();
// null체크
Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
public ResponseEntity<PageResponse<PostWithLikeCountProjection>> getPosts(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@PathVariable int page
) {
if (customUserDetails == null) {
throw new IllegalArgumentException("인증된 사용자 정보가 필요합니다.");
}
Long userId = customUserDetails.getUserId();
if (userId == null) {
throw new IllegalArgumentException("사용자 ID가 없습니다.");
}
Page<PostWithLikeCountProjection> posts = postService.getPosts(userId, page);
PageResponse<PostWithLikeCountProjection> response = new PageResponse<>(posts);
return ResponseEntity.ok(response);
}
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
around lines 58 to 65, the code has a comment about null checking the
authenticated user but lacks the actual implementation. Add a null check for the
customUserDetails parameter at the start of the method, and if it is null,
return an appropriate error response such as
ResponseEntity.status(HttpStatus.UNAUTHORIZED).build() to handle unauthenticated
access properly.

Copy link
Owner Author

Choose a reason for hiding this comment

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

이거 따로 공부해야 하긴함

Copy link

Choose a reason for hiding this comment

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

@Doncham 네, 맞습니다! Spring Security의 인증 처리는 별도로 공부하시는 것이 좋겠네요.

주요 학습 포인트들을 정리해드리면:

  1. @AuthenticationPrincipal 어노테이션의 동작 원리
  2. Spring Security의 인증 필터 체인
  3. JWT 토큰 검증과 사용자 인증
  4. 예외 처리와 에러 응답 설계

당장은 주석으로 남겨두시고, 나중에 Spring Security를 제대로 학습하신 후에 적용하시면 될 것 같습니다.

🐰 계속 배우려는 자세가 훌륭하네요! 차근차근 공부하시면서 진행하세요.


✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/controller/PostController.java:58-65
Timestamp: 2025-07-08T15:09:35.094Z
Learning: 사용자 Doncham은 Spring Security와 인증 처리에 대해 별도로 공부하는 것을 선호하며, 당장 구현보다는 학습을 통해 제대로 이해한 후 적용하는 접근 방식을 취한다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.

Comment on lines +26 to +27
User user = userRepository.getReferenceById(userId);
Post post = postRepository.getReferenceById(postId);
Copy link

@coderabbitai coderabbitai bot Jul 8, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

엔티티 존재 여부 검증 필요

getReferenceById는 지연 로딩 프록시를 반환하므로 실제 엔티티가 존재하지 않을 수 있습니다. 엔티티 존재 여부를 검증하는 것이 좋습니다.

-User user = userRepository.getReferenceById(userId);
-Post post = postRepository.getReferenceById(postId);
+User user = userRepository.findById(userId)
+    .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
+Post post = postRepository.findById(postId)
+    .orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
📝 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
User user = userRepository.getReferenceById(userId);
Post post = postRepository.getReferenceById(postId);
User user = userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다."));
Post post = postRepository.findById(postId)
.orElseThrow(() -> new IllegalArgumentException("게시글을 찾을 수 없습니다."));
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
around lines 26 to 27, replace getReferenceById calls with a method that fetches
the entity and verifies its existence, such as findById. Check if the returned
Optional is present; if not, handle the absence appropriately (e.g., throw an
exception) to ensure the user and post entities actually exist before
proceeding.

Copy link
Owner Author

Choose a reason for hiding this comment

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

만약 post나 user가 존재하지 않으면 likeRepository.save() 할 때 외래키 제약 조건에 의해서(너가 추천해줘서 그제야 넣긴했지만) 예외를 터뜨리기 때문에 굳이 매번 좋아요 할 때마다 user와 post를 DB에서 직접 조회할 필요는 없다고 생각함.

Copy link

Choose a reason for hiding this comment

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

@Doncham 맞습니다! 외래키 제약 조건이 있다면 getReferenceById를 사용하는 접근 방식이 더 효율적입니다.

실제로 존재하지 않는 엔티티에 대해 getReferenceById로 프록시를 생성하고, likeRepository.save() 시점에 외래키 제약 조건 위반으로 예외가 발생하는 것이 맞고, 이는 이미 기존의 try-catch 블록에서 LikeFailureException으로 처리되고 있네요.

매번 좋아요 요청마다 사용자와 게시글 존재 여부를 확인하기 위해 추가 DB 조회를 하는 것보다, 데이터베이스 레벨에서 참조 무결성을 보장하는 방식이 성능상 더 유리합니다.

🐰 (nodding thoughtfully)

   (\   /)
  ( ._.)
 o_(")(")

✏️ Learnings added
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/like/service/LikeService.java:26-27
Timestamp: 2025-07-08T15:21:25.357Z
Learning: LikeService에서 getReferenceById를 사용하는 이유: 외래키 제약 조건이 있으면 실제 엔티티 존재 여부를 사전에 확인할 필요가 없고, save() 시점에 제약 조건 위반으로 예외가 발생하므로 프록시 객체를 사용하는 것이 성능상 더 효율적이다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.

Comment on lines 47 to 56
else{
likeRepository.deleteById(like.get().getId());

return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

좋아요 취소 시 예외 처리 추가

좋아요 취소 시에도 예외 처리가 필요합니다.

 else{
-  likeRepository.deleteById(like.get().getId());
+  try {
+    likeRepository.deleteById(like.get().getId());
+  } catch (Exception e) {
+    throw new LikeFailureException("좋아요 취소에 실패했습니다.", e);
+  }
   
   return LikeResponse.builder()
     .userId(userId)
     .postId(postId)
     .isLiked(false)
     .build();
 }
📝 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
else{
likeRepository.deleteById(like.get().getId());
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}
else{
try {
likeRepository.deleteById(like.get().getId());
} catch (Exception e) {
throw new LikeFailureException("좋아요 취소에 실패했습니다.", e);
}
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(false)
.build();
}
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
around lines 47 to 56, the code deletes a like without handling potential
exceptions. Add exception handling around the likeRepository.deleteById call to
catch and properly handle any exceptions that may occur during the deletion
process, ensuring the service remains robust and errors are managed gracefully.

Comment on lines 30 to 46
if(like.isEmpty()){
try{
likeRepository.save(Like.builder()
.user(user)
// 저장하는 시점에 post가 삭제될 수 있으니까 try-catch로 잡음.
.post(post)
.build()
);
return LikeResponse.builder()
.userId(userId)
.postId(postId)
.isLiked(true)
.build();
}catch (Exception e) {
throw new LikeFailureException(e);
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 개선 필요

현재 구현에서는 다음과 같은 문제가 있습니다:

  1. 너무 광범위한 예외 처리
  2. 동시성 문제 (race condition) 가능성
 if(like.isEmpty()){
   try{
-    likeRepository.save(Like.builder()
-      .user(user)
-      .post(post)
-      .build()
-    );
+    Like newLike = Like.builder()
+      .user(user)
+      .post(post)
+      .build();
+    likeRepository.save(newLike);
     return LikeResponse.builder()
       .userId(userId)
       .postId(postId)
       .isLiked(true)
       .build();
-  }catch (Exception e) {
+  } catch (DataIntegrityViolationException e) {
+    // 동시성으로 인한 중복 처리
+    throw new LikeFailureException("이미 좋아요가 처리되었습니다.");
+  } catch (Exception e) {
     throw new LikeFailureException(e);
   }

필요한 import 추가:

+import org.springframework.dao.DataIntegrityViolationException;
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
between lines 30 and 46, the current exception handling is too broad and may
mask specific issues, and there is a potential race condition when saving likes
concurrently. Refine the exception handling by catching more specific exceptions
related to database constraints or entity state, and implement a mechanism to
handle or prevent race conditions, such as checking existence again before
saving or using database constraints with proper exception handling. Also, add
any necessary imports for the specific exceptions you handle.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@Doncham Doncham merged commit b2bf592 into main Jul 8, 2025
1 of 2 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
backend/src/main/java/org/juniortown/backend/like/entity/Like.java (1)

21-21: 이전 리뷰 제안사항 구현 완료

복합 유니크 제약 조건이 올바르게 구현되었습니다. 이제 동일한 사용자가 같은 게시글에 중복으로 좋아요를 할 수 없도록 방지됩니다.

🧹 Nitpick comments (2)
backend/src/main/java/org/juniortown/backend/like/entity/Like.java (2)

12-12: 사용되지 않는 import 제거

@OneToOne 어노테이션이 임포트되었지만 실제로는 사용되지 않습니다. 코드 정리를 위해 제거하는 것이 좋습니다.

-import jakarta.persistence.OneToOne;

37-41: Builder 패턴 구현 적절

Builder 패턴을 사용한 생성자가 올바르게 구현되었습니다. 간단하고 명확한 구조입니다.

필요에 따라 생성자에서 null 검증을 추가하는 것을 고려해볼 수 있습니다:

 @Builder
 public Like(User user, Post post) {
+    if (user == null || post == null) {
+        throw new IllegalArgumentException("User and Post cannot be null");
+    }
     this.user = user;
     this.post = post;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 509a1f5 and bb6f41a.

📒 Files selected for processing (7)
  • backend/src/main/java/org/juniortown/backend/like/controller/LikeController.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/entity/Like.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/exception/LikeFailureException.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/like/service/LikeService.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (2 hunks)
  • backend/src/test/java/org/juniortown/backend/post/entity/PostTest.java (0 hunks)
  • frontend/src/pages/posts/PostListPage.jsx (5 hunks)
💤 Files with no reviewable changes (1)
  • backend/src/test/java/org/juniortown/backend/post/entity/PostTest.java
🚧 Files skipped from review as they are similar to previous changes (5)
  • backend/src/main/java/org/juniortown/backend/like/exception/LikeFailureException.java
  • backend/src/main/java/org/juniortown/backend/like/controller/LikeController.java
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java
  • frontend/src/pages/posts/PostListPage.jsx
  • backend/src/main/java/org/juniortown/backend/like/service/LikeService.java
🧰 Additional context used
🧠 Learnings (1)
backend/src/main/java/org/juniortown/backend/like/entity/Like.java (1)
Learnt from: Doncham
PR: Doncham/JuniorTown#10
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java:20-28
Timestamp: 2025-07-08T14:49:27.329Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
🔇 Additional comments (3)
backend/src/main/java/org/juniortown/backend/like/entity/Like.java (3)

25-27: ID 필드 구현 적절

기본 키 필드가 JPA 표준에 맞게 올바르게 구현되었습니다. IDENTITY 전략을 사용한 자동 생성 방식이 적절합니다.


29-31: User 관계 설정 적절

User와의 다대일 관계가 올바르게 구성되었습니다. nullable = false 설정으로 데이터 무결성이 보장됩니다.


33-35: Post 관계 설정 적절

Post와의 다대일 관계가 올바르게 구성되었습니다. nullable = false 설정으로 데이터 무결성이 보장됩니다.

@coderabbitai coderabbitai bot mentioned this pull request Jul 26, 2025
@coderabbitai coderabbitai bot mentioned this pull request Aug 15, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant