Skip to content

[feat] 인증.인가 필터 및 소셜 로그인 연동#36

Merged
buzz0331 merged 55 commits intodevelopfrom
feat/#30-authentication-authorization
Jun 29, 2025
Merged

[feat] 인증.인가 필터 및 소셜 로그인 연동#36
buzz0331 merged 55 commits intodevelopfrom
feat/#30-authentication-authorization

Conversation

@buzz0331
Copy link
Contributor

@buzz0331 buzz0331 commented Jun 28, 2025

#️⃣ 연관된 이슈

📝 작업 내용

구글과 카카오톡 소셜 로그인 모두 연동 완료 해두었습니다. 로그인 처리흐름은 다음과 같습니다. 리프레쉬 토큰은 일단은 보류해두었습니다. 추후에 회의해서 결정하는 걸로 하죠!

테스트 코드는 성준님이 작성해둔 회원가입 api 테스트 코드에서 헤더를 검증하는 식으로 간단하게 작성해두었습니다.

[Oauth2 로그인 처리 흐름]

  1. 클라이언트에서 소셜 로그인 요청
    /oauth2/authorization/{provider} 302 리다이렉트 요청 보내주셔야 합니다!

    • provider는 kakao 또는 google
  2. Spring Security가 OAuth2 로그인 요청을 가로채고,
    해당 provider의 인가 코드 요청 URL로 리다이렉트 '/login/oauth2/code/kakao'

  3. 사용자가 로그인 후 인가 코드를 받은 뒤,
    GET /login/oauth2/code/{provider} 로 리다이렉트되어 코드 수신

  4. OAuth2UserService가 인가 코드를 바탕으로 access token 발급 및 사용자 정보 조회

  5. 조회한 사용자 정보로 이미 가입된 회원인지 판단

    • 이미 가입된 경우 → JWT access/refresh 토큰 발급 및 응답
    • 가입되지 않은 경우 → 임시 토큰 발급 후 oauth2Id 프론트에게 반환(oauth2Id, isNewUser) => 프론트는 isNewUser를 통해 사용자가 최초 로그인이라고 판단 => 회원가입 화면에서 필요한 정보 받고 회원가입 요청 (/users/signup + 헤더에 임시 토큰) => 서버에서는 임시 토큰 검증 후 User 테이블 신규 회원 저장 후 액세스 토큰 발급
  6. 클라이언트는 JWT를 저장하고 이후 요청 시 Authorization 헤더에 Bearer {access_token} 형식으로 전송

📸 스크린샷

2025-06-28.6.47.53.mov
2025-06-28.6.48.35.mov

💬 리뷰 요구사항

yml 파일 노션에 업데이트 해두었으니 모두 업데이트 부탁드릴게요!

📌 PR 진행 시 이러한 점들을 참고해 주세요

* P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
* P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
* P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)

Summary by CodeRabbit

  • 신규 기능

    • OAuth2 및 JWT 기반 인증/인가 기능이 추가되었습니다.
    • 소셜 로그인(카카오, 구글) 및 토큰 기반 회원가입/로그인 지원이 도입되었습니다.
    • 사용자 인증 예외 및 오류 코드가 세분화되어 안내됩니다.
  • 버그 수정

    • 닉네임 중복 확인 및 회원가입 요청에 대한 검증 로직이 개선되었습니다.
  • 리팩터

    • 회원가입, 닉네임 검증, 별칭 선택 관련 DTO 및 서비스/컨트롤러/응답 구조가 일관성 있게 리네이밍 및 통합되었습니다.
    • 이메일 기반 회원가입에서 OAuth2 ID 기반 회원가입으로 전환되었습니다.
  • 테스트

    • 테스트 코드가 새로운 인증 방식 및 구조에 맞게 전면 개편되었습니다.
    • 테스트 엔티티 생성 유틸리티가 추가되어 테스트 작성이 간소화되었습니다.
  • 문서화

    • 인증 및 회원가입 관련 API 응답 구조와 예외 메시지가 명확해졌습니다.

@coderabbitai
Copy link

coderabbitai bot commented Jun 28, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

이번 변경에서는 인증 및 인가 로직과 카카오/구글 소셜 로그인, JWT 기반 인증, Spring Security 설정, 사용자 식별을 위한 커스텀 argument resolver, 예외 처리, 관련 DTO 및 테스트 코드 리팩토링이 대규모로 이루어졌습니다. User 엔티티는 이메일 대신 OAuth2 ID 기반으로 변경되었습니다.

Changes

파일/그룹 변경 요약
build.gradle Spring Security, OAuth2, JWT 관련 의존성 추가 및 Querydsl annotation processing 범위 확장
src/main/java/konkuk/thip/common/exception/... 인증 예외(AuthException) 및 인증 관련 ErrorCode 추가, GlobalExceptionHandler에 인증 예외 처리 추가
src/main/java/konkuk/thip/common/security/... OAuth2Id, UserId 어노테이션 및 argument resolver 신설, 인증 상수 enum 추가, JWT 인증 필터/entrypoint/유틸, 커스텀 OAuth2User 및 서비스, 성공 핸들러 등 인증 전반 구현
src/main/java/konkuk/thip/config/... SecurityConfig로 Spring Security 전체 설정, WebMvcConfig로 argument resolver 등록
src/main/java/konkuk/thip/user/adapter/in/web/... UserCommandController, UserQueryController 등에서 OAuth2 기반으로 파라미터 및 DTO/UseCase명 변경, JWT 토큰 발급 및 응답 헤더 처리
src/main/java/konkuk/thip/user/adapter/in/web/request/response UserSignupRequest 등 DTO에서 email 제거, oauth2Id 기반으로 변경, 검증 어노테이션 및 이름 일관화
src/main/java/konkuk/thip/user/adapter/out/jpa UserJpaEntity에서 email 제거, oauth2Id 필드 추가, UserJpaRepository에 findByOauth2Id 추가
src/main/java/konkuk/thip/user/adapter/out/mapper User <-> UserJpaEntity 매핑에서 email 제거, oauth2Id 추가
src/main/java/konkuk/thip/user/adapter/out/persistence Alias 관련 Query/Repository/Impl에서 DTO 및 반환타입 일관화 (UserViewAliasChoiceResult)
src/main/java/konkuk/thip/user/application/port/in/out/dto UseCase, DTO, Command 등 email → oauth2Id로 변경, 관련 명칭 일관화, 불필요 DummyCommand/Query 삭제
src/main/java/konkuk/thip/user/application/service 서비스 클래스 및 인터페이스명, 반환타입 등 일관화 (UserViewAliasChoiceService 등)
src/main/java/konkuk/thip/user/domain/User.java email 제거, oauth2Id 필드 및 withoutId 정적 팩토리 메서드 시그니처 변경
src/test/java/konkuk/thip/common/util/TestEntityFactory.java 테스트용 엔티티 생성 유틸 신설
src/test/java/konkuk/thip//adapter/out/jpa/.java 테스트에서 TestEntityFactory 사용, email → oauth2Id로 변경, AssertJ 등 스타일 통일
src/test/java/konkuk/thip/user/adapter/in/web/*.java UserSignupControllerTest 등에서 email 제거, JWT 토큰 기반 테스트, OAuth2Id 추출 검증, 인증 실패 케이스 추가, 전체 테스트 리팩토링
src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java MockMvc 테스트에서 Security filter 비활성화(addFilters=false)

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant JwtAuthenticationFilter
    participant JwtUtil
    participant SecurityContext
    participant Controller

    Client->>JwtAuthenticationFilter: HTTP 요청 (Authorization 헤더 포함)
    JwtAuthenticationFilter->>JwtUtil: 토큰 추출 및 검증
    JwtUtil-->>JwtAuthenticationFilter: LoginUser 반환/예외 발생
    alt 토큰 유효
        JwtAuthenticationFilter->>SecurityContext: 인증 정보 저장
        JwtAuthenticationFilter->>Controller: 요청 전달 (UserId/Oauth2Id argument resolver 작동)
    else 토큰 없음/유효하지 않음
        JwtAuthenticationFilter->>Controller: 요청 전달 (인증 정보 없음)
    end
Loading
sequenceDiagram
    participant Client
    participant OAuth2Provider
    participant CustomOAuth2UserService
    participant UserJpaRepository
    participant JwtUtil
    participant CustomSuccessHandler

    Client->>OAuth2Provider: OAuth2 인증 요청
    OAuth2Provider->>CustomOAuth2UserService: 인증 결과 전달
    CustomOAuth2UserService->>UserJpaRepository: Oauth2Id로 사용자 조회
    alt 신규 사용자
        CustomOAuth2UserService-->>CustomSuccessHandler: LoginUser(isNewUser)
        CustomSuccessHandler->>JwtUtil: Signup 토큰 생성
        CustomSuccessHandler->>Client: Signup 토큰 응답 헤더+바디
    else 기존 사용자
        CustomOAuth2UserService-->>CustomSuccessHandler: LoginUser(userId)
        CustomSuccessHandler->>JwtUtil: Access 토큰 생성
        CustomSuccessHandler->>Client: Access 토큰 응답 헤더+바디
    end
Loading

Assessment against linked issues

Objective Addressed Explanation
Security filter chain (#30)
카카오 소셜 로그인 연동 (#30)
로그인 api (#30)

Possibly related PRs

  • THIP-TextHip/THIP-Server#36: 인증/인가, 소셜 로그인, JWT, 커스텀 OAuth2UserService, argument resolver, 예외 처리 등 본 PR과 동일한 범위의 기능 및 클래스 변경이 포함되어 있어 직접적인 코드 레벨 연관성이 매우 높음.

Suggested reviewers

  • seongjunnoh
  • hd0rable

Poem

🐰
인증의 바람이 불어와,
소셜 로그인 문이 활짝 열렸네.
JWT 토큰이 춤을 추고,
필터와 핸들러가 질서 있게 줄을 서네.
새로운 유저도, 익숙한 유저도
이제는 OAuth2 ID로 만나요!
(깡총깡총, 코드가 더 안전해졌어요!)


📜 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 533775d and 458a06d.

📒 Files selected for processing (1)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (8 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

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

CodeRabbit Commands (Invoked using PR comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Documentation and Community

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

Copy link

@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: 16

🧹 Nitpick comments (15)
src/main/java/konkuk/thip/common/security/annotation/Oauth2Id.java (1)

10-10: 어노테이션 네이밍 일관성 개선을 고려해보세요.

OAuth2 표준 네이밍에 따라 @interface OAuth2Id로 변경하는 것을 권장합니다. 대문자 'O'와 숫자 '2'를 사용하는 것이 일반적인 OAuth2 명명 규칙입니다.

-public @interface Oauth2Id {
+public @interface OAuth2Id {
src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java (1)

9-9: OAuth2 ID 필드에 대한 검증 또는 문서화 추가를 고려하세요.

email에서 oauth2Id로의 전환은 OAuth2 통합에 적절합니다. 하지만 oauth2Id 필드에 대한 유효성 검증이나 JavaDoc 주석을 추가하여 필드의 목적과 형식을 명확히 하는 것을 권장합니다.

+    /**
+     * OAuth2 provider로부터 받은 고유 사용자 식별자
+     * @param oauth2Id OAuth2 provider의 사용자 고유 ID
+     */
     String oauth2Id
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)

43-45: OAuth2 ID 관련 검증 추가를 고려해 주세요.

