Skip to content

Feat#11 조회수#12

Merged
Doncham merged 13 commits intomainfrom
feat#11-조회수
Jul 27, 2025

Hidden character warning

The head ref may contain hidden characters: "feat#11-\uc870\ud68c\uc218"
Merged

Feat#11 조회수#12
Doncham merged 13 commits intomainfrom
feat#11-조회수

Conversation

@Doncham
Copy link
Owner

@Doncham Doncham commented Jul 26, 2025

1.레디스 연동
2.게시글 상세 조회 시 레디스의 키값 증가, 단시간 내의 중복 조회 막는 로직 구현
3.게시글 조회 시 조회수도 내려주기(DB값 + Redis 증분)
4.주기적으로 Redis 증분값을 DB에 동기화
closed #11

Summary by CodeRabbit

  • 신규 기능

    • 게시글 조회수 집계 기능이 Redis 기반으로 구현되어, 조회수 증가 및 동기화가 효율적으로 처리됩니다.
    • 비회원 방문자도 고유 쿠키를 통해 조회수가 집계됩니다.
    • 게시글 상세 조회 API가 조회수, 좋아요 여부, 좋아요 개수 등 상세 정보를 함께 반환하도록 개선되었습니다.
    • 스케줄러를 통한 조회수 동기화 기능이 추가되어 Redis에 저장된 조회수를 주기적으로 DB에 반영합니다.
    • 비회원 식별을 위한 쿠키 발급 기능이 도입되어 비회원 조회수 집계가 가능해졌습니다.
  • 버그 수정

    • 게시글 목록 및 상세 조회 시 좋아요 개수와 내가 좋아요를 눌렀는지 여부가 정확히 표시됩니다.
  • 테스트

    • Redis 연동 및 조회수 관련 기능에 대한 통합 테스트와 단위 테스트가 추가되었습니다.
    • 조회수 중복 증가 방지, 비회원/회원 조회수 집계, 조회수 동기화 스케줄러 동작 검증 테스트가 포함되었습니다.
  • 환경설정

    • Redis 및 Redisson 클라이언트 의존성이 추가되고, 테스트용 Redis 환경 및 Mock 설정이 도입되었습니다.
    • Redis 연결 설정이 기본 구성에 포함되었습니다.
  • UI 개선

    • 게시글 목록과 상세 페이지에 조회수 컬럼 및 아이콘(👁️)이 추가되어 사용자가 쉽게 조회수를 확인할 수 있습니다.

