Skip to content

Conversation

@anxi01
Copy link
Member

@anxi01 anxi01 commented Jan 4, 2026

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

🚀 작업 내용

이번 PR에서 작업한 내용을 구체적으로 설명해주세요. (이미지 첨부 가능)

  • CustomAdminDetails를 생성해서 AdminUser 엔티티를 UserDetails로 감싸 Security에서 사용했습니다.
    • AdminUser status(PENDING_APPROVAL, APPROVED, MASTER)를 CustomAdminDetails의 authorities로 설정했습니다.
  • 로그인 안하고 접근할 때 발생한 에러 (401), 로그인 후 Role(ADMIN, USER)이 달라 발생한 에러 (403)를 JSON으로 반환하는 클래스 추가했습니다.
  • AdminAuthorizationUtil에서 SecurityContext의 adminId를 가져오도록 수정하여 기존 AdminController, BannerController의 요청 파라미터의 Authorization 헤더를 제거하였습니다.
  • JwtFilter, SecurityConfig 중복으로 요청 인증, 인가 권한을 관리하고 있어 이를 SecurityConfig로 이관 및 통합하였습니다.
  • 테스트 코드에서 ControllerTestConfig을 AdminControllerTestConfig, UserControllerTestConfig 으로 분리했습니다.
    • User, FixedScheduleController는 UserControllerTestConfig 설정
    • Admin, BannerController는 AdminControllerTestConfig 설정
    • 이외 Controller는 ControllerTestConfig으로 설정

로컬에서 테스트 진행했습니다.


📝️ 관련 이슈

본인이 작업한 내용이 어떤 Issue와 관련이 있는지 작성해주세요.


💬 기타 사항 or 추가 코멘트

남기고 싶은 말, 참고 블로그 등이 있다면 기록해주세요.

현재 토큰이 없을 경우 에러가 아래와 같이 변경됩니다. 에러는 기존에 정의되어있는 에러 코드를 사용했는데 기존 에러 반환 혹 에러 코드 수정이 필요해 보입니다.

AS-IS

{
  "is_success": false,
  "code": "TOKEN-008",
  "message": "Authorization 헤더가 존재하지 않거나 형식이 잘못되었습니다.",
  "payload": null
}

TO-BE

{
  "is_success": false,
  "code": "E_UNAUTHORIZED",
  "message": "인증이 필요 합니다.",
  "payload": null
}

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 인증 실패 및 접근 거부 시 개선된 오류 메시지 제공
    • 관리자 역할 기반 접근 제어 강화
  • 버그 수정

    • 보안 요청 처리 및 인증 검증 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@anxi01 anxi01 self-assigned this Jan 4, 2026
@anxi01 anxi01 requested a review from bbbang105 as a code owner January 4, 2026 10:17
@anxi01 anxi01 added 🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 😉 seongmin 성민 PR labels Jan 4, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 4, 2026

Walkthrough

이 PR은 수동 Authorization 헤더 기반 인증에서 Spring Security의 네이티브 인증 컨텍스트로 전환합니다. 새로운 컴포넌트가 JWT 토큰 검증, 관리자/사용자 타입 감지, 커스텀 예외 응답을 처리하며, 컨트롤러와 서비스는 더 이상 Authorization 헤더를 받지 않습니다.

Changes

응집(Cohort) / 파일 변경 요약
인증 및 권한 관리 (Authentication & Authorization)
src/main/java/side/onetime/auth/dto/CustomAdminDetails.java, src/main/java/side/onetime/auth/dto/CustomUserDetails.java, src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java
CustomAdminDetails 새로운 레코드 추가 (AdminUser 래핑, UserDetails 구현); CustomUserDetails 메서드 정리 (getAuthorities 추가, 상태 메서드 제거); CustomAdminDetailsService 구현 (name/ID 기반 관리자 로딩)
Spring Security 예외 처리
src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java, src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java
두 개의 커스텀 예외 핸들러 추가: 접근 거부 및 인증 실패 시 JSON 에러 응답 제공
Spring Security 설정
src/main/java/side/onetime/global/config/SecurityConfig.java
역할 기반 접근 제어 개선 (AUTHENTICATED_USER_URLS, AUTHENTICATED_ADMIN_URLS 분리); 커스텀 예외 핸들러 통합; 이중 역할 검증 구현
JWT 필터 및 토큰 유틸
src/main/java/side/onetime/global/filter/JwtFilter.java, src/main/java/side/onetime/util/JwtUtil.java
JwtFilter: Bearer 토큰 추출 검증, 사용자 타입 기반 로딩 추가; shouldNotFilter 간소화; JwtUtil: admin 관련 메서드 제거
권한 인증 유틸
src/main/java/side/onetime/util/AdminAuthorizationUtil.java, src/main/java/side/onetime/util/UserAuthorizationUtil.java
AdminAuthorizationUtil 신규 추가 (SecurityContext에서 관리자 ID 추출); UserAuthorizationUtil 업데이트 (Optional 기반 null 안전성 개선)
관리자 API 마이그레이션
src/main/java/side/onetime/controller/AdminController.java, src/main/java/side/onetime/service/AdminService.java
모든 메서드 시그니처에서 authorizationHeader 파라미터 제거; AdminAuthorizationUtil 기반 서버 사이드 인증으로 전환
배너 API 마이그레이션
src/main/java/side/onetime/controller/BannerController.java, src/main/java/side/onetime/service/BannerService.java
10개 엔드포인트에서 authorizationHeader 제거; AdminAuthorizationUtil 및 AdminRepository 활용한 관리자 검증 추가
저장소 업데이트
src/main/java/side/onetime/repository/AdminRepository.java
findByName(String) 쿼리 메서드 추가 (이름 기반 관리자 조회)
오류 상태 확장
src/main/java/side/onetime/global/common/status/ErrorStatus.java
_UNIDENTIFIED_USER 상수 추가 (인증 처리 중 서버 오류)
테스트 설정 및 구성
src/test/java/side/onetime/configuration/AdminControllerTestConfig.java, src/test/java/side/onetime/configuration/UserControllerTestConfig.java, src/test/java/side/onetime/configuration/ControllerTestConfig.java
새로운 AdminControllerTestConfig, UserControllerTestConfig 추가 (역할별 인증 컨텍스트); ControllerTestConfig 업데이트 (MockBean 추가, 컨텍스트 초기화)
컨트롤러 테스트 업데이트
src/test/java/side/onetime/admin/AdminControllerTest.java, src/test/java/side/onetime/banner/BannerControllerTest.java, src/test/java/side/onetime/user/UserControllerTest.java, src/test/java/side/onetime/fixed/FixedControllerTest.java
테스트 설정 변경 (AdminControllerTestConfig/UserControllerTestConfig 상속); 서비스 메서드 호출 시 authorizationHeader 파라미터 제거; Authorization 헤더 테스트 요청 제거
기타 테스트 정리
src/test/java/side/onetime/event/EventControllerTest.java, src/test/java/side/onetime/member/MemberControllerTest.java, src/test/java/side/onetime/schedule/ScheduleControllerTest.java, src/test/java/side/onetime/token/TokenControllerTest.java, src/test/java/side/onetime/url/UrlControllerTest.java
JwtUtil, CustomUserDetailsService MockBean 제거; 불필요한 임포트 정리

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant JwtFilter
    participant JWT as JWT Validator
    participant SC as SecurityContext
    participant AdminDetailsSvc as CustomAdminDetailsService
    participant UserDetailsSvc as CustomUserDetailsService
    participant Repository
    
    Client->>JwtFilter: HTTP Request + Authorization Header
    JwtFilter->>JwtFilter: Extract Bearer Token
    alt Token Valid
        JwtFilter->>JWT: Validate Token
        JWT-->>JwtFilter: Token Valid (userType, userId)
        alt userType == ADMIN
            JwtFilter->>AdminDetailsSvc: loadAdminByAdminId(userId)
            AdminDetailsSvc->>Repository: findById(userId)
            Repository-->>AdminDetailsSvc: AdminUser
            AdminDetailsSvc->>AdminDetailsSvc: Wrap in CustomAdminDetails
            AdminDetailsSvc-->>JwtFilter: CustomAdminDetails (UserDetails)
        else userType == USER
            JwtFilter->>UserDetailsSvc: loadUserByUserId(userId)
            UserDetailsSvc->>Repository: findById(userId)
            Repository-->>UserDetailsSvc: User
            UserDetailsSvc->>UserDetailsSvc: Wrap in CustomUserDetails
            UserDetailsSvc-->>JwtFilter: CustomUserDetails (UserDetails)
        end
        JwtFilter->>SC: Set Authentication (UsernamePasswordAuthenticationToken)
        JwtFilter-->>Client: Continue to Controller
    else Token Invalid/Expired
        JwtFilter->>JwtFilter: Forward to CustomAuthenticationEntryPoint
        JwtFilter-->>Client: 401 Unauthorized (JSON Response)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • bbbang105

Poem

🐰 Security's dance, now clean and bright,
No headers passed, just Spring's delight!
Context flows where tokens lead,
Type-aware auth fulfills the need!
A rabbit hops through code so tight,
Authentication shines alight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 변경사항의 핵심인 JwtFilter 인증 로직을 Security FilterChain으로 통합하는 내용을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿의 모든 주요 섹션(PR 유형, 작업 내용, 관련 이슈)을 포함하고 있으며, 변경사항을 구체적으로 설명하고 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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 (12)
src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java (1)

34-41: ObjectMapper를 사용한 JSON 직렬화를 고려하세요.

현재 문자열 연결을 통한 수동 JSON 생성은 작동하지만, ObjectMapper를 사용하면 더 안전하고 유지보수가 쉽습니다. CustomAccessDeniedHandler와 동일한 패턴이 반복되고 있으며, 메시지에 특수 문자(예: 따옴표)가 포함될 경우 이스케이프 문제가 발생할 수 있습니다.

🔎 ObjectMapper를 사용한 개선 제안

공통 응답 DTO를 생성하고 ObjectMapper로 직렬화하는 방식으로 리팩토링할 수 있습니다:

+import com.fasterxml.jackson.databind.ObjectMapper;
+
 @Slf4j
 @Component
 public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+    private final ObjectMapper objectMapper = new ObjectMapper();

     @Override
     public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
         log.error("❌ 인증되지 않은 접근 - 요청 URI: {}, 메서드: {}", request.getRequestURI(), request.getMethod());

         ErrorStatus status = ErrorStatus._UNAUTHORIZED;

         response.setStatus(status.getHttpStatus().value());
         response.setContentType("application/json;charset=UTF-8");
-        response.getWriter().write(
-                "{"
-                        + "\"is_success\": false,"
-                        + "\"code\": \"" + status.getCode() + "\","
-                        + "\"message\": \"" + status.getMessage() + "\","
-                        + "\"payload\": null"
-                        + "}"
-        );
+        
+        Map<String, Object> errorResponse = Map.of(
+            "is_success", false,
+            "code", status.getCode(),
+            "message", status.getMessage(),
+            "payload", (Object) null
+        );
+        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
     }
 }

더 나은 방법으로는, 프로젝트의 기존 응답 DTO 구조(ApiResponse 등)를 재사용하는 것을 권장합니다.

src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java (1)

38-45: JSON 응답 생성 로직의 중복을 제거하세요.

CustomAuthenticationEntryPoint와 동일한 JSON 생성 패턴이 반복되고 있습니다. 두 핸들러 모두 동일한 방식으로 에러 응답을 구성하므로, 공통 유틸리티 메서드나 ObjectMapper를 사용하여 코드 중복을 제거하는 것이 좋습니다.

🔎 공통 응답 빌더 제안

두 핸들러에서 공유할 수 있는 유틸리티 메서드를 생성할 수 있습니다:

// SecurityResponseUtil.java (새 파일)
public class SecurityResponseUtil {
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    public static void writeErrorResponse(HttpServletResponse response, ErrorStatus status) throws IOException {
        response.setStatus(status.getHttpStatus().value());
        response.setContentType("application/json;charset=UTF-8");
        
        Map<String, Object> errorResponse = Map.of(
            "is_success", false,
            "code", status.getCode(),
            "message", status.getMessage(),
            "payload", (Object) null
        );
        response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
    }
}

그런 다음 두 핸들러에서 사용:

 @Override
 public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
     Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
     String username = (authentication != null) ? authentication.getName() : "ANONYMOUS";
     log.error("❌ 금지된 접근 - 사용자: {}, 요청 URI: {}, 메서드: {}", username, request.getRequestURI(), request.getMethod());

-    ErrorStatus status = ErrorStatus._FORBIDDEN;
-
-    response.setStatus(status.getHttpStatus().value());
-    response.setContentType("application/json;charset=UTF-8");
-    response.getWriter().write(
-            "{"
-                    + "\"is_success\": false,"
-                    + "\"code\": \"" + status.getCode() + "\","
-                    + "\"message\": \"" + status.getMessage() + "\","
-                    + "\"payload\": null"
-                    + "}"
-    );
+    SecurityResponseUtil.writeErrorResponse(response, ErrorStatus._FORBIDDEN);
 }
src/main/java/side/onetime/auth/dto/CustomAdminDetails.java (2)

33-51: 계정 상태 검증 로직이 누락되었습니다.

모든 계정 상태 메서드가 무조건 true를 반환합니다. 실제 운영 환경에서는 관리자 계정의 잠금, 만료, 자격 증명 만료 등을 처리해야 할 수 있습니다. 현재는 PENDING_APPROVAL 상태의 관리자도 계정이 활성화된 것으로 간주됩니다.

🔎 계정 상태 검증을 추가하는 제안
 @Override
 public boolean isEnabled() {
-    return true;
+    return admin.getAdminStatus() != AdminStatus.PENDING_APPROVAL;
 }

또는 PENDING_APPROVAL 상태의 관리자가 로그인할 수 있어야 한다면, 권한 검사로만 제한하는 현재 방식이 적절할 수 있습니다. 비즈니스 요구사항을 확인하세요.


15-21: AdminStatus 처리는 의도된 설계이며 올바릅니다.

AdminStatus의 세 가지 값(PENDING_APPROVAL, APPROVED, MASTER)이 모두 적절히 처리되고 있습니다:

  • PENDING_APPROVAL: ROLE_PENDING_APPROVAL (로그인 시 서비스 레이어에서 예외 처리)
  • APPROVED, MASTER: ROLE_ADMIN (동일 권한 공유)

Master 레벨의 권한 검증은 AdminService에서 !AdminStatus.MASTER.equals() 체크를 통해 서비스 계층에서 수행되므로 역할 레벨에서 분리할 필요가 없습니다.

다만 DTO 가이드라인 준수 관점에서 개선이 필요합니다: CustomAdminDetailsAdminUserDetailResponse처럼 정적 팩토리 메서드(from() 또는 of())를 추가하고, 다른 DTO들과 일관성 있게 src/main/java/side/onetime/dto/admin/response/ 위치로 이동하는 것을 고려하세요. (Spring Security UserDetails 구현이므로 현재 위치도 이해할 수 있으나, 프로젝트 DTO 구조와 일관성 유지가 권장됨)

src/test/java/side/onetime/configuration/UserControllerTestConfig.java (1)

13-13: Mock User의 ID가 null일 수 있습니다.

빌더로 생성한 mockUser에 ID가 설정되지 않아 null이 됩니다. 일부 테스트에서 사용자 ID가 필요한 경우 NullPointerException이 발생할 수 있습니다.

🔎 ID를 명시적으로 설정하는 제안

Reflection이나 테스트용 생성자를 사용하여 ID를 설정할 수 있습니다:

-User mockUser = User.builder().nickname("testUser").email("[email protected]").build();
+User mockUser = User.builder().nickname("testUser").email("[email protected]").build();
+// Reflection을 사용하여 ID 설정 (필요한 경우)
+ReflectionTestUtils.setField(mockUser, "id", 1L);

단, 현재 테스트들이 ID를 필요로 하지 않는다면 이 변경은 불필요할 수 있습니다.

src/test/java/side/onetime/configuration/AdminControllerTestConfig.java (1)

15-15: 모든 관리자 테스트에 MASTER 권한을 부여합니다.

AdminControllerTestConfig를 사용하는 모든 테스트가 MASTER 상태의 관리자로 실행됩니다. APPROVED 또는 PENDING_APPROVAL 상태의 관리자에 대한 권한 검증 테스트가 필요한 경우, 개별 테스트에서 인증을 재설정해야 합니다.

필요하다면 다양한 관리자 상태에 대한 테스트 설정을 추가하는 것을 고려하세요:

protected void setAdminAuthentication(AdminStatus status) {
    AdminUser mockAdmin = AdminUser.builder().name("testAdmin").email("[email protected]").build();
    mockAdmin.updateAdminStatus(status);
    // ... rest of setup
}
src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java (1)

47-51: throws UsernameNotFoundException 선언 불필요

loadAdminByAdminId 메서드의 throws UsernameNotFoundException 선언은 실제로 해당 예외를 던지지 않으므로 제거하는 것이 좋습니다. 실제로는 CustomException(RuntimeException)을 던지고 있습니다.

🔎 수정 제안
-    public UserDetails loadAdminByAdminId(Long adminId) throws UsernameNotFoundException {
+    public UserDetails loadAdminByAdminId(Long adminId) {
         AdminUser admin = adminRepository.findById(adminId)
                 .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER));
         return new CustomAdminDetails(admin);
     }
src/main/java/side/onetime/controller/AdminController.java (2)

62-74: Javadoc이 새로운 인증 방식을 반영하지 않습니다.

Javadoc에서 "요청 헤더에 포함된 액세스 토큰을 기반으로"라고 언급하고 있지만, 실제 구현에서는 SecurityContext에서 인증 정보를 가져옵니다. 문서와 코드의 일관성을 위해 Javadoc 업데이트를 권장합니다.

🔎 Javadoc 수정 제안
     /**
      * 관리자 프로필 조회 API.
      *
-     * 요청 헤더에 포함된 액세스 토큰을 기반으로 로그인된 관리자 정보를 조회합니다.
-     * 유효한 토큰이 아닐 경우 예외가 발생하며, 유효한 경우 이름, 이메일 정보를 반환합니다.
+     * SecurityContext에서 인증된 관리자 정보를 조회합니다.
+     * 인증되지 않은 경우 예외가 발생하며, 유효한 경우 이름, 이메일 정보를 반환합니다.
      *
      * @return 관리자 프로필 정보가 포함된 응답 객체
      */

111-124: 탈퇴 API Javadoc도 업데이트 필요.

Line 114에서 "Authorization 헤더에 포함된 액세스 토큰"을 언급하지만, 실제로는 SecurityContext를 사용합니다.

🔎 Javadoc 수정 제안
     /**
      * 관리자 계정 탈퇴 API.
      *
-     * Authorization 헤더에 포함된 액세스 토큰을 통해 인증된 관리자 계정을 삭제합니다.
-     * - 토큰에 포함된 ID로 관리자 정보를 조회하여 삭제합니다.
+     * SecurityContext에서 인증된 관리자 계정을 삭제합니다.
      *
      * @return 성공 응답 메시지
      */
src/main/java/side/onetime/service/AdminService.java (2)

81-88: Javadoc이 새로운 인증 방식을 반영하지 않습니다.

"Authorization 헤더에서 액세스 토큰을 추출"이라고 언급하지만, 실제로는 SecurityContext를 사용합니다.


177-179: 관리자 존재 여부만 확인하고 결과를 사용하지 않습니다.

adminRepository.findById()의 결과를 변수에 할당하지 않고 존재 여부만 확인합니다. 현재 로직상 문제는 없지만, 불필요한 엔티티 조회를 피하려면 existsById()를 사용하는 것이 더 효율적일 수 있습니다.