현재 테스트는 기본적인 저장/조회만 검증하고 있습니다. OAuth2 ID 필드가 새로 추가된 만큼 관련 검증을 추가하는 것이 좋겠습니다.

다음과 같은 검증 추가를 제안합니다:

         // then
         assertThat(foundUser.getNickname()).isEqualTo("테스터");
+        assertThat(foundUser.getOauth2Id()).isEqualTo("kakao_12345678");
         assertThat(foundUser.getAliasForUserJpaEntity().getValue()).isEqualTo("칭호");
         assertThat(foundUser.getRole()).isEqualTo(UserRole.USER);
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)

24-25: @PersistenceContext 대신 @Autowired 사용에 대한 검토 필요

EntityManager에 대해 @PersistenceContext에서 @Autowired로 변경했습니다. JPA 테스트에서는 @PersistenceContext가 더 명시적이고 적절한 선택입니다.

-    @Autowired
+    @PersistenceContext
     private EntityManager em;
src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1)

11-12: OAuth2 제공자별 ID 키에 대한 문서화 고려

Kakao와 Google의 프로바이더별 ID 키가 정의되어 있습니다. 이런 외부 API 의존적인 상수들은 주석으로 출처를 명시하는 것이 좋겠습니다.

+    // Kakao OAuth2 API에서 사용자 ID를 가져오는 키
     KAKAO_PROVIDER_ID_KEY("id"),
+    // Google OAuth2 API에서 사용자 ID를 가져오는 키  
     GOOGLE_PROVIDER_ID_KEY("sub"),
src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java (1)

26-31: 주석 처리된 코드를 제거하는 것을 고려해주세요.

이메일 관련 코드가 주석 처리되어 있는데, OAuth2 ID 기반 인증으로 전환된다면 이 코드를 완전히 제거하는 것이 좋겠습니다.

-//    @Override
-//    public String getEmail() {
-//        Object object = attributes.get("kakao_account");
-//        LinkedHashMap accountMap = (LinkedHashMap) object;
-//        return accountMap.get("email").toString();
-//    }
src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java (1)

29-34: 주석 처리된 코드를 제거해주세요.

KakaoUserDetails와 마찬가지로 이메일 관련 주석 코드를 제거하는 것이 좋겠습니다.

-//    @Override
-//    public String getEmail() {
-//
-//        return attribute.get("email").toString();
-//    }
src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java (1)

18-21: getAttributes() 메서드에서 null 반환이 잠재적 문제를 야기할 수 있습니다.

일부 OAuth2 제공자나 Spring Security 구성 요소는 attributes가 null이 아닐 것으로 예상할 수 있습니다. 빈 Map을 반환하는 것을 고려해보세요.

 @Override
 public Map<String, Object> getAttributes() {
-    return null;
+    return Map.of();
 }
src/main/java/konkuk/thip/common/security/util/JwtUtil.java (2)

26-28: 토큰 만료 시간을 설정 파일로 외부화해야 합니다.

하드코딩된 만료 시간을 환경변수나 설정 파일로 이동해야 합니다.

TODO 주석이 있는 부분을 해결하기 위해 설정 외부화 작업을 도와드릴까요?

- //todo 확정 후 환경변수로 변경
- private final long tokenExpiredMs = 1000 * 60 * 60 * 24; // 24시간
- private final long signupTokenExpiredMs = 5 * 60 * 1000; // 5분
+ @Value("${jwt.access-token.expiration:86400000}")
+ private final long tokenExpiredMs;
+ 
+ @Value("${jwt.signup-token.expiration:300000}")
+ private final long signupTokenExpiredMs;

68-70: 중복된 만료 확인 로직을 제거하는 것을 고려해보세요.

validateToken 메서드에서 이미 ExpiredJwtException을 처리하고 있어 isExpired 메서드가 중복될 수 있습니다.

이 메서드가 필요한 특별한 사용 사례가 있는지 확인해보세요. 없다면 제거하고 validateToken만 사용하는 것이 좋겠습니다.

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1)

41-47: JWT 검증 로직을 통합할 수 있습니다.

validateTokenisExpired를 별도로 호출하는 것은 중복된 파싱을 야기합니다.

JwtUtil.validateToken 메서드가 만료도 함께 확인하므로 isExpired 호출을 제거할 수 있습니다.

 if (!jwtUtil.validateToken(token)) {
     throw new AuthException(AUTH_INVALID_TOKEN);
 }
-
-if (jwtUtil.isExpired(token)) {
-    throw new AuthException(AUTH_EXPIRED_TOKEN);
-}
src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java (2)

43-43: OAuth2 ID 형식을 상수로 정의하세요.

OAuth2 ID 형식이 하드코딩되어 있습니다. 유지보수성을 위해 상수로 정의하는 것이 좋습니다.

AuthParameters 클래스에 추가:

public static final String OAUTH2_ID_DELIMITER = "_";

그리고 이 라인을 다음과 같이 수정:

-String oauth2Id = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId();
+String oauth2Id = oAuth2UserDetails.getProvider() + OAUTH2_ID_DELIMITER + oAuth2UserDetails.getProviderId();

23-52: 트랜잭션 처리를 추가하세요.

데이터베이스 읽기 작업이지만, 동시성 문제를 방지하기 위해 @transactional(readOnly = true) 어노테이션 추가를 고려하세요.

클래스나 메서드 레벨에 추가:

+@Transactional(readOnly = true)
 @Override
 public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java (2)

90-93: 일관된 테스트 데이터 사용을 권장합니다.

기존 테스트들에서 닉네임으로 다양한 값들("테스트 유저", "테스트유저", "11자_닉네임_입니다")을 사용하고 있습니다. 테스트의 일관성과 가독성을 위해 표준화된 테스트 데이터 사용을 고려해보세요.

- "테스트 유저"
+ "테스트유저"

또는 TestEntityFactory 같은 팩토리 클래스를 활용하여 일관된 테스트 데이터를 생성하는 것을 권장합니다.

Also applies to: 96-100, 110-113, 116-120, 130-133, 136-140, 192-195


66-67: 테스트 코드 중복을 줄이기 위한 헬퍼 메서드 도입을 고려해보세요.

여러 테스트에서 동일한 패턴으로 임시 토큰을 생성하고 있습니다. 테스트 유지보수성을 위해 헬퍼 메서드를 도입하는 것을 권장합니다.

private String createTestSignupToken() {
    return jwtUtil.createSignupToken("kakao_12345678");
}

private String createTestSignupToken(String oauth2Id) {
    return jwtUtil.createSignupToken(oauth2Id);
}

그리고 테스트에서 다음과 같이 사용:

- String testToken = jwtUtil.createSignupToken("kakao_12345678");
+ String testToken = createTestSignupToken();

Also applies to: 96-97, 116-117, 136-137, 165-166

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a49e155 and 94edb4c.

📒 Files selected for processing (36)
  • build.gradle (1 hunks)
  • src/main/java/konkuk/thip/common/exception/AuthException.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (3 hunks)
  • src/main/java/konkuk/thip/common/security/annotation/Oauth2Id.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/annotation/UserId.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/LoginUser.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/OAuth2UserDetails.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/util/JwtUtil.java (1 hunks)
  • src/main/java/konkuk/thip/config/SecurityConfig.java (1 hunks)
  • src/main/java/konkuk/thip/config/WebMvcConfig.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserSignupCommand.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/UserSignupService.java (1 hunks)
  • src/main/java/konkuk/thip/user/domain/User.java (1 hunks)
  • src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (3 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (3 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java (7 hunks)
  • src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (13-75)
src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (13-75)
src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (13-75)
src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java (1)
src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java (1)
  • Component (16-38)
src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)
src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)
  • TestEntityFactory (13-75)
src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java (1)
src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java (1)
  • Component (16-38)
src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2User.java (1)
src/main/java/konkuk/thip/user/domain/User.java (1)
  • Getter (7-34)
🪛 Gitleaks (8.26.0)
src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java

164-164: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (39)
src/main/java/konkuk/thip/common/security/annotation/UserId.java (1)

8-11: 어노테이션 구조가 올바르게 설계되었습니다.

Spring의 argument resolver 패턴을 따라 @Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME) 설정이 적절합니다. 컨트롤러 메서드에서 인증된 사용자 ID 주입을 위한 최소한의 구현이 잘 되어 있습니다.

src/main/java/konkuk/thip/common/security/oauth2/OAuth2UserDetails.java (1)

3-7: OAuth2 제공자 추상화를 위한 인터페이스가 잘 설계되었습니다.

Google과 Kakao 소셜 로그인 통합을 위한 깔끔한 추상화입니다. getProvider()getProviderId() 메서드는 현재 요구사항을 충족하며, 향후 다른 OAuth2 제공자 추가 시에도 확장 가능한 구조입니다.

src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (1)

33-34: TestEntityFactory 사용은 좋은 개선입니다.

팩토리 패턴을 사용한 리팩토링으로 테스트 데이터 생성의 일관성과 유지보수성이 향상되었습니다.

src/main/java/konkuk/thip/user/domain/User.java (2)

21-21: OAuth2 ID 필드 추가는 적절합니다.

도메인 엔티티에서 email을 oauth2Id로 대체하는 것은 OAuth2 기반 인증 전환에 적합한 변경입니다.


23-31: 팩토리 메서드 User.withoutId 호출부 검증 완료
src/main/java/konkuk/thip/user/application/service/UserSignupService.java에서
User.withoutId(command.nickname(), alias.getImageUrl(), USER.getType(), alias.getId(), command.oauth2Id())
호출부의 인자 순서가 (nickname, imageUrl, userRole, aliasId, oauth2Id) 시그니처와 일치함을 확인했습니다. 다른 호출부는 존재하지 않습니다.

build.gradle (2)

34-43: 보안 관련 의존성 추가가 적절합니다.

OAuth2 로그인 및 JWT 기반 인증을 위한 필요한 의존성들이 올바르게 추가되었습니다:

  • Spring Security: 기본 보안 프레임워크
  • JWT 라이브러리 (0.12.3): 최신 버전으로 적절
  • OAuth2 클라이언트: 소셜 로그인 지원

50-50: 테스트 의존성 추가가 적절합니다.

Spring Security 테스트 의존성 추가로 보안 관련 기능들을 적절히 테스트할 수 있게 되었습니다.

src/main/java/konkuk/thip/user/adapter/out/mapper/UserMapper.java (2)

17-17: JPA 엔티티 매핑이 올바르게 업데이트되었습니다.

email에서 oauth2Id로의 필드 매핑 변경이 적절하게 이루어졌습니다.


29-29: 도메인 엔티티 매핑이 일관되게 업데이트되었습니다.

JPA 엔티티에서 도메인 엔티티로의 매핑도 oauth2Id 필드로 일관되게 변경되어 데이터 흐름이 올바르게 유지됩니다.

src/main/java/konkuk/thip/config/WebMvcConfig.java (1)

1-25: Spring MVC 설정이 올바르게 구현되었습니다.

인증 관련 argument resolver들이 적절히 등록되어 있고, 표준적인 Spring MVC 설정 패턴을 잘 따르고 있습니다.

src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (3)

6-6: TestEntityFactory 도입으로 테스트 코드가 개선되었습니다.

새로운 import 추가가 적절하며, 특히 AssertJ static import를 통해 더 읽기 쉬운 assertion을 사용할 수 있게 되었습니다.