Doncham added 11 commits July 10, 2025 21:09
RedisConf, application.yml, WebMvcConfig 설정
비회원인 경우 GeustCookieInterceptor를 통해 발급된 쿠키에 있는 UUID를 통해 중복키 생성해서 10분간 조회 카운트 증가 방지
회원의 경우 jwt의 userId를 통해 중복키 생성해서 위 로직 수행
중복키에 10분 TTL 걸고 중복키가 있다면 로깅하고 종료 없다면 redis에 조회수 증분값 1증가
@mock + @Quailfier 조합은 mocking 안됨. 생성자 주입 사용
조회수 = DB 컬럼 + 레디스
테스트 정리(@import(redisTestConfig)  + 조회수 증가(회원/비회원) 테스트 작성, readCountKey 변경, post 관련 테스트 중 레디스가 필요한 테스트 이동
5분마다 레디스에 있는 조회수 키를 DB에 반영해 주는 로직 추가 + 테스트 + Redisson을 통해 여러 인스턴스에서 동시에 로직을 실행하지 않도록 함.
조회수 값에 redis에서 읽어온 캐시값 추가해서 반환
1.페이지 조회 시 조회수 확인
2.페이지 상세 조회 시 조회수 확인
1.DTO를 하나 공유해서 쓰려고 하니까 특정 모듈에 종속적인 기능이 점점 많아져서 분리함.
2.대표적인 테스트에서 반환값의 모든 속성을 검증하여 수정 시 바로 테스트가 깨지도록 함.
@Doncham Doncham self-assigned this Jul 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Jul 26, 2025

"""

Walkthrough

이 변경사항은 게시글 조회수(조회 카운트) 기능을 백엔드와 프론트엔드에 통합적으로 추가합니다. Redis를 활용한 중복 방지 및 실시간 카운팅, 주기적 DB 동기화, 관련 서비스/DTO/테스트 코드, 프론트엔드 조회수 표시 등이 포함됩니다.

Changes

파일/경로 그룹 변경 요약
backend/build.gradle, backend/src/main/resources/application.yml, backend/src/test/resources/application-test.yml Redis, 테스트, Redisson 관련 의존성 및 설정 추가, application.yml에 Redis 호스트/포트 추가, 테스트용 공백 추가
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java
backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java
backend/src/test/java/org/juniortown/backend/config/SyncConfig.java
Redis 설정 클래스 및 테스트용 Mock/설정 추가
backend/src/main/java/org/juniortown/backend/BackendApplication.java @EnableScheduling 어노테이션 추가로 스케줄링 활성화
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java
GuestCookieInterceptor 도입 및 WebMvcConfig에서 인터셉터 등록
backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java "/api/posts/details/**" 경로를 인증 없이 허용
backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java 게시글별 좋아요 개수 카운트 메서드 추가
backend/src/main/java/org/juniortown/backend/like/service/LikeService.java 불필요 공백 제거
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java 상세조회/목록조회 응답에 readCount 포함, ViewCountService 연동, guestId 쿠키 활용
backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java 상세조회 응답 DTO 신설, readCount 필드 및 누적 메서드 추가
backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java readCount, likeCount, isLiked 필드 추가, 빌더/팩토리 메서드 도입
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java getReadCount() 메서드 추가
backend/src/main/java/org/juniortown/backend/post/entity/Post.java readCount 필드 및 누적 메서드 추가
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java 조회 쿼리에 readCount 포함
backend/src/main/java/org/juniortown/backend/post/service/PostService.java Redis readCount 집계, 좋아요/조회수 포함 응답, DTO/리턴타입 변경
backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java Redis 기반 조회수 증가 서비스 신설, 중복방지 로직 구현
backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java Redis→DB 조회수 동기화 서비스 및 스케줄러 신설, 분산락 적용
frontend/src/pages/posts/PostDetailPage.jsx 게시글 상세에 조회수(👁️ {readCount}회) 표시
frontend/src/pages/posts/PostListPage.jsx 게시글 목록에 조회수 컬럼 및 값 표시
backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
RedisTestConfig, SyncConfig import 및 테스트 컨텍스트 등록
backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java RedisTestConfig, SyncConfig import 및 게시글 응답에 대한 상세 검증 추가
backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java RedisTestConfig, SyncConfig import, 상세조회 관련 테스트 제거
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java Redis 기반 조회수 증가/중복방지/동기화/예외 등 통합 인수테스트 신설
backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java Redis/LikeRepository Mock, 리턴타입 변경에 따른 테스트 수정
backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java ViewCountService 단위테스트 신설, Redis 중복키/증가 로직 검증

Sequence Diagram(s)

sequenceDiagram
    participant Guest/User as 게스트/사용자
    participant Web as 프론트엔드
    participant API as PostController
    participant ViewSvc as ViewCountService
    participant Redis as Redis
    participant PostSvc as PostService
    participant DB as DB

    Guest/User->>Web: 게시글 상세 페이지 요청
    Web->>API: GET /api/posts/details/{postId} (쿠키/토큰 포함)
    API->>ViewSvc: readCountUp(userId/guestId, postId)
    ViewSvc->>Redis: 중복키 조회/설정, 조회수 증가
    Redis-->>ViewSvc: 현재 조회수 반환
    API->>PostSvc: getPost(postId)
    PostSvc->>DB: 게시글/좋아요 정보 조회
    PostSvc-->>API: PostDetailResponse (DB readCount)
    API->>API: Redis readCount를 응답에 누적
    API-->>Web: PostDetailResponse (readCount 포함)
    Web-->>Guest/User: 조회수 표시
Loading
sequenceDiagram
    participant Scheduler as 스케줄러
    participant SyncSvc as ViewCountSyncService
    participant Redis as Redis
    participant Redisson as RedissonLock
    participant DB as DB

    Scheduler->>SyncSvc: 5분마다 syncViewCounts() 호출
    SyncSvc->>Redisson: 분산락 획득 시도
    alt 락 획득 성공
        SyncSvc->>Redis: post:viewCount:* 키스캔
        loop 각 postId
            SyncSvc->>Redis: 해당 postId 조회수 가져오기
            SyncSvc->>DB: 게시글 readCount에 Redis값 누적 저장
            SyncSvc->>Redis: 해당 Redis 키 삭제
        end
        SyncSvc->>Redisson: 락 해제
    else 락 획득 실패
        SyncSvc->>SyncSvc: 동기화 스킵
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

  • Feat#9 좋아요기능 #10: 좋아요 관련 기능과 LikeRepository 인터페이스 및 LikeService 변경 사항이 유사하여 연관성이 높음.

Suggested labels

🌠 feat

Poem

🐇
조회수 토끼가 깡총깡총,
Redis에 발자국 남기네!
손님도 회원도 숫자 올려,
십분마다 중복 막고,
다섯 분마다 DB로 점프!
눈동자(👁️)만큼 늘어나는 숫자,
토끼는 오늘도 코드에 춤춘다.

"""

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 0bf5d27 and bd0b669.

📒 Files selected for processing (2)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (8 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat#11-조회수

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 generate unit tests to generate unit tests for 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: 19

🧹 Nitpick comments (15)
backend/src/test/resources/application-test.yml (1)

1-2: 불필요 공백 라인 정리 제안
테스트 설정 파일에 의미 없는 빈 줄이 다수 추가되었습니다. YAML 은 들여쓰기 의존적이므로 불필요한 공백은 줄이는 편이 관리·리뷰 측면에서 좋습니다. 기능에는 영향 없으나 다음 커밋에서 같이 정리해 주세요.

Also applies to: 13-15, 30-30

frontend/src/pages/posts/PostListPage.jsx (1)

147-148: readCount 미존재 시 오류 가능성 검증 필요
백엔드 DTO 에 readCount 가 포함되지 않는 경우 undefined 가 그대로 출력됩니다. 안전하게 post.readCount ?? 0 로 기본값을 주는 편이 좋습니다.

-                  <th>조회수</th>
+                  <th>조회수</th>
...
-                    <td>{post.readCount}</td>
+                    <td>{post.readCount ?? 0}</td>

Also applies to: 176-176

backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)

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

@Modifying 어노테이션을 import했지만 실제로 사용되지 않고 있습니다.

-import org.springframework.data.jpa.repository.Modifying;
backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (1)

223-223: Redis Mock 설정을 개선해주세요.

Redis 관련 Mock 설정이 일부 테스트에만 적용되어 있습니다. 일관성을 위해 모든 관련 테스트 메서드에서 Redis Mock을 설정하는 것을 고려해보세요.

@BeforeEach 메서드에서 공통 Mock 설정을 추가할 수 있습니다:

@BeforeEach
void setUp() {
    when(redisTemplate.opsForValue()).thenReturn(readCountValueOperations);
}
backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java (2)

36-36: 오타 수정 필요

"인스턴스에세" → "인스턴스에서"로 수정해주세요.

-				log.info("동기화 락을 획득하지 못했습니다. 다른 인스턴스에세 이미 실행 중입니다.");
+				log.info("동기화 락을 획득하지 못했습니다. 다른 인스턴스에서 이미 실행 중입니다.");

28-57: 동시성 및 트랜잭션 격리 수준 고려사항

현재 구현에서 다음 사항들을 고려해야 합니다:

  1. Redis 키를 읽고 삭제하는 사이에 새로운 조회수가 추가될 수 있습니다
  2. 여러 게시글을 업데이트하는 동안 트랜잭션이 길어질 수 있습니다

다음과 같은 개선을 고려해보세요:

  • Redis의 GETDEL 명령어 사용을 고려하거나
  • 각 게시글 업데이트를 별도 트랜잭션으로 처리
  • 배치 업데이트를 위한 벌크 연산 사용
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (2)

26-33: RedisTemplate 설정 순서 일관성 필요

keyCheckRedisTemplatereadCountRedisTemplate의 설정 순서가 다릅니다. 일관성을 위해 통일하는 것이 좋습니다.


31-31: Jackson2JsonRedisSerializer 제네릭 타입 경고 해결

Jackson2JsonRedisSerializer에 raw type을 사용하면 컴파일 경고가 발생합니다.

-		redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Boolean.class));
+		Jackson2JsonRedisSerializer<Boolean> serializer = new Jackson2JsonRedisSerializer<>(Boolean.class);
+		serializer.setObjectMapper(new ObjectMapper());
+		redisTemplate.setValueSerializer(serializer);