🔎 Optional: existsById 사용 제안
     @Transactional(readOnly = true)
     public GetAllDashboardEventsResponse getAllDashboardEvents(Pageable pageable, String keyword, String sorting) {
-        adminRepository.findById(AdminAuthorizationUtil.getLoginAdminId())
-                .orElseThrow(() -> new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER));
+        Long adminId = AdminAuthorizationUtil.getLoginAdminId();
+        if (!adminRepository.existsById(adminId)) {
+            throw new CustomException(AdminErrorStatus._NOT_FOUND_ADMIN_USER);
+        }
src/main/java/side/onetime/service/BannerService.java (1)

82-88: 동일한 최적화 기회: existsById 사용 가능.

AdminService와 마찬가지로, 관리자 엔티티를 조회하지만 결과를 사용하지 않는 경우 existsById()가 더 효율적일 수 있습니다. 단, 현재 구현도 기능적으로 올바릅니다.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35d4ccf and 75d1d0e.

📒 Files selected for processing (26)
  • src/main/java/side/onetime/auth/dto/CustomAdminDetails.java
  • src/main/java/side/onetime/auth/dto/CustomUserDetails.java
  • src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java
  • src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java
  • src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java
  • src/main/java/side/onetime/controller/AdminController.java
  • src/main/java/side/onetime/controller/BannerController.java
  • src/main/java/side/onetime/global/config/SecurityConfig.java
  • src/main/java/side/onetime/global/filter/JwtFilter.java
  • src/main/java/side/onetime/repository/AdminRepository.java
  • src/main/java/side/onetime/service/AdminService.java
  • src/main/java/side/onetime/service/BannerService.java
  • src/main/java/side/onetime/util/AdminAuthorizationUtil.java
  • src/main/java/side/onetime/util/JwtUtil.java
  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/test/java/side/onetime/banner/BannerControllerTest.java
  • src/test/java/side/onetime/configuration/AdminControllerTestConfig.java
  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
  • src/test/java/side/onetime/configuration/UserControllerTestConfig.java
  • src/test/java/side/onetime/event/EventControllerTest.java
  • src/test/java/side/onetime/fixed/FixedControllerTest.java
  • src/test/java/side/onetime/member/MemberControllerTest.java
  • src/test/java/side/onetime/schedule/ScheduleControllerTest.java
  • src/test/java/side/onetime/token/TokenControllerTest.java
  • src/test/java/side/onetime/url/UrlControllerTest.java
  • src/test/java/side/onetime/user/UserControllerTest.java
💤 Files with no reviewable changes (3)
  • src/main/java/side/onetime/util/JwtUtil.java
  • src/test/java/side/onetime/event/EventControllerTest.java
  • src/test/java/side/onetime/member/MemberControllerTest.java
🧰 Additional context used
📓 Path-based instructions (7)
src/main/java/**/service/*Service.java

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/java/**/service/*Service.java: Use @service annotation for business logic layer classes
Use @distributedlock annotation for race condition prevention in service methods
Use @transactional annotation for transaction management

Files:

  • src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java
  • src/main/java/side/onetime/service/AdminService.java
  • src/main/java/side/onetime/service/BannerService.java
src/main/java/**/{controller,service}/*.java

📄 CodeRabbit inference engine (CLAUDE.md)

Use constructor injection with @requiredargsconstructor for dependency injection

Files:

  • src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java
  • src/main/java/side/onetime/controller/AdminController.java
  • src/main/java/side/onetime/service/AdminService.java
  • src/main/java/side/onetime/service/BannerService.java
  • src/main/java/side/onetime/controller/BannerController.java
src/main/java/**/repository/*Repository.java

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/java/**/repository/*Repository.java: Use JpaRepository and QueryDSL for data access layer implementation
Use Hibernate fetch join to prevent N+1 query problems in repository implementations
Use QueryDSL for complex database queries with custom repository implementations

Files:

  • src/main/java/side/onetime/repository/AdminRepository.java
src/test/java/**/*Test.java

📄 CodeRabbit inference engine (CLAUDE.md)

src/test/java/**/*Test.java: Use Spring REST Docs and SpringDoc OpenAPI for API documentation generation
Use JUnit 5 with Spring Boot Test for unit and integration testing

Files:

  • src/test/java/side/onetime/schedule/ScheduleControllerTest.java
  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/test/java/side/onetime/fixed/FixedControllerTest.java
  • src/test/java/side/onetime/user/UserControllerTest.java
  • src/test/java/side/onetime/banner/BannerControllerTest.java
  • src/test/java/side/onetime/url/UrlControllerTest.java
  • src/test/java/side/onetime/token/TokenControllerTest.java
src/main/java/**/dto/**/*.java

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/java/**/dto/**/*.java: Create DTOs organized by feature in dto directory with request and response subdirectories
Use toEntity() methods and static factory of() methods for DTO to entity conversion

Files:

  • src/main/java/side/onetime/auth/dto/CustomAdminDetails.java
  • src/main/java/side/onetime/auth/dto/CustomUserDetails.java
src/main/java/**/controller/*Controller.java

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/java/**/controller/*Controller.java: Use @RestController annotation for REST API endpoints
Use generic response wrapper ApiResponse with onSuccess() and onFailure() methods for all API responses
Use RESTful API endpoints with /api/v1/ prefix

Files:

  • src/main/java/side/onetime/controller/AdminController.java
  • src/main/java/side/onetime/controller/BannerController.java
src/main/java/**/{exception,domain/enums}/*.java

📄 CodeRabbit inference engine (CLAUDE.md)

Use domain-specific error status enums (e.g., EventErrorStatus) for error handling

Files:

  • src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java
  • src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java
🧠 Learnings (11)
📓 Common learnings
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/test/java/**/controller/*ControllerTest.java : Use MockMvc for controller integration tests

Applied to files:

  • src/test/java/side/onetime/schedule/ScheduleControllerTest.java
  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/test/java/side/onetime/fixed/FixedControllerTest.java
  • src/test/java/side/onetime/user/UserControllerTest.java
  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
  • src/test/java/side/onetime/banner/BannerControllerTest.java
  • src/test/java/side/onetime/configuration/AdminControllerTestConfig.java
  • src/test/java/side/onetime/configuration/UserControllerTestConfig.java
  • src/test/java/side/onetime/url/UrlControllerTest.java
  • src/test/java/side/onetime/token/TokenControllerTest.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/test/java/**/*Test.java : Use Spring REST Docs and SpringDoc OpenAPI for API documentation generation

Applied to files:

  • src/test/java/side/onetime/schedule/ScheduleControllerTest.java
  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/test/java/side/onetime/fixed/FixedControllerTest.java
  • src/test/java/side/onetime/user/UserControllerTest.java
  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
  • src/test/java/side/onetime/banner/BannerControllerTest.java
  • src/test/java/side/onetime/url/UrlControllerTest.java
  • src/test/java/side/onetime/token/TokenControllerTest.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/controller/*Controller.java : Use RESTful API endpoints with /api/v1/ prefix

Applied to files:

  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/main/java/side/onetime/controller/AdminController.java
  • src/main/java/side/onetime/controller/BannerController.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/controller/*Controller.java : Use RestController annotation for REST API endpoints

Applied to files:

  • src/test/java/side/onetime/admin/AdminControllerTest.java
  • src/main/java/side/onetime/controller/AdminController.java
  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
  • src/main/java/side/onetime/controller/BannerController.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/test/java/**/*Test.java : Use JUnit 5 with Spring Boot Test for unit and integration testing

Applied to files:

  • src/test/java/side/onetime/fixed/FixedControllerTest.java
  • src/test/java/side/onetime/user/UserControllerTest.java
  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/{controller,service}/*.java : Use constructor injection with RequiredArgsConstructor for dependency injection

Applied to files:

  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

Applied to files:

  • src/test/java/side/onetime/configuration/ControllerTestConfig.java
  • src/main/java/side/onetime/global/filter/JwtFilter.java
  • src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java
  • src/main/java/side/onetime/global/config/SecurityConfig.java
📚 Learning: 2025-08-30T09:03:01.457Z
Learnt from: anxi01
Repo: onetime-with-members/backend PR: 269
File: src/main/java/side/onetime/dto/admin/response/GetBannerResponse.java:12-16
Timestamp: 2025-08-30T09:03:01.457Z
Learning: BarBanner와 Banner는 완전히 별개의 엔티티입니다. Banner는 organization, title, subTitle, colorCode, imageUrl 필드를 사용하고, BarBanner는 contentKor, contentEng, backgroundColorCode, textColorCode 필드를 사용합니다. 각각 다른 목적과 스키마를 가진 독립적인 엔티티로 설계되었습니다.

Applied to files:

  • src/test/java/side/onetime/banner/BannerControllerTest.java
📚 Learning: 2025-08-29T11:40:00.748Z
Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 269
File: src/main/java/side/onetime/domain/BarBanner.java:76-78
Timestamp: 2025-08-29T11:40:00.748Z
Learning: BarBanner와 Banner 엔티티에서 소프트 삭제(markAsDeleted) 시에는 isDeleted=true와 함께 isActivated=false도 설정해야 합니다. 이는 삭제된 레코드가 복구될 때 의도치 않게 활성 상태로 남는 것을 방지하기 위함입니다.

Applied to files:

  • src/test/java/side/onetime/banner/BannerControllerTest.java
  • src/main/java/side/onetime/service/BannerService.java
📚 Learning: 2025-08-29T11:40:58.895Z
Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 269
File: src/main/java/side/onetime/util/S3Util.java:63-73
Timestamp: 2025-08-29T11:40:58.895Z
Learning: 배너 이미지 저장 시 /banner/{banner_id}/ 경로 구조를 사용하여 파일을 더 체계적으로 관리하는 것을 선호합니다.

Applied to files:

  • src/main/java/side/onetime/service/BannerService.java
🧬 Code graph analysis (11)
src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java (2)
src/main/java/side/onetime/auth/service/CustomUserDetailsService.java (2)
  • Service (14-52)
  • loadUserByUserId (47-51)
src/main/java/side/onetime/domain/AdminUser.java (1)
  • Entity (11-45)
src/main/java/side/onetime/auth/dto/CustomAdminDetails.java (1)
src/main/java/side/onetime/domain/AdminUser.java (2)
  • Entity (11-45)
  • Builder (34-40)
src/test/java/side/onetime/admin/AdminControllerTest.java (1)
src/test/java/side/onetime/configuration/AdminControllerTestConfig.java (1)
  • AdminControllerTestConfig (10-22)
src/test/java/side/onetime/fixed/FixedControllerTest.java (2)
src/test/java/side/onetime/configuration/UserControllerTestConfig.java (1)
  • UserControllerTestConfig (9-20)
src/main/java/side/onetime/controller/FixedController.java (1)
  • RestController (21-76)
src/test/java/side/onetime/user/UserControllerTest.java (1)
src/test/java/side/onetime/configuration/UserControllerTestConfig.java (1)
  • UserControllerTestConfig (9-20)
src/main/java/side/onetime/service/AdminService.java (2)
src/main/java/side/onetime/util/AdminAuthorizationUtil.java (1)
  • AdminAuthorizationUtil (9-32)
src/main/java/side/onetime/exception/status/AdminErrorStatus.java (2)
  • Override (31-38)
  • Override (40-48)
src/main/java/side/onetime/auth/dto/CustomUserDetails.java (2)
src/main/java/side/onetime/auth/service/CustomUserDetailsService.java (1)
  • Service (14-52)
src/main/java/side/onetime/util/UserAuthorizationUtil.java (1)
  • UserAuthorizationUtil (9-32)
src/test/java/side/onetime/banner/BannerControllerTest.java (1)
src/test/java/side/onetime/configuration/AdminControllerTestConfig.java (1)
  • AdminControllerTestConfig (10-22)
src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java (1)
src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java (1)
  • Slf4j (16-47)
src/main/java/side/onetime/service/BannerService.java (1)
src/main/java/side/onetime/util/AdminAuthorizationUtil.java (1)
  • AdminAuthorizationUtil (9-32)
src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java (1)
src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java (1)
  • Slf4j (14-43)
🔇 Additional comments (33)
src/main/java/side/onetime/auth/dto/CustomUserDetails.java (1)

14-16: LGTM! 권한 설정이 올바르게 구현되었습니다.

getAuthorities()가 불변 리스트로 ROLE_USER 권한을 반환하도록 변경되었습니다. 이는 Spring Security의 역할 기반 접근 제어와 올바르게 통합되며, SecurityConfig의 AUTHENTICATED_USER_URLS와 일관성을 유지합니다.

src/test/java/side/onetime/url/UrlControllerTest.java (1)

35-35: LGTM! 불필요한 목(mock) 제거가 적절합니다.

UrlController가 인증이 필요하지 않은 공개 엔드포인트이므로, JwtUtil 및 CustomUserDetailsService 목을 제거한 것이 올바릅니다. 이는 PR의 인증 로직 통합 목표와 일치합니다.

src/main/java/side/onetime/repository/AdminRepository.java (1)

14-14: 메서드 이름이 이미 일관성 있게 구성되어 있습니다.

findByName 메서드는 AdminRepositoryUserRepository 모두에서 동일한 이름으로 사용되고 있으며, 현재의 명명 패턴이 의도적이고 일관성 있습니다. findAdminUserByEmail과의 이름 차이는 단순 쿼리와 복잡한 쿼리를 구분하는 관례를 따르고 있으며, 메서드는 CustomAdminDetailsService에서 올바르게 사용되고 있습니다.

Likely an incorrect or invalid review comment.

src/test/java/side/onetime/configuration/UserControllerTestConfig.java (1)

11-19: 테스트 설정이 올바르게 구현되었습니다.

Spring Security 컨텍스트에 사용자 인증을 설정하는 로직이 명확하고 적절합니다. CustomUserDetails를 사용하여 권한을 포함한 인증 토큰을 생성합니다.

src/test/java/side/onetime/configuration/ControllerTestConfig.java (2)

36-43: MockBean 추가가 적절합니다.

JWT 및 사용자 상세 서비스를 MockBean으로 선언하여 테스트 환경에서 실제 빈 로딩을 방지합니다. 이는 새로운 Spring Security 기반 인증 흐름과 일관됩니다.


53-53: SecurityContext 초기화가 올바릅니다.

각 테스트 전에 SecurityContextHolder.clearContext()를 호출하여 이전 테스트의 인증 상태가 남지 않도록 합니다. AdminControllerTestConfigUserControllerTestConfig에서 필요에 따라 인증을 설정합니다.

src/test/java/side/onetime/user/UserControllerTest.java (1)

34-34: 테스트 설정 마이그레이션이 올바릅니다.

UserControllerTestConfig로 확장하도록 변경하여 사용자 인증 컨텍스트가 자동으로 설정됩니다. 이는 새로운 Spring Security 기반 인증 구조와 일치합니다.

src/test/java/side/onetime/configuration/AdminControllerTestConfig.java (2)

12-21: 관리자 인증 설정이 올바르게 구현되었습니다.

CustomAdminDetails를 사용하여 MASTER 상태의 관리자 인증을 SecurityContext에 설정합니다. 이는 관리자 테스트에 적절한 권한을 부여합니다.


14-15: mockAdmin의 ID를 명시적으로 설정하세요.

현재 코드에서 빌더로 생성한 mockAdmin은 ID가 null입니다. 다만 실제 테스트는 서비스 계층을 완전히 모킹하므로 즉각적인 문제는 없습니다. 하지만 향후 통합 테스트나 서비스 레이어를 실행하는 테스트가 추가될 경우를 대비해 ID를 명시적으로 설정하는 것이 좋습니다.

AdminUser mockAdmin = AdminUser.builder().name("testAdmin").email("[email protected]").build();
ReflectionTestUtils.setField(mockAdmin, "id", 1L);
mockAdmin.updateAdminStatus(AdminStatus.MASTER);

Likely an incorrect or invalid review comment.

src/test/java/side/onetime/token/TokenControllerTest.java (1)

31-32: LGTM!

@MockBean을 사용한 TokenService 모킹이 적절하게 구성되어 있습니다. WebMvcTest와 함께 사용되어 컨트롤러 테스트에 적합합니다.

src/test/java/side/onetime/schedule/ScheduleControllerTest.java (1)

30-31: LGTM!

명시적 import 사용(any, anyString)과 @MockBean을 통한 ScheduleService 모킹이 올바르게 구성되어 있습니다. 코딩 가이드라인에 따라 MockMvc와 Spring REST Docs를 적절히 활용하고 있습니다.

Also applies to: 41-42

src/test/java/side/onetime/fixed/FixedControllerTest.java (1)

14-14: LGTM!

UserControllerTestConfig으로 변경하여 사용자 인증 컨텍스트를 적절히 설정하고 있습니다. FixedController가 사용자 인증을 필요로 하므로 이 구성이 적합합니다. PR 목표에 따른 테스트 환경 분리가 잘 반영되었습니다.

Also applies to: 32-36

src/main/java/side/onetime/auth/service/CustomAdminDetailsService.java (1)

14-18: LGTM!

@Service@RequiredArgsConstructor를 사용한 의존성 주입이 코딩 가이드라인에 맞게 구현되어 있습니다. CustomUserDetailsService와 일관된 패턴을 따르고 있습니다.

src/main/java/side/onetime/global/config/SecurityConfig.java (2)

120-134: LGTM!

역할 기반 접근 제어(hasRole("USER"), hasRole("ADMIN"))와 커스텀 예외 핸들러 설정이 적절합니다. PUBLIC_URLSAUTHENTICATED_ADMIN_URLS보다 먼저 체크되므로 /api/v1/banners/*/clicks와 같은 공개 엔드포인트가 올바르게 처리됩니다.


48-53: /api/v1/banners/*/clicks 패턴은 현재 구현과 일치합니다

BannerController의 실제 엔드포인트는 @PatchMapping("/banners/{id}/clicks")이며, 단일 배너 ID만을 받습니다. 따라서 * 패턴이 적절하며, **로 변경할 필요가 없습니다.

Likely an incorrect or invalid review comment.

src/main/java/side/onetime/global/filter/JwtFilter.java (3)

49-53: LGTM!

Bearer 토큰 유무를 필터 레벨에서 조기 검사하여 불필요한 처리를 방지하는 좋은 패턴입니다.


80-87: LGTM!

setAuthentication 메서드가 UserDetails를 직접 받도록 리팩토링되어 가독성과 일관성이 향상되었습니다. UsernamePasswordAuthenticationToken에 authorities가 올바르게 전달됩니다.


59-65: 현재 구현은 의도된 동작입니다. 수정 불필요

getClaimFromToken 메서드는 클레임 추출 실패 시 null을 반환하지 않고 예외를 던집니다. 따라서 userType이 null이 되는 경우는 발생하지 않습니다. 토큰 생성 시 userType은 필수 파라미터로 항상 저장되며, 필터의 try-catch 블록이 추출 실패를 처리합니다.

src/test/java/side/onetime/admin/AdminControllerTest.java (3)

37-40: LGTM! AdminControllerTestConfig으로 전환 완료.

테스트 클래스가 AdminControllerTestConfig를 상속하여 @BeforeEach에서 MASTER 권한의 CustomAdminDetails를 SecurityContext에 설정합니다. 이는 Authorization 헤더 기반에서 SecurityContext 기반 인증으로의 전환과 일치합니다.


147-151: 서비스 메서드 시그니처 변경 반영 확인.

adminService.getAdminUserProfile() 호출이 파라미터 없이 변경되었고, mockMvc.perform()에서 Authorization 헤더가 제거되었습니다. AdminService의 새 시그니처와 일치합니다.


270-274: 탈퇴 API 테스트 업데이트 확인.

withdrawAdminUser() 호출이 파라미터 없이 변경되어 새로운 서비스 시그니처와 일치합니다.

src/main/java/side/onetime/controller/AdminController.java (1)

137-146: 대시보드 이벤트 조회 엔드포인트 변경 확인.

Authorization 헤더가 제거되고 서비스 호출이 새 시그니처에 맞게 업데이트되었습니다. RESTful API 규칙(/api/v1/ prefix)을 준수합니다.

src/main/java/side/onetime/service/AdminService.java (1)

91-96: SecurityContext 기반 관리자 조회 패턴 적용 완료.

AdminAuthorizationUtil.getLoginAdminId()를 사용하여 인증된 관리자 ID를 조회하고, repository에서 관리자 정보를 가져오는 패턴이 일관되게 적용되었습니다.

src/main/java/side/onetime/controller/BannerController.java (3)

38-44: 배너 등록 API에서 Authorization 헤더 제거 완료.

서비스 호출이 registerBanner(request, imageFile)로 변경되어 BannerService의 새 시그니처와 일치합니다.


71-75: 배너 단건 조회 API 업데이트 확인.

getBanner(id) 호출로 변경되어 헤더 없는 새 시그니처와 일치합니다.


196-200: 배너 삭제 API 업데이트 확인.

Authorization 헤더가 제거되고 서비스 호출이 deleteBanner(id)로 변경되었습니다.

src/main/java/side/onetime/service/BannerService.java (3)

34-35: AdminRepository 의존성 추가 확인.

JwtUtil 대신 AdminRepository를 주입받아 AdminAuthorizationUtil과 함께 관리자 인증을 처리합니다. Spring Security 컨텍스트 기반 인증으로의 전환이 올바르게 구현되었습니다.


47-54: 배너 등록 메서드 인증 패턴 적용.

AdminAuthorizationUtil.getLoginAdminId()를 통해 SecurityContext에서 관리자 ID를 조회하고, 존재 여부를 검증합니다. 다른 메서드들과 일관된 패턴입니다.


174-180: 공개 엔드포인트에는 관리자 인증 없음 확인.

getAllActivatedBanners()getAllActivatedBarBanners()는 관리자 인증 없이 호출 가능하며, 이는 공개 API로서 적절합니다.

src/test/java/side/onetime/banner/BannerControllerTest.java (4)

17-40: AdminControllerTestConfig으로 전환 완료.

BannerControllerTestAdminControllerTestConfig를 상속하여 SecurityContext 기반 관리자 인증을 사용합니다. 배너 API가 관리자 권한을 요구하므로 적절한 구성입니다.


60-65: 배너 등록 테스트 업데이트 확인.

서비스 mock이 registerBanner(RegisterBannerRequest, MultipartFile) 시그니처에 맞게 업데이트되었고, Authorization 헤더가 제거되었습니다.


372-420: 공개 엔드포인트 테스트는 변경 없음 확인.

getAllActivatedBanners() 테스트는 공개 엔드포인트이므로 관리자 인증이 필요 없습니다. 기존 구현이 유지된 것이 적절합니다.


577-581: 배너 삭제 테스트 업데이트 확인.

deleteBanner(Long) 시그니처에 맞게 mock이 업데이트되었고, mockMvc 요청에서 Authorization 헤더가 제거되었습니다.

Copy link
Member

@bbbang105 bbbang105 left a comment

Choose a reason for hiding this comment

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

고생 많으셨습니다! :)

Comment on lines +62 to +65
UserDetails userDetails = "ADMIN".equals(userType)
? customAdminDetailsService.loadAdminByAdminId(userId)
: customUserDetailsService.loadUserByUserId(userId);
setAuthentication(userDetails);
Copy link
Member

Choose a reason for hiding this comment

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

깔끔하고 좋은 것 같네요!

Comment on lines 33 to 51
@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}
Copy link
Member

Choose a reason for hiding this comment

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

이 메서드 4개는 디폴트가 true 리턴이라고 해서 현재는 불필요 메서드로 나오네요. 나중에 특정 비즈니스 로직이 필요할 때 구현해도 괜찮을 것 같습니다! (유저 details도 동일)

Copy link
Member Author

Choose a reason for hiding this comment

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

좋습니다~ 추후에 필요할 때 추가해서 구현하는걸로!
3f6893f

SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities())
);
SecurityContextHolder.clearContext();
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: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main/java/side/onetime/exception/status/AdminErrorStatus.java (1)

13-25: AdminErrorStatus._UNAUTHORIZED(401)은 현재 로그인한 일반 유저가 관리자 API에 접근할 때 발생하는데, 의미론적으로 403(Forbidden)이어야 합니다.

AdminAuthorizationUtil.getLoginAdminId() 32~34줄에서 로그인된 사용자가 CustomAdminDetails가 아니면 AdminErrorStatus._UNAUTHORIZED(401)를 던집니다. 하지만 이는 인증은 완료되었으나 권한이 없는 경우이므로 HTTP 표준에 따라 403 Forbidden으로 응답하는 것이 올바릅니다.

현재 코드베이스는 모든 역할/권한 검증에서 401을 사용하고 있으나, 다음과 같이 구분되어야 합니다:

  • 401: 토큰 없음/만료/무효 (인증 실패)
  • 403: 인증됨 + 역할 부족 (권한 실패)

GlobalErrorStatus._FORBIDDEN(403)이 이미 정의되어 있으므로, 관리자 역할 검증은 이를 사용하도록 변경 권장합니다.

src/main/java/side/onetime/util/UserAuthorizationUtil.java (1)

26-35: 인증 실패 시나리오(null/타입 불일치)에서 에러 상태 코드가 불일치합니다.

현재 코드는 authentication == null일 때 ErrorStatus._UNAUTHORIZED (코드: "E_UNAUTHORIZED", "인증이 필요 합니다.")를, 그 외 principalCustomUserDetails가 아닐 때 UserErrorStatus._UNAUTHORIZED (코드: "USER-004", "인증된 사용자가 아닙니다.")를 반환합니다. 두 경우 모두 401 응답이지만 메시지와 코드가 달라져 클라이언트 입장에서 혼동을 줄 수 있습니다.

현재 코드베이스에서 AnonymousAuthenticationToken은 사용되지 않으므로, authentication == null이 되는 경우는 실제로는 Spring Security의 보호된 엔드포인트에 도달하기 전에 처리되어야 합니다. 따라서 같은 에러 상태로 통일하거나 각 케이스별 의도를 명확히 문서화할 것을 권장합니다.

🤖 Fix all issues with AI agents
In @src/main/java/side/onetime/util/AdminAuthorizationUtil.java:
- Around line 12-36: The method getLoginAdminId mixes unauthenticated vs
unauthorized handling: keep throwing CustomException(ErrorStatus._UNAUTHORIZED)
when Authentication or principal is missing (unauthenticated), but when a
principal exists but is not a CustomAdminDetails (an authenticated non-admin
user) change the thrown error to a 403 variant; update the defensive check in
getLoginAdminId to throw new CustomException(AdminErrorStatus._FORBIDDEN) (or
the existing AdminErrorStatus constant representing 403) instead of
AdminErrorStatus._UNAUTHORIZED, preserving the
SecurityContextHolder/Authentication checks and returning adminDetails.getId()
when principal is a CustomAdminDetails.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75d1d0e and 1351fb9.

📒 Files selected for processing (5)
  • src/main/java/side/onetime/auth/dto/CustomAdminDetails.java
  • src/main/java/side/onetime/auth/dto/CustomUserDetails.java
  • src/main/java/side/onetime/exception/status/AdminErrorStatus.java
  • src/main/java/side/onetime/util/AdminAuthorizationUtil.java
  • src/main/java/side/onetime/util/UserAuthorizationUtil.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/side/onetime/auth/dto/CustomUserDetails.java
  • src/main/java/side/onetime/auth/dto/CustomAdminDetails.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

Applied to files:

  • src/main/java/side/onetime/util/UserAuthorizationUtil.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/{exception,domain/enums}/*.java : Use domain-specific error status enums (e.g., EventErrorStatus) for error handling

Applied to files:

  • src/main/java/side/onetime/exception/status/AdminErrorStatus.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to Server

Comment on lines 12 to 36
public class AdminAuthorizationUtil {

private AdminAuthorizationUtil() {
throw new AssertionError();
}

/**
* 현재 로그인한 관리자의 ID를 반환하는 메서드.
*
* SecurityContextHolder에서 Authentication을 가져와
* CustomAdminDetails로 캐스팅한 후, 관리자 ID를 추출합니다.
*
* @return 로그인된 관리자의 ID
*/
public static Long getLoginAdminId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = Optional.ofNullable(authentication)
.map(Authentication::getPrincipal)
.orElseThrow(() -> new CustomException(ErrorStatus._UNAUTHORIZED));

if (!(principal instanceof CustomAdminDetails adminDetails)) {
throw new CustomException(AdminErrorStatus._UNAUTHORIZED);
}
return adminDetails.getId();
}
Copy link

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the SecurityConfig/SecurityFilterChain configuration
find . -type f -name "*.java" | xargs grep -l "SecurityFilterChain\|@Configuration" | head -20

Repository: onetime-with-members/backend

Length of output: 619


🏁 Script executed:

# Search for SecurityFilterChain and admin path authorization
rg -n "SecurityFilterChain|authorizeHttpRequests|requestMatchers|antMatchers|hasRole|hasAuthority" src/main/java -A 3 -B 1 | head -100

Repository: onetime-with-members/backend

Length of output: 2668


🏁 Script executed:

# Find how AdminAuthorizationUtil.getLoginAdminId() is called
rg -n "AdminAuthorizationUtil\\.getLoginAdminId\\(" src/main/java

Repository: onetime-with-members/backend

Length of output: 2277


🏁 Script executed:

# Check error status definitions
rg -n "ErrorStatus\._UNAUTHORIZED\|AdminErrorStatus\._UNAUTHORIZED" src/main/java -B 2 -A 2

Repository: onetime-with-members/backend

Length of output: 54


🏁 Script executed:

# Check if UserAuthorizationUtil exists for pattern comparison
find . -type f -name "*AuthorizationUtil.java"

Repository: onetime-with-members/backend

Length of output: 194


🏁 Script executed:

# Look for security configuration files
fd -t f "Security.*Config\.java|Config.*Security\.java" src/main/java

Repository: onetime-with-members/backend

Length of output: 132


🏁 Script executed:

# Get the full SecurityConfig to see the URL patterns and filter chain details
cat -n src/main/java/side/onetime/global/config/SecurityConfig.java | head -150

Repository: onetime-with-members/backend

Length of output: 5900


🏁 Script executed:

# Check the URL patterns defined for admin
rg -n "AUTHENTICATED_ADMIN_URLS\|AUTHENTICATED_USER_URLS\|PUBLIC_URLS" src/main/java/side/onetime/global/config/SecurityConfig.java -B 2 -A 2

Repository: onetime-with-members/backend

Length of output: 54


🏁 Script executed:

# Check error status enums
fd -t f "ErrorStatus.*\.java" src/main/java

Repository: onetime-with-members/backend

Length of output: 750


🏁 Script executed:

# Compare with UserAuthorizationUtil
cat -n src/main/java/side/onetime/util/UserAuthorizationUtil.java

Repository: onetime-with-members/backend

Length of output: 1560


🏁 Script executed:

# Check the error status definitions
cat -n src/main/java/side/onetime/global/common/status/ErrorStatus.java

Repository: onetime-with-members/backend

Length of output: 2129


🏁 Script executed:

# Check AdminErrorStatus definitions
cat -n src/main/java/side/onetime/exception/status/AdminErrorStatus.java

Repository: onetime-with-members/backend

Length of output: 2445


🏁 Script executed:

# Check CustomAccessDeniedHandler to understand how 403 is handled
fd -t f "CustomAccessDeniedHandler.*\.java" src/main/java

Repository: onetime-with-members/backend

Length of output: 144


🏁 Script executed:

# Check CustomAuthenticationEntryPoint to understand how 401 is handled
fd -t f "CustomAuthenticationEntryPoint.*\.java" src/main/java

Repository: onetime-with-members/backend

Length of output: 149


🏁 Script executed:

# Check CustomAccessDeniedHandler (403 handling)
cat -n src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java

Repository: onetime-with-members/backend

Length of output: 2386


🏁 Script executed:

# Check CustomAuthenticationEntryPoint (401 handling)
cat -n src/main/java/side/onetime/auth/exception/CustomAuthenticationEntryPoint.java

Repository: onetime-with-members/backend

Length of output: 2025


🏁 Script executed:

# Check if there are any other usages of AdminErrorStatus._UNAUTHORIZED in the codebase
rg -n "AdminErrorStatus\._UNAUTHORIZED" src/main/java -B 2 -A 2

Repository: onetime-with-members/backend

Length of output: 576


🏁 Script executed:

# Quick check: Verify the util is truly called after filter chain processing
rg -n "getLoginAdminId" src/main/java -B 3 -A 1 | head -50

Repository: onetime-with-members/backend

Length of output: 4809


admin이 아닌 인증된 사용자가 관리자 API를 호출할 때 401/403 처리 불일치가 발생합니다.

SecurityFilterChain에서 /api/v1/admin/** 등의 경로를 hasRole("ADMIN")으로 보호하고 있으므로, 일반 사용자(USER 역할)가 접근하면 CustomAccessDeniedHandler를 통해 403 FORBIDDEN이 반환됩니다. 반면 util의 defensive check에서는 principal이 CustomAdminDetails가 아니면 AdminErrorStatus._UNAUTHORIZED(401)를 던지게 되어, 같은 상황에 대해 다른 HTTP 상태 코드가 반환될 수 있습니다. 이는 클라이언트의 재인증/권한 분기 로직을 혼란스럽게 할 수 있으므로, 인증 여부와 권한 여부를 명확히 분리하도록 수정하세요.

권장 수정 사항
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@
     public static Long getLoginAdminId() {
         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
-        Object principal = Optional.ofNullable(authentication)
-                .map(Authentication::getPrincipal)
-                .orElseThrow(() -> new CustomException(ErrorStatus._UNAUTHORIZED));
+        if (authentication == null
+                || !authentication.isAuthenticated()
+                || authentication instanceof AnonymousAuthenticationToken) {
+            throw new CustomException(ErrorStatus._UNAUTHORIZED);
+        }
+
+        Object principal = authentication.getPrincipal();
 
         if (!(principal instanceof CustomAdminDetails adminDetails)) {
             throw new CustomException(AdminErrorStatus._UNAUTHORIZED);
         }
         return adminDetails.getId();
     }
🤖 Prompt for AI Agents
In @src/main/java/side/onetime/util/AdminAuthorizationUtil.java around lines 12
- 36, The method getLoginAdminId mixes unauthenticated vs unauthorized handling:
keep throwing CustomException(ErrorStatus._UNAUTHORIZED) when Authentication or
principal is missing (unauthenticated), but when a principal exists but is not a
CustomAdminDetails (an authenticated non-admin user) change the thrown error to
a 403 variant; update the defensive check in getLoginAdminId to throw new
CustomException(AdminErrorStatus._FORBIDDEN) (or the existing AdminErrorStatus
constant representing 403) instead of AdminErrorStatus._UNAUTHORIZED, preserving
the SecurityContextHolder/Authentication checks and returning
adminDetails.getId() when principal is a CustomAdminDetails.

Copy link
Member

Choose a reason for hiding this comment

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

둘 다 403 던지는데? 다시 확인해봐.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check SecurityConfig to see which paths require ROLE_ADMIN
cat src/main/java/side/onetime/global/config/SecurityConfig.java | grep -A 50 "AUTHENTICATED_ADMIN_URLS\|hasRole"

Length of output: 2142


🏁 Script executed:

#!/bin/bash
# Check all usages of AdminAuthorizationUtil to see if any are in unprotected paths
rg -n "AdminAuthorizationUtil\.getLoginAdminId" src/main/java -B 3 -A 1

Length of output: 9147


🏁 Script executed:

#!/bin/bash
# Verify what AdminErrorStatus._UNAUTHORIZED actually returns
cat src/main/java/side/onetime/exception/status/AdminErrorStatus.java | grep -A 1 "_UNAUTHORIZED"

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Check CustomAccessDeniedHandler to confirm it returns 403
cat src/main/java/side/onetime/auth/exception/CustomAccessDeniedHandler.java | grep -A 3 "setStatus\|_FORBIDDEN"

Length of output: 236


@bbbang105, 확인해보니 말씀하신 게 맞습니다. 제가 잘못 분석했습니다. 죄송합니다.

실제 동작 흐름:

  1. USER 권한을 가진 사용자가 /api/v1/admin/** 접근 시도
  2. JwtFilter에서 인증 → SecurityContextCustomUserDetails (ROLE_USER) 설정
  3. Spring Security의 FilterSecurityInterceptor가 권한 검사:
    • 해당 경로는 .hasRole("ADMIN") 필요
    • 사용자는 ROLE_USER만 보유
  4. AccessDeniedException 발생CustomAccessDeniedHandler 실행 → 403 반환
  5. 요청이 컨트롤러/서비스 레이어에 도달하지 못함
  6. AdminAuthorizationUtil.getLoginAdminId()는 실행되지 않음

따라서:

  • Spring Security 레벨에서 이미 403 처리됨
  • AdminAuthorizationUtil의 defensive check에서 AdminErrorStatus._UNAUTHORIZED (401)을 던지는 코드는 실제로는 도달 불가능한 코드입니다

제안했던 수정사항은 불필요합니다. 현재 구현이 올바르게 403을 반환하고 있습니다.


🧠 Learnings used
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

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

🤖 Fix all issues with AI agents
In @src/main/java/side/onetime/global/common/status/ErrorStatus.java:
- Around line 20-22: Change the _UNIDENTIFIED_USER enum entry to use
HttpStatus.UNAUTHORIZED (401) instead of HttpStatus.INTERNAL_SERVER_ERROR;
locate the constant named _UNIDENTIFIED_USER in ErrorStatus.java and update its
HttpStatus to UNAUTHORIZED (or replace usages to use the existing _UNAUTHORIZED
entry) so missing authentication produces a 401 rather than a 500.

In @src/main/java/side/onetime/util/AdminAuthorizationUtil.java:
- Around line 1-32: getLoginAdminId currently maps any missing/anonymous
principal to CustomException(ErrorStatus._UNIDENTIFIED_USER); change it to first
check SecurityContextHolder.getContext().getAuthentication() and if null throw
new CustomException(ErrorStatus._UNAUTHORIZED), then if principal is not an
instance of CustomAdminDetails (handle anonymous/test contexts) throw new
CustomException(ErrorStatus._FORBIDDEN) otherwise return ((CustomAdminDetails)
principal).getId(); update references in this method (getLoginAdminId,
SecurityContextHolder, CustomAdminDetails, ErrorStatus, CustomException) to
implement that branching consistent with UserAuthorizationUtil.

In @src/main/java/side/onetime/util/UserAuthorizationUtil.java:
- Around line 25-31: getLoginUserId currently maps any missing or non-matching
authentication/principal to ErrorStatus._UNIDENTIFIED_USER causing 500s; change
it to throw CustomException(ErrorStatus._UNAUTHORIZED) when
SecurityContextHolder.getContext().getAuthentication() is null or getPrincipal()
is null (i.e., unauthenticated/anonymous), and only throw
CustomException(ErrorStatus._UNIDENTIFIED_USER) when a principal exists but is
not an instance of CustomUserDetails; simplify the instanceof + cast by using
CustomUserDetails.class::isInstance and CustomUserDetails.class::cast when
extracting the id from the principal.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1351fb9 and db2c7c0.

📒 Files selected for processing (3)
  • src/main/java/side/onetime/global/common/status/ErrorStatus.java
  • src/main/java/side/onetime/util/AdminAuthorizationUtil.java
  • src/main/java/side/onetime/util/UserAuthorizationUtil.java
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

Applied to files:

  • src/main/java/side/onetime/util/UserAuthorizationUtil.java
  • src/main/java/side/onetime/util/AdminAuthorizationUtil.java
📚 Learning: 2025-12-21T11:41:05.378Z
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/{exception,domain/enums}/*.java : Use domain-specific error status enums (e.g., EventErrorStatus) for error handling

Applied to files:

  • src/main/java/side/onetime/global/common/status/ErrorStatus.java

Comment on lines +20 to 22
_FAILED_TRANSLATE_SWAGGER(HttpStatus.INTERNAL_SERVER_ERROR, "E_FAILED_TRANSLATE_SWAGGER", "Rest Docs로 생성된 json파일을 통한 스웨거 변환에 실패하였습니다."),
_UNIDENTIFIED_USER(HttpStatus.INTERNAL_SERVER_ERROR, "E_UNIDENTIFIED_USER", "인증 정보를 처리하는 과정에서 서버 오류가 발생했습니다."),
;
Copy link

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the ErrorStatus.java file to understand the context
cat src/main/java/side/onetime/global/common/status/ErrorStatus.java

Repository: onetime-with-members/backend

Length of output: 1931


🏁 Script executed:

# Search for where _UNIDENTIFIED_USER is used in the codebase
rg "_UNIDENTIFIED_USER" --type java -A 3 -B 3

Repository: onetime-with-members/backend

Length of output: 2356


🏁 Script executed:

# Search for other authentication-related error codes to understand the pattern
rg "UNAUTHORIZED|UNAUTHORIZED|401|HttpStatus" src/main/java/side/onetime/global/common/status/ --type java -A 2

Repository: onetime-with-members/backend

Length of output: 13398


🏁 Script executed:

# Check for SecurityContext handling and where exceptions are thrown
rg "SecurityContext|_UNIDENTIFIED_USER|ExceptionHandler" src/main/java/side/onetime --type java -l

Repository: onetime-with-members/backend

Length of output: 449


🏁 Script executed:

# Let's examine the UserAuthorizationUtil to understand the context better
cat src/main/java/side/onetime/util/UserAuthorizationUtil.java

Repository: onetime-with-members/backend

Length of output: 1150


🏁 Script executed:

# Check the JwtFilter to see how authentication is handled
cat src/main/java/side/onetime/global/filter/JwtFilter.java

Repository: onetime-with-members/backend

Length of output: 4949


🏁 Script executed:

# Check the GlobalExceptionHandler to see how _UNIDENTIFIED_USER is handled
cat src/main/java/side/onetime/exception/GlobalExceptionHandler.java

Repository: onetime-with-members/backend

Length of output: 8429


🏁 Script executed:

# Search for @PreAuthorize or @Secured annotations to see which endpoints require authentication
rg "@PreAuthorize|@Secured|@AuthenticationPrincipal" src/main/java/side/onetime --type java -B 2 -A 5 | head -100

Repository: onetime-with-members/backend

Length of output: 54


_UNIDENTIFIED_USERHttpStatus.UNAUTHORIZED(401)로 변경해야 합니다.

getLoginUserId() 메서드가 호출될 때 SecurityContext가 비어있다는 것은 클라이언트가 인증 정보를 제공하지 않은 상황입니다. 이는 서버 오류(500)가 아니라 클라이언트의 인증 누락(401)이므로, 적절한 HTTP 상태 코드로 수정이 필요합니다.

현재 JwtFilter는 Bearer 토큰이 없으면 요청을 계속 진행시키고, 보호된 엔드포인트에서 getLoginUserId()를 호출할 때만 예외가 발생합니다. 이 경우 500으로 응답하면 모니터링/클라이언트 입장에서 혼선이 생깁니다. 기존 _UNAUTHORIZED 오류 코드를 사용하거나, 같은 수준의 401 상태를 유지하세요.

🤖 Prompt for AI Agents
In @src/main/java/side/onetime/global/common/status/ErrorStatus.java around
lines 20 - 22, Change the _UNIDENTIFIED_USER enum entry to use
HttpStatus.UNAUTHORIZED (401) instead of HttpStatus.INTERNAL_SERVER_ERROR;
locate the constant named _UNIDENTIFIED_USER in ErrorStatus.java and update its
HttpStatus to UNAUTHORIZED (or replace usages to use the existing _UNAUTHORIZED
entry) so missing authentication produces a 401 rather than a 500.

Copy link
Member Author

Choose a reason for hiding this comment

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

앞단에서 401, 403 인증/인가 처리는 하고 있어
여기까지 오려면 인증/인가 처리는 성공인데 SecurityContext나 서버 오류때매 발생하는 것으로 판단해서 500 에러로 변경했어

Copy link

Choose a reason for hiding this comment

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

@anxi01, 설명 감사합니다! 이해했습니다.

말씀하신 것처럼 JwtFilter와 SecurityConfig의 필터 체인에서 이미 401/403 처리가 완료되었고, 그 이후 서비스 레이어에서 SecurityContext가 비어있다면 이는 인증 실패가 아니라 서버 내부의 컨텍스트 전파 문제이므로 500 에러가 적절합니다.

Spring Security에서 error dispatch나 forward 시 SecurityContext가 의도치 않게 비워질 수 있는 케이스들이 있어, 이러한 상황을 서버 오류로 분류한 설계 판단이 합리적입니다.

🐰


🧠 Learnings used
Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/{exception,domain/enums}/*.java : Use domain-specific error status enums (e.g., EventErrorStatus) for error handling

Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 246
File: src/main/java/side/onetime/auth/handler/OAuthLoginSuccessHandler.java:124-125
Timestamp: 2025-06-04T16:25:27.887Z
Learning: 사용자 bbbang105는 OAuth 플로우에서 User-Agent 헤더가 null인 경우가 드물다고 판단하며, 예외 처리보다는 "Unknown" 같은 기본값으로 실용적으로 처리하는 것을 선호한다.

Comment on lines +1 to +32
package side.onetime.util;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import side.onetime.auth.dto.CustomAdminDetails;
import side.onetime.exception.CustomException;
import side.onetime.global.common.status.ErrorStatus;

import java.util.Optional;

public class AdminAuthorizationUtil {

private AdminAuthorizationUtil() {
throw new AssertionError();
}

/**
* 현재 로그인한 관리자의 ID를 반환하는 메서드.
*
* SecurityContextHolder에서 Authentication을 가져와
* CustomAdminDetails로 캐스팅한 후, 관리자 ID를 추출합니다.
*
* @return 로그인된 관리자의 ID
*/
public static Long getLoginAdminId() {
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getPrincipal)
.filter(principal -> principal instanceof CustomAdminDetails)
.map(principal -> ((CustomAdminDetails) principal).getId())
.orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
}
}
Copy link

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find UserAuthorizationUtil to compare patterns
find . -type f -name "*UserAuthorizationUtil*" | head -20