Also applies to: 10-10, 18-18


42-46: 테스트 데이터 생성이 표준화되었습니다.

수동으로 엔티티를 생성하던 방식에서 TestEntityFactory를 사용하는 방식으로 변경되어 테스트 코드의 일관성과 유지보수성이 향상되었습니다.


55-61: TestEntityFactory의 기본값과 일치하도록 assertion이 올바르게 업데이트되었습니다.

팩토리에서 제공하는 값("방이름", "설명", 3, "책제목" 등)과 일치하도록 테스트 검증 로직이 적절히 수정되었습니다.

src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1)

19-24: 인증 관련 에러코드가 체계적으로 정의되었습니다.

HTTP 401 상태코드와 40100번대 에러코드를 사용하여 인증 관련 오류를 명확하게 분류했습니다. 각 에러 유형(토큰 무효, 만료, 미인증, 토큰 없음, 로그인 실패)이 적절히 구분되어 있습니다.

src/main/java/konkuk/thip/common/exception/AuthException.java (1)

1-19: 인증 예외 클래스가 적절하게 구현되었습니다.

ErrorCode를 포함하는 커스텀 예외 클래스로, 인증 관련 오류를 체계적으로 처리할 수 있도록 설계되었습니다. 두 가지 생성자를 통해 원인 예외 포함 여부를 선택할 수 있어 유연성을 제공합니다.

src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (3)

6-6: TestEntityFactory import가 추가되어 테스트 코드 표준화에 기여합니다.

다른 테스트 클래스들과 동일한 패턴으로 리팩토링되어 프로젝트 전체의 테스트 코드 일관성이 향상되었습니다.


51-57: 엔티티 생성 로직이 TestEntityFactory로 표준화되었습니다.

복잡한 엔티티 관계 설정(alias → user, category, book → room → record)이 팩토리 메서드를 통해 간결하게 처리되어 테스트 코드의 가독성과 유지보수성이 크게 향상되었습니다.


62-62: 테스트 구조가 개선되고 assertion이 팩토리 값과 일치하도록 업데이트되었습니다.

given-when-then 주석으로 테스트 단계가 명확해졌고, nickname 검증이 TestEntityFactory의 기본값("테스터")과 일치하도록 올바르게 수정되었습니다.

Also applies to: 65-65, 69-69

src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java (1)

16-22: OAuth2 ID 기반 변경사항이 올바르게 구현됨

이메일 필드 제거 및 OAuth2 ID 파라미터 추가가 깔끔하게 처리되었습니다. 기존 검증 로직도 적절하게 유지되고 있습니다.

src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (1)

43-52: TestEntityFactory 사용으로 테스트 일관성 향상

중앙화된 TestEntityFactory를 사용하여 테스트 데이터 생성이 일관되고 유지보수가 용이해졌습니다. 좋은 리팩토링입니다.

src/main/java/konkuk/thip/common/security/constant/AuthParameters.java (1)

6-22: 상수 중앙화로 타입 안전성 및 유지보수성 향상

인증 관련 상수들을 enum으로 중앙화하여 타입 안전성과 유지보수성을 크게 향상시켰습니다. 좋은 설계 패턴입니다.

src/main/java/konkuk/thip/common/security/oauth2/LoginUser.java (2)

3-7: 레코드 구조가 깔끔하고 잘 설계되었습니다.

OAuth2 로그인 사용자 정보를 캡슐화하는 불변 데이터 구조로 적절히 구현되었습니다.


8-14: 정적 팩토리 메서드가 명확하고 사용하기 쉽게 구현되었습니다.

createNewUsercreateExistingUser 메서드가 명확한 의도를 보여주며, 각각의 사용 시나리오를 잘 표현하고 있습니다.

src/main/java/konkuk/thip/user/application/service/UserSignupService.java (1)

27-29: User.withoutId 호출부 일관성 확인 완료

실행한 검색 결과 User.withoutId 호출은 UserSignupService.java 한 곳뿐이며, 새 시그니처(닉네임, 이미지 URL, USER 타입, aliasId, oauth2Id)가 일관되게 적용된 것을 확인했습니다. 추가 검토나 수정은 불필요합니다.

src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java (1)

8-14: 카카오 OAuth2 사용자 세부정보 구현이 적절합니다.

OAuth2UserDetails 인터페이스를 올바르게 구현하였고, 생성자를 통해 속성을 주입받는 구조가 명확합니다.

src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (3)

27-28: EntityManager 주입 방식 변경이 적절합니다.

@PersistenceContext에서 @Autowired로 변경한 것이 일관성 있고 명확합니다.


51-57: TestEntityFactory 사용으로 테스트 코드가 크게 개선되었습니다.

중앙화된 엔티티 팩토리를 사용하여 테스트 설정이 간소화되고 일관성이 향상되었습니다. given/when/then 구조도 명확합니다.


75-76: 추가된 검증이 테스트 커버리지를 향상시켰습니다.

투표 내용과 사용자 닉네임에 대한 추가적인 assert가 테스트의 완성도를 높였습니다.

src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1)

27-34: OAuth2 기반 회원가입 로직이 올바르게 구현되었습니다.

JWT 토큰 생성과 헤더 설정이 적절히 처리되고 있습니다. OAuth2 ID 주입도 커스텀 어노테이션을 통해 깔끔하게 구현되었네요.

src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationEntryPoint.java (1)

14-30: 인증 진입점 구현이 잘 되어있습니다.

HandlerExceptionResolver를 활용한 중앙집중식 예외 처리가 적절합니다. request 속성에서 예외를 우선 확인하고 fallback하는 로직도 올바릅니다.

src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java (2)

75-82: AuthException 핸들러 추가가 적절합니다.

인증/인가 관련 예외를 별도로 처리하는 것은 좋은 접근입니다. 기존 패턴을 잘 따르고 있습니다.


94-109: 예외 핸들러 분리가 적절합니다.

RuntimeException과 IllegalStateException을 분리하여 처리하는 것은 더 명확한 예외 처리를 가능하게 합니다.

src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java (1)

26-43: 로그인 성공 처리 로직이 잘 구현되었습니다.

새 사용자와 기존 사용자를 적절히 구분하여 처리하고 있습니다.

src/test/java/konkuk/thip/common/util/TestEntityFactory.java (1)

13-75: 테스트 엔티티 팩토리 구현이 훌륭합니다.

테스트 데이터 생성을 중앙화하여 일관성을 높이고 중복을 줄이는 좋은 접근입니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserCommandControllerTest.java (5)

5-5: 의존성 및 상수 추가가 적절하게 구성되었습니다.

JWT 기반 인증 시스템 도입에 따른 필요한 의존성들이 올바르게 추가되었습니다.

Also applies to: 22-22, 46-47


60-63: 회원가입 요청 구조 변경이 올바르게 반영되었습니다.

이메일 필드 제거와 OAuth2 기반 인증을 위한 임시 토큰 사용이 적절하게 구현되었습니다.

Also applies to: 66-70


147-186: OAuth2 ID 추출 로직 검증 테스트가 잘 구현되었습니다.

임시 토큰에서 OAuth2 ID를 올바르게 추출하여 사용자 엔티티에 저장되는지 검증하는 중요한 테스트 케이스입니다. 테스트 로직이 명확하고 검증 부분이 적절합니다.


189-204: 인증 실패 시나리오 테스트가 적절하게 추가되었습니다.

토큰 없이 요청할 때 401 Unauthorized 응답과 적절한 에러 코드/메시지를 반환하는지 검증하는 보안 관련 테스트가 잘 구현되었습니다.


164-164: 테스트용 OAuth2 ID 사용에 대한 정적 분석 경고를 무시해도 됩니다.

정적 분석 도구가 "kakao_12345678"을 API 키로 감지했지만, 이는 테스트용 가짜 OAuth2 ID이므로 보안상 문제가 없습니다.

Comment on lines +27 to +28
@Column(name = "oauth2_id", length = 50, nullable = false)
private String oauth2Id;
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

데이터 마이그레이션 및 제약조건 고려가 필요합니다.

OAuth2 ID 필드 추가는 적절하지만, 다음 사항들을 고려해 주세요:

  1. Unique 제약조건 추가: OAuth2 ID는 사용자 식별자로 사용되므로 unique 제약조건이 필요합니다.
  2. 필드 길이 검증: 다양한 OAuth2 제공자(Google, Kakao, Naver 등)의 ID 길이를 확인하여 50자가 충분한지 검증해 주세요.
  3. 기존 데이터 마이그레이션: 현재 사용자 데이터가 있다면 마이그레이션 전략이 필요합니다.

다음 수정을 제안합니다:

-    @Column(name = "oauth2_id", length = 50, nullable = false)
+    @Column(name = "oauth2_id", length = 100, nullable = false, unique = true)
     private String oauth2Id;

다양한 OAuth2 제공자의 ID 길이를 확인하는 스크립트:


🏁 Script executed:

#!/bin/bash
# OAuth2 제공자별 ID 형식 및 길이 확인
echo "OAuth2 제공자별 ID 형식 및 최대 길이 확인이 필요합니다."
echo "Google: 숫자 21자리 (예: 123456789012345678901)"
echo "Kakao: 숫자 최대 15자리 정도"
echo "Naver: 문자+숫자 조합"

Length of output: 269


OAuth2 ID 필드에 대한 unique 제약조건 및 마이그레이션 전략 적용 필요

  • 수정 위치:
    src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java (27–28줄)
  • 고려 사항:
    • OAuth2 ID는 사용자 식별자로 사용되므로 반드시 unique 제약조건을 걸어야 합니다.
    • Google(21자리), Kakao(15자리), Naver(알파벳·숫자 조합) 등 모든 제공자의 ID를 수용할 수 있도록 길이를 100자로 여유 있게 설정하세요.
    • 이미 운영 중인 테이블에 NOT NULL·UNIQUE 제약을 추가할 때는 데이터 마이그레이션 스크립트(ALTER TABLE, 기존 NULL 처리 등) 를 별도로 준비해야 합니다.

제안 차이(diff):

-    @Column(name = "oauth2_id", length = 50, nullable = false)
+    @Column(name = "oauth2_id", length = 100, nullable = false, unique = true)
     private String oauth2Id;

추가로, 복합 제약 설정이 필요할 경우 엔티티 클래스 상단에 다음과 같이 @TableuniqueConstraints를 활용할 수도 있습니다:

@Table(
  name = "users",
  uniqueConstraints = @UniqueConstraint(columnNames = "oauth2_id")
)

데이터 마이그레이션 계획도 함께 수립해 주세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java around
lines 27 to 28, update the oauth2Id field to have a length of 100 and add a
unique constraint to ensure uniqueness at the database level. Modify the @Column
annotation accordingly and add a @Table annotation at the class level with
uniqueConstraints set on the oauth2_id column. Additionally, prepare a data
migration script to handle existing NULL values and apply NOT NULL and UNIQUE
constraints safely on the production database.

Comment on lines +62 to +67
} catch (Exception e) {
log.error("JWT 필터에서 오류 발생: {}", e.getMessage());
request.setAttribute("exception", e);
} finally {
filterChain.doFilter(request, response);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

예외 처리 시 보안 정보 노출을 주의해야 합니다.

로그에 전체 예외 메시지를 출력하면 민감한 정보가 노출될 수 있습니다.

 } catch (Exception e) {
-    log.error("JWT 필터에서 오류 발생: {}", e.getMessage());
+    log.error("JWT 필터에서 오류 발생", e);
     request.setAttribute("exception", e);
 }
