Skip to content

Feat#15 prometheus,k6#16

Merged
Doncham merged 8 commits intomainfrom
feat#15-prometheus,k6
Aug 16, 2025
Merged

Feat#15 prometheus,k6#16
Doncham merged 8 commits intomainfrom
feat#15-prometheus,k6

Conversation

@Doncham
Copy link
Owner

@Doncham Doncham commented Aug 15, 2025

1.프로메테우스,그라파나,k6 적용
근데 k6로 부하테스트 하려다보니 부하 생성기와 서버를 분리할 필요를 느껴서 프로젝트 완성도를 좀 더 높혀서 aws에 배포 후 부하테스트 및 쿼리 튜닝을 진행하기로 함.
2.비회원을 위한 게시글 목록 조회 구현
3.게시글 목록에서 좋아요 클릭 기능 삭제
4.게시글 상세 조회에서 좋아요 클릭 로직 적용.
5.프론트에서 JWT 관리를 위해 useContext 학습 후 적용

Summary by CodeRabbit

  • New Features
    • 시스템 모니터링 활성화: 헬스/메트릭 엔드포인트 공개 및 로컬 Prometheus·Grafana·k6 실행 지원
    • 전역 인증 컨텍스트 도입: 로그인 상태 관리, 헤더에서 사용자 인사·로그아웃 제공
    • 게시글 상세에 좋아요 버튼 추가(낙관적 업데이트 및 중복 요청 방지)
    • 비로그인 사용자의 게시글 목록 조회 지원 개선
    • 로그인 토큰에 사용자 정보 확장(클라이언트 표시 개선)
  • Bug Fixes
    • 인증 정보 부재 시 발생하던 조회 오류(NPE 등) 예방
  • Chores
    • 부하 테스트 스크립트 및 모니터링 구성 추가 (Docker Compose 포함)

Doncham added 7 commits August 8, 2025 19:42
1.레디스 캐시 조회를 map을 이용하여 리팩토링 하기 쉽게 변경 2. 레디스 장애 시에도 게시글 조회가 잘 동작하도록 구현(캐시 적용x,db값만 적용) 3.테스트 리팩토링
jwt에 username 추가해서 로그인 시 username 환영 메세지 보여주기
useContext를 통해 jwt를 전역적으로 관리
게시글 목록에서 좋아요 기능 삭제, 게시글 상세 조회 페이지에서 좋아요 누르는게 맞지
좋아요 기능 추가, UI 수정
@Doncham Doncham self-assigned this Aug 15, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 15, 2025

Walkthrough

Actuator와 Prometheus 의존성·설정 및 Prometheus/Grafana/k6 docker-compose와 k6 스크립트를 추가했습니다. 보안 설정에 /actuator/** 및 숫자형 포스트 경로를 허용하고, JWT에 username/email 클레임을 확장했습니다. 포스트 조회는 사용자/비사용자 분기와 Redis 다중조회 Map 병합으로 재구성했고, 프런트엔드는 AuthContext로 JWT 관리를 중앙화했습니다.

Changes

Cohort / File(s) Summary
Observability & Load Testing
backend/build.gradle, backend/src/main/resources/application.yml, backend/docker-compose.yml, backend/prometheus.yml, backend/scripts/myscript.js
Actuator 및 Micrometer Prometheus 의존성 추가. management.endpoint에 health/prometheus 노출 설정. Prometheus/Grafana/k6용 docker-compose 추가. Prometheus 스크랩 대상 /actuator/prometheus로 설정. k6 부하 스크립트 도입.
Security & JWT
backend/src/main/java/.../config/SecurityConfig.java, .../user/jwt/JWTUtil.java, .../user/jwt/LoginFilter.java, backend/src/main/java/.../user/jwt/JWTFilter.java
SecurityConfig에 /actuator/health 및 /actuator/prometheus 및 숫자형 /api/posts/\d+ 허용 추가. JWTUtil에 username/email claim 상수·엑세서 추가 및 createJwt 시그니처에 username(또는 nickname) 추가. LoginFilter/JWTFilter에서 토큰 생성/복원에 email/username 포함.
Guest Cookie
backend/src/main/java/.../interceptor/GuestCookieInterceptor.java
게스트 쿠키 생성 시 동일 키로 request attribute에 guestId 저장하여 downstream에서 참조 가능하도록 추가.
Posts — Repo / Service / Controller
backend/src/main/java/.../post/repository/PostRepository.java, .../post/service/PostService.java, .../post/controller/PostController.java
Repository: 사용자용 메서드명 변경(findAllWithLikeCount → findAllWithLikeCountForUser) 및 비사용자 전용 findAllWithLikeCountForNonUser 추가. Service: userId null 분기, 정렬을 createdAt+id로 변경, Redis에서 다중 조회해 Map으로 병합하고 DataAccessException 처리로 견고성 강화. Controller: 인증정보 null-safe 처리로 NPE 방지.
CustomUserDetails
backend/src/main/java/.../user/dto/CustomUserDetails.java
getUsername()가 user.getName()을 반환하도록 변경하고 getUserEmail() 추가.
Frontend — AuthContext 도입
frontend/src/App.jsx, frontend/src/auth/AuthContext.jsx
AuthContext 구현 및 Provider로 앱 래핑. 로컬저장·Axios 기본 헤더 연동, 토큰 파싱으로 user 제공, login/logout API 추가.
Frontend — Pages & Header
frontend/src/pages/components/Layout/Header.js, frontend/src/pages/posts/*.jsx, frontend/src/pages/users/LoginPage.jsx
각 페이지에서 로컬스토리지 대신 AuthContext로 토큰/유저 취득으로 마이그레이션. 댓글/작성/수정/삭제에서 Authorization 헤더 제거(로그인 게이트는 UI에서 처리). PostDetail에 낙관적 좋아요 처리 추가. Header는 인증 상태에 따라 메뉴/로그아웃 처리. LoginPage는 context.login 사용.
Tests
backend/src/test/java/.../comment/service/CommentServiceTest.java, .../controller/PostControllerPagingTest.java, .../post/service/PostServiceTest.java, .../controller/AuthControllerTest.java, .../controller/PostControllerTest.java
JWT 생성 시그니처 변경 반영(추가 nickname 인자), 테스트 기대 예외 및 리포지토리 메서드명 변경 반영, 비로그인 페이징 테스트 추가, 일부 정렬/메타 검증 보강, JWT 관련 검증에서 username→email 사용으로 조정.

Sequence Diagram(s)

sequenceDiagram
  participant U as 사용자 (브라우저)
  participant FE as 프론트엔드 (AuthContext)
  participant BE as 백엔드 (AuthController / LoginFilter / JWTUtil)
  U->>FE: 자격증명 제출
  FE->>BE: POST /api/auth/login
  BE->>BE: 인증 후 JWT 생성(id,email,username,role)
  BE-->>FE: 200 + JWT
  FE->>FE: 로컬저장·Axios 헤더 설정, user 상태 갱신
Loading
sequenceDiagram
  participant C as 클라이언트
  participant B as 백엔드 (PostController/PostService)
  participant R as PostRepository
  participant RD as Redis
  C->>B: GET /api/posts/{page} (+ Authorization? )
  alt 인증됨
    B->>R: findAllWithLikeCountForUser(userId, pageable)
  else 비인증
    B->>R: findAllWithLikeCountForNonUser(pageable)
  end
  B->>RD: mget(readCount keys)
  RD-->>B: Map<id,delta>
  B-->>C: Page 응답 (isLiked, merged readCount 등)
Loading
sequenceDiagram
  participant P as Prometheus
  participant S as SpringBoot (Actuator)
  P->>S: GET /actuator/prometheus
  S-->>P: 메트릭 응답
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feat#9 좋아요기능 #10: post 좋아요 및 Repository/Controller 변경과 직접 연관 — ForUser/ForNonUser 분리와 기능 중첩 가능성 있음.
  • Feat#11 조회수 #12: Redis 기반 읽음수 처리 및 캐시 관련 로직이 본 PR의 PostService/Interceptor 수정과 중복·연계됨.

Poem

당근 모아서 메트릭 쌓고, 그래프에 킁킁 웃네 🥕📈
토큰 품은 주머니로, 길 따라 집을 찾아가요.
읽음수는 살포시 더해져, 좋아요는 튀어오르고 —
토끼는 서비스와 테스트를 껴안고 폴짝, 행복한 배포의 밤! 🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ 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#15-prometheus,k6

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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: 11

🔭 Outside diff range comments (6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (2)

29-35: CORS에서 쿠키 전송을 위해 allowCredentials(true) 추가 필요

프론트에서 쿠키를 전송하려면 클라이언트에 withCredentials: true 설정과 서버 CORS에 allowCredentials(true)가 필요합니다. 레포 검사 결과 backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java(라인 ~24)에 allowedOrigins만 설정되어 있고 allowCredentials 호출이 없습니다.

수정 위치(예):

  • backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (around line 24)

권장 변경 예시:

registry.addMapping("/**")
    .allowedOrigins("http://localhost:3000")
    .allowedMethods("GET","POST","PUT","PATCH","DELETE","OPTIONS")
    .allowCredentials(true);

참고: Set-Cookie 헤더는 JS에서 읽을 수 없으므로 exposedHeaders에 추가할 필요는 없습니다.


29-35: 쿠키 보안 속성 추가 필요 — Secure 최소, SameSite 권장

간단 요약: GuestCookieInterceptor에서 생성하는 식별자(guest) 쿠키에 Secure/SameSite 설정이 빠져 있습니다. backend/build.gradle에 Spring Boot 3.4.5 및 spring-boot-starter-web이 포함되어 있어 ResponseCookie 사용(옵션 B)이 가능합니다. 리포지토리에서 ForwardedHeaderFilter / server.forward-headers-strategy / X-Forwarded-Proto 관련 설정은 발견되지 않아, 프록시 뒤 배포 시 HTTPS 판단 설정을 반드시 확인해야 합니다.

수정 대상:

  • backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java — 쿠키 생성부

옵션 A (최소 변경 — Secure 플래그만 추가)

@@
   cookie.setPath("/");
   cookie.setMaxAge(60 * 60 * 24 * 15); // 15 days
   cookie.setHttpOnly(true);
+  // 로컬 HTTP 개발환경을 고려해 요청이 HTTPS일 때만 Secure 적용
+  cookie.setSecure(request.isSecure());
   request.setAttribute(COOKIE_NAME, guestId);
   response.addCookie(cookie);

옵션 B (권장 — SameSite 명시, ResponseCookie 사용)

-      Cookie cookie = new Cookie(COOKIE_NAME, guestId);
-      cookie.setPath("/");
-      cookie.setMaxAge(60 * 60 * 24 * 15); // 15 days
-      cookie.setHttpOnly(true);
-      request.setAttribute(COOKIE_NAME, guestId);
-      response.addCookie(cookie);
+      ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, guestId)
+          .path("/")
+          .maxAge(Duration.ofDays(15))
+          .httpOnly(true)
+          .secure(request.isSecure())   // 프록시 뒤에 있다면 ForwardedHeaderFilter/forward-headers-strategy 설정 권장
+          .sameSite("Lax")              // 필요 시 "None" (+ Secure 필수)로 조정
+          .build();
+      request.setAttribute(COOKIE_NAME, guestId);
+      response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

필요한 import (옵션 B 사용 시 파일 상단 추가):

import java.time.Duration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;

검증 결과(요약):

  • backend/build.gradle에 Spring Boot 3.4.5 및 spring-boot-starter-web 명시 → ResponseCookie 사용 가능.
  • 리포지토리 검색 결과 ForwardedHeaderFilter / server.forward-headers-strategy / X-Forwarded-Proto 관련 설정 없음 → 프록시 환경에서 request.isSecure()가 기대대로 동작하지 않을 수 있음.

권장 조치:

  • 단기: Option A로 Secure 추가.
  • 장기/권장: Option B로 SameSite 명시(및 HttpOnly/Secure 유지). 프록시 환경이면 server.forward-headers-strategy 또는 ForwardedHeaderFilter를 설정해 request.isSecure()가 올바르게 동작하는지 확인하세요.
frontend/src/pages/posts/PostEditPage.jsx (1)

24-44: useEffect 종속성 배열에 token 누락

fetchPost 함수 내에서 token을 사용하고 있지만 useEffect의 종속성 배열에 포함되지 않았습니다. 토큰이 변경될 때 데이터를 다시 가져와야 할 수 있습니다.

종속성 배열에 token을 추가하세요:

-  }, [id]);
+  }, [id, token]);
frontend/src/pages/posts/CommentPage.jsx (1)

38-54: 대댓글 작성 시 로그인 확인 누락

일반 댓글 작성에는 로그인 확인이 있지만, 대댓글 작성 시에는 없습니다. 일관성을 위해 추가가 필요합니다.

대댓글 작성에도 로그인 확인을 추가하세요:

   const handleReplySubmit = async (e, parentId) => {
     e.preventDefault();
+    if (!token) {
+      alert('로그인이 필요합니다.');
+      return;
+    }
     if (!replyContent.trim()) return;
frontend/src/pages/users/LoginPage.jsx (1)

35-35: 민감정보(이메일/비밀번호) 콘솔 출력 제거

credentials 전체를 로그로 남기면 비밀번호가 노출됩니다. 즉시 제거하세요.

-    console.log('로그인 데이터:', credentials);
+    // console.debug('로그인 시도: %s', credentials.email);
frontend/src/pages/posts/PostListPage.jsx (1)

32-36: 비회원 조회 목표와 상충: Authorization 헤더에 'undefined' 전송 가능 + 토큰 변경 시 재조회 누락

토큰이 없을 때도 Authorization: "undefined"가 전송되어 서버 요청이 실패할 수 있습니다. 또한 useEffect 의존성에 token이 없어 로그인/로그아웃 후 헤더가 갱신되지 않습니다.

아래처럼 헤더를 조건부로 설정하고, 토큰 변경 시 재조회되도록 의존성 배열을 업데이트하세요.

-        const response = await axios.get(`/api/posts/${page}`, {
-          headers: {
-            'Authorization': `${token}`,
-          },
-        });
+        const response = await axios.get(
+          `/api/posts/${page}`,
+          token ? { headers: { Authorization: token } } : undefined
+        );
-  }, [page]);
+  }, [page, token]);

Also applies to: 48-49

♻️ Duplicate comments (1)
backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (1)

60-60: JWTFilter의 클레임 매핑도 함께 업데이트 필요(앞 코멘트와 동일 이슈)

토큰에 email/username이 분리되었으므로, JWTFilter에서 email은 getUserEmail(token)으로, display name은 getUsername(token)으로 사용하도록 교정되어야 합니다.

🧹 Nitpick comments (33)
backend/src/test/java/org/juniortown/backend/comment/service/CommentServiceTest.java (9)

270-281: 메서드명과 시나리오 불일치: post_not_exist → parent_comment_not_exist로 변경 제안

테스트 시나리오는 "부모 댓글이 존재하지 않음"인데 메서드명은 "parent_post_not_exist"로 되어 있어 혼동 소지가 있습니다. 메서드명을 아래처럼 정정하는 것을 권장합니다.

- void create_child_comment_fail_by_parent_post_not_exist() {
+ void create_child_comment_fail_by_parent_comment_not_exist() {

122-129: 중복 스텁 제거: comment.getParent() 두 번 설정됨

동일 mock 메서드를 연속으로 스텁하는 것은 불필요합니다. 아래와 같이 첫 번째 스텁을 제거하면 의도가 명확해집니다.

   when(comment.getPost()).thenReturn(post);
-  when(comment.getParent()).thenReturn(null);
   when(comment.getUser()).thenReturn(user);
   when(comment.getParent()).thenReturn(parentComment);

162-169: 검증 시점 정리: 호출 전 verify(never())는 불필요

서비스 호출 전에 save 미호출을 검증하는 코드는 의미가 없습니다. 호출 이후 검증만 남기세요.

- verify(commentRepository, never()).save(any(Comment.class));
- // 예외 터진거 확인은 어떻게 해?
   Assertions.assertThatThrownBy(() -> commentService.createComment(USER_ID, commentCreateRequest))
     .isInstanceOf(ParentPostMismatchException.class)
     .hasMessage(ParentPostMismatchException.MESSAGE);
   verify(commentRepository, never()).save(any(Comment.class));

191-197: 불필요한 사전 검증 제거(2): 호출 전 verify(never()) 정리

위와 동일한 패턴입니다. 호출 이후 검증만 유지하세요.

- verify(commentRepository, never()).save(any(Comment.class));
   Assertions.assertThatThrownBy(() -> commentService.createComment(USER_ID, commentCreateRequest))
     .isInstanceOf(DepthLimitTwoException.class)
     .hasMessage(DepthLimitTwoException.MESSAGE);
   verify(commentRepository, never()).save(any(Comment.class));

220-226: 불필요한 사전 검증 제거(3): 호출 전 verify(never()) 정리

동일한 중복 검증 패턴입니다.

- verify(commentRepository, never()).save(any(Comment.class));
   Assertions.assertThatThrownBy(() -> commentService.createComment(USER_ID, commentCreateRequest))
     .isInstanceOf(AlreadyDeletedCommentException.class)
     .hasMessage(AlreadyDeletedCommentException.MESSAGE);
   verify(commentRepository, never()).save(any(Comment.class));

240-246: Optional.empty() 사용으로 가독성 향상

null을 넣은 Optional.ofNullable(null) 대신 Optional.empty()가 의도에 더 명확합니다.

- when(userRepository.findById(USER_ID)).thenReturn(Optional.ofNullable(null));
+ when(userRepository.findById(USER_ID)).thenReturn(Optional.empty());

358-371: DisplayName 오기 수정 제안

해당 테스트는 "댓글이 존재하지 않음" 시나리오입니다. DisplayName 문구를 아래처럼 정정하면 혼동을 줄일 수 있습니다.

- @DisplayName("댓글 삭제 실패 테스트 - 게시글이 존재하지 않음")
+ @DisplayName("댓글 삭제 실패 테스트 - 댓글이 존재하지 않음")

37-39: @activeprofiles("test")는 순수 Mockito 단위 테스트에서 효과 없음 — 제거 고려

스프링 컨텍스트를 띄우지 않는 Mockito 테스트라면 프로파일 설정은 동작하지 않습니다. 유지 비용을 줄이기 위해 제거를 고려해 주세요. 추후 스프링 테스트로 전환 계획이 있다면 그대로 둬도 무방합니다.


65-73: 중복된 CommentCreateRequest 생성 로직/스텁 추출 제안

여러 테스트에서 동일한 요청 빌더/스텁이 반복됩니다. 헬퍼 메서드로 추출하면 가독성과 유지보수성이 좋아집니다.

예시(테스트 클래스 내부 유틸 메서드 추가):

private static CommentCreateRequest req(String content, Long postId, Long parentId) {
    return CommentCreateRequest.builder()
        .content(content)
        .postId(postId)
        .parentId(parentId)
        .build();
}

사용:

CommentCreateRequest commentCreateRequest = req("This is a comment", POST_ID, parentId);
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (3)

33-33: 요청 스코프에 guestId 저장 추가는 합리적입니다 — 이후 요청에서도 일관되게 접근 가능하도록 항상 request attribute를 세팅하는 것을 권장합니다.

현재는 쿠키가 없는 첫 요청에서만 Line 33에서 request attribute를 세팅합니다. 다운스트림 코드가 매 요청마다 동일한 접근 경로(request.getAttribute)만 사용하도록 하려면, 쿠키가 이미 존재하는 경우에도 동일 attribute를 세팅하는 편이 단순합니다.

아래처럼 쿠키가 발견된 경우에도 request attribute를 세팅하도록 루프 안을 보완해 주세요.

@@
-      for(Cookie c : cookies) {
-        if (COOKIE_NAME.equals(c.getName())) {
-          hasGuestId = true;
-          break;
-        }
-      }
+      for (Cookie c : cookies) {
+        if (COOKIE_NAME.equals(c.getName())) {
+          hasGuestId = true;
+          // 쿠키가 이미 있더라도 downstream에서 일관되게 접근할 수 있도록 속성 세팅
+          request.setAttribute(COOKIE_NAME, c.getValue());
+          break;
+        }
+      }

17-35: (선택) 빈값·비정상 guestId 쿠키에 대한 방어 로직

드물지만 빈값이거나 포맷이 잘못된 guestId 쿠키가 들어올 수 있습니다. 이 경우 새 UUID로 재발급하는 방어 로직이 있으면 안전합니다.

간단한 예시:

@@
-    boolean hasGuestId = false;
+    boolean hasGuestId = false;
+    String existingGuestId = null;
@@
-        if (COOKIE_NAME.equals(c.getName())) {
-          hasGuestId = true;
+        if (COOKIE_NAME.equals(c.getName())) {
+          existingGuestId = c.getValue();
+          hasGuestId = existingGuestId != null && !existingGuestId.isBlank();
           break;
         }
@@
-    if(!hasGuestId) {
+    if(!hasGuestId) {
       String guestId = UUID.randomUUID().toString();
@@
-      request.setAttribute(COOKIE_NAME, guestId);
+      request.setAttribute(COOKIE_NAME, guestId);
       response.addCookie(cookie);
+    } else {
+      // 유효한 쿠키가 있으면 그 값을 attribute로 노출
+      request.setAttribute(COOKIE_NAME, existingGuestId);
     }

27-35: 테스트 보강 제안: 쿠키 발급·플래그 확인, request attribute 노출 검증

비회원 최초 요청에서

  • Set-Cookie에 guestId가 포함되는지,
  • HttpOnly(+ 선택적으로 Secure/SameSite)가 설정되는지,
  • 컨트롤러 단에서 request attribute로 guestId를 읽어 동작하는지

를 검증하는 테스트를 추가하면 회귀를 방지할 수 있습니다.

예시(MockMvc):

mockMvc.perform(get("/api/any-get-endpoint"))
  .andExpect(status().isOk())
  .andExpect(header().string(HttpHeaders.SET_COOKIE, Matchers.containsString("guestId=")))
  .andExpect(header().string(HttpHeaders.SET_COOKIE, Matchers.containsString("HttpOnly")))
  // Secure/SameSite를 적용했다면 다음도 추가
  //.andExpect(header().string(HttpHeaders.SET_COOKIE, Matchers.containsString("Secure")))
  //.andExpect(header().string(HttpHeaders.SET_COOKIE, Matchers.containsString("SameSite=Lax")));

테스트 작성 도와드릴까요?

frontend/src/auth/AuthContext.jsx (2)

30-46: useEffect 실행 조건 개선 필요

useEffecttoken이 변경될 때마다 실행되는데, 초기 마운트 시에도 동일한 로직이 중복 실행됩니다 (Line 22-27의 초기화 로직과 중복). 또한 setReady(true)token이 null일 때도 실행되는데, 이는 의도된 동작인지 확인이 필요합니다.

초기 로딩 플래그를 활용하여 중복 실행을 방지하는 것을 고려해보세요:

+  const [isInitialized, setIsInitialized] = useState(false);

   useEffect(() => {
+    if (!isInitialized) {
+      setIsInitialized(true);
+      setReady(true);
+      return;
+    }
     if (token) {
       localStorage.setItem(STORAGE_KEY, token);
       axios.defaults.headers.common['Authorization'] = token;
       const payload = parseJwt(token);
       setUser(payload
         ? { id: payload.userId, email: payload.email, userRole: payload.userRole, username: payload.username ?? payload.name }
         : null
       );
     } else {
       localStorage.removeItem(STORAGE_KEY);
       delete axios.defaults.headers.common['Authorization'];
       setUser(null);
     }
-    setReady(true);
   }, [token]);

8-19: JWT 파싱 함수의 견고성 개선 필요

parseJwt 함수가 유효하지 않은 JWT 형식에 대해 조용히 실패합니다. 디버깅을 위해 에러 로깅을 추가하는 것이 좋겠습니다.

개발 환경에서 디버깅을 돕기 위해 에러 로깅을 추가하세요:

   } catch {
+    if (process.env.NODE_ENV === 'development') {
+      console.warn('JWT 파싱 실패:', token);
+    }
     return null;
   }
frontend/src/pages/users/LoginPage.jsx (1)

4-4: AuthContext import 경로 일관화 (확장자 생략)

동일 프로젝트 내 다른 파일들(Header.js, PostListPage.jsx, PostDetailPage.jsx)은 .jsx 확장자를 생략하여 임포트합니다. 일관성을 위해 확장자를 생략해 주세요.

-import { useAuth } from '../../auth/AuthContext.jsx'
+import { useAuth } from '../../auth/AuthContext'
frontend/src/pages/components/Layout/Header.js (2)

7-7: 사용하지 않는 변수 제거: ready

ready가 구조 분해 되었지만 사용되지 않습니다. 린트 경고 제거를 위해 삭제하세요.

-  const { user, isAuthenticated, ready, logout } = useAuth();
+  const { user, isAuthenticated, logout } = useAuth();

9-12: 로그아웃 후 히스토리 정리

뒤로가기로 다시 로그인 상태 화면이 보이지 않도록 replace: true를 사용하는 것이 UX에 유리합니다.

   const handleLogout = () => {
     logout();
-    navigate('/login');
+    navigate('/login', { replace: true });
   };
frontend/src/pages/posts/PostListPage.jsx (1)

26-31: 주석 처리된 로그인 게이팅 코드 정리 제안

비회원도 목록 조회 가능하도록 바뀌었으니, 불필요한 주석 블록은 제거해 코드 가독성을 높이는 것을 권장합니다.

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

90-91: 불필요한 ESLint 예외 주석 제거

fetchPost를 의존성에 포함하고 있어 exhaustive-deps 비활성화가 필요 없습니다. 주석을 제거해 린트 규칙을 유지하세요.

   useEffect(() => {
     fetchPost();
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [fetchPost]);

233-237: 댓글 prop 안전성 확보 (undefined 방지)

post.comments가 없을 수 있는 상황을 대비해 기본값을 제공하는 게 안전합니다.

   <CommentSection
     postId={post.id}
-    comments={post.comments}
+    comments={post.comments ?? []}
     myUserId={myUserId}
     refreshPost={fetchPost}
   />
backend/prometheus.yml (2)

5-8: job_name 혼동 가능 — 애플리케이션 용도로 명확한 이름 사용 권장

job_name: prometheus는 일반적으로 Prometheus 서버 자체 스크랩에 쓰는 이름과 혼동될 수 있습니다. 애플리케이션 식별이 쉬운 이름으로 바꾸면 대시보드/알림 설정 시 가독성이 좋아집니다. 또한 Docker Desktop 외 환경(Linux)에서는 host.docker.internal이 동작하지 않을 수 있으므로 환경에 맞는 타겟 설정(예: 백엔드도 docker-compose에 올리고 서비스명:포트 사용)을 검토해 주세요.

-  - job_name: prometheus
+  - job_name: spring-boot
     metrics_path: '/actuator/prometheus'
     static_configs:
-      - targets: ['host.docker.internal:8080']
+      - targets: ['host.docker.internal:8080']  # Linux 환경에서는 서비스명:포트 권장

환경 확인용 체크리스트:

  • Docker Desktop(Mac/Windows) 사용 시: host.docker.internal 정상 동작
  • Linux Docker Engine 사용 시: 백엔드를 동일 compose에 올리고 targets: ['<backend-service-name>:8080'] 형태로 전환

8-8: 파일 끝 개행 누락

YAMLlint가 EOF 개행 누락을 지적했습니다. 일부 도구/플러그인에서 경고가 발생할 수 있으니 마지막 줄에 개행을 추가해 주세요.

backend/docker-compose.yml (3)

3-3: 서비스 키 오타: promethus → prometheus

서비스 키 오타는 기능상 치명적이진 않지만 가독성과 일관성을 위해 수정 권장합니다(컨테이너 이름과도 일치).

-  promethus:
+  prometheus:

21-27: k6 컨테이너도 동일 네트워크에 연결 권장

현재 k6 서비스에 networks가 지정되어 있지 않습니다. host.docker.internal로 직접 호스트를 치는 구조여도, 모니터링 스택과 동일 네트워크에 두면 추후 내부 서비스 호출로 전환 시 유연합니다.

   k6:
     image: grafana/k6
     volumes:
       - ./scripts:/scripts
     entrypoint: ""
     command: ["k6", "run", "/scripts/myscript.js"]
+    networks:
+      - monitoring-network

15-20: Grafana 데이터 영속화 옵션 고려

재기동 시 대시보드/데이터 소실을 방지하려면 Grafana의 /var/lib/grafana를 호스트 볼륨에 마운트하는 것을 권장합니다.

예:

  • 서비스 내: volumes: - ./grafana-data:/var/lib/grafana
  • 또는 named volume 사용
backend/scripts/myscript.js (2)

1-1: 사소한 스타일: import 앞 여분의 공백 제거

코드 스타일 일관성을 위해 불필요한 이중 공백을 제거해 주세요.

-import  http from 'k6/http';
+import http from 'k6/http';

8-21: 시나리오 명/실행기 정합성 개선 및 기본 임계치(thresholds) 추가 제안

지금 설정은 사실상 고정 도착률이므로 constant-arrival-rate 사용이 더 간결합니다. 또한 기본 품질 게이트(오류율/지연시간)에 대한 thresholds를 추가하면 CI/CD 자동화 시 유용합니다.

-export let options = {
+export const options = {
     scenarios: {
-        constant_request_rate: {
-            executor: 'ramping-arrival-rate',
-            startRate: 167, // 초당 약 167개의 요청으로 시작
-            timeUnit: '1s'
-            preAllocatedVus: 50, // 사전 할당된 VU 수
-            maxVUs: 100, // 최대 VU 수
-            stages: [
-                { duration: '1m', target: 167},
-            ]
-        }
+        constant_arrival_rate: {
+            executor: 'constant-arrival-rate',
+            rate: 167,               // 초당 167 rps
+            timeUnit: '1s',
+            duration: '1m',
+            preAllocatedVUs: 50,
+            maxVUs: 100,
+        },
     }
+    ,thresholds: {
+        http_req_failed: ['rate<0.01'],       // 실패율 < 1%
+        http_req_duration: ['p(95)<500'],     // 95퍼센타일 < 500ms
+    }
 }
backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java (1)

39-39: 코딩 스타일 정리: 한 줄 메서드를 일반 포맷으로 변경 권장

가독성 및 팀 컨벤션 일관성을 위해 한 줄 메서드를 일반 블록 포맷으로 변경을 제안합니다.

-  public String getUserEmail() {return user.getEmail();}
+  public String getUserEmail() {
+    return user.getEmail();
+  }
backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (1)

82-82: 정규식 매처 사용은 목적에 부합합니다만, 앵커 추가를 고려하세요

현 패턴도 동작하지만, 명시적으로 전체 경로 일치임을 드러내려면 ^/$ 앵커를 추가하는 것이 안전합니다.

- .requestMatchers(RegexRequestMatcher.regexMatcher("/api/posts/\\d+")).permitAll()
+ .requestMatchers(RegexRequestMatcher.regexMatcher("^/api/posts/\\d+$")).permitAll()
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (2)

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

jakarta.persistence.Index는 실제로 사용되지 않고 있습니다. 이 import 문을 제거해주세요.

-import jakarta.persistence.Index;
-

30-30: 메서드 명명 규칙 개선 제안

findAllWithLikeCountForUserfindAllWithLikeCountForNonUser라는 이름보다는 Spring Data JPA의 명명 규칙에 따라 findAllWithLikeCountByUserIdfindAllWithLikeCount 같은 이름이 더 적절할 수 있습니다. 또는 더 명확하게 findAllWithLikeCountForAuthenticatedUserfindAllWithLikeCountForAnonymousUser로 명명하는 것도 고려해보세요.

Also applies to: 41-41

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

98-100: null 체크 로직 개선 가능

현재 코드도 정상 동작하지만, 더 명확한 의도 전달을 위해 Optional을 활용하는 것도 고려해보세요.

-Page<PostWithLikeCountProjection> postPage = (userId == null)
-    ? postRepository.findAllWithLikeCountForNonUser(pageable)
-    : postRepository.findAllWithLikeCountForUser(userId, pageable);
+Page<PostWithLikeCountProjection> postPage = Optional.ofNullable(userId)
+    .map(id -> postRepository.findAllWithLikeCountForUser(id, pageable))
+    .orElseGet(() -> postRepository.findAllWithLikeCountForNonUser(pageable));

134-138: Map 생성 로직 최적화 가능

현재 구현도 정확하지만, Stream API를 활용하여 더 간결하게 작성할 수 있습니다.

-HashMap<Long, Long> map = new HashMap<>();
-for (int i = 0; i < ids.size(); i++) {
-    Long readCount = (readCounts != null && i < readCounts.size()) ? readCounts.get(i) : null;
-    map.put(ids.get(i), (readCount != null) ? readCount : 0L);
-}
-return map;
+return IntStream.range(0, ids.size())
+    .boxed()
+    .collect(Collectors.toMap(
+        ids::get,
+        i -> Optional.ofNullable(readCounts)
+            .filter(counts -> i < counts.size())
+            .map(counts -> counts.get(i))
+            .orElse(0L)
+    ));
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6507a3b and 7fde0ae.

📒 Files selected for processing (25)
  • backend/build.gradle (1 hunks)
  • backend/docker-compose.yml (1 hunks)
  • backend/prometheus.yml (1 hunks)
  • backend/scripts/myscript.js (1 hunks)
  • backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java (3 hunks)
  • backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (2 hunks)
  • backend/src/main/resources/application.yml (1 hunks)
  • backend/src/test/java/org/juniortown/backend/comment/service/CommentServiceTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (2 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (3 hunks)
  • frontend/src/App.jsx (1 hunks)
  • frontend/src/auth/AuthContext.jsx (1 hunks)
  • frontend/src/pages/components/Layout/Header.js (1 hunks)
  • frontend/src/pages/posts/CommentPage.jsx (2 hunks)
  • frontend/src/pages/posts/PostCreatePage.jsx (1 hunks)
  • frontend/src/pages/posts/PostDetailPage.jsx (3 hunks)
  • frontend/src/pages/posts/PostEditPage.jsx (2 hunks)
  • frontend/src/pages/posts/PostListPage.jsx (3 hunks)
  • frontend/src/pages/users/LoginPage.jsx (2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-26T12:44:23.618Z
Learnt from: Doncham
PR: Doncham/JuniorTown#12
File: backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java:7-8
Timestamp: 2025-07-26T12:44:23.618Z
Learning: Spring Boot 통합 테스트(SpringBootTest)에서는 전체 애플리케이션 컨텍스트가 로드되므로, 특정 컨트롤러가 직접 사용하지 않는 의존성이라도 해당 의존성의 빈 생성 과정에서 외부 서비스(Redis, DB 등) 연결을 시도할 수 있다. 이런 경우 테스트용 mock 설정이 필요하다.

Applied to files:

  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java
📚 Learning: 2025-07-08T14:49:27.380Z
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)는 실제로 사용되지 않는다.

Applied to files:

  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java
  • backend/src/main/java/org/juniortown/backend/post/service/PostService.java
🧬 Code Graph Analysis (19)
backend/src/test/java/org/juniortown/backend/comment/service/CommentServiceTest.java (2)
backend/src/main/java/org/juniortown/backend/comment/exception/CommentNotFoundException.java (1)
  • CommentNotFoundException (5-16)
backend/src/test/java/org/juniortown/backend/comment/controller/CommentControllerTest.java (3)
  • Test (316-342)
  • Test (218-250)
  • Test (252-277)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java (2)
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (2)
  • addInterceptors (15-19)
  • WebMvcConfig (11-26)
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
  • Test (204-217)
frontend/src/auth/AuthContext.jsx (6)
frontend/src/pages/posts/PostListPage.jsx (1)
  • useAuth (11-11)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
frontend/src/pages/components/Layout/Header.js (1)
  • useAuth (7-7)
frontend/src/pages/posts/PostCreatePage.jsx (1)
  • useAuth (9-9)
frontend/src/pages/posts/PostEditPage.jsx (1)
  • useAuth (14-14)
frontend/src/pages/users/LoginPage.jsx (1)
  • useAuth (9-9)
backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java (5)
frontend/src/auth/AuthContext.jsx (1)
  • user (23-27)
backend/src/main/java/org/juniortown/backend/user/service/CustomUserDetailsService.java (2)
  • loadUserByUsername (19-28)
  • CustomUserDetailsService (15-29)
backend/src/main/java/org/juniortown/backend/user/repository/UserRepository.java (1)
  • UserRepository (9-14)
backend/src/main/java/org/juniortown/backend/user/entity/User.java (1)
  • User (21-47)
backend/src/main/java/org/juniortown/backend/user/repository/UserRepositoryCustom.java (1)
  • UserRepositoryCustom (7-9)
frontend/src/pages/posts/PostCreatePage.jsx (4)
frontend/src/pages/posts/PostDetailPage.jsx (1)
  • useAuth (60-60)
frontend/src/pages/components/Layout/Header.js (1)
  • useAuth (7-7)
frontend/src/pages/users/LoginPage.jsx (1)
  • useAuth (9-9)
frontend/src/auth/AuthContext.jsx (2)
  • useAuth (63-63)
  • useAuth (63-63)
backend/build.gradle (1)
backend/src/main/java/org/juniortown/backend/BackendApplication.java (1)
  • SpringBootApplication (9-18)
backend/src/main/java/org/juniortown/backend/post/controller/PostController.java (1)
frontend/src/pages/posts/PostListPage.jsx (1)
  • page (17-17)
frontend/src/App.jsx (2)
frontend/src/auth/AuthContext.jsx (1)
  • AuthProvider (21-61)
frontend/src/pages/posts/PostCreatePage.jsx (1)
  • PostCreatePage (7-74)
frontend/src/pages/components/Layout/Header.js (3)
frontend/src/pages/users/LoginPage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/auth/AuthContext.jsx (4)
  • useAuth (63-63)
  • useAuth (63-63)
  • logout (49-49)
  • user (23-27)
frontend/src/pages/components/Layout/Layout.js (1)
  • Layout (3-13)
backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (2)
backend/src/main/java/org/juniortown/backend/config/SwaggerConfig.java (1)
  • SwaggerConfig (12-38)
backend/src/main/java/org/juniortown/backend/config/WebMvcConfig.java (1)
  • WebMvcConfig (11-26)
backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (3)
backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (1)
  • JWTFilter (18-62)
backend/src/main/java/org/juniortown/backend/user/entity/User.java (1)
  • User (21-47)
backend/src/main/java/org/juniortown/backend/user/service/CustomUserDetailsService.java (1)
  • CustomUserDetailsService (15-29)
frontend/src/pages/posts/PostEditPage.jsx (7)
frontend/src/pages/posts/PostDetailPage.jsx (1)
  • useAuth (60-60)
frontend/src/pages/posts/PostListPage.jsx (1)
  • useAuth (11-11)
frontend/src/pages/components/Layout/Header.js (1)
  • useAuth (7-7)
frontend/src/pages/posts/PostCreatePage.jsx (1)
  • useAuth (9-9)
frontend/src/pages/users/LoginPage.jsx (1)
  • useAuth (9-9)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
frontend/src/auth/AuthContext.jsx (2)
  • useAuth (63-63)
  • useAuth (63-63)
frontend/src/pages/posts/CommentPage.jsx (7)
frontend/src/pages/posts/PostListPage.jsx (1)
  • useAuth (11-11)
frontend/src/pages/posts/PostDetailPage.jsx (1)
  • useAuth (60-60)
frontend/src/auth/AuthContext.jsx (3)
  • useAuth (63-63)
  • useAuth (63-63)
  • token (22-22)
frontend/src/pages/components/Layout/Header.js (1)
  • useAuth (7-7)
frontend/src/pages/posts/PostCreatePage.jsx (1)
  • useAuth (9-9)
frontend/src/pages/posts/PostEditPage.jsx (1)
  • useAuth (14-14)
frontend/src/pages/users/LoginPage.jsx (1)
  • useAuth (9-9)
frontend/src/pages/users/LoginPage.jsx (5)
frontend/src/pages/posts/PostListPage.jsx (1)
  • useAuth (11-11)
frontend/src/pages/components/Layout/Header.js (1)
  • useAuth (7-7)
frontend/src/pages/posts/PostEditPage.jsx (1)
  • useAuth (14-14)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
frontend/src/auth/AuthContext.jsx (4)
  • useAuth (63-63)
  • useAuth (63-63)
  • token (22-22)
  • login (48-48)
frontend/src/pages/posts/PostListPage.jsx (7)
frontend/src/pages/posts/PostDetailPage.jsx (2)
  • navigate (59-59)
  • useAuth (60-60)
frontend/src/pages/components/Layout/Header.js (2)
  • navigate (6-6)
  • useAuth (7-7)
frontend/src/pages/posts/PostCreatePage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/PostEditPage.jsx (2)
  • navigate (13-13)
  • useAuth (14-14)
frontend/src/pages/users/LoginPage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
frontend/src/auth/AuthContext.jsx (2)
  • useAuth (63-63)
  • useAuth (63-63)
frontend/src/pages/posts/PostDetailPage.jsx (5)
frontend/src/pages/posts/PostEditPage.jsx (5)
  • useParams (12-12)
  • navigate (13-13)
  • useAuth (14-14)
  • loading (19-19)
  • error (20-20)
frontend/src/pages/posts/PostListPage.jsx (4)
  • navigate (10-10)
  • useAuth (11-11)
  • loading (14-14)
  • error (15-15)
frontend/src/pages/posts/PostCreatePage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/CommentPage.jsx (2)
  • useAuth (14-14)
  • handleDelete (57-68)
frontend/src/auth/AuthContext.jsx (2)
  • user (23-27)
  • token (22-22)
backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (4)
backend/src/main/java/org/juniortown/backend/like/repository/LikeRepository.java (2)
  • LikeRepository (9-13)
  • findByUserIdAndPostId (11-11)
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCountProjection.java (1)
  • PostWithLikeCountProjection (6-16)
backend/src/main/java/org/juniortown/backend/like/service/LikeService.java (2)
  • likePost (24-62)
  • LikeService (18-64)
backend/src/main/java/org/juniortown/backend/post/dto/response/PostWithLikeCount.java (1)
  • PostWithLikeCount (20-28)
backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java (2)
backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (1)
  • JWTFilter (18-62)
backend/src/main/java/org/juniortown/backend/user/entity/User.java (1)
  • User (21-47)
backend/src/main/java/org/juniortown/backend/post/service/PostService.java (1)
backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java (1)
  • Test (219-250)
🪛 YAMLlint (1.37.1)
backend/prometheus.yml

[error] 8-8: no new line character at the end of file

(new-line-at-end-of-file)

🪛 ast-grep (0.38.6)
backend/src/main/java/org/juniortown/backend/interceptor/GuestCookieInterceptor.java

[warning] 33-33: 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] 33-33: 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 (18)
backend/src/test/java/org/juniortown/backend/comment/service/CommentServiceTest.java (1)

289-290: 부모 댓글 미존재 시 CommentNotFoundException 기대 변경 — 도메인 의미에 부합, LGTM

컨트롤러 테스트(CommentControllerTest)도 동일 메시지/예외를 기대하고 있어 일관성이 확보되었습니다.

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

5-9: 인증 컨텍스트 통합 완료

localStorage 기반 토큰 관리에서 AuthContext를 통한 중앙집중식 인증 관리로 성공적으로 마이그레이션되었습니다. 코드가 더 깔끔하고 유지보수가 용이해졌습니다.

frontend/src/App.jsx (1)

11-28: AuthProvider 통합 성공적으로 완료

AuthProvider로 전체 앱을 감싸서 모든 컴포넌트에서 인증 컨텍스트를 사용할 수 있게 되었습니다. JSX 문법 오류도 수정되어 좋습니다.

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

27-31: 확인: 게시물 상세 조회는 인증 없이 접근 허용됩니다

SecurityConfig에서 "/api/posts/details/**" 경로가 permitAll로 설정되어 있고, PostRedisReadControllerTest에도 비회원/회원 조회 테스트가 존재합니다. 따라서 프론트엔드에서 토큰 없이 요청해도 백엔드에서 조회가 허용됩니다.

  • backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java — requestMatchers("/api/posts/details/**").permitAll()
  • backend/src/test/java/org/juniortown/backend/controller/PostRedisReadControllerTest.java — 비회원/회원 조회 테스트 (MockMvcRequestBuilders.get("/api/posts/details/{postId}", postId))
  • frontend/src/pages/posts/PostEditPage.jsx (라인 27-31) — axios 호출에 Authorization 헤더 포함
frontend/src/pages/posts/CommentPage.jsx (2)

4-14: 인증 컨텍스트 마이그레이션 성공

localStorage 기반 토큰 관리에서 AuthContext를 사용한 중앙집중식 관리로 성공적으로 전환되었습니다.


18-21: 로그인 확인 로직 추가 완료

댓글 작성 시 로그인 여부를 확인하는 보호 로직이 적절하게 추가되었습니다. 사용자 경험이 개선되었습니다.

frontend/src/pages/components/Layout/Header.js (1)

25-31: 컨텍스트 연동 및 조건부 네비게이션 렌더링 👍

컨텍스트 기반 인증 상태에 따라 링크/버튼을 분기하는 구조가 명확하고, as={Link}로 라우팅 일관성도 확보되어 있습니다.

Also applies to: 33-42

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

41-41: 에러 로깅 추가 👍

실패 원인을 콘솔에 남기는 점 좋습니다. 필요 시 err.response?.data?.message도 함께 로그하면 문제 파악이 더 쉬워집니다.

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

109-151: 낙관적 업데이트 + 중복 클릭 방지 + 최신 응답 반영 로직 매우 우수

likeBusylastLikeReqIdRef를 활용한 동시성 제어와 롤백 처리까지 깔끔합니다. 실사용 중 발생할 비정상 토글/레이스 컨디션을 잘 방어합니다.

backend/build.gradle (1)

61-63: Actuator/Prometheus 의존성 추가 적절 — LGTM

Spring Boot BOM을 통해 micrometer-registry-prometheus 버전이 정렬되므로 버전 명시 없이도 일관성이 유지됩니다. 실행 시 /actuator/prometheus 노출과 연계되어 정상 동작할 것으로 보입니다.

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

64-68: 비로그인 사용자를 고려한 null-safe userId 처리, 좋습니다

AuthenticationPrincipal이 없을 때 NPE 없이 비회원 조회로 분기되도록 안전하게 처리되었습니다.

backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (1)

49-53: JWT에 email/username을 분리해서 담는 방향, 적절합니다

로그인 성공 시 email과 username을 각각 추출해 별도 클레임으로 발급하도록 준비된 점 좋습니다.

backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java (1)

23-29: 클레임 접근자(getUsername, getUserEmail) 추가, 방향 좋습니다

명시적 클레임 접근 메서드로 가독성과 유지보수성이 좋아졌습니다.

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

326-350: 비회원 테스트 추가 확인

비회원 게시글 목록 조회 테스트가 잘 추가되었습니다. Authorization 헤더 없이 요청하여 비회원 시나리오를 적절히 테스트하고 있습니다.

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

96-96: 정렬 기준 추가 확인

id를 추가 정렬 기준으로 사용하여 동일한 createdAt 값을 가진 게시글들의 순서를 보장하도록 개선되었습니다. 이는 페이징 시 일관된 결과를 보장하는 좋은 방법입니다.


122-144: Redis 장애 처리 로직 개선됨

Redis 접근 실패 시 적절한 에러 로깅과 함께 기본값(0L)을 반환하는 방어적 프로그래밍이 잘 구현되었습니다. Map 구조로 변경하여 각 게시글 ID별로 조회수를 관리하는 것도 좋은 개선입니다.

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

210-210: 테스트 정렬 기준 업데이트 확인

모든 테스트 케이스에서 정렬 기준이 Sort.by(Sort.Order.desc("createdAt"), Sort.Order.desc("id"))로 일관되게 업데이트되었습니다. 실제 서비스 코드와 동일한 정렬 기준을 사용하여 테스트의 정확성이 향상되었습니다.

Also applies to: 250-250, 290-290


245-283: 비회원 테스트 케이스 추가 확인

비회원 게시글 조회 테스트가 적절히 추가되었습니다. userId가 null일 때 findAllWithLikeCountForNonUser가 호출되는지 정확히 검증하고 있습니다.

Comment on lines 34 to 35
let response = http.get('http://host.docker.internal:8080/api/posts/' + pageId, payload, params);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

http.get에 body 전달은 잘못된 시그니처 — 현재 payload/세 번째 인자(params)는 무시되거나 오동작합니다

k6의 http.get(url, params?)는 바디를 받지 않습니다. 바디가 필요하면 http.post 등을 사용해야 합니다. GET이라면 바디/Content-Type 제거 후 params만 전달하세요.

-    const payload = JSON.stringify({
-        userId: pageId
-    });
-    const params = {
-        headers: {
-            'Content-Type': 'application/json',
-        },
-    };
-
-    let response = http.get('http://host.docker.internal:8080/api/posts/' + pageId, payload, params);
+    const params = {
+        headers: {
+            'Accept': 'application/json',
+        },
+    };
+    let response = http.get(`http://host.docker.internal:8080/api/posts/${pageId}`, params);
📝 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
let response = http.get('http://host.docker.internal:8080/api/posts/' + pageId, payload, params);
const params = {
headers: {
'Accept': 'application/json',
},
};
let response = http.get(`http://host.docker.internal:8080/api/posts/${pageId}`, params);
🤖 Prompt for AI Agents
In backend/scripts/myscript.js around lines 34-35, the http.get call wrongly
passes a payload and params (http.get(url, params?) accepts no body); either
change the request to http.post/http.put when a body is required, or remove the
payload and call http.get(url, params) so only headers/query params are passed;
also remove any Content-Type/body-related headers when using GET.

Comment on lines 35 to 37
public String getUsername() {
return user.getEmail();
return user.getName();
}
Copy link

@coderabbitai coderabbitai bot Aug 15, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

JWT/인증 컨텍스트 간 'username' 의미가 바뀌면서 JWTFilter와 불일치(버그 가능성)

이제 getUsername()이 name을 반환합니다. 그런데 JWTFilter는 여전히 토큰의 username 클레임을 User.email에 주입하고 있어, 인증 컨텍스트의 User.email이 사용자 이름으로 채워집니다. 이메일을 사용하는 로직(예: 권한 조회, 레포지토리 연동 등)에서 혼선/버그가 발생할 수 있습니다. JWTFilter에서 email은 getUserEmail(token)으로, name은 getUsername(token)으로 분리해 매핑하도록 수정이 필요합니다.

아래와 같이 JWTFilter의 사용자 재구성 로직을 정리하는 것을 권장합니다(참고용 코드):

// JWTFilter.java 내부 예시
String email = jwtUtil.getUserEmail(token);
String displayName = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
Long userId = jwtUtil.getUserId(token);

User user = User.builder()
    .id(userId)
    .email(email)
    .name(displayName) // 필요 시 name 세팅
    .role(role)
    .build();

토큰 클레임-도메인 매핑이 일관되게 적용되는지 점검해 주세요. 빠르게 확인하려면 다음 스크립트로 JWTFilter의 사용처를 찾아보세요.


🏁 Script executed:

#!/bin/bash
# JWTFilter에서 username/email 클레임 사용 위치 점검
rg -n -C3 'JWTFilter' backend/src/main/java
rg -n -C2 'getUsername\s*\(' backend/src/main/java/org/juniortown/backend/user/jwt
rg -n -C2 'getUserEmail\s*\(' backend/src/main/java/org/juniortown/backend/user/jwt

Length of output: 5390


🏁 Script executed:

#!/bin/bash
set -euo pipefail
files=(
  backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java
  backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java
  backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java
  backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java
)
for f in "${files[@]}"; do
  echo "===== $f ====="
  sed -n '1,240p' "$f" || true
  echo
done

Length of output: 10063


JWTFilter: 토큰 클레임 → 도메인 필드 매핑 불일치 — 수정 필요

검증 결과 JWTFilter가 토큰의 "username" 클레임(jwtUtil.getUsername)을 User.email로 설정하고 있어 인증 컨텍스트에서 username/email의 의미가 섞여 있습니다. LoginFilter는 토큰 생성 시 email과 username을 각각 넣고 있으므로, JWTFilter에서 email은 getUserEmail(token)으로, 표시명(이름)은 getUsername(token)으로 분리 매핑해야 합니다.

수정 대상(요약)

  • backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java
    • 현재: String username = jwtUtil.getUsername(token); … User.builder().email(username) — 수정 필요
  • backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java
    • getUsername()은 user.getName()을 반환(현재 상태) — 유지(단 JWTFilter가 name을 올바르게 세팅해야 함)
  • backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java
    • 토큰 생성 시 createJwt(userId, userEmail, username, …)로 둘 다 채움 — 일관성 확인(변경 불필요)

권장 수정 예시 (JWTFilter 내부):

String email = jwtUtil.getUserEmail(token);
String displayName = jwtUtil.getUsername(token);
String role = jwtUtil.getRole(token);
Long userId = jwtUtil.getUserId(token);

User user = User.builder()
    .id(userId)
    .email(email)
    .name(displayName)
    .role(role)
    .build();

토큰 클레임 ↔ 도메인 필드 매핑이 전체 코드(LoginFilter/JWTFilter/CustomUserDetails/토큰 생성/검증)에 걸쳐 일관된지 점검해 주세요.

🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java
around lines 35-37, getUsername() currently returns user.getName() and should
remain returning the display name (no code change here); instead fix JWTFilter
to map JWT claims correctly by reading email via jwtUtil.getUserEmail(token) and
display name via jwtUtil.getUsername(token) when constructing the User (set
.email(email) and .name(displayName)), keep CustomUserDetails using
user.getName() as the principal display, and then verify LoginFilter still
issues both email and username claims so the mappings are consistent across
token creation and validation.

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는 복잡한 프레임워크라 세부사항을 기억하기 어려운 게 당연해요. 😊

JWT 토큰 클레임 매핑 부분만 정리해주시면 될 것 같습니다. 제가 찾은 JWTFilter 수정이 필요한 부분을 도와드릴까요? 아니면 나중에 시간 될 때 천천히 정리하셔도 괜찮습니다.

중요한 건 기능이 잘 동작한다는 점이니까요! 👍

  (\   /)
 ( ._. )
o_(")(")  *thoughtful rabbit*

Comment on lines 23 to 27
const [user, setUser] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
const payload = saved ? parseJwt(saved) : null;
return payload ? { id: payload.userId, email: payload.email, username: payload.username } : null;
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사용자 정보 초기화 로직의 불일치

사용자 정보 초기화 시 Line 26에서는 username 필드를 사용하지만, Line 37에서는 username ?? payload.name으로 폴백 처리를 하고 있습니다. 또한 Line 26에서는 userRole이 없지만 Line 37에서는 포함되어 있어 일관성이 없습니다.

다음과 같이 두 위치의 로직을 일치시키는 것을 권장합니다:

-    return payload ? { id: payload.userId, email: payload.email, username: payload.username } : null;
+    return payload ? { id: payload.userId, email: payload.email, userRole: payload.userRole, username: payload.username ?? payload.name } : null;
📝 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 [user, setUser] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
const payload = saved ? parseJwt(saved) : null;
return payload ? { id: payload.userId, email: payload.email, username: payload.username } : null;
});
const [user, setUser] = useState(() => {
const saved = localStorage.getItem(STORAGE_KEY);
const payload = saved ? parseJwt(saved) : null;
return payload ? { id: payload.userId, email: payload.email, userRole: payload.userRole, username: payload.username ?? payload.name } : null;
});
🤖 Prompt for AI Agents
In frontend/src/auth/AuthContext.jsx around lines 23 to 27 (and the later user
initialization around line 37), the initial user object shape is inconsistent:
the earlier initializer uses username but no userRole, while the later code uses
username ?? payload.name and includes userRole. Make both places construct the
same user shape by reading payload.name as a fallback for username and including
userRole (e.g., payload.role or payload.userRole) in the returned object; update
the initial useState initializer to return { id, email, username:
payload.username ?? payload.name, userRole: payload.userRole ?? payload.role }
(or the correct payload field names) so both locations produce identical user
objects.

useEffect(() => {
if (token) {
localStorage.setItem(STORAGE_KEY, token);
axios.defaults.headers.common['Authorization'] = token;
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

Authorization 헤더 형식 확인 필요

axios 기본 헤더에 토큰을 설정할 때 Bearer 접두어 없이 토큰만 설정하고 있습니다. 백엔드가 이 형식을 예상하는지 확인이 필요합니다.

일반적으로 JWT는 Bearer ${token} 형식을 사용합니다. 백엔드 API가 현재 형식을 지원하는지 확인해주세요.


🏁 Script executed:

#!/bin/bash
# 백엔드에서 Authorization 헤더 처리 방식 확인
rg -A 5 -B 5 "Authorization|Bearer" --type java

Length of output: 77475


Authorization 헤더에 Bearer 접두어 포함 필요 — 백엔드가 'Bearer ' 형식을 기대합니다

간단히: 백엔드가 Authorization 헤더에 "Bearer " 접두어를 전제로 처리하므로, 프론트엔드에서 토큰만 설정하면 인증이 실패합니다.

수정이 필요한 위치

  • frontend/src/auth/AuthContext.jsx (라인 ~34)
    • 현재: axios.defaults.headers.common['Authorization'] = token;
  • 백엔드 근거
    • backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java: response.addHeader("Authorization", "Bearer " + token);
    • backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java: if (authorization == null || !authorization.startsWith("Bearer ")) { ... }
    • 테스트들(예: backend/src/test/.../AuthControllerTest.java)도 "Bearer " 접두어 사용
    • CORS 설정에서 Authorization 헤더 노출 설정 존재 (SecurityConfig.java)

권장 수정 (안전한 방식)

// 기존
axios.defaults.headers.common['Authorization'] = token;

// 변경(이미 Bearer가 붙어있을 수 있으니 중복 방지)
axios.defaults.headers.common['Authorization'] =
  token?.startsWith('Bearer ') ? token : `Bearer ${token}`;
🤖 Prompt for AI Agents
In frontend/src/auth/AuthContext.jsx around line 34, the Authorization header is
set to the raw token but the backend expects the "Bearer " prefix; update the
assignment so it sets axios.defaults.headers.common['Authorization'] to the
token prefixed with "Bearer " only when missing (i.e., if token already starts
with "Bearer " leave it unchanged), and ensure you safely handle null/undefined
tokens when setting the header.

1.JWT 만들 때 아주 이상하게 만들던거 수정
2.actuator 전부 허용하던거 필요한 부분만 허용(아직 공부 안해서 잘 모름)
3.프론트 JWT useContext 사용을 안하고 있었던 부분 리팩토링
4.jwt 관련 테스트 깨지던거 수정
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: 2

🔭 Outside diff range comments (3)
frontend/src/pages/users/LoginPage.jsx (1)

36-36: 민감 정보(비밀번호 포함) 콘솔 로그 제거 필요

credentials에는 비밀번호가 포함됩니다. 콘솔/로그 수집 시스템으로 유출될 수 있으므로 즉시 제거하세요.

-    console.log('로그인 데이터:', credentials);
backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (2)

259-266: user2에 email/role 미설정으로 JWT 생성 시 NPE/권한 오류 가능

saveUser2.getEmail()saveUser2.getRole()이 null이면 JWT 생성 중 예외 또는 권한 해석 오류가 날 수 있습니다. 테스트의 의도와 무관한 실패를 막기 위해 필드를 명시적으로 설정해 주세요.

-        User user2 = User.builder()
-            .name("다른 유저")
-            .password("1234")
-            .build();
-        User saveUser2 = userRepository.save(user2);
-
-        String jwt = "Bearer " + jwtUtil.createJwt(saveUser2.getId(), saveUser2.getEmail(),"doncham", saveUser2.getRole(), 10000L);
+        User user2 = User.builder()
+            .name("다른 유저")
+            .email(UUID.randomUUID() + "@example.com")
+            .password("1234")
+            .role("ROLE_USER")
+            .build();
+        User saveUser2 = userRepository.save(user2);
+
+        String jwt = "Bearer " + jwtUtil.createJwt(
+            saveUser2.getId(), saveUser2.getEmail(), "doncham", saveUser2.getRole(), 10_000L);

347-354: 동일 이슈 재발: user2에 email/role 미설정

위와 동일하게 이메일과 역할을 지정해주세요.

-        User user2 = User.builder()
-            .name("다른 유저")
-            .password("1234")
-            .build();
-        User saveUser2 = userRepository.save(user2);
-
-        String jwt = "Bearer " + jwtUtil.createJwt(saveUser2.getId(), saveUser2.getEmail(),"doncham", saveUser2.getRole(), 10000L);
+        User user2 = User.builder()
+            .name("다른 유저")
+            .email(UUID.randomUUID() + "@example.com")
+            .password("1234")
+            .role("ROLE_USER")
+            .build();
+        User saveUser2 = userRepository.save(user2);
+
+        String jwt = "Bearer " + jwtUtil.createJwt(
+            saveUser2.getId(), saveUser2.getEmail(), "doncham", saveUser2.getRole(), 10_000L);
♻️ Duplicate comments (2)
frontend/src/pages/users/LoginPage.jsx (1)

1-4: AuthContext 기반 인증 흐름으로의 이행 잘 하셨습니다. 중복 제거 반영됨

로컬스토리지/axios 기본 헤더 직접 설정을 제거하고, useAuth().login()만 호출하도록 정리된 점이 좋습니다. 과거 리뷰 피드백도 충실히 반영되었습니다.

Also applies to: 9-9

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

80-83: application.yml의 actuator 노출 범위도 함께 점검 필요

이전에 모든 actuator 엔드포인트 노출(*)을 지적드렸습니다. 보안 설정과 일관되게 yml에서도 health/prometheus만 노출되도록 확인 바랍니다.

다음 스크립트로 확인할 수 있습니다:

#!/bin/bash
# actuator 노출 범위 점검
set -e
rg -n -C2 'management\.endpoints\.(web\.)?exposure\.include' backend/src/main/resources || true
🧹 Nitpick comments (29)
frontend/src/pages/posts/PostDetailPage.jsx (8)

25-55: Like 버튼 접근성·폼 내 사용 안전성 보완 제안

  • 버튼에 type="button"을 지정해 폼 안에서 의도치 않은 submit을 방지하세요.
  • busy 상태를 보조기기에 전달하도록 aria-busy를 추가하고, 변경되는 카운트를 스크린리더가 읽을 수 있도록 aria-live="polite"를 권장합니다.
-  <Button
+  <Button
     variant={isLiked ? 'danger' : 'outline-danger'}
     disabled={busy}
     onClick={onClick}
     className="d-inline-flex align-items-center"
     style={{
       borderRadius: 999,
       padding: '6px 12px',
       fontWeight: 600,
       transition: 'transform 120ms ease',
       boxShadow: isLiked ? '0 2px 6px rgba(220,53,69,.25)' : 'none'
     }}
     aria-pressed={isLiked}
     aria-label={isLiked ? '좋아요 취소' : '좋아요'}
+    aria-busy={busy}
+    type="button"
   >
@@
-    <span style={{ minWidth: 28, textAlign: 'right' }}>
+    <span style={{ minWidth: 28, textAlign: 'right' }} aria-live="polite">
       {Number(count ?? 0).toLocaleString()}
     </span>

73-85: 로그인 사용자의 개인화 필드 반영을 위해 Authorization 헤더 조건부 전송 제안

현재 fetchPost는 토큰을 사용하지 않습니다. 로그인 상태에서 개인화된 필드(예: isLiked)를 반환받으려면 토큰이 있을 때만 Authorization 헤더를 붙이는 편이 안전합니다. 의도대로 “비회원 접근 허용, 회원은 개인화 응답”을 모두 충족합니다.

-      const res = await axios.get(`/api/posts/details/${id}`);
+      const res = await axios.get(
+        `/api/posts/details/${id}`,
+        token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
+      );

86-90: 불필요한 ESLint 비활성화 주석 제거

useEffect의 deps로 fetchPost를 이미 넘기고 있으므로 exhaustive-deps 비활성화 주석은 불필요합니다. 유지 시 다른 누락을 가릴 수 있습니다.

-    // eslint-disable-next-line react-hooks/exhaustive-deps

94-103: 삭제 요청에 인증 헤더 누락 가능성 (확인 필요)

axios 전역 인터셉터로 토큰을 붙이지 않는다면 401이 발생할 수 있습니다. 토큰을 조건부로 붙이는 것이 안전합니다. 인터셉터가 이미 있다면 이 변경은 불필요합니다.

-      await axios.delete(`/api/posts/${id}`);
+      await axios.delete(
+        `/api/posts/${id}`,
+        token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
+      );

확인 요청:

  • axios 인스턴스/인터셉터에서 Authorization을 자동으로 주입하고 있나요? 그렇지 않다면 위와 같이 개별 호출에 헤더를 붙여주세요.

105-146: 좋아요 토글: 인증 헤더 누락 가능성 + 서버 카운트 우선 반영

  • 로그인 게이트는 있되 요청에 Authorization 헤더가 없습니다. 인터셉터가 없다면 401이 납니다.
  • 서버가 최신 likeCount를 내려준다면 그 값을 우선 사용해 동시성에 더 강하게 만듭니다. 값이 없을 때만 현재의 보정식을 사용하세요.
-      const res = await axios.post(`/api/posts/likes/${post.id}`);
+      const res = await axios.post(
+        `/api/posts/likes/${post.id}`,
+        null,
+        token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
+      );
@@
-        const serverIsLiked = !!res.data?.isLiked;
-        const fixedCount = serverIsLiked
-          ? (prevIsLiked ? prevCount : prevCount + 1)
-          : (prevIsLiked ? Math.max(0, prevCount - 1) : prevCount);
+        const serverIsLiked = !!res.data?.isLiked;
+        const serverCount = Number(res.data?.likeCount);
+        const fixedCount = Number.isFinite(serverCount)
+          ? serverCount
+          : (serverIsLiked
+              ? (prevIsLiked ? prevCount : prevCount + 1)
+              : (prevIsLiked ? Math.max(0, prevCount - 1) : prevCount));

추가 메모:

  • likeBusy가 중복 클릭을 이미 막고 있으므로 lastLikeReqIdRef는 과감히 제거 가능하지만(간소화), 유지해도 문제는 없습니다.

186-193: createdAt 누락 시 안전한 폴백 표시

createdAt이 없거나 파싱 실패 시 “Invalid Date”가 될 수 있습니다. 폴백 문자열을 권장합니다.

-                        {new Date(post.createdAt).toLocaleString('ko-KR', {
-                          year: 'numeric',
-                          month: '2-digit',
-                          day: '2-digit',
-                          hour: '2-digit',
-                          minute: '2-digit',
-                        })}
+                        {post.createdAt
+                          ? new Date(post.createdAt).toLocaleString('ko-KR', {
+                              year: 'numeric',
+                              month: '2-digit',
+                              day: '2-digit',
+                              hour: '2-digit',
+                              minute: '2-digit',
+                            })
+                          : '작성일 정보 없음'}

202-204: readCount가 없을 때 '회'만 보이는 UX

readCount가 undefined면 숫자 없이 “회”만 보일 수 있습니다. 0회 또는 비표시로 처리하세요.

-                        <EyeIcon />
-                        {post.readCount?.toLocaleString()}회
+                        <EyeIcon />
+                        {post.readCount != null
+                          ? `${post.readCount.toLocaleString()}회`
+                          : '0회'}

73-85: Axios 인증 전략을 공통화하면 유지보수성이 올라갑니다

개별 호출에 토큰을 붙이는 대신 axios 인스턴스 + 인터셉터로 Authorization을 일괄 주입하는 방식을 권장합니다. 토큰이 없는 경우만 헤더를 제거하면 됩니다. 또한 401 시 자동 리다이렉트(또는 토스트) 처리도 중앙에서 할 수 있습니다.

원한다면 별도 axios 인스턴스 파일을 만들고 여기서는 그 인스턴스를 import해 사용하도록 변경하는 패치를 제공하겠습니다.

Also applies to: 105-146, 94-103

frontend/src/pages/users/LoginPage.jsx (5)

23-27: Authorization 헤더 형태(Bearer 접두어) 정규화 권장

백엔드가 Authorization: Bearer <jwt> 혹은 <jwt> 등 다양한 형태를 반환할 수 있으므로, 컨텍스트가 기대하는 형식에 맞춰 정규화하면 안전합니다. 동일 접두어 중복 부착도 방지합니다.

아래처럼 처리하면 두 경우 모두 대응 가능합니다.

-        const token = res.headers['authorization'];
-        if (!token) {
+        const rawAuth = res.headers['authorization'];
+        if (!rawAuth) {
           throw new Error('토큰이 없습니다.');
         }
-        login(token);
+        // 헤더가 "Bearer <token>" 또는 "<token>" 어느 쪽이든 컨텍스트가 기대하는 형식으로 맞춥니다.
+        const token = rawAuth.startsWith('Bearer ') ? rawAuth : `Bearer ${rawAuth}`;
+        login(token);

20-20: 불필요한 TODO 주석 제거 권장

이미 로그인 API 호출이 구현되어 있으므로 주석은 제거해도 좋습니다.

-    // TODO: 백엔드 API 호출

32-35: 오류 메시지 구체화 및 네트워크/서버 오류 구분 제안

서버에서 전달하는 메시지나 HTTP 상태코드 기반으로 알림을 조금 더 구체화하면 UX가 좋아집니다.

-      .catch(error => {
-        console.error('로그인 실패:', error);
-        alert('로그인에 실패했습니다. 이메일과 비밀번호를 확인해 주세요.');
-      });
+      .catch((error) => {
+        console.error('로그인 실패:', error);
+        const msg =
+          error?.response?.data?.message
+            ?? (error?.response ? `서버 오류 (${error.response.status})` : '네트워크 오류');
+        alert(`로그인에 실패했습니다. ${msg}`);
+      });

46-53: 폼 자동완성 힌트 추가로 UX 개선

브라우저의 자동완성/패스워드 매니저와 잘 연동되도록 autoComplete를 명시하세요.

           <input
             type="email"
             className="form-control"
             id="loginEmail"
             name="email"
             value={credentials.email}
             onChange={handleChange}
             required
+            autoComplete="email"
           />

58-65: 패스워드 필드 자동완성 힌트 추가

현재 비밀번호 입력 필드에도 자동완성 힌트를 추가하세요.

           <input
             type="password"
             className="form-control"
             id="loginPassword"
             name="password"
             value={credentials.password}
             onChange={handleChange}
             required
+            autoComplete="current-password"
           />
frontend/src/pages/posts/PostListPage.jsx (8)

11-11: 토큰을 가져오지만 사용하지 않음 — Authorization 헤더 전송 또는 제거 권장

목록은 비회원 접근 허용이지만, 로그인한 사용자가 보는 경우 isLiked 등 개인화 필드가 필요하면 토큰을 헤더로 보내는 편이 낫습니다. 그렇지 않다면 token 비구조화 할당을 제거해 린트 경고를 없애주세요.


26-33: 공개 엔드포인트이되, 토큰이 있으면 Authorization 헤더를 붙여 개인화 필드 수신

주석은 쿼리 파라미터를 권장한다고 되어 있으나 현재는 path variable을 사용하고 있습니다. 혼동을 줄이기 위해 주석을 정정하고, 토큰 존재 시 헤더를 조건부로 추가하는 것이 좋습니다.

-        //서버가 쿼리 파라미터로 page를 받는 걸 권장
-        const response = await axios.get(`/api/posts/${page}`);
+        // 백엔드는 path variable(/api/posts/{page})로 페이지 조회를 허용(비회원 접근 허용).
+        // 토큰이 있으면 Authorization 헤더를 붙여 개인화 필드(isLiked 등)를 함께 받습니다.
+        const config = token ? { headers: { Authorization: `Bearer ${token}` } } : undefined;
+        const response = await axios.get(`/api/posts/${page}`, config);

44-46: 토큰 변경 시 목록 재조회

로그인/로그아웃 후 isLiked 등 개인화 데이터가 반영되도록 의존성 배열에 token을 포함하는 것을 권장합니다.

-  }, [page]);
+  }, [page, token]);

37-39: 네트워크 오류 메시지 분기(UX 개선)

네트워크 단절과 서버 오류를 구분해 사용자 메시지를 조금 더 친절하게 표현하는 것을 권장합니다.

-        console.error('게시물 목록 조회 실패:', err);
-        setError('게시물 목록을 가져오지 못했습니다.');
+        console.error('게시물 목록 조회 실패:', err);
+        setError(
+          err.code === 'ERR_NETWORK'
+            ? '서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.'
+            : '게시물 목록을 가져오지 못했습니다.'
+        );

120-121: 페이지당 항목 수 10 하드코딩 제거 → 백엔드 size 사용

페이지 크기를 하드코딩하면 백엔드 설정 변경 시 번호가 어긋납니다. 응답의 size(또는 content.length)를 사용하세요.

-                    <td>{index + 1 + page * 10}</td>
+                    <td>{index + 1 + page * pageSize}</td>

변경에 필요한 추가 코드(선택 적용):

// 상태 정의(기존 상태들과 같이)
const [pageSize, setPageSize] = useState(10);

// fetch 성공 시
setPageSize(response.data?.size ?? response.data?.content?.length ?? 10);

126-139: 좋아요 버튼은 비대화형 표시로 전환(접근성/혼동 방지)

클릭 핸들러가 제거된 상태에서 버튼 요소를 유지하면 사용자가 클릭 가능하다고 오해하기 쉽고, 보조기기에도 동작 가능한 컨트롤로 읽힙니다. 단순 아이콘(span)으로 렌더링하는 것을 권장합니다.

-                    <td style={{ textAlign: 'center', fontSize: '1.25rem' }}>
-                      <button
-                        style={{
-                          border: 'none',
-                          background: 'none',
-                          padding: 0,
-                          cursor: 'pointer',
-                          color: post.isLiked ? '#dc3545' : '#adb5bd',
-                        }}
-                        aria-label={post.isLiked ? '좋아요 취소' : '좋아요'}
-                      >
-                        {post.isLiked ? '❤️' : '🤍'}
-                      </button>
-                      <span style={{ fontWeight: 'bold', marginLeft: 4 }}>{post.likeCount}</span>
-                    </td>
+                    <td style={{ textAlign: 'center', fontSize: '1.25rem' }}>
+                      <span
+                        style={{
+                          display: 'inline-block',
+                          color: post.isLiked ? '#dc3545' : '#adb5bd',
+                        }}
+                        aria-hidden="true"
+                      >
+                        {post.isLiked ? '❤️' : '🤍'}
+                      </span>
+                      <span style={{ fontWeight: 'bold', marginLeft: 4 }}>{post.likeCount}</span>
+                    </td>

121-123: 제목 셀 네비게이션은 Link 사용 권장(접근성/UX)

셀 전체 클릭 대신 링크로 제공하면 키보드 접근성과 새 탭 열기(중클릭, Ctrl+클릭)가 쉬워집니다.

-                    <td style={{ cursor: 'pointer' }} onClick={() => navigate(`/posts/${post.id}`)}>
-                      {post.title}
-                    </td>
+                    <td>
+                      <Link to={`/posts/${post.id}`}>{post.title}</Link>
+                    </td>

추가 임포트(지원 코드):

import { Link } from 'react-router-dom';

21-45: 빠른 페이지 전환/언마운트 시 이전 요청 취소(중복 응답/경쟁 상태 방지)

사용자가 페이지를 빠르게 넘길 때 이전 요청의 응답이 뒤늦게 도착해 최신 상태를 덮어쓸 수 있습니다. AbortController로 요청을 취소하세요.

예시 구현(개념 코드):

useEffect(() => {
  const controller = new AbortController();

  const fetchPosts = async () => {
    setLoading(true);
    setError(null);
    try {
      const config = {
        ...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
        signal: controller.signal,
      };
      const response = await axios.get(`/api/posts/${page}`, config);
      setPosts(response.data.content);
      setTotalPages(response.data.totalPages);
      // setPageSize(response.data?.size ?? response.data?.content?.length ?? 10);
    } catch (err) {
      if (err.name === 'CanceledError') return;
      console.error('게시물 목록 조회 실패:', err);
      setError(
        err.code === 'ERR_NETWORK'
          ? '서버에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.'
          : '게시물 목록을 가져오지 못했습니다.'
      );
    } finally {
      setLoading(false);
    }
  };

  fetchPosts();
  return () => controller.abort();
}, [page, token]);
frontend/src/pages/posts/PostEditPage.jsx (4)

7-9: 미사용 import(base-64) 제거 권장

base64가 본 파일에서 사용되지 않습니다. 번들 크기와 정적 검사 경고를 줄이기 위해 제거하는 것이 좋습니다.

-import base64 from 'base-64';

27-31: 상세 조회 요청에서 Authorization 헤더 제거 — 의도 확인 필요 및 조건부 첨부 제안

백엔드가 해당 상세 조회 엔드포인트를 완전 공개로 전환한 것이 맞다면 문제 없습니다. 다만 “편집 페이지” 관점에서는 인증된 요청으로 서버의 권한 체크(401/403)를 명확히 받는 편이 안전합니다. 토큰이 있을 때만 조건부로 헤더를 붙이는 타협안을 제안합니다.

-        const response = await axios.get(`/api/posts/details/${id}`);
+        const response = await axios.get(
+          `/api/posts/details/${id}`,
+          token ? { headers: { Authorization: `Bearer ${token}` } } : undefined
+        );

확인 요청:

  • SecurityConfig에서 GET /api/posts/details/{id}가 비인증 허용되는지, 그리고 편집 화면에서 권한 기반 필드(비공개 콘텐츠 등)가 노출되지 않는지 확인 부탁드립니다.

48-52: 토큰 없음 분기에서 setSaving(false) 누락

로그인 필요 시 즉시 navigate 하는 흐름은 OK입니다. 다만 setSaving(true) 이후 반환하므로 상태를 원복해 두는 편이 안전합니다.

     if (!token) {
       alert('로그인이 필요합니다.');
+      setSaving(false);
       navigate('/login', { replace: true });
       return;
     }

63-66: 권한 오류(401/403) 케이스를 명확히 처리

수정 요청에서 인증 누락/권한 부족 시 사용자에게 명확한 안내와 리다이렉트를 제공하면 UX가 좋아집니다.

-    } catch (err) {
-      console.error('게시물 수정 중 오류 발생:', err);
-      setError('게시물을 수정하는 데 실패했습니다.');
+    } catch (err) {
+      console.error('게시물 수정 중 오류 발생:', err);
+      if (axios.isAxiosError?.(err)) {
+        const status = err.response?.status;
+        if (status === 401 || status === 403) {
+          alert('로그인 또는 권한이 필요합니다.');
+          navigate('/login', { replace: true });
+          return;
+        }
+      }
+      setError('게시물을 수정하는 데 실패했습니다.');
backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java (1)

199-204: 토큰 파싱 변수명/중복 split 정리 (가독성·안전성 소폭 개선)

Authorization 헤더 존재와 Bearer 접두사는 이미 검증되므로, 토큰 파싱을 한 번만 수행하고 변수명을 명확히 하면 읽기 쉬워집니다.

적용 제안:

-        String Token = mvcResult.getResponse().getHeader("Authorization");
-        String extractedUsername = jwtUtil.getUserEmail(Token.split(" ")[1]);
-        Assertions.assertEquals("init@gmail.com", extractedUsername);
-        Assertions.assertFalse(jwtUtil.isExpired(Token.split(" ")[1]));
+        String authorization = mvcResult.getResponse().getHeader("Authorization");
+        String token = authorization.split(" ")[1];
+        String extractedEmail = jwtUtil.getUserEmail(token);
+        Assertions.assertEquals("init@gmail.com", extractedEmail);
+        Assertions.assertFalse(jwtUtil.isExpired(token));
backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (2)

120-120: 테스트 토큰 TTL 1초는 플래키 위험 — 여유 있는 값으로 증가 권장

네트워크/CI 환경에 따라 1초 TTL은 만료 경합을 유발할 수 있습니다. 최소 수십 초 이상으로 늘리면 불안정성 완화에 도움이 됩니다.

-        String ghostToken = jwtUtil.createJwt(100L, "test@gmail.com","doncham", "ROLE_USER", 1000L);
+        String ghostToken = jwtUtil.createJwt(100L, "test@gmail.com", "doncham", "ROLE_USER", 60_000L);

120-120: 중복된 테스트용 JWT 생성 로직을 헬퍼로 캡슐화 제안

JWT 생성 시그니처가 또 확장될 경우 테스트 전반의 수정 비용이 커집니다. 헬퍼 메서드로 감싸 중복과 시그니처 드리프트를 줄이길 권장합니다.

예시(클래스 내부 유틸):

private String bearerJwtFor(Long id, String email, String nickname, String role, long ttlMillis) {
    return "Bearer " + jwtUtil.createJwt(id, email, nickname, role, ttlMillis);
}

Also applies to: 265-266, 353-354

backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (1)

44-45: username 변수는 닉네임 의미 — 오해 방지를 위해 변수명 변경 고려

코드 독자의 혼란을 줄이기 위해 usernamenickname으로 명명 개선을 권장합니다.

-        String username = jwtUtil.getUsername(token);
+        String nickname = jwtUtil.getUsername(token);
...
-            .name(username)
+            .name(nickname)

Also applies to: 53-53

📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7fde0ae and 7c0776f.

📒 Files selected for processing (17)
  • backend/scripts/myscript.js (1 hunks)
  • backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java (2 hunks)
  • backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (1 hunks)
  • backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java (2 hunks)
  • backend/src/main/resources/application.yml (1 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java (1 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java (2 hunks)
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (3 hunks)
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java (3 hunks)
  • frontend/src/auth/AuthContext.jsx (1 hunks)
  • frontend/src/pages/posts/CommentPage.jsx (5 hunks)
  • frontend/src/pages/posts/PostCreatePage.jsx (2 hunks)
  • frontend/src/pages/posts/PostDetailPage.jsx (3 hunks)
  • frontend/src/pages/posts/PostEditPage.jsx (3 hunks)
  • frontend/src/pages/posts/PostListPage.jsx (2 hunks)
  • frontend/src/pages/users/LoginPage.jsx (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • backend/src/main/resources/application.yml
  • frontend/src/pages/posts/PostCreatePage.jsx
  • backend/src/main/java/org/juniortown/backend/post/repository/PostRepository.java
  • frontend/src/pages/posts/CommentPage.jsx
  • backend/src/test/java/org/juniortown/backend/controller/PostControllerPagingTest.java
  • backend/src/main/java/org/juniortown/backend/user/jwt/JWTUtil.java
  • backend/scripts/myscript.js
  • frontend/src/auth/AuthContext.jsx
  • backend/src/test/java/org/juniortown/backend/post/service/PostServiceTest.java
🧰 Additional context used
🧬 Code Graph Analysis (6)
backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (4)
frontend/src/auth/AuthContext.jsx (2)
  • token (22-22)
  • user (23-27)
backend/src/main/java/org/juniortown/backend/user/entity/User.java (1)
  • User (21-47)
backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (2)
  • successfulAuthentication (48-76)
  • LoginFilter (28-94)
backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java (1)
  • CustomUserDetails (13-42)
backend/src/test/java/org/juniortown/backend/controller/AuthControllerTest.java (3)
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (1)
  • result (93-95)
backend/src/main/java/org/juniortown/backend/user/jwt/LoginFilter.java (1)
  • successfulAuthentication (48-76)
backend/src/main/java/org/juniortown/backend/user/dto/CustomUserDetails.java (1)
  • getUsername (34-37)
frontend/src/pages/posts/PostListPage.jsx (8)
frontend/src/pages/posts/PostEditPage.jsx (2)
  • navigate (13-13)
  • useAuth (14-14)
frontend/src/pages/posts/PostCreatePage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/PostDetailPage.jsx (2)
  • navigate (59-59)
  • useAuth (60-60)
frontend/src/pages/users/LoginPage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/components/Layout/Header.js (2)
  • navigate (6-6)
  • useAuth (7-7)
frontend/src/auth/AuthContext.jsx (2)
  • useAuth (63-63)
  • useAuth (63-63)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
backend/scripts/myscript.js (1)
  • response (31-31)
frontend/src/pages/users/LoginPage.jsx (8)
frontend/src/pages/posts/PostEditPage.jsx (2)
  • navigate (13-13)
  • useAuth (14-14)
frontend/src/pages/posts/PostCreatePage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/PostDetailPage.jsx (2)
  • navigate (59-59)
  • useAuth (60-60)
frontend/src/pages/posts/PostListPage.jsx (2)
  • navigate (10-10)
  • useAuth (11-11)
frontend/src/pages/components/Layout/Header.js (2)
  • navigate (6-6)
  • useAuth (7-7)
frontend/src/auth/AuthContext.jsx (4)
  • useAuth (63-63)
  • useAuth (63-63)
  • token (22-22)
  • login (48-48)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
frontend/src/pages/components/Error.jsx (1)
  • Error (3-8)
backend/src/test/java/org/juniortown/backend/controller/PostControllerTest.java (2)
backend/src/test/java/org/juniortown/backend/comment/controller/CommentControllerTest.java (2)
  • BeforeEach (83-119)
  • result (106-108)
backend/src/test/java/org/juniortown/backend/like/controller/LikeControllerTest.java (2)
  • result (93-95)
  • BeforeAll (70-96)
frontend/src/pages/posts/PostEditPage.jsx (7)
frontend/src/pages/posts/PostDetailPage.jsx (3)
  • useParams (58-58)
  • navigate (59-59)
  • useAuth (60-60)
frontend/src/pages/posts/PostCreatePage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/users/LoginPage.jsx (2)
  • navigate (8-8)
  • useAuth (9-9)
frontend/src/pages/posts/PostListPage.jsx (2)
  • navigate (10-10)
  • useAuth (11-11)
frontend/src/pages/components/Layout/Header.js (2)
  • navigate (6-6)
  • useAuth (7-7)
frontend/src/auth/AuthContext.jsx (2)
  • useAuth (63-63)
  • useAuth (63-63)
frontend/src/pages/posts/CommentPage.jsx (1)
  • useAuth (14-14)
🔇 Additional comments (9)
frontend/src/pages/posts/PostDetailPage.jsx (3)

9-23: SVG 아이콘 컴포넌트 분리 좋습니다

가벼운 EyeIcon을 별도 컴포넌트로 둔 점, stroke 기반 간결한 구현 모두 적절합니다.


60-68: AuthContext 도입과 사용자 ID 메모이제이션 적절합니다

로컬스토리지 직접 접근/파싱 대신 useAuth 사용, myUserId 메모이제이션 모두 방향성 좋아 보입니다.


171-215: 전체 UI 구성 및 오너 액션 게이트, 주석 가독성 좋습니다

헤더/본문/푸터 구획화, 오너 전용 액션 분기, 댓글 영역의 refreshPost 연결까지 흐름이 명확합니다.

Also applies to: 218-231, 235-249

frontend/src/pages/users/LoginPage.jsx (1)

23-31: AuthContext의 토큰 영속화 및 axios 헤더 설정 확인 완료

  • 초기 상태에서 useState(() => localStorage.getItem(STORAGE_KEY)) 로 저장된 토큰 복원
  • useEffect([token])에서 토큰 변경 시
    • localStorage.setItem(STORAGE_KEY, token)
    • axios.defaults.headers.common['Authorization'] = token
    • 삭제 시 localStorage.removeItem 및 헤더 삭제
  • login(token)setToken 호출만으로 위 로직이 자동 적용되어 로그인 후 헤더 설정 및 새로고침 시 유지 보장

LoginPage에서는 추가 작업 없이 login(token) 호출만으로 충분합니다.

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

8-15: AuthContext 도입으로 토큰 접근 구조 정리된 점 좋습니다

로컬 스토리지 직접 접근 대신 컨텍스트로 일관되게 토큰을 읽어오는 방향이 바람직합니다. 이후 요청에 인증이 필요한 지점(PATCH 등)에서 헤더로 붙여 사용만 보완하면 됩니다.

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

201-201: JWT 이메일 클레임으로의 전환 일관성 OK

jwtUtil.getUserEmail(...) 사용으로 테스트가 최신 JWT 클레임 변화와 일치합니다.

backend/src/main/java/org/juniortown/backend/user/jwt/JWTFilter.java (2)

43-47: 이메일/닉네임 클레임 분리 적용 적절

토큰에서 email과 username(닉네임)을 분리해 추출한 변경이 도메인 모델과 일치합니다.


50-55: User 빌더 매핑 타당

JWT 클레임을 User.email/name/role에 정확히 매핑하고 있으며, SecurityContext의 주체 정보 일관성이 좋아졌습니다.

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

80-82: Actuator 최소 공개로 보안이 개선되었습니다

/actuator/health, /actuator/prometheuspermitAll로 둔 점 좋습니다.

.requestMatchers("/api/auth/login", "/", "/api/auth/signup","/swagger-ui/**","/v3/api-docs/**","/api/posts/details/**").permitAll()
.requestMatchers("/api/auth/login", "/", "/api/auth/signup","/swagger-ui/**","/v3/api-docs/**",
"/api/posts/details/**", "/actuator/health","/actuator/prometheus").permitAll()
.requestMatchers(RegexRequestMatcher.regexMatcher("/api/posts/\\d+")).permitAll()
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

중요: /api/posts/\d+ 전부 permitAll은 DELETE/PATCH까지 무방비 — GET으로 한정 필요

현재 정규식 매처는 HTTP 메서드 제약이 없어 삭제·수정 등 민감 요청도 인증 없이 통과할 수 있습니다. GET만 허용하도록 제한하세요.

-                .requestMatchers(RegexRequestMatcher.regexMatcher("/api/posts/\\d+")).permitAll()
+                .requestMatchers(RegexRequestMatcher.regexMatcher(org.springframework.http.HttpMethod.GET, "/api/posts/\\d+")).permitAll()

또는 별도 import를 추가하고 간결히 표기할 수 있습니다:

import org.springframework.http.HttpMethod;
-                .requestMatchers(RegexRequestMatcher.regexMatcher("/api/posts/\\d+")).permitAll()
+                .requestMatchers(RegexRequestMatcher.regexMatcher(HttpMethod.GET, "/api/posts/\\d+")).permitAll()
🤖 Prompt for AI Agents
In backend/src/main/java/org/juniortown/backend/config/SecurityConfig.java
around line 82, the RegexRequestMatcher currently permits all HTTP methods for
/api/posts/\d+; change it to only allow GET by specifying the HTTP method in the
matcher (use the RegexRequestMatcher constructor's second argument "GET" or use
HttpMethod.GET.name() with an added import) so DELETE/PATCH are not
unauthenticated, and keep the rest of the rule logic unchanged.

Comment on lines 54 to +60
},
});
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

PATCH 요청에 Authorization 헤더 누락 — 수정 요청이 401/403로 실패할 가능성 높음

편집(수정) 엔드포인트는 인증이 필수일 가능성이 큽니다. 현재 헤더가 없어 서버에서 인증 실패가 날 수 있습니다. 또한 주석의 메서드 표기가 실제 코드와 불일치합니다(PUT vs PATCH).

-      // PUT /api/posts/{id}로 수정 요청
-
-      await axios.patch(`/api/posts/${id}`, {
-        title,
-        content,
-      },
-      );
+      // PATCH /api/posts/{id}로 수정 요청
+      await axios.patch(
+        `/api/posts/${id}`,
+        { title, content },
+        { headers: { Authorization: `Bearer ${token}` } }
+      );
📝 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
// PUT /api/posts/{id}로 수정 요청
await axios.patch(`/api/posts/${id}`, {
title,
content,
}, {
headers: {
'Authorization': `${token}`,
},
});
},
);
// PATCH /api/posts/{id}로 수정 요청
await axios.patch(
`/api/posts/${id}`,
{ title, content },
{ headers: { Authorization: `Bearer ${token}` } }
);
🤖 Prompt for AI Agents
In frontend/src/pages/posts/PostEditPage.jsx around lines 54 to 60, the code
sends a PATCH without Authorization and the comment incorrectly says PUT;
retrieve the current auth token (e.g., from your auth context or localStorage),
include it in the request headers (Authorization: `Bearer ${token}`) when
calling axios.patch, and update the comment to correctly state PATCH so the HTTP
method and comment match.

@Doncham Doncham merged commit dedd19b into main Aug 16, 2025
2 checks passed
@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