Repository: onetime-with-members/backend

Length of output: 132


🏁 Script executed:

# Find ErrorStatus enum to check error codes
find . -type f -name "ErrorStatus.java" | head -20

Repository: onetime-with-members/backend

Length of output: 138


🏁 Script executed:

# Search for ErrorStatus definitions to understand _UNIDENTIFIED_USER and _UNAUTHORIZED
rg "ErrorStatus\._" --type java | grep -E "(UNIDENTIFIED_USER|UNAUTHORIZED)" | head -20

Repository: onetime-with-members/backend

Length of output: 632


🏁 Script executed:

cat -n ./src/main/java/side/onetime/global/common/status/ErrorStatus.java

Repository: onetime-with-members/backend

Length of output: 2253


🏁 Script executed:

cat -n ./src/main/java/side/onetime/util/UserAuthorizationUtil.java

Repository: onetime-with-members/backend

Length of output: 1374


getLoginAdminId() 메서드는 미인증/anonymous 요청에 500 상태코드를 반환하고 있어, 401 또는 403으로 먼저 분리할 것을 권장합니다.

현재 구현에서 SecurityContextHolder.getContext().getAuthentication()이 null인 경우(미인증), 또는 principal이 CustomAdminDetails가 아닌 정상 케이스(anonymous, 테스트 컨텍스트, 비동기 컨텍스트 등)는 모두 _UNIDENTIFIED_USER(500)으로 처리됩니다. 이는 의미상 서버 오류가 아닌 클라이언트의 인증 부재 문제이므로, authentication == null 또는 anonymous 계열은 _UNAUTHORIZED(401) 또는 _FORBIDDEN(403)으로 분리하는 흐름을 권장합니다. 이는 UserAuthorizationUtil과 동일한 패턴으로 나타나고 있습니다.

🤖 Prompt for AI Agents
In @src/main/java/side/onetime/util/AdminAuthorizationUtil.java around lines 1 -
32, getLoginAdminId currently maps any missing/anonymous principal to
CustomException(ErrorStatus._UNIDENTIFIED_USER); change it to first check
SecurityContextHolder.getContext().getAuthentication() and if null throw new
CustomException(ErrorStatus._UNAUTHORIZED), then if principal is not an instance
of CustomAdminDetails (handle anonymous/test contexts) throw new
CustomException(ErrorStatus._FORBIDDEN) otherwise return ((CustomAdminDetails)
principal).getId(); update references in this method (getLoginAdminId,
SecurityContextHolder, CustomAdminDetails, ErrorStatus, CustomException) to
implement that branching consistent with UserAuthorizationUtil.

Copy link
Member Author

Choose a reason for hiding this comment

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

앞단에서 401, 403 인증/인가 처리는 하고 있어
여기까지 오려면 인증/인가 처리는 성공인데 SecurityContext나 서버 오류때매 발생하는 것으로 판단해서 500 에러로 변경했어

Copy link

Choose a reason for hiding this comment

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

@anxi01, 설명 감사합니다! 이해했습니다.

말씀하신 아키텍처 구조가 타당합니다. Spring Security filter chain에서는 인증 필터가 성공한 후 권한 필터가 실행되며, 인증 실패 시 ExceptionTranslationFilter가 AuthenticationEntryPoint로 포워딩하여 인증 challenge를 반환합니다.

현재 구조에서:

  1. JwtFilter에서 토큰 검증 및 SecurityContext 설정
  2. SecurityConfig에서 ROLE_ADMIN 기반 권한 검증
  3. CustomAuthenticationEntryPoint(401)와 CustomAccessDeniedHandler(403)가 필터 레벨에서 인증/권한 오류 처리