📝 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
} catch (Exception e) {
log.error("JWT 필터에서 오류 발생: {}", e.getMessage());
request.setAttribute("exception", e);
} finally {
filterChain.doFilter(request, response);
}
} catch (Exception e) {
log.error("JWT 필터에서 오류 발생", e);
request.setAttribute("exception", e);
} finally {
filterChain.doFilter(request, response);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java
around lines 62 to 67, the current exception logging outputs the full exception
message, which may expose sensitive information. Modify the log statement to
avoid printing detailed exception messages; instead, log a generic error message
or only the exception type without sensitive details. This prevents potential
security risks from information leakage in logs.

Comment on lines +45 to +52
private void writeResponse(HttpServletResponse response, Object value) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(CONTENT_TYPE);
response.setCharacterEncoding(ENCODING);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(value);
response.getWriter().write(body);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ObjectMapper를 필드로 선언하여 재사용하세요.

ObjectMapper 인스턴스를 매번 생성하는 것은 비효율적입니다. 클래스 필드로 선언하여 재사용하는 것이 좋습니다.

 @Component
 @RequiredArgsConstructor
 public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
     private static final String CONTENT_TYPE = "application/json";
     private static final String ENCODING = "UTF-8";
+    private final ObjectMapper objectMapper = new ObjectMapper();
     private final JwtUtil jwtUtil;

 private void writeResponse(HttpServletResponse response, Object value) throws IOException {
     response.setStatus(HttpServletResponse.SC_OK);
     response.setContentType(CONTENT_TYPE);
     response.setCharacterEncoding(ENCODING);
-    ObjectMapper objectMapper = new ObjectMapper();
     String body = objectMapper.writeValueAsString(value);
     response.getWriter().write(body);
 }
📝 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
private void writeResponse(HttpServletResponse response, Object value) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(CONTENT_TYPE);
response.setCharacterEncoding(ENCODING);
ObjectMapper objectMapper = new ObjectMapper();
String body = objectMapper.writeValueAsString(value);
response.getWriter().write(body);
}
@Component
@RequiredArgsConstructor
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final String CONTENT_TYPE = "application/json";
private static final String ENCODING = "UTF-8";
private final ObjectMapper objectMapper = new ObjectMapper();
private final JwtUtil jwtUtil;
private void writeResponse(HttpServletResponse response, Object value) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(CONTENT_TYPE);
response.setCharacterEncoding(ENCODING);
String body = objectMapper.writeValueAsString(value);
response.getWriter().write(body);
}
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java
around lines 45 to 52, the ObjectMapper is instantiated inside the writeResponse
method every time it is called, which is inefficient. To fix this, declare
ObjectMapper as a private final field in the class and initialize it once, then
reuse this instance in the writeResponse method instead of creating a new one
each time.

Comment on lines +42 to +49
// //테스트를 위한 url
"/api/test/public",
"/api/test/auth-status",
"/api/test/protected",
"/auth/kakao/**",
"/kakao-login-test.html",
"/google-login-test.html",
"/index.html",
};
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

프로덕션 환경에서 테스트 엔드포인트를 제거하세요.

WHITELIST에 테스트용 엔드포인트가 포함되어 있습니다. 프로파일 기반으로 조건부 설정을 고려하세요.

+@Value("${spring.profiles.active:default}")
+private String activeProfile;

 private static final String[] WHITELIST = {
         "/swagger-ui/**", "/api-docs/**", "/swagger-ui.html",
         "/v3/api-docs/**","/oauth2/authorization/**",
         "/login/oauth2/code/**"
-
-//            //테스트를 위한 url
-        "/api/test/public",
-        "/api/test/auth-status",
-        "/api/test/protected",
-        "/auth/kakao/**",
-        "/kakao-login-test.html",
-        "/google-login-test.html",
-        "/index.html",
 };

+private String[] getWhitelist() {
+    if ("dev".equals(activeProfile) || "local".equals(activeProfile)) {
+        return ArrayUtils.addAll(WHITELIST, 
+            "/api/test/public",
+            "/api/test/auth-status",
+            "/api/test/protected",
+            "/auth/kakao/**",
+            "/kakao-login-test.html",
+            "/google-login-test.html",
+            "/index.html"
+        );
+    }
+    return WHITELIST;
+}

그리고 설정에서 사용:

 .authorizeHttpRequests(auth -> auth
-        .requestMatchers(WHITELIST).permitAll()
+        .requestMatchers(getWhitelist()).permitAll()
         .anyRequest().authenticated()
 )
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/config/SecurityConfig.java around lines 42 to 50,
the WHITELIST array includes test endpoints that should not be accessible in
production. Refactor the code to conditionally include these test URLs only when
the application is running in a non-production profile, such as 'dev' or 'test'.
Use Spring's profile-based configuration or conditional logic to exclude these
test endpoints from the whitelist in the production environment.

Comment on lines +91 to +95
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*")); // 배포 시 도메인 명시
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

CORS 설정에 보안 문제가 있습니다.

setAllowedOrigins("*")setAllowCredentials(true)는 함께 사용할 수 없습니다. 이는 보안상 위험합니다.

다음 중 하나를 선택하세요:

옵션 1 (권장): 특정 도메인만 허용

-config.setAllowedOrigins(Collections.singletonList("*"));
+config.setAllowedOrigins(List.of("http://localhost:3000", "https://yourdomain.com"));
 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
 config.setAllowedHeaders(Collections.singletonList("*"));
 config.setAllowCredentials(true);

옵션 2: 모든 origin 허용하되 credentials 비활성화

-config.setAllowedOrigins(Collections.singletonList("*"));
+config.setAllowedOriginPatterns(Collections.singletonList("*"));
 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
 config.setAllowedHeaders(Collections.singletonList("*"));
