-
Notifications
You must be signed in to change notification settings - Fork 1
[REFACTOR] #197: 기존 신입 멤버 가입 신청 및 조회 api 리팩토링 #202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- 해당 필드가 HR 팀에서 입금 확인 여부 체크 용도로 사용될 거라 false로 저장되도록 변경합니다.
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... 📒 Files selected for processing (32)
Tip CodeRabbit can enforce grammar and style rules using `languagetool`.Configure ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
CI status |
There was a problem hiding this 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: 오류 응답 포맷 통일 권장
점검 결과ErrorResponse와ApiResponse.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()사용에 문제가 없습니다.선택적 권장 사항:
- 서비스 계층에서
entities가null로 넘어올 가능성이 있다면, 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.java70행의
@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.
📒 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: 패키지 마이그레이션 반영 LGTMglobal.exception으로의 import 이동이 일관성 있게 반영되었습니다. 동작 변화는 없으며 컴파일 타임 정합성만 개선됩니다.
src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java (1)
3-3: 패키지 경로 업데이트 LGTMimport 경로를 global.exception으로 통일한 변경이 적절합니다. 동작 변화는 없습니다.
src/main/java/inha/gdgoc/domain/user/exception/UserException.java (1)
3-4: 레거시 import 검증 완료 — 문제 없음검사 스크립트 결과
inha.gdgoc.global.error패키지를 참조하는 import가 전혀 발견되지 않았습니다.
BusinessException과ErrorCode선언은 모두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 위치 변경 반영 OKglobal.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 호환성 점검 요청현재
stripPrefixAndSetServer는fullPrefix가 “/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 반환으로의 전환: LGTMsrc/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: 인증/인가 예외 핸들링 추가: LGTM401/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 문서, 클라이언트 라이브러리 등 레포 외부 의존이 있을 수 있으니, 최종적으로 한 차례 더 검증해 주세요.
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.Pattern; | ||
| import lombok.RequiredArgsConstructor; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
메서드 파라미터 검증 활성화를 위한 @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.
| @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)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[중요] 엔드포인트 경로 변경으로 인증 요구됨 + 일관성 깨짐
@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.
| @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.
| 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 | ||
| ); |
There was a problem hiding this comment.
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*\)' -C2Length 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"
doneLength 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 srcLength 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.
| // 401 Unauthorized | ||
| UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), | ||
|
|
There was a problem hiding this comment.
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 여부 확인/추가 필요
해결 방안:
- 인증 실패 예외(AuthenticationException 등)를 처리하는 핸들러 메소드에
ErrorResponse.of(GlobalErrorCode.UNAUTHORIZED_USER)반환 로직이 구현되어 있는지 확인 - 필요 시 해당 메소드에
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, "권한이 부족합니다."), | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
버그: 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.
| 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.
| @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)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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`
|
Note Generated docstrings for this pull request at #203 |
📌 연관된 이슈
✨ 작업 내용
💬 리뷰 요구사항(선택)
Summary by CodeRabbit