이 모든 과정을 통과한 후 AdminAuthorizationUtil.getLoginAdminId()가 호출되므로, 이 시점에서 SecurityContext가 null이거나 잘못된 principal 타입을 갖는 것은 예상치 못한 서버 내부 상태 불일치입니다. 따라서 500 에러(_UNIDENTIFIED_USER)가 적절합니다.


✏️ Learnings added
Learnt from: anxi01
Repo: onetime-with-members/backend PR: 312
File: src/main/java/side/onetime/util/AdminAuthorizationUtil.java:1-32
Timestamp: 2026-01-10T07:56:05.197Z
Learning: In the `onetime-with-members/backend` repository, `AdminAuthorizationUtil.getLoginAdminId()` intentionally throws `CustomException(ErrorStatus._UNIDENTIFIED_USER)` (500 error) when SecurityContext is null or contains wrong principal type. This is because authentication (401) and authorization (403) are already handled upstream in the security filter chain (JwtFilter, SecurityConfig, CustomAuthenticationEntryPoint, CustomAccessDeniedHandler). If execution reaches this utility method, auth should have succeeded, so any SecurityContext failure indicates a server-side state inconsistency, not a client authentication issue.

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


🧠 Learnings used
Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 246
File: src/main/java/side/onetime/auth/handler/OAuthLoginSuccessHandler.java:124-125
Timestamp: 2025-06-04T16:25:27.887Z
Learning: 사용자 bbbang105는 OAuth 플로우에서 User-Agent 헤더가 null인 경우가 드물다고 판단하며, 예외 처리보다는 "Unknown" 같은 기본값으로 실용적으로 처리하는 것을 선호한다.

Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

Comment on lines 25 to 31
public static Long getLoginUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();

if (!(principal instanceof CustomUserDetails userDetails)) {
throw new CustomException(UserErrorStatus._UNAUTHORIZED);
}
return userDetails.getId();
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getPrincipal)
.filter(principal -> principal instanceof CustomUserDetails)
.map(principal -> ((CustomUserDetails) principal).getId())
.orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
}
Copy link

@coderabbitai coderabbitai bot Jan 10, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the ErrorStatus class to verify the error codes
find . -type f -name "*.java" | xargs grep -l "enum ErrorStatus\|class ErrorStatus" | head -5

Repository: onetime-with-members/backend

Length of output: 138


🏁 Script executed:

# Find and examine UserAuthorizationUtil.java
find . -type f -name "UserAuthorizationUtil.java" -exec cat -n {} +

Repository: onetime-with-members/backend

Length of output: 1374


🏁 Script executed:

# Search for _UNAUTHORIZED, _UNIDENTIFIED_USER, _FORBIDDEN definitions
rg "_UNAUTHORIZED|_UNIDENTIFIED_USER|_FORBIDDEN" --type java -A 2 -B 2 | head -100

Repository: onetime-with-members/backend

Length of output: 5307


🏁 Script executed:

# Check CustomUserDetails class
find . -type f -name "CustomUserDetails.java" -exec cat -n {} +

Repository: onetime-with-members/backend

Length of output: 1154


미인증/anonymous 경로가 500(_UNIDENTIFIED_USER)로 떨어질 수 있어 401/403와 분리해 주세요.

현재 코드는 Authentication이 null인 경우, Principal이 null인 경우, 그리고 principalCustomUserDetails가 아닌 경우 모두 500으로 응답합니다. 이는 API 클라이언트 관점에서 문제가 있습니다. permitAll 엔드포인트에서 실수로 호출되거나, anonymous가 활성화된 환경, 혹은 비동기 실행(@async 등)에서 인증 정보가 없으면 _UNAUTHORIZED(401)로, 인증 정보는 있지만 타입이 맞지 않으면 _UNIDENTIFIED_USER(500)로 분기하는 것이 HTTP 의미론적으로 안전합니다.

추가로 instanceof와 명시적 캐스팅을 isInstance/cast 메서드로 정리하면 더 간결합니다.

제안 diff
 public static Long getLoginUserId() {
-        return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
-                .map(Authentication::getPrincipal)
-                .filter(principal -> principal instanceof CustomUserDetails)
-                .map(principal -> ((CustomUserDetails) principal).getId())
-                .orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null || authentication.getPrincipal() == null) {
+            throw new CustomException(ErrorStatus._UNAUTHORIZED);
+        }
+
+        return Optional.of(authentication.getPrincipal())
+                .filter(CustomUserDetails.class::isInstance)
+                .map(CustomUserDetails.class::cast)
+                .map(CustomUserDetails::getId)
+                .orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
 }
📝 Committable suggestion

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

Suggested change
public static Long getLoginUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (!(principal instanceof CustomUserDetails userDetails)) {
throw new CustomException(UserErrorStatus._UNAUTHORIZED);
}
return userDetails.getId();
return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
.map(Authentication::getPrincipal)
.filter(principal -> principal instanceof CustomUserDetails)
.map(principal -> ((CustomUserDetails) principal).getId())
.orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
}
public static Long getLoginUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getPrincipal() == null) {
throw new CustomException(ErrorStatus._UNAUTHORIZED);
}
return Optional.of(authentication.getPrincipal())
.filter(CustomUserDetails.class::isInstance)
.map(CustomUserDetails.class::cast)
.map(CustomUserDetails::getId)
.orElseThrow(() -> new CustomException(ErrorStatus._UNIDENTIFIED_USER));
}
🤖 Prompt for AI Agents
In @src/main/java/side/onetime/util/UserAuthorizationUtil.java around lines 25 -
31, getLoginUserId currently maps any missing or non-matching
authentication/principal to ErrorStatus._UNIDENTIFIED_USER causing 500s; change
it to throw CustomException(ErrorStatus._UNAUTHORIZED) when
SecurityContextHolder.getContext().getAuthentication() is null or getPrincipal()
is null (i.e., unauthenticated/anonymous), and only throw
CustomException(ErrorStatus._UNIDENTIFIED_USER) when a principal exists but is
not an instance of CustomUserDetails; simplify the instanceof + cast by using
CustomUserDetails.class::isInstance and CustomUserDetails.class::cast when
extracting the id from the principal.

Copy link
Member Author

Choose a reason for hiding this comment

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

앞단에서 401, 403 인증/인가 처리는 하고 있어
여기까지 오려면 인증/인가 처리는 성공인데 SecurityContext나 서버 오류때매 발생하는 것으로 판단해서 500 에러로 변경했어

Copy link

Choose a reason for hiding this comment

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

@anxi01, 맞습니다! 설명 주셔서 감사합니다.

필터 체인에서 인증이 성공한 후 SecurityContext에 인증 정보가 저장되고, 인가 필터들이 SecurityContext에 의존하여 접근 제어를 수행하는 Spring Security의 아키텍처를 고려하면, 귀하의 접근 방식이 타당합니다.

아키텍처 관점:

  • JwtFilterSecurityConfig에서 이미 인증/인가(401/403)를 처리
  • 서비스 레이어의 유틸리티는 필터 체인 통과 후에만 호출됨
  • 이 시점에 SecurityContext가 비어있다면 비동기 메서드 실행 시 SecurityContext가 손실되는 것처럼 SecurityContext 전파 실패 등 서버 측 문제를 나타냄

다만 향후 유지보수를 위해 해당 설계 의도를 코드에 주석으로 남겨두시는 것을 권장드립니다:

/**
 * 현재 로그인한 사용자의 ID를 반환하는 메서드.
 *
 * SecurityContextHolder에서 Authentication을 가져와
 * CustomUserDetails로 캐스팅한 후, 사용자 ID를 추출합니다.
 *
 * 주의: 이 메서드는 JwtFilter와 SecurityConfig에서 인증/인가가
 * 성공한 후에만 호출됩니다. SecurityContext가 null이거나
 * Principal이 예상 타입이 아닌 경우, 이는 클라이언트 인증 실패가 아닌
 * SecurityContext 전파 실패 등 서버 내부 오류를 의미합니다.
 *
 * @return 로그인된 사용자의 ID
 * @throws CustomException ErrorStatus._UNIDENTIFIED_USER (500) - SecurityContext 전파 실패 등 서버 오류
 */

제안 철회하겠습니다. 현재 구현이 적절합니다! 👍


🧠 Learnings used
Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 246
File: src/main/java/side/onetime/auth/handler/OAuthLoginSuccessHandler.java:124-125
Timestamp: 2025-06-04T16:25:27.887Z
Learning: 사용자 bbbang105는 OAuth 플로우에서 User-Agent 헤더가 null인 경우가 드물다고 판단하며, 예외 처리보다는 "Unknown" 같은 기본값으로 실용적으로 처리하는 것을 선호한다.

Learnt from: bbbang105
Repo: onetime-with-members/backend PR: 246
File: src/main/java/side/onetime/service/TokenService.java:37-37
Timestamp: 2025-06-04T16:25:22.271Z
Learning: JwtUtil의 getClaimFromToken 메서드는 JWT 파싱 및 클레임 추출 과정에서 발생하는 JwtException과 IllegalArgumentException을 catch하여 _TOKEN_CLAIM_EXTRACTION_ERROR로 변환하여 던진다. 따라서 클레임이 누락되거나 추출에 실패하는 경우에 대한 별도의 null 체크는 불필요하다.

Learnt from: CR
Repo: onetime-with-members/backend PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T11:41:05.378Z
Learning: Applies to src/main/java/**/auth/*.java : Use Spring Security, OAuth2 (Google, Kakao, Naver), and JWT (JJWT) for authentication and authorization

@anxi01 anxi01 merged commit 6b01752 into develop Jan 10, 2026
4 checks passed
@anxi01 anxi01 deleted the refactor/#311 branch January 10, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 feat 새로운 기능 추가 / 일부 코드 추가 / 일부 코드 수정 (리팩토링과 구분) / 디자인 요소 수정 🔄 refactor 코드 리팩토링 😉 seongmin 성민 PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] : JwtFilter의 인증 로직을 Security FilterChain 으로 통합한다

3 participants