-config.setAllowCredentials(true);
+config.setAllowCredentials(false);
📝 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
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*")); // 배포 시 도메인 명시
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000", "https://yourdomain.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
Suggested change
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Collections.singletonList("*")); // 배포 시 도메인 명시
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(Collections.singletonList("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowCredentials(false);
config.setMaxAge(3600L);
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/config/SecurityConfig.java around lines 91 to 96,
the CORS configuration uses setAllowedOrigins("*") together with
setAllowCredentials(true), which is a security risk. To fix this, either specify
a list of allowed origins explicitly instead of "*", or if you want to allow all
origins, setAllowCredentials(false) to disable credentials. Choose one of these
options and update the code accordingly.

seongjunnoh
seongjunnoh previously approved these changes Jun 29, 2025
Copy link
Collaborator

@seongjunnoh seongjunnoh left a comment

Choose a reason for hiding this comment

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

고생하셨습니다! oauth2 + spring security 의 조합은 처음이어서 저도 공부하면서 재미있게 리뷰할 수 있었습니다! 리뷰 단 부분 확인 부탁드립니다!

Comment on lines +6 to +15
public enum AuthParameters {
JWT_HEADER_KEY("Authorization"),
JWT_PREFIX("Bearer "),
KAKAO("kakao"),
GOOGLE("google"),
KAKAO_PROVIDER_ID_KEY("id"),
GOOGLE_PROVIDER_ID_KEY("sub"),
JWT_ACCESS_TOKEN_KEY("userId"),
JWT_SIGNUP_TOKEN_KEY("oauth2Id"),
;
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM! authorization 관련 string value 들을 enum으로 모아두니 관리하기 편할 것 같습니다!

Comment on lines +8 to +14
public static LoginUser createNewUser(String oauth2Id) {
return new LoginUser(oauth2Id, null, true);
}

public static LoginUser createExistingUser(String oauth2Id, Long userId) {
return new LoginUser(oauth2Id, userId, false);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class)
Copy link
Collaborator

Choose a reason for hiding this comment

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

p3 : SecurityConfig가 JwtAuthenticationFilter 를 private attribute로 가지도록 하고, addFilterBefore 메서드에서 해당 인스턴스 변수를 파라미터로 주입하는건 어떤가요?

SecurityConfig 가 굳이 JwtUtil 과의 의존성을 가질 필요 없이 이미 @component를 통해 빈으로 등록된 JwtAuthenticationFilter 를 SecurityConfig 에서 사용하는 구조를 말씀드린 겁니다!

이렇게 수정하면 JwtAuthenticationFilter 만이 JwtUtil 과의 의존성을 맺고 있으므로, JWT관련 모든 책임들을 JwtAuthenticationFilter 가 담당하도록 강제할 수 있는 효과가 있을 것 같습니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오호 너무 좋은 리뷰네요!! 굿굿 👍🏻

//when : 회원가입 api 호출 + 임시 토큰 발급
String testToken = jwtUtil.createSignupToken("kakao_12345678");
ResultActions result = mockMvc.perform(post("/users/signup")
.header("Authorization", "Bearer " + testToken) //헤더 추가
Copy link
Collaborator

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

수고하셨어요🤩🤩 시큐리티내용은 언제나봐도 어질어질하네요,,

import lombok.Getter;

@Getter
public class AuthException extends RuntimeException {
Copy link
Member

Choose a reason for hiding this comment

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

LGTM

import lombok.Getter;

@Getter
public enum AuthParameters {
Copy link
Member

Choose a reason for hiding this comment

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

와우 이거 상수로싹다 정리하신거 너뮤좋네요 💯

}

String oauth2Id = oAuth2UserDetails.getProvider() + "_" + oAuth2UserDetails.getProviderId(); //kakao_1234567890
UserJpaEntity existingUser = userJpaRepository.findByOauth2Id(oauth2Id);
Copy link
Member

Choose a reason for hiding this comment

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

p2: null로반환하지않고, optional로 안전하게 반환하는게 낫지않을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵! 적용했습니다 !!


private final SecretKey secretKey;

//todo 확정 후 환경변수로 변경
Copy link
Member

Choose a reason for hiding this comment

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

👍🏻

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: 6

🧹 Nitpick comments (10)
src/main/java/konkuk/thip/book/application/port/out/SearchBookQueryPort.java (1)

5-7: 도메인 모델 추상화 개선을 고려해보세요.

현재 인터페이스가 NaverBookParseResult라는 구체적인 API 응답 모델에 의존하고 있습니다. 포트 인터페이스는 외부 시스템에 독립적인 도메인 모델을 사용하는 것이 헥사고날 아키텍처의 원칙에 더 부합합니다.

-import konkuk.thip.book.adapter.out.api.dto.NaverBookParseResult;
+import konkuk.thip.book.domain.BookSearchResult;

public interface SearchBookQueryPort {
-    NaverBookParseResult findBooksByKeyword(String keyword, int start);
+    BookSearchResult findBooksByKeyword(String keyword, int start);
}
src/main/java/konkuk/thip/book/application/service/BookSearchService.java (2)

30-30: TODO 주석을 처리해 주세요.

최근 검색어 로직 추가가 계획되어 있습니다. 향후 구현 일정을 명확히 하거나 이슈로 등록하는 것을 권장합니다.

최근 검색어 기능 구현에 도움이 필요하시면 별도 이슈로 등록해 드릴까요?


37-37: 코딩 스타일 일관성을 개선해 주세요.

!= 연산자 앞뒤에 공백을 추가하여 가독성을 향상시켜 주세요.

-        if ( totalElements!=0 && page > totalPages) {
+        if (totalElements != 0 && page > totalPages) {
src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (2)

16-16: 변수명 네이밍 컨벤션을 수정해 주세요.

Java 네이밍 컨벤션에 따라 변수명은 camelCase로 작성해야 합니다.

-        List<NaverBookParseResult.NaverBook> Naverbooks = new ArrayList<>();
+        List<NaverBookParseResult.NaverBook> naverBooks = new ArrayList<>();

그리고 46번 라인의 Naverbooks.add(naverBook);naverBooks.add(naverBook);로 수정해 주세요.


49-51: 예외 처리 범위를 구체화하는 것을 고려해 주세요.

현재 모든 예외를 catch하고 있어 구체적인 파싱 오류 정보가 손실될 수 있습니다. 필요에 따라 로깅을 추가하거나 더 구체적인 예외 처리를 고려해 보세요.

        } catch (Exception e) {
+           // 로깅 추가 고려
+           log.warn("XML 파싱 중 오류 발생: {}", e.getMessage(), e);
            throw new BusinessException(ErrorCode.BOOK_NAVER_API_PARSING_ERROR);
        }
src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1)

47-47: 변수명을 더 의미있게 작성해 주세요.

text 변수명이 용도를 명확히 나타내지 못합니다.

-        String text = null;
+        String encodedKeyword = null;
         try {
-            text = URLEncoder.encode(keyword, "UTF-8");
+            encodedKeyword = URLEncoder.encode(keyword, "UTF-8");
         } catch (UnsupportedEncodingException e) {
             throw new BusinessException(BOOK_KEYWORD_ENCODING_FAILED);
         }
-        return text;
+        return encodedKeyword;
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (2)

10-19: 레코드 구조가 잘 설계되었습니다.

페이지네이션에 필요한 모든 정보가 포함되어 있고, 필드명이 명확합니다. 하지만 @Builder 어노테이션이 레코드에 필요한지 검토해보세요. 레코드는 이미 모든 필드를 받는 생성자가 있어서 빌더 패턴이 불필요할 수 있습니다.

-@Builder
 public record GetBookSearchListResponse(

10-19: record에서 @builder 어노테이션 사용을 재검토하세요.

record는 이미 모든 필드를 받는 canonical constructor를 제공하므로 @builder 어노테이션이 불필요할 수 있습니다. record의 간결성을 유지하기 위해 @builder 제거를 고려해보세요.

-@Builder
 public record GetBookSearchListResponse(
src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (2)

37-37: 오타 수정이 필요합니다.

오류 메시지에서 쉼표와 따옴표 사이에 공백이 누락되었습니다.

-    BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."),
+    BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001, "네이버 API 요청에 실패하였습니다."),

36-44: 도서 검색 관련 에러 코드 설계를 개선할 수 있습니다.

일부 에러의 HTTP 상태 코드가 부적절할 수 있습니다. 네이버 API 연결 실패나 응답 오류는 서버 측 문제이므로 5xx 상태 코드가 더 적절할 수 있습니다.

다음과 같이 수정을 고려해보세요:

-BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
-BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.BAD_REQUEST, 80005,"네이버 API 응답에 실패하였습니다."),
+BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_GATEWAY, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
+BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.BAD_GATEWAY, 80005,"네이버 API 응답에 실패하였습니다."),
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 73acece and f5ce80f.

📒 Files selected for processing (24)
  • src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/request/DummyRequest.java (0 hunks)
  • src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (1 hunks)
  • src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/port/in/DummyUseCase.java (0 hunks)
  • src/main/java/konkuk/thip/book/application/port/out/SearchBookQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1 hunks)
  • src/main/java/konkuk/thip/book/application/service/BookService.java (0 hunks)
  • src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/BusinessException.java (1 hunks)
  • src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java (1 hunks)
  • src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java (1 hunks)
  • src/main/java/konkuk/thip/config/SecurityConfig.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/konkuk/thip/book/application/service/BookService.java
  • src/main/java/konkuk/thip/book/application/port/in/DummyUseCase.java
  • src/main/java/konkuk/thip/book/adapter/in/web/request/DummyRequest.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/konkuk/thip/book/application/port/in/BookSearchUseCase.java
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java
  • src/main/java/konkuk/thip/common/security/argument_resolver/UserIdArgumentResolver.java
  • src/main/java/konkuk/thip/common/security/oauth2/GoogleUserDetails.java
  • src/main/java/konkuk/thip/common/security/argument_resolver/Oauth2IdArgumentResolver.java
  • src/main/java/konkuk/thip/config/SecurityConfig.java
  • src/main/java/konkuk/thip/common/security/oauth2/KakaoUserDetails.java
  • src/main/java/konkuk/thip/common/security/oauth2/CustomOAuth2UserService.java
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java (1)
src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1)
  • RequiredArgsConstructor (18-108)
src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java (2)
src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java (1)
  • RequiredArgsConstructor (18-108)
src/main/java/konkuk/thip/book/adapter/out/api/NaverBookXmlParser.java (1)
  • NaverBookXmlParser (13-63)
🔇 Additional comments (23)
src/main/java/konkuk/thip/common/dto/ErrorResponse.java (1)

11-11: 중요한 JSON 속성명 버그 수정 완료

JSON 속성명에서 불필요한 콜론을 제거한 것은 올바른 수정입니다. 이전의 "isSuccess:"는 잘못된 JSON 속성명을 생성하여 API 응답 형식에 문제를 일으킬 수 있었습니다.

src/main/java/konkuk/thip/common/exception/BusinessException.java (1)

10-18: 예외 메시지 전파 개선이 잘 되었습니다.

ErrorCode의 메시지를 상위 클래스로 전달하여 예외 메시지가 일관되게 표시되도록 개선된 점이 좋습니다. 로깅과 디버깅 시 더 명확한 오류 정보를 제공할 수 있습니다.

src/main/java/konkuk/thip/book/adapter/out/api/BookApiAdapter.java (1)

8-19: 어댑터 구현이 깔끔하게 잘 되었습니다.

관심사가 잘 분리되어 있고 어댑터 패턴을 적절히 구현했습니다:

  • API 호출 로직은 NaverApiUtil에 위임
  • XML 파싱 로직은 NaverBookXmlParser에 위임
  • 어댑터는 단순히 두 컴포넌트를 연결하는 역할만 수행

NaverApiUtilNaverBookXmlParser에서 이미 적절한 예외 처리가 되어 있어 안전합니다.

src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java (1)

36-69: 테스트 케이스 구성이 적절합니다.

성공 케이스와 예외 케이스를 모두 다루고 있어 좋습니다. 테스트 메서드명도 명확하고 한국어로 의도가 잘 드러나 있습니다. AssertJ를 활용한 검증도 적절합니다.

src/main/java/konkuk/thip/book/application/service/BookSearchService.java (1)

22-24: 입력 유효성 검사가 잘 구현되었습니다.

키워드가 null이거나 빈 문자열인 경우를 적절히 처리하고 있습니다.

src/main/java/konkuk/thip/book/adapter/out/api/dto/NaverBookParseResult.java (1)

8-23: 잘 설계된 데이터 구조입니다.

레코드 클래스를 적절히 활용하여 불변 객체로 설계되었고, Builder 패턴과 정적 팩토리 메서드를 제공하여 객체 생성의 편의성을 높였습니다.

src/test/java/konkuk/thip/book/adapter/in/web/BookQueryControllerTest.java (1)

26-77: 포괄적인 테스트 커버리지를 제공합니다.

성공 시나리오와 다양한 실패 시나리오(키워드 누락, 잘못된 페이지 번호, 페이지 범위 초과)를 모두 테스트하고 있어 API의 견고성을 보장합니다.

src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java (9)

26-28: 스트림 처리가 효율적입니다.

toList() 메서드 사용으로 간결하고 명확한 변환 로직을 구현했습니다.


40-57: BookDto 레코드 구조가 적절합니다.

네이버 API 응답을 클라이언트 응답으로 변환하는 매핑이 명확합니다. 필드명도 직관적이고 책 정보를 나타내기에 충분합니다.


20-39: PAGE_SIZE 고정값(10)으로 0으로 나누기 오류 없음
NaverApiUtil.PAGE_SIZEpublic static final int PAGE_SIZE = 10으로 정의되어 있어 0이 될 수 없습니다. 따라서 나누기 0 예외를 방지하기 위한 추가 검증은 필요하지 않습니다.
또한 빈 결과(totalElements == 0) 시 totalPages = 0, first = last = true가 되는데, 이 동작이 의도된 것이라면 별도 수정 없이도 괜찮습니다.


22-23: 페이지네이션 로직이 올바르게 구현되었습니다.

totalPages 계산과 last 페이지 판단 로직이 정확합니다. Math.ceil을 사용한 페이지 수 계산이 적절합니다.


26-28: 스트림 처리가 효율적으로 구현되었습니다.

NaverBook을 BookDto로 변환하는 로직이 깔끔하게 구현되었습니다.


40-56: BookDto 중첩 레코드 설계가 적절합니다.

정적 팩토리 메서드를 통한 객체 생성 패턴이 일관성 있게 적용되었습니다.


26-28: 스트림 처리 로직이 올바르게 구현되었습니다.

NaverBook을 BookDto로 변환하는 스트림 처리가 깔끔하게 구현되어 있습니다.


40-56: nested record 구현이 적절합니다.

BookDto record와 factory method가 명확하고 일관성 있게 구현되어 있습니다. 필드 매핑도 올바릅니다.


20-24: totalPages 계산에서 잠재적 오버플로우를 방지하세요.

Math.ceil((double) totalElements / PAGE_SIZE) 계산에서 매우 큰 숫자의 경우 정밀도 손실이나 오버플로우가 발생할 수 있습니다.

-        int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
+        int totalPages = (totalElements + PAGE_SIZE - 1) / PAGE_SIZE; // 정수 연산으로 ceiling 계산

Likely an incorrect or invalid review comment.

src/main/java/konkuk/thip/common/exception/code/ErrorCode.java (7)

19-25: 인증 관련 오류 코드가 잘 정의되었습니다.

모든 인증 오류가 HTTP 401 상태 코드를 사용하여 일관성이 있고, 오류 메시지도 명확합니다. 오류 코드 번호 체계(40100번대)도 적절합니다.


36-44: 책 검색 관련 오류 코드가 체계적으로 정의되었습니다.

네이버 API와의 연동에서 발생할 수 있는 다양한 오류 상황을 잘 커버하고 있습니다. 오류 코드 번호(80000번대)와 HTTP 400 상태 코드 사용도 적절합니다.


30-30: ALIAS_NOT_FOUND 오류 코드 변경 내역 재확인 요청

원격 기본 브랜치와의 diff 확인이 실패하여, ErrorCode.java에서 ALIAS_NOT_FOUND가 실제로 변경되었는지 다시 한번 검토가 필요합니다. 기존 API 응답에 영향이 없는지 아래 명령어로 확인해 주세요.

# 원격 브랜치 이름이 main이 아닐 경우 적절히 변경
git fetch origin
git diff origin/main -- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java

만약 여전히 diff가 보이지 않는다면, 먼저 원격 브랜치 목록을 확인한 뒤 올바른 브랜치 이름으로 재실행 부탁드립니다.


19-24: 인증 관련 에러 코드가 적절하게 추가되었습니다.

모든 인증 에러 코드가 HTTP 401 상태코드를 사용하며, 코드 번호 체계(40100번대)가 일관성 있게 적용되었습니다. 메시지도 명확하고 이해하기 쉽습니다.


30-30: ALIAS_NOT_FOUND enum 상수 위치 변경은 안전합니다.

  • Java enum 상수는 선언 순서와 무관하게 이름으로 참조되므로, 위치 변경이 코드호환성에 영향을 주지 않습니다.
  • 실제로 AliasCommandPersistenceAdapterUserCommandPersistenceAdapter에서 정상적으로 import static …ErrorCode.ALIAS_NOT_FOUND; 후 사용되고 있습니다.

19-24: 인증 관련 에러 코드들이 적절하게 정의되었습니다.

모든 인증 에러가 HTTP 401 상태 코드를 사용하고, 에러 코드 번호체계(40100-40105)도 일관성이 있습니다. 에러 메시지도 명확합니다.


33-35: 에러 코드 번호체계의 일관성을 확인하세요.

60000번대에서 80000번대로 점프하는 것이 프로젝트의 에러 코드 체계와 일치하는지 확인해주세요. 중간 번호대(70000번대)가 다른 용도로 예약되어 있는지 검토가 필요합니다.

Comment on lines 16 to 26
@GetMapping("/books")
public BaseResponse<GetBookSearchListResponse> getBookSearchList(@RequestParam final String keyword,
@RequestParam final int page) {
NaverBookParseResult result = bookSearchUseCase.searchBooks(keyword, page);
return BaseResponse.ok(GetBookSearchListResponse.of(result, page));
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

입력 검증 및 API 문서화 추가를 권장합니다.

현재 컨트롤러에 입력 검증과 API 문서화가 부족합니다. 다음 개선사항을 고려해보세요:

+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;

@GetMapping("/books")
+@Operation(summary = "도서 검색", description = "키워드로 도서를 검색합니다")
public BaseResponse<GetBookSearchListResponse> getBookSearchList(
-    @RequestParam final String keyword,
-    @RequestParam final int page) {
+    @Parameter(description = "검색 키워드") @RequestParam @NotBlank final String keyword,
+    @Parameter(description = "페이지 번호 (1부터 시작)") @RequestParam @Min(1) final int page) {
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/in/web/BookQueryController.java around
lines 16 to 22, the getBookSearchList method lacks input validation and API
documentation. Add validation annotations to the method parameters to ensure
keyword is not empty and page is a positive integer. Also, include API
documentation annotations such as Swagger's @Operation and @Parameter to
describe the endpoint and its parameters for better clarity and maintainability.

Comment on lines 15 to 34
private NaverApiUtil createTestUtil() {
NaverApiUtil util = Mockito.spy(new NaverApiUtil());
// @Value로 주입되는 필드를 직접 세팅
try {
Field clientIdField = NaverApiUtil.class.getDeclaredField("clientId");
clientIdField.setAccessible(true);
clientIdField.set(util, "dummy-client-id");

Field clientSecretField = NaverApiUtil.class.getDeclaredField("clientSecret");
clientSecretField.setAccessible(true);
clientSecretField.set(util, "dummy-client-secret");

Field bookSearchUrlField = NaverApiUtil.class.getDeclaredField("bookSearchUrl");
bookSearchUrlField.setAccessible(true);
bookSearchUrlField.set(util, "https://dummy-url.com/search?query=");
} catch (Exception e) {
throw new RuntimeException(e);
}
return util;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

테스트 설정 방식 개선을 권장합니다.

리플렉션을 사용한 필드 설정 방식은 취약하고 유지보수가 어렵습니다. 다음 대안들을 고려해보세요:

Option 1: 테스트 전용 생성자 추가

// NaverApiUtil에 패키지 가시성 생성자 추가
NaverApiUtil(String clientId, String clientSecret, String bookSearchUrl) {
    this.clientId = clientId;
    this.clientSecret = clientSecret; 
    this.bookSearchUrl = bookSearchUrl;
}

Option 2: @TestConfiguration 사용

@TestConfiguration
static class TestConfig {
    @Bean
    @Primary
    NaverApiUtil testNaverApiUtil() {
        return new NaverApiUtil("test-id", "test-secret", "test-url");
    }
}
🤖 Prompt for AI Agents
In src/test/java/konkuk/thip/book/adapter/out/api/NaverApiUtilTest.java around
lines 15 to 34, the test setup uses reflection to set private fields, which is
fragile and hard to maintain. Instead, add a package-private constructor in
NaverApiUtil that accepts clientId, clientSecret, and bookSearchUrl as
parameters and use it in the test to instantiate the object with test values.
Alternatively, create a @TestConfiguration class with a @Bean method that
returns a preconfigured NaverApiUtil instance for testing, avoiding reflection
altogether.



private String readBody(InputStream body){
InputStreamReader streamReader = new InputStreamReader(body);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

문자 인코딩을 명시적으로 지정해 주세요.

InputStreamReader에 문자 인코딩을 명시하지 않으면 플랫폼 기본 인코딩을 사용하여 예상치 못한 문자 깨짐이 발생할 수 있습니다.

-        InputStreamReader streamReader = new InputStreamReader(body);
+        InputStreamReader streamReader = new InputStreamReader(body, StandardCharsets.UTF_8);

그리고 파일 상단에 import를 추가해 주세요:

import java.nio.charset.StandardCharsets;
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java at line 92,
the InputStreamReader is created without specifying a character encoding, which
can cause character corruption due to platform default encoding. Fix this by
explicitly specifying StandardCharsets.UTF_8 when constructing the
InputStreamReader. Also, add the import statement 'import
java.nio.charset.StandardCharsets;' at the top of the file.

Comment on lines 68 to 70
} else { // 오류 발생
return readBody(con.getErrorStream());
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

에러 응답 처리를 개선해 주세요.

HTTP 오류 상황에서도 응답 본문을 반환하고 있는데, 이는 예상치 못한 동작을 야기할 수 있습니다. 오류 상황에서는 적절한 예외를 발생시키는 것이 좋습니다.

            int responseCode = con.getResponseCode();
            if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
                return readBody(con.getInputStream());
            } else { // 오류 발생
-                return readBody(con.getErrorStream());
+                String errorResponse = readBody(con.getErrorStream());
+                throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR);
            }
📝 Committable suggestion

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

Suggested change
} else { // 오류 발생
return readBody(con.getErrorStream());
}
int responseCode = con.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) { // 정상 호출
return readBody(con.getInputStream());
} else { // 오류 발생
String errorResponse = readBody(con.getErrorStream());
throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/NaverApiUtil.java around lines
68 to 70, the code currently returns the error response body directly when an
HTTP error occurs, which can cause unexpected behavior. Instead of returning the
error stream, modify the code to throw a meaningful exception that includes the
error details from the error stream. This will ensure proper error handling and
clearer failure signals to the caller.

Comment on lines 20 to 24
public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
int totalElements = result.total();
int totalPages = (int) Math.ceil((double) totalElements / PAGE_SIZE);
boolean last = (page >= totalPages);
boolean first = (page == 1);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 안전성과 null 체크 개선이 필요합니다.

result.total() 반환 타입이 int가 아닐 경우 캐스팅 오류가 발생할 수 있으며, result 매개변수에 대한 null 체크가 없습니다.

다음과 같이 개선할 수 있습니다:

public static GetBookSearchListResponse of(NaverBookParseResult result, int page) {
+   if (result == null) {
+       throw new IllegalArgumentException("NaverBookParseResult cannot be null");
+   }
+   if (page < 1) {
+       throw new IllegalArgumentException("Page number must be greater than 0");
+   }
    int totalElements = result.total();
🤖 Prompt for AI Agents
In
src/main/java/konkuk/thip/book/adapter/in/web/response/GetBookSearchListResponse.java
around lines 20 to 24, improve type safety and add null checks for the result
parameter. Ensure that result is not null before accessing its total method, and
verify or safely convert the return type of result.total() to int to avoid
casting errors. Add appropriate null checks and handle potential type mismatches
to prevent runtime exceptions.

Comment on lines 36 to 44
BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.BAD_REQUEST, 80000, "검색어 인코딩에 실패했습니다."),
BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."),
BOOK_NAVER_API_PARSING_ERROR(HttpStatus.BAD_REQUEST, 80002,"네이버 API 응답 파싱에 실패하였습니다."),
BOOK_NAVER_API_URL_ERROR(HttpStatus.BAD_REQUEST, 80003,"네이버 API URL이 잘못되었습니다."),
BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.BAD_REQUEST, 80005,"네이버 API 응답에 실패하였습니다."),
BOOK_SEARCH_PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, 80006,"검색어 페이지가 범위를 벗어났습니다."),
BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."),
BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다.");
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

일부 책 관련 에러의 HTTP 상태 코드를 재검토하세요.

네이버 API 연결 실패나 응답 에러의 경우 클라이언트 요청 문제가 아닌 서버 측 문제일 수 있으므로 HTTP 500 상태 코드가 더 적절할 수 있습니다.

-    BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."),
+    BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80001,"네이버 API 요청에 실패하였습니다."),
-    BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
+    BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
-    BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.BAD_REQUEST, 80005,"네이버 API 응답에 실패하였습니다."),
+    BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80005,"네이버 API 응답에 실패하였습니다."),
📝 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
BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.BAD_REQUEST, 80000, "검색어 인코딩에 실패했습니다."),
BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.BAD_REQUEST, 80001,"네이버 API 요청에 실패하였습니다."),
BOOK_NAVER_API_PARSING_ERROR(HttpStatus.BAD_REQUEST, 80002,"네이버 API 응답 파싱에 실패하였습니다."),
BOOK_NAVER_API_URL_ERROR(HttpStatus.BAD_REQUEST, 80003,"네이버 API URL이 잘못되었습니다."),
BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.BAD_REQUEST, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.BAD_REQUEST, 80005,"네이버 API 응답에 실패하였습니다."),
BOOK_SEARCH_PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, 80006,"검색어 페이지가 범위를 벗어났습니다."),
BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."),
BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다.");
BOOK_KEYWORD_ENCODING_FAILED(HttpStatus.BAD_REQUEST, 80000, "검색어 인코딩에 실패했습니다."),
BOOK_NAVER_API_REQUEST_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80001,"네이버 API 요청에 실패하였습니다."),
BOOK_NAVER_API_PARSING_ERROR(HttpStatus.BAD_REQUEST, 80002,"네이버 API 응답 파싱에 실패하였습니다."),
BOOK_NAVER_API_URL_ERROR(HttpStatus.BAD_REQUEST, 80003,"네이버 API URL이 잘못되었습니다."),
BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, 80004,"네이버 API 요청 중, HTTP 연결에 실패하였습니다."),
BOOK_NAVER_API_RESPONSE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80005,"네이버 API 응답에 실패하였습니다."),
BOOK_SEARCH_PAGE_OUT_OF_RANGE(HttpStatus.BAD_REQUEST, 80006,"검색어 페이지가 범위를 벗어났습니다."),
BOOK_KEYWORD_REQUIRED(HttpStatus.BAD_REQUEST, 80007, "검색어는 필수 입력값입니다."),
BOOK_PAGE_NUMBER_INVALID(HttpStatus.BAD_REQUEST, 80008, "페이지 번호는 1 이상의 값이어야 합니다.");
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/exception/code/ErrorCode.java around lines
36 to 44, some book-related error codes use HttpStatus.BAD_REQUEST (400) for
issues like Naver API connection failure or response errors, which are likely
server-side problems. Change the HTTP status codes for
BOOK_NAVER_API_REQUEST_ERROR, BOOK_NAVER_API_PARSING_ERROR,
BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED, and BOOK_NAVER_API_RESPONSE_ERROR from
HttpStatus.BAD_REQUEST to HttpStatus.INTERNAL_SERVER_ERROR (500) to better
reflect server-side errors.

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