동일하게 Long 타입에도 적용:

-		redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Long.class));
+		Jackson2JsonRedisSerializer<Long> serializer = new Jackson2JsonRedisSerializer<>(Long.class);
+		serializer.setObjectMapper(new ObjectMapper());
+		redisTemplate.setValueSerializer(serializer);

Also applies to: 40-40

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

22-25: 주석 형식 개선 및 상수 활용

키 형식을 주석으로만 설명하고 있는데, 이를 메소드로 추출하면 더 안전합니다.

-	// 조회수증분키 형태, post:viewCount:{postId}:{userId}
 	public static final String VIEW_COUNT_KEY = "post:viewCount:";
-	// 중복방지키 형태, postDup:key:{postId}:{userId}
 	public static final String DUP_PREVENT_KEY = "postDup:key:";
+	
+	private String buildViewCountKey(String postId) {
+		return VIEW_COUNT_KEY + postId;
+	}
+	
+	private String buildDupPreventKey(String postId, String userId) {
+		return DUP_PREVENT_KEY + postId + ":" + userId;
+	}
backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (2)

3-6: 사용하지 않는 import 제거

다음 import들이 사용되지 않고 있습니다:

  • ViewCountSyncService
  • Test
  • Mockito
  • RedissonClient
  • Configuration

Also applies to: 10-10


30-36: RedisTemplate 설정 순서 통일

RedisConfig와 동일한 순서로 설정하면 일관성이 향상됩니다.

 	public RedisTemplate<String, Boolean> keyCheckRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
 		RedisTemplate<String,Boolean> redisTemplate = new RedisTemplate<>();
+		redisTemplate.setConnectionFactory(redisConnectionFactory);
 		redisTemplate.setKeySerializer(new StringRedisSerializer());
 		redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(Boolean.class));
-		redisTemplate.setConnectionFactory(redisConnectionFactory);
 		return redisTemplate;
 	}

Also applies to: 39-45

backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java (1)

7-7: 사용하지 않는 import 제거

RedisTestConfigImport 어노테이션이 사용되지 않고 있습니다.

Also applies to: 15-15

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

52-54: 가변 메서드 대신 불변 접근 방식 고려

addReadCount 메서드가 객체 상태를 변경합니다. 불변 DTO 패턴을 따르려면 새로운 인스턴스를 반환하거나, 이 로직을 서비스 레이어로 이동하는 것을 고려해보세요.

현재 구조를 유지하려면 최소한 null 체크를 추가하세요:

 public void addReadCount(Long redisReadCount) {
+    if (redisReadCount == null) {
+        return;
+    }
     this.readCount += redisReadCount;
 }
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1)

8-10: 사용하지 않는 import 제거

PostWithLikeCountPostWithLikeCountProjection은 사용되지 않습니다.

-import org.juniortown.backend.post.dto.response.PostWithLikeCount;
 import org.juniortown.backend.post.dto.response.PostResponse;
-import org.juniortown.backend.post.dto.response.PostWithLikeCountProjection;
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)

129-133: 주석 처리된 코드 제거

사용하지 않는 주석 코드는 제거하는 것이 좋습니다.

-// @AfterEach
-// void clean() {
-// 	userRepository.deleteAll();
-// 	postRepository.deleteAll();
-// }

동기화 락 테스트도 필요하다면 수정하여 활성화하고, 그렇지 않다면 제거하세요.

Also applies to: 291-317

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between b2bf592 and 344c616.

📒 Files selected for processing (30)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/org/juniortown/backend/BackendApplication.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.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 (3 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/entity/Post.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (3 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java (1 hunks)
  • backend/src/main/resources/application.yml (2 hunks)
  • backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/config/SyncConfig.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java (3 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (4 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (3 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (3 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (7 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java (1 hunks)
  • backend/src/test/resources/application-test.yml (3 hunks)
  • frontend/src/pages/posts/PostDetailPage.jsx (1 hunks)
  • frontend/src/pages/posts/PostListPage.jsx (2 hunks)
🧰 Additional context used
🧠 Learnings (15)
📓 Common learnings
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.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1)

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

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

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

backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (1)

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

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

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

backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)

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

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

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

backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (1)

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

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

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

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

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

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

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

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

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

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

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

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

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

backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)

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

🧬 Code Graph Analysis (2)
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (1)
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1)
  • Configuration (11-26)
backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (2)
backend/src/test/java/org/juniortown/backend/config/SyncConfig.java (1)
  • TestConfiguration (9-17)
backend/src/main/java/org/juniortown/backend/config/RedisConfig.java (1)
  • Configuration (13-43)
🪛 ast-grep (0.38.6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java

[warning] 31-31: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)


[warning] 31-31: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute

(cookie-missing-secure-flag-java)


[warning] 31-31: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly

(cookie-missing-httponly-java)

🔇 Additional comments (19)
backend/src/main/java/org/juniortown/backend/BackendApplication.java (1)

7-12: 스케줄링 활성화 추가 👍
@EnableScheduling 추가로 스케줄 태스크가 구동됩니다. 문제 없습니다. 단, @Scheduled 메서드 예외가 전파되면 다음 실행이 중단될 수 있으므로 서비스 단에서 예외 처리(try-catch & 로그) 확실히 해 주세요.

backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (1)

12-12: Spring Data JPA 파생 쿼리 메서드가 올바르게 구현되었습니다.

메서드명과 시그니처가 Spring Data JPA 규칙에 부합하며, 특정 게시글의 좋아요 개수를 조회하는 기능이 명확하게 표현되어 있습니다.

frontend/src/pages/posts/PostDetailPage.jsx (1)

114-116: 백엔드에서 readCount 필드 정상 확인됨

  • PostDetailResponse / PostResponse DTO에 readCount가 선언되어 있으며, 엔티티 초기값이 0L로 설정되어 null 반환이 불가능합니다.
  • ViewCountService#readCountUp도 null 대신 0L을 반환하므로, 프론트엔드에서는 항상 숫자 값을 안전하게 받아옵니다.

UI에서는

<small className="text-muted ms-2">
  👁️ {post.readCount?.toLocaleString()}</small>

로 optional chaining 및 toLocaleString()을 적용해, 카운트가 “0”일 때도 👁️ 0회로 표시됩니다.

필요에 따라 조회수가 0일 경우 UI에서 숨기고 싶다면, 아래처럼 조건부 렌더링을 추가하세요:

- {post.readCount?.toLocaleString() && (
-   <small className="text-muted ms-2">
-     👁️ {post.readCount.toLocaleString()}회
-   </small>
- )}
+ {post.readCount > 0 && (
+   <small className="text-muted ms-2">
+     👁️ {post.readCount.toLocaleString()}회
+   </small>
+ )}
backend/src/main/java/org/juniortown/backend/like/service/LikeService.java (1)

37-37: 코드 정리가 잘 되었습니다.

Trailing whitespace 제거로 코드 품질이 개선되었습니다.

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

13-13: PostWithLikeCountProjection에 readCount 프로젝션 검증 필요

아래 두 가지를 확인해 주세요.

  • PostRepository의 @query 문에 p.readCount AS readCount가 SELECT 절에 포함되어 있는지
  • Post 엔티티 클래스에 readCount 필드가 정의되어 있고, 실제 DB 테이블 컬럼과 매핑되어 있는지
backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (1)

79-79: 게시글 상세 엔드포인트 공개 허용 검토 결과

  • ViewCountService에서 Redis 기반 중복 방지 로직(DUP_PREVENT_KEY, 10분 TTL) 및 증감 로직(VIEW_COUNT_KEY increment)이 정상 구현되어 있습니다.
  • ViewCountSyncService를 통한 DB 동기화 로직도 스케줄러 및 락 방식으로 올바르게 작동합니다.
  • Post 엔티티에는 ID, 제목, 본문(content), 작성자(user), 조회수(readCount) 등 외부 공개에 문제가 될 민감 필드가 없습니다.

따라서 /api/posts/details/**에 대한 permitAll() 설정은 합리적이며, 추가 조치 없이 승인합니다.

backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (1)

15-16: 테스트 설정 개선이 적절함

Post 관련 테스트에 Redis 설정을 추가한 것은 조회수 기능 구현에 적합합니다. 또한 게시글 상세 조회 테스트를 별도의 PostRedisReadControllerTest로 분리한 것도 관심사 분리 측면에서 좋은 접근입니다.

Also applies to: 41-41, 59-59

backend/src/test/java/org/juniortown/backend/config/SyncConfig.java (1)

1-17: 테스트용 Redis 설정이 적절하게 구현됨

테스트 환경에서 실제 Redis 연결 없이 Redisson 기능을 테스트할 수 있도록 Mock 객체를 제공하는 설정이 잘 구현되었습니다. @TestConfiguration@Profile("test") 사용으로 테스트 환경에서만 활성화되도록 한 것도 적절합니다.

backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (1)

18-18: 조회수 필드가 적절하게 추가됨

findAllWithLikeCount 쿼리에 readCount 필드를 SELECT절과 GROUP BY절에 일관성 있게 추가한 것이 좋습니다. 조회수 기능 구현에 필요한 변경사항이 올바르게 적용되었습니다.

Also applies to: 27-27

backend/src/main/java/org/juniortown/backend/post/entity/Post.java (1)

43-44: 조회수 필드 추가가 올바르게 구현되었습니다.

readCount 필드가 적절한 JPA 어노테이션과 함께 추가되었으며, 기본값 0L 설정이 적합합니다.

backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1)

16-35: 게스트 사용자 추적 로직이 올바르게 구현되었습니다.

UUID를 사용한 게스트 ID 생성과 쿠키 존재 여부 확인 로직이 적절합니다.

backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1)

14-19: 인터셉터 등록이 올바르게 구현되었습니다.

GuestCookieInterceptor가 모든 경로에 대해 적절히 등록되어 게스트 사용자 추적이 가능합니다.

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

12-13: Redis 테스트 설정이 적절히 추가되었습니다.

RedisTestConfigSyncConfig import가 Redis 기반 조회수 기능 테스트를 위해 올바르게 추가되었습니다.

Also applies to: 35-35, 49-49


143-151: 테스트 검증 로직이 크게 향상되었습니다.

게시글 상세 정보에 대한 포괄적인 검증이 추가되어 readCount, likeCount, isLiked 등의 새로운 필드들이 올바르게 반환되는지 확인할 수 있습니다.

backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (2)

10-10: Redis 통합을 위한 Mock 객체들이 적절히 추가되었습니다.

LikeRepository, RedisTemplate, ValueOperations Mock이 새로운 Redis 기반 조회수 기능 테스트를 위해 올바르게 추가되었습니다.

Also applies to: 34-35, 49-49, 58-60


225-225: 서비스 레이어 변경사항에 맞춰 반환 타입이 올바르게 업데이트되었습니다.

기존 학습 내용에 따르면 이 프로젝트는 JPA 인터페이스 기반 프로젝션을 사용하므로, PostResponsePostDetailResponse로의 변경이 서비스 레이어의 실제 구현과 일치합니다.

Also applies to: 238-238, 255-255, 286-286

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

102-102: Redis null 처리 잘됨

Redis에서 가져온 값이 null일 경우를 적절히 처리하고 있습니다.

backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (2)

77-79: 비표준 Redis 포트 사용 이유 확인

Redis를 6380 포트로 실행하도록 설정되어 있습니다. 특별한 이유가 있나요?

표준 포트(6379)를 사용하는 것이 일반적입니다:

-    .withCommand("redis-server --port 6380")
-    .withExposedPorts(6380);
+    .withExposedPorts(6379);

273-289: 조회수 동기화 테스트 잘 작성됨

Redis에서 DB로의 동기화 로직을 잘 테스트하고 있습니다. 동기화 후 Redis 키가 삭제되는지 확인하는 것도 좋습니다.

Comment on lines 51 to 62

implementation 'org.springframework.boot:spring-boot-starter-data-redis'


testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'com.redis:testcontainers-redis:2.2.2'

implementation 'org.redisson:redisson-spring-boot-starter:3.27.0'

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중복·불필요 의존성 정리 필요

  1. spring-boot-starter-test 가 이미 JUnit Jupiter를 포함하므로 testImplementation 'org.junit.jupiter:junit-jupiter' 는 중복입니다.
  2. redisson-spring-boot-starter 내부에 spring-boot-starter-data-redis 가 포함되어 있어 두 스타터를 동시 선언하면 RedisConnectionFactory 빈 충돌이 발생할 수 있습니다(실행 시 BeanDefinitionOverrideException 가능). 하나만 유지하거나 redisson-spring-boot-starter 에서 exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-redis' 로 중복을 제거해 주세요.
  3. 필요하지 않은 테스트컨테이너 모듈이 포함됐는지 확인 바랍니다.
-    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
-    ...
-    testImplementation 'org.junit.jupiter:junit-jupiter'
+    // Redis
+    implementation 'org.redisson:redisson-spring-boot-starter:3.27.0' // 포함된 lettuce 혹은 jedis 중복 여부 확인
+    // 테스트
+    // spring-boot-starter-test 내부에 Jupiter 포함 → 중복 제거

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/build.gradle around lines 51 to 62, remove the redundant
'org.junit.jupiter:junit-jupiter' dependency since 'spring-boot-starter-test'
already includes it. Also, avoid declaring both 'spring-boot-starter-data-redis'
and 'redisson-spring-boot-starter' together to prevent RedisConnectionFactory
bean conflicts; either keep only one or exclude 'spring-boot-starter-data-redis'
from 'redisson-spring-boot-starter' using the exclude directive. Finally, review
and remove any unnecessary testcontainers dependencies that are not used in the
project.

Comment on lines 29 to 32
Cookie cookie = new Cookie(COOKIE_NAME, guestId);
cookie.setPath("/");
cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year
response.addCookie(cookie);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

쿠키 보안 설정을 강화해주세요.

정적 분석 도구에서 지적한 바와 같이 쿠키 보안 설정이 부족합니다.

다음과 같이 수정하여 보안을 강화하세요:

 Cookie cookie = new Cookie(COOKIE_NAME, guestId);
 cookie.setPath("/");
 cookie.setMaxAge(60 * 60 * 24 * 365); // 1 year
+cookie.setHttpOnly(true);
+cookie.setSecure(true); // HTTPS 환경에서만
+cookie.setSameSite(Cookie.SameSite.STRICT.attributeValue());
 response.addCookie(cookie);

참고: HTTPS 환경이 아닌 경우 setSecure(false)로 설정하되, 프로덕션에서는 반드시 HTTPS를 사용하세요.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 31-31: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)


[warning] 31-31: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute

(cookie-missing-secure-flag-java)


[warning] 31-31: A cookie was detected without setting the 'HttpOnly' flag. The 'HttpOnly' flag for cookies instructs the browser to forbid client-side scripts from reading the cookie. Set the 'HttpOnly' flag by calling 'cookie.setHttpOnly(true);
Context: response.addCookie(cookie);
Note: [CWE-1004] Sensitive Cookie Without 'HttpOnly' Flag. [REFERENCES]
- https://owasp.org/www-community/HttpOnly

(cookie-missing-httponly-java)

🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java
around lines 29 to 32, the cookie security settings are insufficient. Enhance
security by setting the cookie's Secure flag to true when in a production HTTPS
environment, and set HttpOnly to true to prevent client-side script access.
Also, consider setting the SameSite attribute to Lax or Strict to mitigate CSRF
risks. If HTTPS is not used, explicitly set Secure to false, but ensure
production uses HTTPS.

Comment on lines 78 to 91
Long redisReadCount = 0L;
if(customUserDetails == null) {
// 비회원인 경우
// 쿠키값을 레디스에 등록
redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId));
} else {
// 회원인 경우
// userId,postId를 조합해서 레디스에 등록
Long userId = customUserDetails.getUserId();
redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId));
}

PostDetailResponse postDetailResponse = postService.getPost(postId);
postDetailResponse.addReadCount(redisReadCount);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

컨트롤러의 비즈니스 로직을 서비스 레이어로 이동

조회수 증가 로직이 컨트롤러에 있습니다. 이는 서비스 레이어의 책임입니다. 또한 응답 DTO를 생성 후 수정하는 것은 좋지 않은 패턴입니다.

서비스 메서드가 사용자 정보를 받아 완전한 응답을 반환하도록 수정:

-Long redisReadCount = 0L;
-if(customUserDetails == null) {
-    // 비회원인 경우
-    // 쿠키값을 레디스에 등록
-    redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId));
-} else {
-    // 회원인 경우
-    // userId,postId를 조합해서 레디스에 등록
-    Long userId = customUserDetails.getUserId();
-    redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId));
-}
-
-PostDetailResponse postDetailResponse = postService.getPost(postId);
-postDetailResponse.addReadCount(redisReadCount);
+String viewerId = customUserDetails != null 
+    ? String.valueOf(customUserDetails.getUserId()) 
+    : guestId;
+Long currentUserId = customUserDetails != null ? customUserDetails.getUserId() : null;
+
+PostDetailResponse postDetailResponse = postService.getPostDetail(postId, currentUserId, viewerId);
📝 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
Long redisReadCount = 0L;
if(customUserDetails == null) {
// 비회원인 경우
// 쿠키값을 레디스에 등록
redisReadCount = viewCountService.readCountUp(guestId, String.valueOf(postId));
} else {
// 회원인 경우
// userId,postId를 조합해서 레디스에 등록
Long userId = customUserDetails.getUserId();
redisReadCount = viewCountService.readCountUp(String.valueOf(userId), String.valueOf(postId));
}
PostDetailResponse postDetailResponse = postService.getPost(postId);
postDetailResponse.addReadCount(redisReadCount);
// 컨트롤러에서는 비즈니스 로직을 호출만 합니다.
// viewCount 증가와 DTO 조립은 서비스 레이어로 위임
String viewerId = customUserDetails != null
? String.valueOf(customUserDetails.getUserId())
: guestId;
Long currentUserId = customUserDetails != null ? customUserDetails.getUserId() : null;
PostDetailResponse postDetailResponse =
postService.getPostDetail(postId, currentUserId, viewerId);
🤖 Prompt for AI Agents
In
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
around lines 78 to 91, move the read count increment logic from the controller
to the service layer. Refactor the service method to accept user information and
postId, handle the read count update internally, and return a fully constructed
PostDetailResponse with the updated read count included. This eliminates
business logic from the controller and avoids modifying the response DTO after
creation.

Comment on lines +8 to +9
import org.juniortown.backend.config.RedisTestConfig;
import org.juniortown.backend.config.SyncConfig;
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Like 기능에서 Redis 설정 필요성 확인

좋아요 기능 테스트에 Redis 관련 설정을 추가하셨는데, LikeController나 LikeService에서 실제로 Redis 기능을 사용하는지 확인이 필요합니다.

Also applies to: 29-29, 40-40


🏁 Script executed:

#!/bin/bash
# Like 관련 컨트롤러와 서비스에서 Redis 사용 여부 확인
rg -A 5 -B 5 "Redis|ViewCount" backend/src/main/java/org/juniortown/backend/like/

# LikeController에서 Redis 관련 의존성 주입 확인
ast-grep --pattern 'class LikeController {
  $$$
}'

Length of output: 137


추가로 Redis 설정의 실제 사용 여부를 확인하기 위해 아래 스크립트를 실행해 주세요.


🏁 Script executed:

#!/bin/bash
# 1) Like 패키지 내 Redis 언급 검색
echo "▶ Like 모듈 내 Redis 관련 코드 검색"
rg -i redis backend/src/main/java/org/juniortown/backend/like

# 2) RedisTestConfig 클래스 내용 확인
echo "▶ RedisTestConfig 클래스 내용"
rg -A5 -B5 'class RedisTestConfig' backend/src/main/java/org/juniortown/backend/config

# 3) SyncConfig 클래스 내용 확인
echo "▶ SyncConfig 클래스 내용"
rg -A5 -B5 'class SyncConfig' backend/src/main/java/org/juniortown/backend/config

Length of output: 407


RedisTestConfig와 SyncConfig의 실제 경로 및 내용을 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 1) RedisTestConfig, SyncConfig 클래스 파일 경로 검색
echo "▶ Config 클래스 경로 찾기"
rg -l "class RedisTestConfig"
rg -l "class SyncConfig"

# 2) 찾은 파일 내용 상위 20줄 확인
echo "▶ RedisTestConfig 내용"
for f in $(rg -l "class RedisTestConfig"); do
  echo "----- $f -----"
  sed -n '1,20p' "$f"
done

echo "▶ SyncConfig 내용"
for f in $(rg -l "class SyncConfig"); do
  echo "----- $f -----"
  sed -n '1,20p' "$f"
done

# 3) LikeControllerTest 상단 imports 및 설정 확인
echo "▶ LikeControllerTest imports"
sed -n '1,30p' backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java

Length of output: 4013


LikeControllerTest에서 실제로 @import@activeprofiles로 해당 설정을 불러오는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
echo "▶ LikeControllerTest 어노테이션 및 설정 확인 (1~80줄)"
sed -n '1,80p' backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java

Length of output: 2974


LikeControllerTest에서 불필요한 RedisTestConfig·SyncConfig 제거

LikeController/LikeService 패키지 내에 Redis를 참조하는 코드는 없으므로, 테스트에 Redis 설정을 임포트할 필요가 없습니다. 아래 위치에서 관련 import 및 @Import 어노테이션을 제거해 주세요.

  • 파일: backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
    • import 문(8–9행)
    @Import({RedisTestConfig.class, SyncConfig.class}) 어노테이션
- import org.juniortown.backend.config.RedisTestConfig;
- import org.juniortown.backend.config.SyncConfig;
...
 @SpringBootTest
 @AutoConfigureMockMvc
 @ActiveProfiles("test")
- @Import({RedisTestConfig.class, SyncConfig.class})
 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
 @Transactional
 class LikeControllerTest {
📝 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
import org.juniortown.backend.config.RedisTestConfig;
import org.juniortown.backend.config.SyncConfig;
// … other import statements …
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@Transactional
class LikeControllerTest {
// …
}
🤖 Prompt for AI Agents
In
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java
around lines 8 to 9, remove the unnecessary imports of RedisTestConfig and
SyncConfig since LikeController and LikeService do not reference Redis. Also,
locate and delete the @Import annotation that includes RedisTestConfig.class and
SyncConfig.class to clean up the test configuration.

1.Controller에서 처리하던 조회수 증가 로직을 서비스단으로 옮김.
2.PostDetailResponse에서 readCount 증가 로직 삭제
3.PostService에서 getPost의 자기가 좋아요 했는지 여부를 확인하기 위한 코드에 오류가 있어서 수정
4.ViewCountService에서 메서드를 싹 다 분리
5.PostService에서 viewerId가 Long인지 UUID인지에 따라서 다르게 동작
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: 1

♻️ Duplicate comments (2)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1)

27-34: 쿠키 보안 설정을 강화해주세요.

정적 분석 도구에서 지적한 바와 같이 쿠키 보안 설정이 부족합니다. Secure 플래그와 SameSite 속성을 추가하여 보안을 강화하세요.

다음과 같이 수정을 권장합니다:

 if(!hasGuestId) {
     String guestId = UUID.randomUUID().toString();
     Cookie cookie = new Cookie(COOKIE_NAME, guestId);
     cookie.setPath("/");
     cookie.setMaxAge(60 * 60 * 24 * 15); // 15 days
     cookie.setHttpOnly(true);
+    // 프로덕션 환경에서는 반드시 true로 설정
+    cookie.setSecure(true); // HTTPS 환경에서만
+    // Spring Boot 3.x 이상에서는 ResponseCookie 사용 권장
     response.addCookie(cookie);
 }

참고: 개발 환경에서 HTTPS를 사용하지 않는 경우 프로파일 기반으로 설정을 분리하는 것을 고려하세요.

backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)

157-157: 중복된 테스트 이름을 수정하세요.

두 번째 테스트의 DisplayName이 "회원"으로 되어 있지만, 실제로는 비회원 테스트입니다.

-@DisplayName("게시글 조회수 증가 성공(회원) - 중복키 존재 x")
+@DisplayName("게시글 조회수 증가 성공(비회원) - 중복키 존재 x")
🧹 Nitpick comments (2)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)

139-146: 메서드명을 더 명확하게 변경하세요.

isLong 메서드명은 의도를 명확히 전달하지 못합니다.

-private boolean isLong(String str) {
+private boolean isMemberId(String viewerId) {
     try {
-        Long.parseLong(str);
+        Long.parseLong(viewerId);
         return true;
     } catch (NumberFormatException e) {
         return false;
     }
 }

또는 더 명시적으로:

private boolean isAuthenticatedUser(String viewerId) {
    // 회원 ID는 숫자, 게스트 ID는 UUID 형식
    return viewerId != null && viewerId.matches("\\d+");
}
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)

290-315: 주석 처리된 테스트 코드를 정리하세요.

주석 처리된 동기화 락 실패 테스트는 다음 중 하나를 선택하세요:

  1. 테스트가 필요하다면 제대로 구현
  2. 불필요하다면 완전히 제거

현재 상태로는 코드베이스를 어지럽히기만 합니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 344c616 and 0bf5d27.

📒 Files selected for processing (13)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (3 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java (2 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (8 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java
🚧 Files skipped from review as they are similar to previous changes (9)
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountSyncService.java
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostDetailResponse.java
  • backend/build.gradle
  • backend/src/main/java/org/juniortown/backend/post/controller/PostController.java
  • backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java
  • backend/src/test/java/org/juniortown/backend/config/RedisTestConfig.java
  • backend/src/main/java/org/juniortown/backend/post/service/ViewCountService.java
  • backend/src/test/java/org/juniortown/backend/post/service/ViewCountServiceTest.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: Doncham
PR: Doncham/JuniorTown#12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.
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.380Z
Learning: 이 프로젝트에서는 JPA 인터페이스 기반 프로젝션(PostWithLikeCountProjection)을 사용하여 데이터를 조회하므로, 클래스 기반 DTO(PostWithLikeCount)는 실제로 사용되지 않는다.
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (4)

Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.

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

Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:40-52
Timestamp: 2025-07-26T02:35:05.655Z
Learning: PostResponse.from() 메서드에 기본값을 설정하면 update 작업에서도 해당 메서드를 사용하기 때문에 likeCount가 0으로 초기화되어 문제가 발생한다. 대신 update 작업을 위한 별도의 응답 클래스를 만드는 것이 권장된다.

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

backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (3)

Learnt from: Doncham
PR: #12
File: backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java:7-8
Timestamp: 2025-07-26T12:44:23.551Z
Learning: Spring Boot 통합 테스트(@SpringBootTest)에서는 전체 애플리케이션 컨텍스트가 로드되므로, 특정 컨트롤러가 직접 사용하지 않는 의존성이라도 해당 의존성의 빈 생성 과정에서 외부 서비스(Redis, DB 등) 연결을 시도할 수 있다. 이런 경우 테스트용 mock 설정이 필요하다.

Learnt from: Doncham
PR: #12
File: backend/src/main/java/org/juniortown/backend/post/dto/response/PostResponse.java:18-20
Timestamp: 2025-07-26T02:32:29.833Z
Learning: PostResponse와 PostDetailResponse 클래스에서 readCount 필드는 Redis에서 실시간 조회수를 가져와 addReadCount() 메서드로 더해주는 로직 때문에 가변 필드로 유지되어야 한다. 단순한 불변성 일관성보다는 실제 비즈니스 로직의 필요성을 우선 고려해야 한다.

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

🧬 Code Graph Analysis (1)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (4)
backend/src/main/java/org/juniortown/backend/post/exception/PostDeletePermissionDeniedException.java (1)
  • PostDeletePermissionDeniedException (6-18)
backend/src/main/java/org/juniortown/backend/post/exception/PostNotFoundException.java (1)
  • PostNotFoundException (5-15)
backend/src/main/java/org/juniortown/backend/post/exception/PostUpdatePermissionDeniedException.java (1)
  • PostUpdatePermissionDeniedException (6-17)
backend/src/main/java/org/juniortown/backend/user/exception/UserNotFoundException.java (1)
  • UserNotFoundException (6-15)
🪛 ast-grep (0.38.6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java

[warning] 32-32: A cookie was detected without setting the 'secure' flag. The 'secure' flag for cookies prevents the client from transmitting the cookie over insecure channels such as HTTP. Set the 'secure' flag by calling '.setSecure(true);'.
Context: response.addCookie(cookie);
Note: [CWE-614] Sensitive Cookie in HTTPS Session Without 'Secure' Attribute. [REFERENCES]
- https://owasp.org/www-community/controls/SecureCookieAttribute

(cookie-missing-secure-flag-java)


[warning] 32-32: The application does not appear to verify inbound requests which can lead to a Cross-site request forgery (CSRF) vulnerability. If the application uses cookie-based authentication, an attacker can trick users into sending authenticated HTTP requests without their knowledge from any arbitrary domain they visit. To prevent this vulnerability start by identifying if the framework or library leveraged has built-in features or offers plugins for CSRF protection. CSRF tokens should be unique and securely random. The Synchronizer Token or Double Submit Cookie patterns with defense-in-depth mechanisms such as the sameSite cookie flag can help prevent CSRF. For more information, see: [Cross-site request forgery prevention](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Req\ uest_Forgery_Prevention_Cheat_Sheet.html).
Context: response.addCookie(cookie);
Note: [CWE-352] Cross-Site Request Forgery (CSRF). [REFERENCES]
- https://stackoverflow.com/questions/42717210/samesite-cookie-in-java-application

(cookie-missing-samesite-java)

🔇 Additional comments (4)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3)

46-54: 팩토리 메서드 패턴 사용이 일관성 있게 적용되었습니다.

PostResponse.from() 정적 팩토리 메서드를 사용하여 DTO 생성 방식이 통일되었습니다.


72-84: update 메서드도 동일한 팩토리 메서드 패턴을 사용합니다.


110-137: 좋아요 확인 로직이 올바르게 수정되었습니다.

이전 리뷰에서 지적된 문제가 해결되어, 현재 조회하는 사용자의 ID(viewerId)로 좋아요 여부를 확인하도록 수정되었습니다.

backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)

78-88: 테스트 컨테이너 설정이 잘 구성되었습니다.

Redis Testcontainers를 사용하여 테스트 격리가 잘 이루어지고 있습니다. 커스텀 포트(6380) 사용은 기본 Redis 포트와의 충돌을 방지합니다.

mGet을 통해 레디스에게 10번 통신이 아닌 1번의 통신으로 10개의 게시글에 조회수 캐시를 받아온다.
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 ea3a65c into main Jul 27, 2025
2 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Aug 15, 2025
@coderabbitai coderabbitai bot mentioned this pull request Sep 8, 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