Skip to content

Conversation

@kaswhy
Copy link
Member

@kaswhy kaswhy commented Aug 26, 2025

📌 연관된 이슈

✨ 작업 내용

  • 리팩함

💬 리뷰 요구사항(선택)

Summary by CodeRabbit

  • New Features
    • API 문서(OpenAPI) 추가 및 Bearer 인증 표시.
    • 중복 확인 API가 쿼리 파라미터 입력과 isExists를 포함한 응답 객체로 반환.
    • 멤버 상세 조회에 설문 답변 포함, 관리자 권한 필요.
    • 비밀번호 재설정 경로 공개 허용.
  • Changes
    • OAuth 로그인 응답 키가 exists → isExists로 변경.
    • 신규 모집 신청 생성 시 결제 상태가 항상 미결제 처리.
  • Bug Fixes
    • 유효성 오류(400), 인증 실패(401), 권한 없음(403) 시 일관된 JSON 에러 응답 제공.

@kaswhy kaswhy self-assigned this Aug 26, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

📥 Commits

Reviewing files that changed from the base of the PR and between e2062cf and f314721.

📒 Files selected for processing (32)
  • src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java (0 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1 hunks)
  • src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (3 hunks)
  • src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2 hunks)
  • src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/BusinessException.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (5 hunks)
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java (4 hunks)
 _________________
< Shell we dance? >
 -----------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure languagetool in your project's settings in CodeRabbit enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

✨ Finishing Touches
  • 📝 Docstrings were successfully generated. (🔄 Check again to generate docstrings again)
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/issue-197

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

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

Support

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

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

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

CodeRabbit Configuration File (.coderabbit.yaml)

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

Status, Documentation and Community

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

@github-actions
Copy link

github-actions bot commented Aug 26, 2025

CI status
✅ Assemble 성공
✅ Test 성공

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

Caution

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

⚠️ Outside diff range comments (7)
src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (3)

115-118: 로그에 리프레시 토큰 값이 그대로 노출됨

refreshCookie.toString()에는 Set-Cookie 헤더 값(토큰 본문 포함)이 그대로 출력됩니다. 운영 로그/수집 시스템으로 PII/자격증명이 유출될 수 있어 즉시 제거/마스킹이 필요합니다.

안전 로그로 교체:

-        log.info("Response Cookie에 저장된 Refresh Token: {}", refreshCookie.toString());
+        // 민감정보 노출 방지: 토큰 값 미출력
+        log.debug("Refresh token cookie added to response.");

152-154: 패스워드 로그인 흐름도 동일한 토큰 로그 노출 문제 존재

위와 동일하게 민감정보가 로그에 남습니다. 같은 방식으로 제거/마스킹하세요.

-        log.info("Response Cookie에 저장된 Refresh Token: {}", refreshCookie.toString());
+        log.debug("Refresh token cookie added to response.");

65-73: 외부 호출 예외/에러 응답 미처리로 NPE 및 5xx 전파 위험

postForEntity/exchange의 실패(status 4xx/5xx)나 body null 상황을 체크하지 않아 "access_token"/userinfo 추출 시 NPE 가능성이 있습니다. RestClientException 계열 예외도 상위로 전파됩니다. 상태코드 확인, 널 가드, 예외 매핑을 추가하세요.

예시(요지):

ResponseEntity<Map> tokenResponse = restTemplate.postForEntity(...);
if (!tokenResponse.getStatusCode().is2xxSuccessful() || tokenResponse.getBody() == null) {
    // 적절한 커스텀 예외로 매핑
    throw new OAuthExchangeFailedException("Google token response invalid");
}
Object at = tokenResponse.getBody().get("access_token");
if (!(at instanceof String googleAccessToken) || googleAccessToken.isBlank()) {
    throw new OAuthExchangeFailedException("access_token missing");
}

또한 ResponseEntity<Map>의 로 타입 사용을 지양하고 Map<String, Object>로 제네릭스를 명시하세요.

Also applies to: 79-85

src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java (1)

28-29: 요청 DTO에서 isPayed를 무시: 클라이언트 위변조 차단 관점으론 OK, 스키마에 노출은 제거/숨김 권장

toEntity에서 .isPayed(false)로 고정해 입력값을 무시하는 전략은 안전합니다. 다만 DTO에 필드를 남겨두면 API 문서/프론트에 혼선을 줍니다. 선택지:

  • 권장 A: DTO에서 필드 제거.
  • 권장 B: 입력 비직렬화 처리(@JsonProperty READ_ONLY) + 문서 비노출(@Schema READ_ONLY)로 혼선 제거.

아래는 B안의 최소 변경 diff입니다.

@@
 import lombok.Getter;
 import lombok.NoArgsConstructor;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
@@
-    private Boolean isPayed;
+    @Schema(accessMode = Schema.AccessMode.READ_ONLY)
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean isPayed;
@@
-                .isPayed(false)
+                .isPayed(false)

필드 자체를 제거하려면 다음 한 줄을 삭제하면 됩니다.

-    private Boolean isPayed;

추가로, 엔티티 레벨에서도 기본값을 보강해 두면 다른 생성 경로에서도 안전합니다(참고 코드 아래 댓글).

Also applies to: 43-44

src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (1)

9-9: 단위 표기 오류: Mb → MB로 정정 필요

사용자 응답 메시지의 단위가 잘못되었습니다. Mb(메가비트) 대신 MB(메가바이트)를 사용해야 합니다. 오해 소지가 있어 정정 권장합니다.

-    INVALID_BIG_FILE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10Mb를 넘을 수 없습니다.");
+    INVALID_BIG_FILE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10MB를 넘을 수 없습니다.");
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)

34-45: [중요] 저장 시 SurveyType 불일치 가능성 → 조회 결과가 비는 위험

저장은 SurveyType.fromType("recruit form")를 사용하지만, 조회는 SurveyType.RECRUIT 고정값으로 필터링합니다. 매핑 결과가 다르면 저장된 답변을 조회하지 못할 수 있습니다. 저장/조회 모두 동일한 enum 상수를 직접 사용하도록 통일하는 것이 안전합니다.

다음과 같이 상수를 직접 사용하도록 변경을 제안합니다.