🧹 Nitpick comments (9)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java (1)

5-11: QueryDSL 기반 리포지토리 구조가 적절히 준비되었습니다.

향후 복잡한 조회 로직 구현을 위한 기반이 잘 마련되었습니다. 다른 QueryDSL 구현체들과 일관된 패턴을 따르고 있습니다.

향후 QueryDSL 쿼리 구현 시 JPAQueryFactory 의존성 주입을 미리 준비하는 것을 고려해보세요:

+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+
 @Repository
+@RequiredArgsConstructor
 public class UserQueryRepositoryImpl implements UserQueryRepository {
+
+    private final JPAQueryFactory jpaQueryFactory;
 
     /**
      * QueryDsl 을 활용한 복잡한 조회 로직 구현
      */
 }
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (1)

20-40: 빈 결과에 대한 처리를 고려해보세요.

현재 구현은 정상적이지만, 데이터가 없을 때의 동작을 명시적으로 문서화하거나 로깅을 추가하는 것을 고려해볼 수 있습니다.

선택적으로 다음과 같은 개선을 고려할 수 있습니다:

@Override
public UserViewAliasChoiceResult getAllAliasesAndCategories() {
    QAliasJpaEntity alias = QAliasJpaEntity.aliasJpaEntity;
    QCategoryJpaEntity category = QCategoryJpaEntity.categoryJpaEntity;

    List<UserViewAliasChoiceResult.AliasChoice> aliasChoices = jpaQueryFactory
            .select(Projections.constructor(
                    UserViewAliasChoiceResult.AliasChoice.class,
                    alias.aliasId,
                    alias.value,
                    category.value,
                    alias.imageUrl,
                    alias.color
            ))
            .from(alias)
            .leftJoin(category)
            .on(category.aliasForCategoryJpaEntity.eq(alias))
            .orderBy(alias.aliasId.asc())
            .fetch();

+   // 로깅이나 빈 결과 처리 로직 추가 고려
    return new UserViewAliasChoiceResult(aliasChoices);
}
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (4)

67-71: JSON 응답 파싱 로직 개선 제안

응답을 파싱하기 위해 JsonNode를 직접 사용하고 있습니다. 더 간결하고 타입 안전한 방법으로 개선할 수 있습니다.

다음과 같이 리팩토링을 제안합니다:

-        String json = result.andReturn().getResponse().getContentAsString();
-        JsonNode jsonNode = objectMapper.readTree(json);
-        boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean();
-
-        assertThat(isVerified).isTrue();
+        result.andExpect(jsonPath("$.data.isVerified").value(true));

78-92: 테스트 데이터 설정 로직 중복

사용자 엔티티 생성 로직이 반복되고 있습니다. 테스트 헬퍼 메서드로 추출하여 중복을 제거할 수 있습니다.

다음과 같이 헬퍼 메서드를 추가하는 것을 제안합니다:

+    private UserJpaEntity createTestUser(String nickname, String oauth2Id) {
+        AliasJpaEntity aliasJpaEntity = AliasJpaEntity.builder()
+                .value("칭호")
+                .color("blue")
+                .imageUrl("http://image.url")
+                .build();
+        aliasJpaRepository.save(aliasJpaEntity);
+
+        UserJpaEntity userJpaEntity = UserJpaEntity.builder()
+                .nickname(nickname)
+                .imageUrl("http://image.url")
+                .role(USER)
+                .oauth2Id(oauth2Id)
+                .aliasForUserJpaEntity(aliasJpaEntity)
+                .build();
+        return userJpaRepository.save(userJpaEntity);
+    }

105-109: JSON 응답 파싱 로직 일관성

앞선 테스트와 동일한 JSON 파싱 패턴이 반복되고 있습니다. 일관성을 위해 동일한 개선 방법을 적용하는 것을 권장합니다.

앞서 제안한 것과 동일하게 jsonPath 매처를 사용하여 개선할 수 있습니다:

-        String json = result.andReturn().getResponse().getContentAsString();
-        JsonNode jsonNode = objectMapper.readTree(json);
-        boolean isVerified = jsonNode.path("data").path("isVerified").asBoolean();
-
-        assertThat(isVerified).isFalse();
+        result.andExpect(jsonPath("$.data.isVerified").value(false));

124-124: 에러 메시지 검증 개선

에러 메시지를 containsString으로 부분 검증하고 있습니다. 더 정확한 검증을 위해 전체 메시지를 확인하는 것을 고려해보세요.

-                .andExpect(jsonPath("$.message", containsString("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)")));
+                .andExpect(jsonPath("$.message").value("닉네임은 한글, 영어, 숫자로만 구성되어야 합니다.(공백불가)"));
src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (2)

66-77: 응답 검증 로직 개선

복잡한 JSON 파싱과 객체 변환 로직을 사용하고 있습니다. jsonPath를 더 활용하여 간소화할 수 있습니다.

다음과 같이 개선을 제안합니다:

         result.andExpect(status().isOk())
-                .andExpect(jsonPath("$.data.aliasChoices").exists());
-
-        String json = result.andReturn().getResponse().getContentAsString();
-        JsonNode jsonNode = objectMapper.readTree(json);
-        UserViewAliasChoiceResponse showResponse = objectMapper.treeToValue(jsonNode.get("data"), UserViewAliasChoiceResponse.class);
-        List<UserViewAliasChoiceResponse.AliasChoice> choices = showResponse.aliasChoices();
-
-        assertThat(choices).hasSize(2);
-        assertThat(choices)
-                .extracting("aliasName", "categoryName", "imageUrl", "color")
-                .containsExactlyInAnyOrder(
-                        tuple("문학가", "문학", "문학가_image", "red"),
-                        tuple("과학자", "과학/IT", "과학자_image", "blue")
-                );
+                .andExpect(jsonPath("$.data.aliasChoices").exists())
+                .andExpect(jsonPath("$.data.aliasChoices").isArray())
+                .andExpect(jsonPath("$.data.aliasChoices", hasSize(2)))
+                .andExpect(jsonPath("$.data.aliasChoices[?(@.aliasName == '문학가' && @.categoryName == '문학')]").exists())
+                .andExpect(jsonPath("$.data.aliasChoices[?(@.aliasName == '과학자' && @.categoryName == '과학/IT')]").exists());

80-106: 테스트 데이터 생성 헬퍼 메서드 구조 개선

헬퍼 메서드가 좋은 접근이지만, 매직 넘버와 하드코딩된 값들을 개선할 수 있습니다.

상수를 도입하여 가독성을 높이는 것을 제안합니다:

+    private static final String TEST_ALIAS_1 = "문학가";
+    private static final String TEST_CATEGORY_1 = "문학";
+    private static final String TEST_ALIAS_2 = "과학자";
+    private static final String TEST_CATEGORY_2 = "과학/IT";
+
     private void saveAliasesAndCategories() {
         AliasJpaEntity alias1 = AliasJpaEntity.builder()
-                .value("문학가")
+                .value(TEST_ALIAS_1)
                 .imageUrl("문학가_image")
                 .color("red")
                 .build();
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (1)

171-171: 테스트 데이터의 보안 검토

정적 분석 도구에서 API 키로 감지된 "kakao_12345678"은 실제로는 테스트용 OAuth2 ID입니다. 하지만 더 명확하게 테스트 데이터임을 표시할 수 있습니다.

테스트 데이터임을 더 명확하게 하기 위해 다음과 같이 개선을 제안합니다:

-        String expectedOauth2Id = "kakao_12345678";
+        String expectedOauth2Id = "test_kakao_12345678"; // 테스트용 OAuth2 ID
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between afb5ac5 and a4374c2.

📒 Files selected for processing (35)
  • build.gradle (1 hunks)
  • src/main/java/konkuk/thip/config/QuerydslConfig.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserVerifyNicknameResponse.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserViewAliasChoiceResponse.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java (1 hunks)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/UserVerifyNicknameUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/UserViewAliasChoiceUseCase.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyCommand.java (0 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyQuery.java (0 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java (0 hunks)
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserViewAliasChoiceResult.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java (1 hunks)
  • src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java (1 hunks)
  • src/test/java/konkuk/thip/config/TestQuerydslConfig.java (1 hunks)
  • src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java (2 hunks)
  • src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java (3 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (6 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (1 hunks)
  • src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java (2 hunks)
💤 Files with no reviewable changes (3)
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyQuery.java
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyResult.java
  • src/main/java/konkuk/thip/user/application/port/in/dto/DummyCommand.java
✅ Files skipped from review due to trivial changes (8)
  • src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepository.java
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserVerifyNicknameResponse.java
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserVerifyNicknameRequest.java
  • src/main/java/konkuk/thip/config/QuerydslConfig.java
  • src/test/java/konkuk/thip/config/TestQuerydslConfig.java
  • src/main/java/konkuk/thip/user/application/port/in/dto/UserViewAliasChoiceResult.java
  • src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryPersistenceAdapter.java
  • src/main/java/konkuk/thip/user/adapter/in/web/response/UserViewAliasChoiceResponse.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/test/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntityTest.java
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntityTest.java
  • src/test/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntityTest.java
  • src/test/java/konkuk/thip/room/adapter/out/jpa/VoteJpaEntityTest.java
  • src/test/java/konkuk/thip/room/adapter/out/jpa/RecordJpaEntityTest.java
  • src/main/java/konkuk/thip/user/adapter/in/web/request/UserSignupRequest.java
🧰 Additional context used
🧠 Learnings (2)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (1)
Learnt from: seongjunnoh
PR: THIP-TextHip/THIP-Server#36
File: src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java:7-7
Timestamp: 2025-06-29T09:47:31.299Z
Learning: Spring Data JPA에서 findBy{FieldName} 패턴의 메서드는 명시적 선언 없이 자동으로 생성되며, Optional<Entity> 반환 타입을 사용하는 것이 null 안전성을 위해 권장됩니다.
build.gradle (1)
Learnt from: hd0rable
PR: THIP-TextHip/THIP-Server#36
File: build.gradle:30-30
Timestamp: 2025-06-29T09:17:39.787Z
Learning: THIP-Server 프로젝트에서 Redis 의존성(spring-boot-starter-data-redis)은 향후 사용 예정으로 의도적으로 추가되었음. 현재는 사용하지 않지만 추후 개발에서 활용할 계획임.
🧬 Code Graph Analysis (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryRepositoryImpl.java (1)
src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (1)
  • Repository (13-41)
🪛 Gitleaks (8.26.0)
src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java

171-171: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (23)
src/main/java/konkuk/thip/user/application/port/out/UserQueryPort.java (1)

5-5: 닉네임 중복 확인 메서드가 적절히 추가되었습니다.

메서드명과 시그니처가 명확하고, 포트 인터페이스로서 적절한 추상화 수준을 유지하고 있습니다.

src/main/java/konkuk/thip/user/application/port/in/UserVerifyNicknameUseCase.java (1)

1-6: 닉네임 검증 유스케이스가 잘 정의되었습니다.

단일 책임 원칙을 따르며, 메서드명이 의도를 명확히 표현하고 있습니다. 헥사고날 아키텍처의 인바운드 포트 역할을 적절히 수행합니다.

src/main/java/konkuk/thip/user/application/port/in/UserViewAliasChoiceUseCase.java (1)

1-8: 별칭 조회 유스케이스가 명확하게 정의되었습니다.

메서드명이 기능을 잘 표현하고 있으며, 적절한 DTO를 리턴타입으로 사용하고 있습니다. 헥사고날 아키텍처 패턴을 잘 따르고 있습니다.

src/main/java/konkuk/thip/user/application/port/out/AliasQueryPort.java (1)

1-8: 별칭 조회 포트가 적절히 정의되었습니다.

인바운드 포트와 일관된 메서드명을 사용하고 있으며, 포트 인터페이스로서 적절한 추상화 수준을 유지하고 있습니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepository.java (1)

5-8: 인터페이스 설계가 깔끔합니다.

리포지토리 패턴을 잘 따르고 있으며, 메서드명이 명확하고 반환 타입도 적절합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/UserQueryPersistenceAdapter.java (2)

12-12: 필드명 변경이 코드 가독성을 향상시킵니다.

jpaRepository에서 userJpaRepository로 변경하여 필드의 목적이 더 명확해졌습니다.


15-18: 닉네임 존재 여부 확인 로직이 올바릅니다.

리포지토리에 위임하는 구조가 적절하며, 메서드 구현이 정확합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/AliasJpaRepository.java (1)

6-6: 인터페이스 확장이 Spring Data JPA 모범 사례를 따릅니다.

표준 JPA 리포지토리 기능과 커스텀 쿼리 기능을 적절히 결합했습니다.

src/main/java/konkuk/thip/user/application/service/UserVerifyNicknameService.java (1)

8-18: 서비스 구현이 클린 아키텍처 원칙을 잘 따릅니다.

의존성 주입과 포트 활용이 적절하며, 닉네임 유일성 검증 로직(!userQueryPort.existsByNickname)이 정확합니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/AliasQueryRepositoryImpl.java (1)

24-37: QueryDSL 쿼리 구현이 올바르게 작성되었습니다.

LEFT JOIN을 사용하여 카테고리가 없는 별칭도 포함시키는 것이 적절하며, Projections.constructor를 통한 DTO 매핑도 깔끔합니다. 조인 조건(category.aliasForCategoryJpaEntity.eq(alias))도 엔티티 관계를 올바르게 반영하고 있습니다.

src/main/java/konkuk/thip/user/adapter/in/web/UserQueryController.java (1)

14-21: 새로운 별명 선택 API 엔드포인트가 잘 구현되었습니다.

Clean Architecture 원칙을 잘 따르고 있으며, 기존 코드 패턴과 일관성을 유지하고 있습니다. BaseResponse로 응답을 래핑하는 것도 좋은 설계입니다.

src/main/java/konkuk/thip/user/application/service/UserViewAliasChoiceService.java (1)

9-18: Clean Architecture 원칙을 잘 준수한 서비스 구현입니다.

Use Case 인터페이스를 구현하여 애플리케이션 계층의 역할을 명확히 하고, 포트를 통한 위임으로 관심사 분리가 잘 되어 있습니다.

src/main/java/konkuk/thip/user/adapter/out/persistence/UserJpaRepository.java (1)

8-10: Spring Data JPA 모범 사례를 잘 적용했습니다.

과거 리뷰 논의에서 제기된 Optional 반환 타입 사용 권장사항이 잘 반영되었습니다. findByOauth2Id에서 Optional<UserJpaEntity>을 사용하여 null 안전성을 확보했으며, existsByNickname은 표준 Spring Data JPA 패턴을 따르고 있습니다.

src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java (2)

30-38: OAuth2 기반 회원가입 API가 잘 구현되었습니다.

@Oauth2Id 어노테이션을 통한 OAuth2 ID 주입과 JWT 토큰 생성 및 응답 헤더 설정이 적절하게 구현되어 있습니다. RESTful API 설계 원칙에도 부합합니다.


40-45: 별명 중복 검사 API가 간결하게 구현되었습니다.

요청 유효성 검사와 응답 래핑이 일관된 패턴으로 잘 구현되어 있습니다.

build.gradle (2)

37-46: 보안 관련 의존성이 적절하게 추가되었습니다.

Spring Security, JWT (0.12.3), OAuth2 Client 의존성이 모두 최신 버전으로 추가되어 있어 보안 기능 구현에 필요한 라이브러리가 잘 준비되었습니다.


59-84: Querydsl 설정이 매우 상세하고 올바르게 구성되었습니다.

Jakarta 네임스페이스 사용, Q-클래스 생성 디렉토리 설정, 테스트 컴파일 시 중복 생성 방지, 소스 세트 포함, clean 시 정리 등 모든 측면이 체계적으로 설정되어 있습니다. 이는 Querydsl을 프로덕션 환경에서 안정적으로 사용할 수 있는 좋은 기반을 제공합니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserViewAliasChoiceControllerTest.java (1)

29-31: 보안 필터 비활성화 검증 완료
UserQueryController.showAliasChoiceView()에 별도의 인증·인가 어노테이션이 없어 해당 엔드포인트는 공개 API로 동작합니다.
테스트에서 @AutoConfigureMockMvc(addFilters = false)로 보안 필터를 비활성화해도 프로덕션 동작과 일치하므로 추가 수정은 필요 없습니다.

src/test/java/konkuk/thip/user/adapter/in/web/UserSignupControllerTest.java (5)

47-48: JWT 유틸리티 의존성 추가 확인

JwtUtil이 테스트에 새로 추가되었습니다. 테스트 전용 설정이나 모킹이 필요한지 확인이 필요합니다.

JWT 기반 인증 시스템 도입에 따른 자연스러운 의존성 추가입니다.


50-54: 테스트 격리를 위한 정리 로직 추가

@AfterEach 메서드를 추가하여 테스트 간 격리를 보장하고 있습니다. 좋은 개선사항입니다.

테스트 격리를 위한 적절한 정리 로직이 추가되었습니다.


72-75: JWT 임시 토큰 기반 인증 플로우

OAuth2 기반 회원가입 플로우에 맞춘 임시 토큰 생성 및 헤더 설정이 적절히 구현되어 있습니다.

새로운 인증 플로우에 맞는 올바른 구현입니다.


154-193: OAuth2 ID 추출 검증 테스트

JWT 토큰에서 OAuth2 ID를 정확히 추출하는지 검증하는 중요한 테스트입니다. 로직이 올바르게 구현되어 있습니다.

새로운 토큰 기반 인증 시스템의 핵심 기능을 잘 테스트하고 있습니다.


196-211: 인증 실패 케이스 테스트

토큰 없이 요청했을 때의 401 에러 처리를 적절히 테스트하고 있습니다.

보안 요구사항에 맞는 적절한 에러 처리 테스트입니다.

Comment on lines 29 to 31
@SpringBootTest
@ActiveProfiles("test")
@AutoConfigureMockMvc(addFilters = false)
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

테스트 설정 검토: 보안 필터 비활성화

@AutoConfigureMockMvc(addFilters = false) 설정으로 보안 필터를 비활성화하고 있습니다. 이는 닉네임 검증 API가 인증이 필요 없는 공개 API임을 의미하는 것으로 보입니다. 실제 API 명세와 일치하는지 확인이 필요합니다.

다음 스크립트를 실행하여 닉네임 검증 API의 보안 설정을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# 닉네임 검증 API의 보안 설정 확인
ast-grep --pattern $'@PostMapping("/users/nickname")
$$$'

Length of output: 58


🏁 Script executed:

#!/bin/bash
# 닉네임 검증 API 컨트롤러 매핑 및 보안 어노테이션 확인
rg "@PostMapping\\(\"/users/nickname\"\\)" -C5 -n src

Length of output: 1436


🏁 Script executed:

#!/bin/bash
# 보안 구성 파일에서 인증 및 인가 설정 검색
echo "> WebSecurityConfigurerAdapter 여부"
rg "WebSecurityConfigurerAdapter" -n src || true

echo "> SecurityFilterChain 빈 정의 여부"
rg "SecurityFilterChain" -n src || true

echo "> HttpSecurity 설정 (authorize, antMatchers 등)"
rg "HttpSecurity" -n src || true
rg "authorizeRequests" -n src || true
rg "authorizeHttpRequests" -n src || true
rg "antMatchers" -n src || true
rg "anyRequest" -n src || true

Length of output: 1220


🏁 Script executed:

#!/bin/bash
# SecurityConfig에서 공개 API 설정 확인
rg "permitAll" -n src

# users/nickname 경로에 대한 보안 설정 확인
rg "\"/users/nickname\"" -n src

Length of output: 1009


🏁 Script executed:

#!/bin/bash
# WHITELIST 상수 정의 및 경로 확인
rg "WHITELIST" -n -C5 src/main/java/konkuk/thip/config/SecurityConfig.java

Length of output: 1275


보안 필터 비활성화 검토: /users/nickname 엔드포인트 보안 조건 확인 필요
현재 SecurityConfig에서 POST /users/nickname은 WHITELIST에 포함되지 않아 인증이 필요한 상태입니다.
테스트에서 @AutoConfigureMockMvc(addFilters = false)로 보안 필터를 비활성화하면 실제 보안 설정과 불일치하여 테스트가 놓치는 부분이 생길 수 있습니다.

– 다음 중 하나를 선택해 조치해주세요:
• API가 공개(public) 엔드포인트여야 한다면
- SecurityConfig.WHITELIST 배열에 "/users/nickname" 경로 추가
• API가 인증(protected) 엔드포인트여야 한다면
- 테스트에서 addFilters = false 제거 후 보안 필터 적용하여 인증/인가 시나리오 추가

확인 파일:

  • src/main/java/konkuk/thip/config/SecurityConfig.java
  • src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java
🤖 Prompt for AI Agents
In
src/test/java/konkuk/thip/user/adapter/in/web/UserVerifyNicknameControllerTest.java
around lines 29 to 31, the test disables security filters with
@AutoConfigureMockMvc(addFilters = false), which causes a mismatch with the
actual security configuration where POST /users/nickname requires
authentication. To fix this, either add "/users/nickname" to the WHITELIST array
in src/main/java/konkuk/thip/config/SecurityConfig.java if the endpoint should
be public, or remove addFilters = false from the test annotation and update the
test to include authentication and authorization scenarios to reflect the
protected status of the endpoint.

@buzz0331 buzz0331 force-pushed the feat/#30-authentication-authorization branch from a4374c2 to 533775d Compare June 29, 2025 18:48
@hd0rable hd0rable self-requested a review June 29, 2025 18:51
hd0rable
hd0rable previously approved these changes Jun 29, 2025
Copy link
Member

@hd0rable hd0rable left a comment

Choose a reason for hiding this comment

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

👍🏻👍🏻

@buzz0331 buzz0331 merged commit 46e7800 into develop Jun 29, 2025
1 check passed
@buzz0331 buzz0331 deleted the feat/#30-authentication-authorization branch June 29, 2025 18:55
@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[THIP2025-64] [feat] 인증.인가 로직 + 로그인 api 개발

3 participants