-                        return new Answer(member, SurveyType.fromType("recruit form"), InputType.fromQuestion(
+                        return new Answer(member, SurveyType.RECRUIT, InputType.fromQuestion(
                                 entry.getKey()), jsonValue);
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)

33-39: 필터 skipPaths와 실제 엔드포인트 경로 일치 작업 필요

TokenAuthenticationFilter의 skipPaths 목록이 /auth/** 기반으로 설정되어 있으나, 실제 AuthController의 클래스 레벨 매핑은 /api/v1/auth입니다. 이로 인해 토큰 인증 필터가 로그인·회원가입 등 인증이 불필요한 엔드포인트에서도 불필요하게 동작할 수 있습니다.

점검 및 수정이 필요한 위치:

  • src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (약 45행): skipPaths 리스트
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java (authorizeHttpRequests 설정): /api/v1/auth/**에 대한 permitAll() 포함 여부

예상 수정 예시:

- List<String> skipPaths = List.of(
-     "/auth/refresh", "/auth/login", "/auth/oauth2/google/callback",
-     "/auth/signup", "/auth/findId", "/auth/password-reset/request",
-     "/auth/password-reset/verify", "/auth/password-reset/confirm"
- );
+ List<String> skipPaths = List.of(
+     "/api/v1/auth/refresh", "/api/v1/auth/login", "/api/v1/auth/oauth2/google/callback",
+     "/api/v1/auth/signup", "/api/v1/auth/findId", "/api/v1/auth/password-reset/request",
+     "/api/v1/auth/password-reset/verify", "/api/v1/auth/password-reset/confirm"
+ );

또는 패턴 매칭 로직으로 /api/v1 프리픽스를 유연하게 처리하도록 변경할 수도 있습니다.

🧹 Nitpick comments (31)
src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (1)

106-113: 리프레시 쿠키 도메인 설정 불일치(OAuth 로그인은 도메인 지정, 패스워드 로그인은 미지정)

OAuth 로그인에서는 .domain(".gdgocinha.com")로 서브도메인 전역 쿠키가 발급되지만, 패스워드 로그인은 도메인을 지정하지 않아 호스트 한정 쿠키가 됩니다. 서브도메인/환경 간 동작이 달라져 리프레시 토큰 전송이 비일관적일 수 있습니다. 의도라면 주석/문서화, 아니라면 통일을 권장합니다.

Also applies to: 144-150

src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java (1)

3-5: 중복체크 응답 boolean 컴포넌트명 통일 제안 (‘exists’ 사용)

현재 레포에는 이메일‧학번‧휴대폰 중복 확인용 Response 레코드들이 각기 다른 boolean 필드명을 사용하고 있습니다.

  • CheckDuplicatedEmailResponse → isDuplicated
  • CheckStudentIdResponse → isExists
  • CheckPhoneNumberResponse → isExists

JSON/스키마 가독성 및 클라이언트 일관성을 위해, 세 클래스를 모두 다음처럼 exists로 통일할 것을 권장드립니다.

적용 예시:

--- src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckDuplicatedEmailResponse.java
-public record CheckDuplicatedEmailResponse(boolean isDuplicated) {
+public record CheckDuplicatedEmailResponse(boolean exists) {
 }
--- src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java
-public record CheckStudentIdResponse(boolean isExists) {
+public record CheckStudentIdResponse(boolean exists) {
 }
--- src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java
-public record CheckPhoneNumberResponse(boolean isExists) {
+public record CheckPhoneNumberResponse(boolean exists) {
 }
  • 레코드 컴포넌트명 변경에 따라 기존 isDuplicated(), isExists() 호출부를 모두 exists()로 함께 수정해야 합니다.
  • 변경된 JSON 필드명(exists)이 API 소비자(프론트엔드, 테스트 등)에 미치는 영향을 확인해 주세요.
src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java (1)

3-5: 불리언 필드 명명 'isExists' → 'exists'로 변경 고려 (JSON/스키마 가독성)

위 학번 응답과 동일한 맥락입니다. 동일 패턴의 DTO들은 가능한 한 동일한 프로퍼티명을 유지하는 것이 좋습니다. 아래 리네임을 제안드립니다.

 package inha.gdgoc.domain.recruit.dto.response;

-public record CheckPhoneNumberResponse(boolean isExists) {
+public record CheckPhoneNumberResponse(boolean exists) {
 
 }

이미 컨트롤러/서비스에서 isExists()를 사용 중이라면 exists()로 함께 수정되어야 합니다. 위 코멘트의 스크립트로 전체 일관성도 확인 부탁드립니다.

src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1)

3-9: 에러 코드 enum에서 Lombok 사용이 다른 enum과 불일치 — 한 가지 스타일로 통일 권장

본 파일은 @requiredargsconstructor로 생성자를 생성하는 반면, StudyAttendeeErrorCode 등은 명시적 생성자를 둡니다. 두 방식 모두 문제는 없지만, 동일 계층의 에러코드 enum은 하나의 스타일로 맞추는 것이 유지보수에 유리합니다. 현재 코드베이스 다수의 enum이 명시적 생성자를 사용하는 흐름이라면 Lombok 의존을 제거하는 방향을 추천드립니다.

제안 패치:

 package inha.gdgoc.domain.recruit.exception;

-import inha.gdgoc.global.exception.ErrorCode;
-import lombok.RequiredArgsConstructor;
+import inha.gdgoc.global.exception.ErrorCode;
 import org.springframework.http.HttpStatus;
 
-@RequiredArgsConstructor
 public enum RecruitMemberErrorCode implements ErrorCode {
 
     // 404 NOT FOUND
     RECRUIT_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 멤버를 찾을 수 없습니다.");
 
     private final HttpStatus status;
     private final String message;
 
+    RecruitMemberErrorCode(HttpStatus status, String message) {
+        this.status = status;
+        this.message = message;
+    }
+
     @Override
     public HttpStatus getStatus() {
         return status;
     }

반대로 Lombok으로 전환을 선호한다면, 다른 에러코드 enum들에도 @requiredargsconstructor를 적용해 일관성을 맞춰 주세요.

src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1)

3-3: LGTM: 패키지 경로 정리 적절

import를 global.exception으로 옮긴 변경은 적절합니다. 이 파일은 명시적 생성자 패턴을 쓰고 있으므로, 위 RecruitMemberErrorCode와의 스타일만 추후 한 방향으로 정리되면 좋겠습니다.

src/main/java/inha/gdgoc/global/exception/BusinessException.java (1)

3-3: NPE 방지 및 원인 체인 보존을 위한 생성자 보강 제안

errorCode가 null이면 생성자에서 즉시 NPE가 발생합니다. 방어적 null 체크와 cause를 받는 오버로드를 추가하면 추적성이 좋아집니다.

아래 변경을 권장합니다.

 import lombok.Getter;
+import java.util.Objects;

 @Getter
 public class BusinessException extends RuntimeException {

     private final ErrorCode errorCode;

     public BusinessException(ErrorCode errorCode) {
-        super(errorCode.getMessage());
-        this.errorCode = errorCode;
+        super(Objects.requireNonNull(errorCode, "errorCode must not be null").getMessage());
+        this.errorCode = errorCode;
     }
+
+    public BusinessException(ErrorCode errorCode, Throwable cause) {
+        super(Objects.requireNonNull(errorCode, "errorCode must not be null").getMessage(), cause);
+        this.errorCode = errorCode;
+    }
 }

Also applies to: 10-13

src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)

13-21: 오류 응답 포맷 통일 권장
점검 결과 ErrorResponseApiResponse.error(...)가 혼용되어 있어, 엔드포인트별 응답 스키마가 상이하게 반환되고 있습니다.

주요 사용처

  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java (60–64, 72–76)

    • new ErrorResponse(GlobalErrorCode.UNAUTHORIZED_USER)
    • new ErrorResponse(GlobalErrorCode.FORBIDDEN_USER)
  • src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (38, 52, 69, 83, 101, 113, 125, 138, 151)

    • ApiResponse.error(errorCode, meta)
    • ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta) 등 다수

API 일관성을 위해 하나의 포맷(예: ApiResponse.error(...))으로 통일하여 사용하도록 리팩토링을 권장드립니다.

src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (2)

29-35: 에러 응답 편의 오버로드 추가 제안(error(ErrorCode))

meta가 필요 없는 케이스가 많아 보입니다. null 전달을 없애는 편의 오버로드를 추가하면 호출부가 간결해집니다.

다음 변경을 제안합니다.

 public static ApiResponse<Void, ErrorMeta> error(ErrorCode errorCode, ErrorMeta meta) {
     return new ApiResponse<>(errorCode.getStatus().value(), errorCode.getMessage(), null, meta);
 }
 
 public static ApiResponse<Void, ErrorMeta> error(int code, String message, ErrorMeta meta) {
     return new ApiResponse<>(code, message, null, meta);
 }
+
+public static ApiResponse<Void, ErrorMeta> error(ErrorCode errorCode) {
+    return error(errorCode, null);
+}

9-27: 매직 넘버(200/201) 제거 니트픽

상태 코드를 상수 200/201 대신 HttpStatus로 표현하면 가독성과 일관성이 개선됩니다. 선택사항입니다.

예시:

+import org.springframework.http.HttpStatus;
 ...
-    return new ApiResponse<>(200, message, data, null);
+    return new ApiResponse<>(HttpStatus.OK.value(), message, data, null);
 ...
-    return new ApiResponse<>(201, message, null, null);
+    return new ApiResponse<>(HttpStatus.CREATED.value(), message, null, null);
src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1)

8-16: 메시지 포맷팅 유틸 추가(선택)

GlobalErrorCode에는 format(Object...)이 있어 메시지 가변 인자 대체가 가능합니다. 동일한 패턴을 도입하면 일관성이 올라갑니다. 필수는 아닙니다.

예시:

 public enum StudyErrorCode implements ErrorCode {
 ...
     private final HttpStatus status;
     private final String message;
 ...
     public String getMessage() {
         return message;
     }
+
+    public String format(Object... args) {
+        return String.format(this.message, args);
+    }
src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (2)

108-113: JWT 시크릿 Base64 인코딩 로직 중복 제거

서명(signWith)과 파서(setSigningKey)에서 동일한 인코딩 로직이 반복됩니다. 메서드로 추출해 유지보수성을 높이는 것이 좋습니다.

예시:

+    private String encodedSecretKey() {
+        return Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes());
+    }
...
-                .signWith(SignatureAlgorithm.HS256,
-                        Base64.getEncoder().encodeToString(
-                                jwtProperties.getSecretKey().getBytes()
-                        )
-                )
+                .signWith(SignatureAlgorithm.HS256, encodedSecretKey())
...
-        return Jwts.parser()
-                .setSigningKey(
-                        Base64.getEncoder().encodeToString(
-                                jwtProperties.getSecretKey().getBytes()
-                        )
-                )
+        return Jwts.parser()
+                .setSigningKey(encodedSecretKey())
                 .parseClaimsJws(token)
                 .getBody();

Also applies to: 117-122


81-83: 권한 컬렉션 타입 범용화(선택)

authorities를 Set로 고정할 필요는 없습니다. Collection<? extends GrantedAuthority>로 넓히면 유연성이 올라갑니다. 영향도는 낮아 선택사항입니다.

-        Set<SimpleGrantedAuthority> authorities = Collections.singleton(
-                new SimpleGrantedAuthority(roleName)
-        );
+        Collection<? extends GrantedAuthority> authorities =
+                Collections.singleton(new SimpleGrantedAuthority(roleName));
src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java (2)

24-48: JSON → 안전한 원시 타입 변환으로 변경 제안(ObjectMapper default typing 영향 차단)

convertValue(Map/List)는 글로벌 ObjectMapper에 default typing이 활성화된 경우 예기치 못한 타입 인스턴스화를 유발할 여지가 있습니다. 트리(JsonNode)를 직접 순회하여 문자열/숫자/불리언/리스트/맵의 안전한 조합으로만 변환하는 편이 더 안전하고 예측 가능하며 GC 부담도 낮습니다.

아래처럼 수동 변환 루틴을 사용해 보세요.

@@
-    private static Object toFriendlyValue(String json, ObjectMapper om) {
-        if (json == null || json.isBlank()) return null;
-        try {
-            JsonNode node = om.readTree(json);
-
-            if (node.isTextual()) {
-                return node.asText();
-            }
-            if (node.isArray()) {
-                return om.convertValue(node, new TypeReference<List<Object>>() {});
-            }
-            if (node.isObject()) {
-                return om.convertValue(node, new TypeReference<Map<String, Object>>() {});
-            }
-            if (node.isNumber()) {
-                return node.numberValue();
-            }
-            if (node.isBoolean()) {
-                return node.booleanValue();
-            }
-            return null;
-        } catch (Exception e) {
-            return json;
-        }
-    }
+    private static Object toFriendlyValue(String json, ObjectMapper om) {
+        if (json == null || json.isBlank()) return null;
+        try {
+            JsonNode node = om.readTree(json);
+            return toPlainNode(node);
+        } catch (Exception e) {
+            // 파싱 실패 시 원문을 그대로 반환해 클라이언트가 진단할 수 있도록 유지
+            return json;
+        }
+    }
+
+    private static Object toPlainNode(JsonNode node) {
+        if (node == null || node.isNull()) return null;
+        if (node.isTextual()) return node.asText();
+        if (node.isNumber()) return node.numberValue();   // BigDecimal/BigInteger 유지
+        if (node.isBoolean()) return node.booleanValue();
+        if (node.isArray()) {
+            java.util.List<Object> list = new java.util.ArrayList<>(node.size());
+            for (JsonNode child : node) {
+                list.add(toPlainNode(child));
+            }
+            return list;
+        }
+        if (node.isObject()) {
+            java.util.Map<String, Object> map = new java.util.LinkedHashMap<>();
+            node.fields().forEachRemaining(e -> map.put(e.getKey(), toPlainNode(e.getValue())));
+            return map;
+        }
+        return null;
+    }

부가적으로, 변환 실패를 로깅하고 싶다면 catch 블록에 debug 수준 로그를 남기는 것도 고려해 주세요(현재 클래스에는 로거가 없어 선택 사항).


16-22: ObjectMapper 주입 null 방어(선택)

from(...)에서 om이 null이면 NPE가 발생합니다. 방어 코드 또는 사전 조건 검사로 조기 실패를 명확히 하는 것이 좋습니다.

-    public static AnswerResponse from(Answer answer, ObjectMapper om) {
+    public static AnswerResponse from(Answer answer, ObjectMapper om) {
+        if (om == null) {
+            throw new IllegalArgumentException("ObjectMapper must not be null");
+        }
         return new AnswerResponse(
                 answer.getId(),
                 answer.getInputType(),
                 toFriendlyValue(answer.getResponseValue(), om)
         );
     }
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1)

10-12: 메시지 포맷터 추가(선택)

GlobalErrorCode와의 일관성을 위해 format(Object...) 유틸을 추가하는 것을 고려해 보세요. 반복되는 String.format 호출을 줄일 수 있습니다.

     public String getMessage() {
         return message;
     }
+
+    public String format(Object... args) {
+        return String.format(this.message, args);
+    }
src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java (1)

43-44: 엔티티 기본값 보강으로 일관성 확보 권장

현재는 DTO → Entity 변환 경로에서만 false가 보장됩니다. 엔티티 빌더를 직접 쓰는 다른 코드 경로가 생기면 null 가능성이 재발할 수 있습니다. 엔티티 필드에 기본값과 @Builder.Default를 추가해 이중 안전장치를 권장합니다.

아래는 엔티티 측 참고 코드(외부 파일 지원용 예시):

// in RecruitMember.java
@Builder.Default
@Column(name = "is_payed", nullable = false)
private Boolean isPayed = false;

또한 표기법은 일반적으로 "Paid"가 맞습니다. 도메인/DB 마이그레이션 범위가 크므로 본 PR에선 유지하되, 후속 PR에서 isPayed → isPaid 및 컬럼명 정합화 권장.

src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java (2)

11-14: 목록 정렬 보장 및 조회 패턴 확장 제안

현재 파생 쿼리는 반환 순서를 보장하지 않습니다. 응답 DTO가 리스트를 그대로 노출한다면, 일관된 정렬을 보장하는 시그니처를 함께 두는 편이 안전합니다.

다음과 같이 정렬 보장 메서드를 추가하는 것을 권장합니다(기존 메서드는 유지):

 public interface AnswerRepository extends JpaRepository<Answer, Long> {
 
     List<Answer> findByRecruitMemberAndSurveyType(
             RecruitMember recruitMember,
             SurveyType surveyType
     );
+
+    // id 기준 오름차순 정렬 보장
+    List<Answer> findByRecruitMemberAndSurveyTypeOrderByIdAsc(
+            RecruitMember recruitMember,
+            SurveyType surveyType
+    );
 }

또한, 이후 서비스 계층에서 엔티티 자체를 보유하지 않고도 조회할 수 있도록 ID 기반 시그니처를 병행하는 것도 고려해보세요.

List<Answer> findByRecruitMemberIdAndSurveyTypeOrderByIdAsc(Long recruitMemberId, SurveyType surveyType);

11-14: 쿼리 성능 대비: 복합 인덱스 권장 (recruit_member, survey_type)

해당 조회는 다건 빈도가 높을 가능성이 있습니다. 데이터가 커지면 (recruit_member, survey_type) 복합 인덱스가 큰 도움이 됩니다.

Answer 엔티티에 아래와 같이 인덱스를 추가하는 것을 권장합니다(별도 파일 변경):

// src/main/java/inha/gdgoc/domain/recruit/entity/Answer.java
// class 선언 위에 추가
@Table(
    name = "answer",
    indexes = {
        @Index(name = "idx_answer_member_type", columnList = "recruit_member,survey_type")
    }
)
src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java (1)

10-16: Java 17 확인 완료 및 null 입력 방어 로직 추가(선택적 권장)

확인 결과:

  • build.gradle의 Java Toolchain 설정에서 languageVersion = JavaLanguageVersion.of(17)로 지정되어 있어, JDK 16+ 기능인 Stream.toList() 사용에 문제가 없습니다.

선택적 권장 사항:

  • 서비스 계층에서 entitiesnull로 넘어올 가능성이 있다면, NPE 발생을 방지하기 위해 아래와 같이 null/빈 리스트 방어 로직을 추가하는 것을 권장드립니다.
  • 반대로, 서비스 계층에서 항상 non-null 리스트를 보장하는 설계 계약이 명확하다면 해당 방어 로직은 생략하셔도 무방합니다.

수정 예시:

-    public static AnswersResponse from(List<Answer> entities, ObjectMapper objectMapper) {
-        return new AnswersResponse(
-                entities.stream()
-                        .map(a -> AnswerResponse.from(a, objectMapper))
-                        .toList()
-        );
-    }
+    public static AnswersResponse from(List<Answer> entities, ObjectMapper objectMapper) {
+        if (entities == null || entities.isEmpty()) {
+            return new AnswersResponse(List.of());
+        }
+        return new AnswersResponse(
+                entities.stream()
+                        .map(a -> AnswerResponse.from(a, objectMapper))
+                        .toList()
+        );
+    }
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)

18-32: 전역 SecurityRequirement 추가로 Swagger UI “자물쇠” 아이콘 보장하기

  • 확인 결과, RecruitMemberController.java 70행의
    @Operation(..., security = { @SecurityRequirement(name = "BearerAuth") })
    어노테이션이 schemeName = "BearerAuth"와 정확히 일치합니다.

따라서 컨트롤러 쪽 name 매칭은 오케이이며, 전역 설정만 추가해주시면 됩니다.
OpenApiConfig.java를 아래와 같이 수정해 주세요:

     @Bean
     public OpenAPI openAPI() {
         String schemeName = "BearerAuth";
-        return new OpenAPI()
+        return new OpenAPI()
                 .info(new Info().title("GDGoC API").version("v1"))
                 .components(new Components().addSecuritySchemes(
                         schemeName,
                         new SecurityScheme()
                                 .name(schemeName)
                                 .type(SecurityScheme.Type.HTTP)
                                 .scheme("bearer")
                                 .bearerFormat("JWT")
                         )
-                );
+                )
+                .addSecurityItem(new SecurityRequirement().addList(schemeName));
     }

추가로, 파일 상단에 SecurityRequirement 임포트를 넣어두시면 가독성이 좋아집니다:

import io.swagger.v3.oas.models.security.SecurityRequirement;
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (3)

41-43: JSON 직렬화 예외 처리 범위 축소 및 도메인 예외 변환 제안

catch (Exception e)는 과도하게 넓습니다. JsonProcessingException 등 구체 예외로 좁히고, 글로벌 에러 정책에 맞는 비즈니스 예외로 변환하는 것을 권장합니다(예: INVALID_JSON_REQUEST).

-                    } catch (Exception e) {
-                        throw new RuntimeException("JSON 변환 오류", e);
+                    } catch (com.fasterxml.jackson.core.JsonProcessingException e) {
+                        throw new BusinessException(GlobalErrorCode.INVALID_JSON_REQUEST, e);
                     }

29-48: 엔터티 연관 및 트랜잭션 관점에서의 저장 전략 점검

현재는 member 저장 후, answers를 별도로 saveAll 합니다. 연관 매핑이 mappedBy = "recruitMember", cascade = CascadeType.ALL로 설정되어 있어 멤버에 answers를 추가하고 멤버만 저장해도 됩니다. 도메인 연관 일관성을 위해 member.getAnswers().addAll(answers) 방식으로 정착하는 것도 고려해보세요.


62-69: 조회 메서드 readOnly 트랜잭션 적용 권장

쓰기 없는 조회 메서드에 @Transactional(readOnly = true)를 적용하면 JPA 플러시/스냅샷 비용을 줄일 수 있습니다.

-    public SpecifiedMemberResponse findSpecifiedMember(Long id) {
+    @Transactional(Transactional.TxType.SUPPORTS) // jakarta.transaction 사용 시
+    public SpecifiedMemberResponse findSpecifiedMember(Long id) {

또는 Spring @transactional을 사용 중이라면:

@org.springframework.transaction.annotation.Transactional(readOnly = true)
src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1)

12-14: 용어 일관성(naming) 제안: isPayed → isPaid

도메인에 이미 isPayed가 존재해 당장 변경은 어렵겠지만, 외부 공개 DTO에서는 일반적으로 isPaid 표기를 사용합니다. 차기 리비전에서의 명칭 통일을 고려해 주세요.

src/main/java/inha/gdgoc/global/security/SecurityConfig.java (3)

47-49: permitAll 경로 중복/오표기 정리 제안

"/api/v1/password-reset/**"는 실제 엔드포인트가 "/api/v1/auth/password-reset/**"이며, 또한 "/api/v1/auth/**"에 의해 이미 허용됩니다. 불필요하거나 잘못된 패턴은 혼선을 줄 수 있으니 정리 권장합니다.

-                                "/api/v1/password-reset/**")
+                                // "/api/v1/auth/**"에 포함되므로 별도 항목 불필요
+                                )

필요 시 구체화하려면:

-                                "/api/v1/password-reset/**")
+                                "/api/v1/auth/password-reset/**")

58-69: 에러 응답 직렬화 중복 제거 및 ObjectMapper 주입 사용

두 핸들러에서 ObjectMapper 인스턴스를 매번 생성합니다. 빈으로 주입받아 재사용하고, 공통 메서드로 중복을 줄이는 것을 권장합니다.

 public class SecurityConfig {
 
     private final TokenAuthenticationFilter tokenAuthenticationFilter;
+    private final ObjectMapper objectMapper;
@@
-                        .authenticationEntryPoint((request, response, authException) -> {
-                            response.setStatus(HttpStatus.UNAUTHORIZED.value());
-                            response.setContentType("application/json; charset=UTF-8");
-
-                            ErrorResponse errorResponse = new ErrorResponse(
-                                    GlobalErrorCode.UNAUTHORIZED_USER
-                            );
-
-                            ObjectMapper objectMapper = new ObjectMapper();
-                            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
-                            response.getWriter().flush();
-                        })
+                        .authenticationEntryPoint((request, response, authException) ->
+                                writeError(response, HttpStatus.UNAUTHORIZED, GlobalErrorCode.UNAUTHORIZED_USER))
                         .accessDeniedHandler((request, response, accessDeniedException) -> {
-                            response.setStatus(HttpStatus.FORBIDDEN.value());
-                            response.setContentType("application/json; charset=UTF-8");
-
-                            ErrorResponse errorResponse = new ErrorResponse(
-                                    GlobalErrorCode.FORBIDDEN_USER
-                            );
-
-                            ObjectMapper objectMapper = new ObjectMapper();
-                            response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
-                            response.getWriter().flush();
+                            writeError(response, HttpStatus.FORBIDDEN, GlobalErrorCode.FORBIDDEN_USER);
                         })
                 );
 
         return http.build();
     }
+
+    private void writeError(HttpServletResponse response, HttpStatus status, GlobalErrorCode code) throws IOException {
+        response.setStatus(status.value());
+        response.setContentType("application/json; charset=UTF-8");
+        response.getWriter().write(objectMapper.writeValueAsString(new ErrorResponse(code)));
+        response.getWriter().flush();
+    }

필요 import: import jakarta.servlet.http.HttpServletResponse; import java.io.IOException;

Also applies to: 70-81


90-99: CORS 로컬 개발 포트 확장 고려

프론트가 Vite(기본 5173) 등을 사용할 경우를 대비해 http://localhost:5173 추가를 고려해 주세요. 운영 영향은 없고 개발 편의성만 향상됩니다.

src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (2)

1-4: 패키지 이동 및 static import 중복 정리

패키지 이동은 적절합니다. 다만 Line 3의 와일드카드 static import와 Line 4의 FORBIDDEN_USER 단일 import가 중복입니다. 하나만 유지하세요.

-import static inha.gdgoc.global.exception.GlobalErrorCode.*;
-import static inha.gdgoc.global.exception.GlobalErrorCode.FORBIDDEN_USER;
+import static inha.gdgoc.global.exception.GlobalErrorCode.*;

72-84: 상태코드 소스 일관화 제안

ResponseEntity.status(BAD_REQUEST.getStatus())와 바디의 HttpStatus.BAD_REQUEST.value() 혼용은 독자성에 문제는 없지만, 한 소스(예: BAD_REQUEST)로 통일하면 유지보수성이 좋아집니다.

-        return ResponseEntity.status(BAD_REQUEST.getStatus())
-                .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
+        return ResponseEntity.status(BAD_REQUEST.getStatus())
+                .body(ApiResponse.error(BAD_REQUEST, meta));

(위는 ApiResponse.error(ErrorCode, meta) 시그니처가 있다면 적용)

src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (2)

36-43: 신청 저장 요청 본문 검증 활성화 제안

ApplicationRequest에 필드 제약이 있다면 @Valid를 붙여 Bean Validation을 트리거하세요. 현재는 무검증으로 들어올 수 있습니다.

-    public ResponseEntity<ApiResponse<Void, Void>> recruitMemberAdd(
-            @RequestBody ApplicationRequest applicationRequest
+    public ResponseEntity<ApiResponse<Void, Void>> recruitMemberAdd(
+            @RequestBody @jakarta.validation.Valid ApplicationRequest applicationRequest
     ) {

45-55: (선택) 메시지 키/응답 형식 통일성 점검

학생증/전화번호 중복 체크 응답의 메시지 키와 DTO 구조가 통일되어 있습니다. 추후 확장(예: 중복일 때의 추가 메타) 시에도 동일한 스키마를 유지해 주세요.

Also applies to: 57-68

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

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

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e2062cf and f314721.

📒 Files selected for processing (32)
  • src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java (0 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1 hunks)
  • src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (3 hunks)
  • src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2 hunks)
  • src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/BusinessException.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java (1 hunks)
  • src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (5 hunks)
  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java (4 hunks)
💤 Files with no reviewable changes (1)
  • src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java
🧰 Additional context used
🧬 Code graph analysis (27)
src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java (1)
src/main/java/inha/gdgoc/domain/recruit/entity/Answer.java (2)
  • Entity (24-58)
  • Answer (52-57)
src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (2)
src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java (2)
  • Slf4j (40-189)
  • Transactional (148-172)
src/main/java/inha/gdgoc/global/error/BusinessException.java (1)
  • Getter (5-15)
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (2)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1)
src/main/java/inha/gdgoc/global/error/BusinessException.java (1)
  • Getter (5-15)
src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (2)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/recruit/controller/message/RecruitMemberMessage.java (1)
  • RecruitMemberMessage (3-8)
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (3)
src/main/java/inha/gdgoc/global/error/BusinessException.java (2)
  • Getter (5-15)
  • BusinessException (10-13)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (1)
  • ExceptionHandler (20-32)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/Answer.java (2)
  • Entity (24-58)
  • Answer (52-57)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1)
src/main/java/inha/gdgoc/global/error/BusinessException.java (1)
  • Getter (5-15)
src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java (1)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (2)
src/main/java/inha/gdgoc/global/error/BusinessException.java (2)
  • Getter (5-15)
  • BusinessException (10-13)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (1)
  • ExceptionHandler (20-32)
src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (2)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (2)
  • ErrorCode (5-10)
  • getStatus (7-7)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java (2)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (3)
src/main/java/inha/gdgoc/global/error/BusinessException.java (2)
  • Getter (5-15)
  • BusinessException (10-13)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (1)
  • ExceptionHandler (20-32)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (1)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (3)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (2)
  • ExceptionHandler (20-32)
  • ExceptionHandler (92-103)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java (1)
src/main/java/inha/gdgoc/global/error/ErrorCode.java (1)
  • ErrorCode (5-10)
src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)
  • isExistsByEmail (31-33)
src/main/java/inha/gdgoc/global/exception/BusinessException.java (1)
src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java (2)
  • ExceptionHandler (20-32)
  • Slf4j (16-108)
src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (2)
  • Override (39-70)
  • Slf4j (18-96)
src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/recruit/entity/Answer.java (2)
  • Entity (24-58)
  • Answer (52-57)
src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (1)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java (2)
src/main/java/inha/gdgoc/domain/recruit/entity/Answer.java (2)
  • Entity (24-58)
  • Answer (52-57)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/global/exception/ErrorCode.java (2)
src/main/java/inha/gdgoc/global/error/BusinessException.java (1)
  • Getter (5-15)
src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java (1)
  • GlobalErrorCode (5-45)
src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (2)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)
  • RequiredArgsConstructor (22-70)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (1)
src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java (1)
  • Entity (25-79)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (2)
src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (1)
  • Override (39-70)
src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java (1)
  • Slf4j (47-174)
🔇 Additional comments (28)
src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (1)

93-98: API 응답 키 ‘isExists’ 변경에 따른 클라이언트 호환성 위험

리포지토리 전체를 검색한 결과, "exists" 키 사용처는 발견되지 않았으나, 외부 클라이언트(웹/앱/테스트/문서)에서 여전히 "exists"를 기대할 가능성이 높습니다.

  • src/main/java/inha/gdgoc/domain/auth/service/AuthService.java의 94, 120라인에서만 "isExists" 사용 확인
  • 문서(*.yml, *.yaml, *.md) 및 코드 내 다른 참조 없음

해결 방안 제안

  • 과도기 동안 양쪽 키를 함께 반환하여 하위 호환 유지
  • 이후 단계적 제거(문서화된 Deprecation 절차 수립)
  • 장기적으론 DTO 도입으로 응답 스키마를 고정하여 타입 안정성 확보

최소 변경(되돌리기) 예시

@@ -93,6 +93,6 @@
-        return Map.of(
-            "isExists", false,
+        return Map.of(
+            "exists", false,
             "email", email,
             "name", name
         );
@@ -119,6 +119,6 @@
-        return Map.of(
-            "isExists", true,
+        return Map.of(
+            "exists", true,
             "accessToken", jwtAccessToken
         );

DTO 활용 예시

public record OAuthLoginResponse(
    boolean exists,
    String email,
    String name,
    String accessToken
) {}

…

// 비회원
return new OAuthLoginResponse(false, email, name, null);
// 기존 회원
return new OAuthLoginResponse(true, null, null, jwtAccessToken);

추가 검증 필요

  • 외부 클라이언트 및 API 문서에서 "exists" 키 사용 여부 확인
  • 변경 시점 및 절차에 대한 협의 및 문서화 진행
src/main/java/inha/gdgoc/global/exception/ErrorCode.java (1)

1-1: 패키지 마이그레이션 잔여 참조 및 중복 정의 없음 확인 완료

스크립트 실행 결과 아래 사항이 모두 정상임을 확인했습니다.

  • 레거시 패키지 inha.gdgoc.global.error에 대한 import 참조 없음
  • 레거시 패키지 선언(package inha.gdgoc.global.error) 잔존 없음
  • ErrorCode 인터페이스 정의는 src/main/java/inha/gdgoc/global/exception/ErrorCode.java 단일 파일에서만 확인됨

위 사항을 바탕으로 추가 조치가 필요하지 않음을 알려드립니다.

src/main/java/inha/gdgoc/global/exception/BusinessException.java (1)

1-15: 패키지 이동 적절 — 공개 API 유지됨

inha.gdgoc.global.error → inha.gdgoc.global.exception로의 이전이 깨지는 변경 없이 잘 반영되었습니다. 클래스 시그니처와 동작은 그대로입니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyException.java (2)

3-4: import 경로 변경 LGTM

예외 계층의 패키지 리네임에 맞춘 import 정리가 일관되게 적용되었습니다.


3-4: 레거시 inha.gdgoc.global.error 패키지 참조 없음 확인

전체 코드베이스에서 기존 inha.gdgoc.global.error 패키지에 대한 참조가 모두 제거되었음을 확인했습니다.

  • 레거시 import 사용 위치: 없음
  • 레거시 package 선언 파일: 없음
  • 레거시 디렉토리(src/main/java/inha/gdgoc/global/error): 존재하지 않음
src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java (1)

3-4: import 경로 정리 OK

BusinessException, ErrorCode의 새 패키지 경로로의 업데이트가 정확합니다. 동작 변화 없습니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java (1)

3-4: 일관된 예외 패키지 적용 확인

Study 도메인의 참석자 예외도 동일한 방식으로 마이그레이션되어 일관성이 좋습니다.

src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java (1)

3-3: ErrorCode import 경로 변경 적합

새 예외 패키지로의 import 변경만 이루어졌으며, 직렬화되는 응답 필드 불변성(final) 유지가 좋습니다.

src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java (1)

4-4: 예외 패키지 마이그레이션 정합성 확인 완료

  • inha.gdgoc.global.error 패키지 사용 흔적이 전혀 없습니다.
  • ErrorCode 인터페이스는 src/main/java/inha/gdgoc/global/exception/ErrorCode.java에 단일 정의만 존재합니다.
  • GlobalExceptionHandler는 일관되게 src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java에 위치합니다.

추가 검토나 조치가 필요하지 않습니다.

src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java (1)

3-3: 패키지 마이그레이션 반영 LGTM

global.exception으로의 import 이동이 일관성 있게 반영되었습니다. 동작 변화는 없으며 컴파일 타임 정합성만 개선됩니다.

src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1)

3-3: 패키지 경로 업데이트 LGTM

import 경로를 global.exception으로 통일한 변경이 적절합니다. 동작 변화는 없습니다.

src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1)

3-4: 레거시 import 검증 완료 — 문제 없음

검사 스크립트 결과 inha.gdgoc.global.error 패키지를 참조하는 import가 전혀 발견되지 않았습니다.
BusinessExceptionErrorCode 선언은 모두 inha.gdgoc.global.exception에 위치하므로 컴파일 충돌 우려가 없습니다.

src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java (1)

3-4: 패키지 경로 정리 및 레거시 import 제거 완료 확인

  • rg -nP --type=java -C1 'import\s+inha\.gdgoc\.global\.error\.(BusinessException|ErrorCode)\b' -g '!**/build/**' 실행 결과: 0건 발견
    → 모든 레거시 경로 참조가 제거되어 inha.gdgoc.global.exception 패키지로 일관되게 업데이트되었습니다.

머지해도 좋습니다.

src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java (1)

3-3: ErrorCode 위치 변경 반영 OK

global.exception.ErrorCode로의 import 변경은 일관성 있습니다. 나머지 참조들도 동일 기준으로 맞춰주세요(스크립트는 다른 코멘트 참고).

src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java (1)

3-4: 패키지 경로 및 잔존 레거시 import 없음 확인

  • BusinessException, ErrorCode 클래스는 모두 inha.gdgoc.global.exception 패키지에만 정의되어 있습니다.
  • 추가로 GlobalErrorCode, GlobalExceptionHandler 역시 동일 패키지에 존재하며, 중복 정의나 레거시 경로가 없습니다.
  • ResourceException 파일의 import 교체는 문제 없이 완료되었습니다.

위 검증 결과, 잔존 레거시 import나 이중 정의는 발견되지 않았습니다. 코드 변경 사항을 승인합니다.

src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java (1)

3-3: 패키지 이동 반영 OK

ErrorCode의 import 경로를 global.exception으로 변경한 부분이 전체 예외 패키지 리팩토링과 일관됩니다. 기능적 변화 없이 컴파일 타임 의존성만 정리된 것으로 보입니다.

src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)

60-81: 프리픽스 정규화(슬래시 처리) 및 서버 URL 호환성 점검 요청

현재 stripPrefixAndSetServerfullPrefix가 “/api/v1” 형태라고 가정하고 있어, 입력값에 trailing slash(“/api/v1/”)가 포함되면 경로 매핑이 어긋날 수 있습니다. 방어적으로 입력값을 정규화할 것을 권장하며, openApi.setServers(List.of(new Server().url(fullPrefix)))에 상대경로를 지정할 때 사용하는 코드 생성기나 클라이언트에서 처리 차이가 발생할 수 있으므로 직접 검증이 필요합니다.

  • 파일: src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java
  • 라인: 60–81

제안하는 최소 정규화 코드:

 private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) {
     return openApi -> {
+        // trailing slash 제거
+        if (fullPrefix.endsWith("/") && fullPrefix.length() > 1) {
+            fullPrefix = fullPrefix.substring(0, fullPrefix.length() - 1);
+        }
         Paths src = openApi.getPaths();
         if (src == null || src.isEmpty()) {
             return;
         }

레포지토리 전체를 검색했으나 openapi-generator 혹은 swagger-codegen 관련 설정이나 언급을 찾지 못했습니다. 실제 사용 중인 코드 생성기나 클라이언트 라이브러리 환경에서 상대경로("/api/v1") 처리 호환성을 반드시 확인해 주세요.

src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java (2)

50-54: 중복 학번 확인 DTO 반환으로의 전환: LGTM

불린이 아닌 CheckStudentIdResponse로 래핑하여 확장성/일관성을 높인 선택 좋습니다.


56-60: 중복 전화번호 확인 DTO 반환으로의 전환: LGTM

src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java (1)

16-27: Answers 포함한 팩토리 메서드 확장: LGTM

AnswersResponse.from(answers, objectMapper)로의 위임과 Boolean.TRUE.equals를 통한 null-safe 처리 모두 적절합니다.

src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)

24-24: 메서드 보안 활성화: LGTM

@EnableMethodSecurity 추가로 컨트롤러의 @PreAuthorize가 효과를 가집니다.

src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java (4)

48-53: 메시지 포맷 일관성: LGTM

MISSING_HEADER.format(ex.getHeaderName()) 사용으로 메시지 템플릿-파라미터를 일관되게 처리하고 있습니다.


104-126: 인증/인가 예외 핸들링 추가: LGTM

401/403에 대한 메타 포함 JSON 응답으로 일관성 있게 처리되었습니다.


137-139: 리소스 미발견 처리: LGTM

이전 에러코드 체계와도 정합적입니다.


150-152: 서버 내부 오류 처리: LGTM

내부 오류를 공통 포맷으로 응답합니다.

src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java (3)

57-68: 전화번호 중복 확인 엔드포인트: LGTM

경로/검증/응답 DTO 모두 일관적입니다.


70-79: 관리자 전용 상세 조회 보안 설정: LGTM

@PreAuthorize("hasRole('ADMIN')")과 OpenAPI 보안 스키마 적용 적절합니다.


45-55: 브레이킹 체인지 경로 검증 결과

  • 클래스 레벨 매핑

    @RequestMapping("/api/v1")
  • 메서드 레벨 매핑

    @GetMapping("/studentId")

    → 실제 엔드포인트: GET /api/v1/studentId

  • 레포 전체 검색 결과

    • /api/v1/check/studentId 관련 참조 없음
    • /api/v1/studentId 관련 참조 없음

레포 내 코드 및 문서(*.yaml/*.yml/*.md)에서는 기존 또는 변경된 경로가 전혀 사용되지 않는 것으로 확인되었습니다.
다만 프론트엔드, 외부 API 문서, 클라이언트 라이브러리 등 레포 외부 의존이 있을 수 있으니, 최종적으로 한 차례 더 검증해 주세요.

Comment on lines +16 to 18
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.RequiredArgsConstructor;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

메서드 파라미터 검증 활성화를 위한 @validated 필요

컨트롤러에 @Validated가 없어 @RequestParam@NotBlank/@Pattern 제약이 적용되지 않습니다. 클래스 레벨에 추가해 주세요.

 @RequestMapping("/api/v1")
 @RequiredArgsConstructor
 @RestController
+@org.springframework.validation.annotation.Validated
 public class RecruitMemberController {

Also applies to: 29-32

🤖 Prompt for AI Agents
In
src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java
around lines 16-18 and 29-32, the controller is missing the class-level
@Validated annotation so parameter-level javax/jakarta validation annotations
like @NotBlank and @Pattern on @RequestParam are not being enforced; add the
org.springframework.validation.annotation.Validated annotation to the controller
class declaration (and the necessary import) so Spring activates
method-parameter validation for those request parameters.

Comment on lines +45 to 55
@GetMapping("/studentId")
public ResponseEntity<ApiResponse<CheckStudentIdResponse, Void>> duplicatedStudentIdDetails(
@RequestParam
@NotBlank(message = "학번은 필수 입력 값입니다.")
@Pattern(regexp = "^12[0-9]{6}$", message = "유효하지 않은 학번 값입니다.")
String studentId
) {
boolean exists = recruitMemberService.isRegisteredStudentId(studentIdRequest.getStudentId());
CheckStudentIdResponse response = recruitMemberService.isRegisteredStudentId(studentId);

return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, exists));
return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, 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

[중요] 엔드포인트 경로 변경으로 인증 요구됨 + 일관성 깨짐

@GetMapping("/studentId")는 기존 "/check/**" 네임스페이스를 벗어나 Security에서 permitAll에 포함되지 않습니다. 현재 설정상 "/api/v1/check/**"만 허용이므로 이 엔드포인트는 인증이 필요해집니다. 또한 전화번호 경로는 "/check/phoneNumber"로 유지되어 일관성도 깨집니다.

다음과 같이 경로를 복구/정렬하는 것을 권장합니다.

-    @GetMapping("/studentId")
+    @GetMapping("/check/studentId")

대안으로 SecurityConfig의 permitAll에 "/api/v1/studentId"를 추가할 수도 있으나, 네임스페이스 일관 관점에서는 위 수정이 더 적합합니다.

📝 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
@GetMapping("/studentId")
public ResponseEntity<ApiResponse<CheckStudentIdResponse, Void>> duplicatedStudentIdDetails(
@RequestParam
@NotBlank(message = "학번은 필수 입력 값입니다.")
@Pattern(regexp = "^12[0-9]{6}$", message = "유효하지 않은 학번 값입니다.")
String studentId
) {
boolean exists = recruitMemberService.isRegisteredStudentId(studentIdRequest.getStudentId());
CheckStudentIdResponse response = recruitMemberService.isRegisteredStudentId(studentId);
return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, exists));
return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, response));
}
@GetMapping("/check/studentId")
public ResponseEntity<ApiResponse<CheckStudentIdResponse, Void>> duplicatedStudentIdDetails(
@RequestParam
@NotBlank(message = "학번은 필수 입력 값입니다.")
@Pattern(regexp = "^12[0-9]{6}$", message = "유효하지 않은 학번 값입니다.")
String studentId
) {
CheckStudentIdResponse response = recruitMemberService.isRegisteredStudentId(studentId);
return ResponseEntity.ok(ApiResponse.ok(STUDENT_ID_DUPLICATION_CHECK_SUCCESS, response));
}
🤖 Prompt for AI Agents
src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java
around lines 45-55: the endpoint mapping currently uses
@GetMapping("/studentId") which falls outside the existing "/check/**" namespace
and therefore requires authentication; change the mapping to restore consistency
with other check endpoints by using @GetMapping("/check/studentId") (so it
matches the permitAll pattern "/api/v1/check/**" and aligns with
"/check/phoneNumber"), update any related API docs/tests if present to reflect
the new path.

Comment on lines +73 to 90
Number idNum = claims.get("id", Number.class);
if (idNum == null) throw new BusinessException(INVALID_JWT_REQUEST);
Long userId = idNum.longValue();

String username = claims.getSubject();

UserRole userRole = UserRole.valueOf(claims.get("role", String.class));
String roleName = "ROLE_" + userRole.name();
Set<SimpleGrantedAuthority> authorities = Collections.singleton(
new SimpleGrantedAuthority(userRole.getRole())
new SimpleGrantedAuthority(roleName)
);

CustomUserDetails userDetails = new CustomUserDetails(userId, username, "", authorities);
return new UsernamePasswordAuthenticationToken(
userDetails,
token,
null,
authorities
);
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

JWT 클레임 검증 보강 필요: id/subject/role 누락·오류 시 일관된 예외로 처리

현재 id만 검증하고 role은 valueOf에서 예외가 나면 필터에서 일반 Exception으로 삼켜집니다. subject(=username)도 누락 가능성이 있습니다. 세 클레임 모두에 대해 INVALID_JWT_REQUEST로 통일 처리하면 보안/진단 일관성이 좋아집니다.

다음 패치를 제안합니다.

     public Authentication getAuthentication(String token) {
         Claims claims = getClaims(token);
 
-        Number idNum = claims.get("id", Number.class);
-        if (idNum == null) throw new BusinessException(INVALID_JWT_REQUEST);
-        Long userId = idNum.longValue();
-
-        String username = claims.getSubject();
-
-        UserRole userRole = UserRole.valueOf(claims.get("role", String.class));
-        String roleName = "ROLE_" + userRole.name();
+        Number idNum = claims.get("id", Number.class);
+        if (idNum == null) {
+            throw new BusinessException(INVALID_JWT_REQUEST);
+        }
+        Long userId = idNum.longValue();
+
+        String username = claims.getSubject();
+        if (username == null || username.isBlank()) {
+            throw new BusinessException(INVALID_JWT_REQUEST);
+        }
+
+        String roleStr = claims.get("role", String.class);
+        if (roleStr == null || roleStr.isBlank()) {
+            throw new BusinessException(INVALID_JWT_REQUEST);
+        }
+        UserRole userRole;
+        try {
+            userRole = UserRole.valueOf(roleStr);
+        } catch (IllegalArgumentException ex) {
+            throw new BusinessException(INVALID_JWT_REQUEST);
+        }
+        String roleName = "ROLE_" + userRole.name();
 
-        Set<SimpleGrantedAuthority> authorities = Collections.singleton(
-                new SimpleGrantedAuthority(roleName)
-        );
+        Set<SimpleGrantedAuthority> authorities =
+                Collections.singleton(new SimpleGrantedAuthority(roleName));
 
         CustomUserDetails userDetails = new CustomUserDetails(userId, username, "", authorities);
         return new UsernamePasswordAuthenticationToken(
                 userDetails,
-                null,
+                null,
                 authorities
         );
     }

추가로, 기존 userRole.getRole() 사용에서 "ROLE_" + name 패턴으로 바뀌었으므로, UserRole 정의와 SecurityConfig의 hasRole/hasAuthority 사용 방식이 일관적인지 점검 부탁드립니다.

레포 전역 점검 스크립트:


🏁 Script executed:

#!/bin/bash
# UserRole 정의 확인
rg -nP 'enum\s+UserRole\b' -C2

# getRole() 유무/사용처 확인
rg -nP '\bgetRole\s*\(' -C2

# hasRole/hasAuthority 사용 패턴 확인 ("ROLE_" 접두어 기대치 점검)
rg -nP 'hasRole\s*\(\s*".+?"\s*\)|hasAuthority\s*\(\s*".+?"\s*\)' -C2

Length of output: 503


🏁 Script executed:

#!/bin/bash
echo "=== UserRole.java ==="
head -n200 src/main/java/inha/gdgoc/domain/user/enums/UserRole.java

echo "=== SecurityConfig files ==="
rg -l -P 'class\s+SecurityConfig\b' -n src | while read -r file; do
  echo "=== $file ==="
  head -n200 "$file"
done

Length of output: 5941


🏁 Script executed:

#!/bin/bash
echo "=== Method-level Security Annotations ==="
rg -nP "@PreAuthorize" -C2 src
rg -nP "@Secured" -C2 src
rg -nP "@RolesAllowed" -C2 src

Length of output: 873


JWT 클레임 검증 강화 필요

아래 위치(src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java, 73–90)에 반드시 다음 보강을 적용해주세요. 클레임(id, subject, role) 누락·오류 시 모두 INVALID_JWT_REQUEST로 일관된 BusinessException을 던지면 보안·진단 측면에서 일관성이 높아집니다.

  • Number idNum = …; null 체크 → BusinessException(INVALID_JWT_REQUEST)
  • String username = claims.getSubject(); null·blank 체크 추가 → 동일 예외
  • String roleStr = claims.get("role", String.class); null·blank 체크 → 동일 예외
  • UserRole.valueOf(roleStr)은 try/catch로 감싸 IllegalArgumentException 발생 시 동일 예외로 변환

예시 패치 스니펫:

 Number idNum = claims.get("id", Number.class);
 if (idNum == null) {
     throw new BusinessException(INVALID_JWT_REQUEST);
 }
 Long userId = idNum.longValue();

 String username = claims.getSubject();
+if (username == null || username.isBlank()) {
+    throw new BusinessException(INVALID_JWT_REQUEST);
+}

 String roleStr = claims.get("role", String.class);
+if (roleStr == null || roleStr.isBlank()) {
+    throw new BusinessException(INVALID_JWT_REQUEST);
+}
 UserRole userRole;
 try {
     userRole = UserRole.valueOf(roleStr);
 } catch (IllegalArgumentException ex) {
     throw new BusinessException(INVALID_JWT_REQUEST);
 }
 String roleName = "ROLE_" + userRole.name();

 Set<SimpleGrantedAuthority> authorities =
     Collections.singleton(new SimpleGrantedAuthority(roleName));

추가 검증 결과:

  • UserRole.getRole() 메서드는 코드베이스에서 사용되지 않습니다.
  • 메서드 단위 보안(@PreAuthorize("hasRole('ADMIN')"))은 Spring Security 규칙에 따라 내부적으로 "ROLE_ADMIN" 권한을 검사하므로, roleName = "ROLE_"+name과 완벽히 일치합니다.
  • SecurityConfig 의 filterChain에서는 별도 hasRole/hasAuthority 사용이 없습니다.

위 보강 사항은 보안 취약 여지를 제거하기 위한 필수 수정보강이므로, 반드시 반영해주시기 바랍니다.

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

🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java around lines
73–90, strengthen JWT claim validation: check that idNum is non-null (throw new
BusinessException(INVALID_JWT_REQUEST) if null), validate claims.getSubject() is
non-null/non-blank (same exception if invalid), extract role as a String and
validate non-null/non-blank (same exception if invalid), and wrap
UserRole.valueOf(roleStr) in try/catch to convert IllegalArgumentException into
BusinessException(INVALID_JWT_REQUEST); only after these checks construct
roleName, authorities, CustomUserDetails and return the authentication token.

Comment on lines +7 to +9
// 401 Unauthorized
UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),

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

신규 401 코드 추가 확인

UNAUTHORIZED_USER 추가로 401 영역이 보완되었습니다. 보안 필터/핸들러에서 본 코드 사용이 일관되는지만 확인하면 됩니다(예: 인증 실패시 401, 인가 실패시 403).

인증/인가 핸들러에서 매핑이 올바른지 호출부를 전역적으로 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# GlobalErrorCode 사용처 점검
rg -nP "GlobalErrorCode\.(UNAUTHORIZED_USER|FORBIDDEN_USER)"

Length of output: 473


GlobalExceptionHandler에 인증 실패(401) 매핑 로직 누락 여부 확인 필요

SecurityConfig에서는 UNAUTHORIZED_USER(401)와 FORBIDDEN_USER(403)를 정상적으로 사용하고 있으나, GlobalExceptionHandler에는 현재 FORBIDDEN_USER만 static import되어 있어 인증 실패 시 UNAUTHORIZED_USER 코드가 핸들러에 반영되는지 확인이 필요합니다.

점검 포인트:

  • src/main/java/inha/gdgoc/global/security/SecurityConfig.java
    • 63행: GlobalErrorCode.UNAUTHORIZED_USER 사용 확인
    • 75행: GlobalErrorCode.FORBIDDEN_USER 사용 확인
  • src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java
    • import static GlobalErrorCode.FORBIDDEN_USER; → 인증 실패 핸들러에 UNAUTHORIZED_USER 매핑 메소드 및 static import 여부 확인/추가 필요

해결 방안:

  1. 인증 실패 예외(AuthenticationException 등)를 처리하는 핸들러 메소드에 ErrorResponse.of(GlobalErrorCode.UNAUTHORIZED_USER) 반환 로직이 구현되어 있는지 확인
  2. 필요 시 해당 메소드에 import static inha.gdgoc.global.exception.GlobalErrorCode.UNAUTHORIZED_USER; 추가 및 매핑 로직 보완
🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java around
lines 7 to 9, the file currently only statically imports
GlobalErrorCode.FORBIDDEN_USER so authentication failures (401) may not be
mapped; add handling to map AuthenticationException (or other auth-failure
exceptions thrown by SecurityConfig) to return
ErrorResponse.of(GlobalErrorCode.UNAUTHORIZED_USER) and, if desired, add the
static import import static
inha.gdgoc.global.exception.GlobalErrorCode.UNAUTHORIZED_USER; to keep usage
consistent with FORBIDDEN_USER; ensure the handler method catches the
appropriate exception type and returns the UNAUTHORIZED error code.

// 403 FORBIDDEN
INVALID_JWT_REQUEST(HttpStatus.FORBIDDEN, "잘못된 JWT 토큰입니다."),
FORBIDDEN_USER(HttpStatus.NOT_FOUND, "권한이 부족합니다."),

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

버그: FORBIDDEN_USER의 HTTP 상태값이 잘못되었습니다 (404 → 403)

주석과 메시지는 403에 해당하나, 실제 코드는 404로 설정되어 있습니다. 접근 제어 오류가 404로 내려가면 클라이언트가 문제 원인을 오인할 수 있습니다.

다음과 같이 수정해 주세요:

-    FORBIDDEN_USER(HttpStatus.NOT_FOUND, "권한이 부족합니다."),
+    FORBIDDEN_USER(HttpStatus.FORBIDDEN, "권한이 부족합니다."),
📝 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
FORBIDDEN_USER(HttpStatus.NOT_FOUND, "권한이 부족합니다."),
FORBIDDEN_USER(HttpStatus.FORBIDDEN, "권한이 부족합니다."),
🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java around line
17, the FORBIDDEN_USER enum constant uses HttpStatus.NOT_FOUND but should use
HttpStatus.FORBIDDEN; update the enum entry so the HTTP status is
HttpStatus.FORBIDDEN while leaving the message unchanged to correctly represent
a 403 Forbidden error.

Comment on lines +86 to +102
@ExceptionHandler(jakarta.validation.ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void, ErrorMeta>> handleConstraintViolation(
jakarta.validation.ConstraintViolationException ex,
HttpServletRequest request
) {
String message = ex.getConstraintViolations()
.stream()
.findFirst()
.map(ConstraintViolation::getMessage)
.orElse("유효하지 않은 요청입니다.");

log.error("ConstraintViolationException 발생: {}", message);

ErrorMeta meta = createMeta(request);
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

ConstraintViolation 처리 추가: 컨트롤러 @validated 누락 시 동작하지 않음

메서드 파라미터 수준의 제약(@RequestParam의 @NotBlank/@pattern 등)이 동작하려면 컨트롤러 클래스에 @Validated가 필요합니다. 현재 RecruitMemberController에는 누락되어 보입니다. 추가하지 않으면 본 핸들러가 트리거되지 않습니다.

RecruitMemberController에 다음을 추가해 주세요.

 @RestController
+@org.springframework.validation.annotation.Validated
 public class RecruitMemberController {

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

🤖 Prompt for AI Agents
In src/main/java/inha/gdgoc/recruit/controller/RecruitMemberController.java
around the class declaration (where controller annotations are present), add the
Spring @Validated annotation (import
org.springframework.validation.annotation.Validated) to the controller class so
method-parameter constraints like @NotBlank/@Pattern on @RequestParam are
enforced and trigger the GlobalExceptionHandler's ConstraintViolationException
path; ensure the import is added and the annotation is applied at the class
level.

coderabbitai bot added a commit that referenced this pull request Aug 26, 2025
Docstrings generation was requested by @kaswhy.

* #202 (comment)

The following files were modified:

* `src/main/java/inha/gdgoc/domain/auth/service/AuthService.java`
* `src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java`
* `src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java`
* `src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java`
* `src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java`
* `src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java`
* `src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java`
* `src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java`
* `src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java`
* `src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java`
* `src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java`
* `src/main/java/inha/gdgoc/global/security/SecurityConfig.java`
@coderabbitai
Copy link

coderabbitai bot commented Aug 26, 2025

Note

Generated docstrings for this pull request at #203

@kaswhy kaswhy merged commit a6be106 into develop Aug 26, 2025
2 checks passed
@kaswhy kaswhy deleted the feature/issue-197 branch August 26, 2025 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants