findByRecruitMemberAndSurveyType(
RecruitMember recruitMember,
SurveyType surveyType
diff --git a/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java b/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java
index f6f2255..ec90196 100644
--- a/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java
+++ b/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java
@@ -26,6 +26,15 @@ public class RecruitMemberService {
private final AnswerRepository answerRepository;
private final ObjectMapper objectMapper;
+ /**
+ * 모집 지원 정보를 저장한다.
+ *
+ * 요청에 포함된 지원자 정보(ApplicationRequest.member)를 RecruitMember 엔티티로 변환하여 저장하고,
+ * 요청의 답변 맵(ApplicationRequest.answers)의 각 항목을 JSON 문자열로 직렬화하여 Answer 엔티티 목록을 생성한 뒤 일괄 저장한다.
+ *
+ * @param applicationRequest 저장할 지원자 정보와 답변을 담은 요청 객체
+ * @throws RuntimeException JSON 직렬화 오류 발생 시 ("JSON 변환 오류" 메시지와 원인 예외 포함)
+ */
@Transactional
public void addRecruitMember(ApplicationRequest applicationRequest) {
RecruitMember member = applicationRequest.getMember().toEntity();
@@ -47,18 +56,39 @@ public void addRecruitMember(ApplicationRequest applicationRequest) {
answerRepository.saveAll(answers);
}
+ /**
+ * 주어진 학번이 이미 등록되어 있는지 확인한다.
+ *
+ * @param studentId 확인할 학번
+ * @return 등록 여부를 담은 CheckStudentIdResponse (exists=true이면 등록된 학번)
+ */
public CheckStudentIdResponse isRegisteredStudentId(String studentId) {
boolean exists = recruitMemberRepository.existsByStudentId(studentId);
return new CheckStudentIdResponse(exists);
}
+ /**
+ * 주어진 휴대전화 번호가 이미 등록되어 있는지 확인하고 그 결과를 반환합니다.
+ *
+ * @param phoneNumber 확인할 휴대전화 번호
+ * @return 등록 여부를 담은 {@link CheckPhoneNumberResponse}
+ */
public CheckPhoneNumberResponse isRegisteredPhoneNumber(String phoneNumber) {
boolean exists = recruitMemberRepository.existsByPhoneNumber(phoneNumber);
return new CheckPhoneNumberResponse(exists);
}
+ /**
+ * 주어진 ID에 해당하는 모집 멤버와 해당 멤버의 모집 설문 응답을 조회하여 지정된 응답 DTO로 변환해 반환합니다.
+ *
+ * 조회된 멤버가 존재하면 해당 멤버를 기준으로 SurveyType.RECRUIT에 해당하는 Answer 목록을 함께 조회하고,
+ * ObjectMapper를 이용해 SpecifiedMemberResponse를 생성하여 반환합니다.
+ *
+ * @return 조회된 멤버와 응답을 조합한 {@link SpecifiedMemberResponse}
+ * @throws RecruitMemberException 멤버를 찾을 수 없을 경우(상수 RECRUIT_MEMBER_NOT_FOUND)
+ */
public SpecifiedMemberResponse findSpecifiedMember(Long id) {
RecruitMember member = recruitMemberRepository.findById(id)
.orElseThrow(() -> new RecruitMemberException(RECRUIT_MEMBER_NOT_FOUND));
diff --git a/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java b/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java
index ae7cb26..9de1139 100644
--- a/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java
+++ b/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java
@@ -67,6 +67,17 @@ public Claims validToken(String token) throws ExpiredJwtException, UnsupportedJw
return getClaims(token);
}
+ /**
+ * JWT에서 인증 정보를 추출해 Spring Security Authentication 객체를 생성합니다.
+ *
+ * 토큰의 클레임에서 숫자형 "id"를 읽어 사용자 ID(Long)로 변환하고, 서브젝트를 사용자명(이메일)으로 사용합니다.
+ * "role" 클레임을 UserRole로 변환한 뒤 "ROLE_" 형태의 권한을 하나 생성하여 CustomUserDetails를 만들고
+ * 해당 사용자 정보를 담은 UsernamePasswordAuthenticationToken을 반환합니다.
+ *
+ * @param token JWT 문자열
+ * @return 토큰으로부터 생성된 Authentication (principal: CustomUserDetails, credentials: null, authorities 포함)
+ * @throws BusinessException token에 숫자형 "id" 클레임이 없을 경우 INVALID_JWT_REQUEST로 발생합니다.
+ */
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
diff --git a/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java b/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java
index 85d88d1..8ab10bd 100644
--- a/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java
+++ b/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java
@@ -15,6 +15,14 @@
@Configuration
public class OpenApiConfig {
+ /**
+ * OpenAPI 메인 빈을 생성합니다.
+ *
+ * OpenAPI 인스턴스에 API 정보(제목 "GDGoC API", 버전 "v1")를 설정하고,
+ * HTTP Bearer JWT 형식의 보안 스킴 "BearerAuth"를 Components에 추가합니다.
+ *
+ * @return 구성된 OpenAPI 인스턴스
+ */
@Bean
public OpenAPI openAPI() {
String schemeName = "BearerAuth";
@@ -31,6 +39,13 @@ public OpenAPI openAPI() {
);
}
+ /**
+ * 모든 엔드포인트를 포함하는 GroupedOpenApi 빈을 생성합니다.
+ *
+ * 그룹 이름은 "all"이며, 모든 경로(" /**")를 매칭하도록 구성됩니다.
+ *
+ * @return 모든 경로를 포함하는 GroupedOpenApi 인스턴스
+ */
@Bean
public GroupedOpenApi all() {
return GroupedOpenApi.builder()
@@ -39,16 +54,41 @@ public GroupedOpenApi all() {
.build();
}
+ /**
+ * v1 API 그룹을 위한 GroupedOpenApi 빈을 생성합니다.
+ *
+ * "v1" 그룹 이름과 "/api/v1" 경로 접두사를 사용하여 groupedApi(...)를 호출하여,
+ * 해당 접두사가 제거된 경로와 그룹 전용 Server URL이 설정된 GroupedOpenApi 인스턴스를 반환합니다.
+ *
+ * @return "/api/v1" 접두사가 적용된 엔드포인트들을 그룹화하고 프리픽스를 제거한 GroupedOpenApi
+ */
@Bean
public GroupedOpenApi v1Api() {
return groupedApi("v1", "/api/v1");
}
+ /**
+ * v2 버전의 API 문서 그룹(GroupedOpenApi)을 생성하여 반환합니다.
+ *
+ * 이 그룹은 이름 "v2"를 사용하고 경로 접두사 "/api/v2" 아래의 모든 엔드포인트를 포함하도록 구성됩니다.
+ *
+ * @return v2 그룹에 대응하는 GroupedOpenApi 인스턴스
+ */
@Bean
public GroupedOpenApi v2Api() {
return groupedApi("v2", "/api/v2");
}
+ /**
+ * 지정한 그룹 이름과 전체 경로 접두사로 GroupedOpenApi 인스턴스를 생성한다.
+ *
+ * 생성된 GroupedOpenApi는 fullPrefix 이하의 모든 경로(fullPrefix/**)에 매칭되며,
+ * stripPrefixAndSetServer(fullPrefix) 커스터마이저를 적용해 문서 내 경로에서 접두사를 제거하고 서버 URL을 설정한다.
+ *
+ * @param group 문서화될 API 그룹 이름 (예: "v1")
+ * @param fullPrefix 그룹에 대응되는 전체 경로 접두사 (예: "/api/v1")
+ * @return 구성된 GroupedOpenApi 인스턴스
+ */
private GroupedOpenApi groupedApi(String group, String fullPrefix) {
return GroupedOpenApi.builder()
.group(group)
@@ -57,6 +97,25 @@ private GroupedOpenApi groupedApi(String group, String fullPrefix) {
.build();
}
+ /**
+ * 그룹화된 API 문서에서 엔드포인트 경로의 그룹 접두사를 제거하고 해당 그룹의 서버 URL을 설정하는 OpenApiCustomizer를 생성한다.
+ *
+ *
동작:
+ *
+ * - openApi.getPaths()가 null이거나 비어 있으면 아무 작업도 수행하지 않는다.
+ * - 각 경로에 대해
+ *
+ * - 경로가 fullPrefix와 정확히 같으면 "/"로 매핑한다.
+ * - 경로가 fullPrefix + "/"로 시작하면 접두사 부분을 제거한다.
+ * - 그 외의 경로는 그대로 유지된다.
+ *
+ *
+ * - 변환된 경로 집합으로 OpenAPI의 paths를 교체하고, servers를 fullPrefix를 URL로 갖는 단일 Server로 설정한다.
+ *
+ *
+ * @param fullPrefix 경로에서 제거할 그룹 접두사이자 설정할 서버 URL (예: "/api/v1")
+ * @return 접두사 제거 및 서버 설정을 적용하는 OpenApiCustomizer
+ */
private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) {
return openApi -> {
Paths src = openApi.getPaths();
diff --git a/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java b/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java
index a139c84..c7c5493 100644
--- a/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java
+++ b/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java
@@ -38,6 +38,16 @@ public ResponseEntity> handleBusinessException(
.body(ApiResponse.error(errorCode, meta));
}
+ /**
+ * 요청에서 필요한 헤더가 누락된 경우 400 Bad Request 응답을 반환합니다.
+ *
+ * 상세: 누락된 헤더 이름을 기반으로 오류 메시지를 생성하고 요청 정보를 사용해 ErrorMeta를 구성한 뒤,
+ * ApiResponse 형태의 에러 바디와 함께 HTTP 400 상태로 응답합니다.
+ *
+ * @param ex 누락된 헤더 정보를 포함하는 MissingRequestHeaderException
+ * @param request 에러 발생 시점의 HttpServletRequest (응답용 ErrorMeta 생성에 사용)
+ * @return HTTP 400 상태와 ApiResponse 형태의 에러 응답을 담은 ResponseEntity
+ */
@ExceptionHandler(MissingRequestHeaderException.class)
public ResponseEntity> handleMissingRequestException(
MissingRequestHeaderException ex,
@@ -69,6 +79,14 @@ public ResponseEntity> handleValidationException(
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
}
+ /**
+ * 요청 파라미터의 타입 불일치(MethodArgumentTypeMismatchException)를 처리합니다.
+ *
+ * 예외로부터 불일치한 파라미터 이름을 사용해 사용자용 메시지를 생성하고,
+ * 요청 정보로부터 ErrorMeta를 만들어 HTTP 400(Bad Request) 상태의 ApiResponse 오류 응답을 반환합니다.
+ *
+ * @return HTTP 400 상태와 에러 메시지·메타를 포함한 ApiResponse를 담은 ResponseEntity
+ */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseEntity> handleTypeMismatch(
MethodArgumentTypeMismatchException ex,
@@ -83,6 +101,17 @@ public ResponseEntity> handleTypeMismatch(
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
}
+ /**
+ * 검증(Bean Validation) 제약 위반 예외를 처리하고 HTTP 400 응답을 반환합니다.
+ *
+ * 발생한 ConstraintViolationException에서 첫 번째 제약 위반 메시지를 추출하여
+ * ApiResponse 형태의 오류 본문과 함께 상태 코드 400(BAD_REQUEST)을 반환합니다.
+ * 제약 위반 메시지가 없으면 "유효하지 않은 요청입니다."를 사용하며, 요청 정보로부터 생성한 ErrorMeta를 포함합니다.
+ *
+ * @param ex 처리할 ConstraintViolationException
+ * @param request 현재 HTTP 요청 (응답에 포함할 메타 정보 생성에 사용)
+ * @return 상태 코드 400과 ApiResponse 오류 본문을 담은 ResponseEntity
+ */
@ExceptionHandler(jakarta.validation.ConstraintViolationException.class)
public ResponseEntity> handleConstraintViolation(
jakarta.validation.ConstraintViolationException ex,
@@ -101,6 +130,14 @@ public ResponseEntity> handleConstraintViolation(
.body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta));
}
+ /**
+ * 인증 관련 예외(AuthenticationCredentialsNotFoundException, AuthenticationException)를 처리하여
+ * HTTP 401 Unauthorized 상태와 함께 표준 ApiResponse 오류 응답(코드: UNAUTHORIZED_USER, meta 포함)을 반환한다.
+ *
+ * ErrorMeta는 요청 URI와 현재 타임스탬프를 기반으로 생성된다.
+ *
+ * @return HTTP 401 상태와 ApiResponse 오류 본문을 담은 ResponseEntity
+ */
@ExceptionHandler({ AuthenticationCredentialsNotFoundException.class, AuthenticationException.class })
public ResponseEntity> handleAuthentication(
AuthenticationException ex,
@@ -113,6 +150,16 @@ public ResponseEntity> handleAuthentication(
.body(ApiResponse.error(UNAUTHORIZED_USER, meta));
}
+ /**
+ * 인증된 사용자가 접근 권한이 없어 요청을 수행할 수 없을 때 이를 처리한다.
+ *
+ * 요청 URI와 현재 시각으로 구성된 ErrorMeta를 생성하여
+ * HTTP 403 Forbidden 상태와 함께 표준 에러 응답(ApiResponse)으로 반환한다.
+ *
+ * @param ex 발생한 AccessDeniedException
+ * @param request 에러 메타 생성에 사용되는 HttpServletRequest
+ * @return HTTP 403 상태와 GlobalErrorCode.FORBIDDEN_USER 및 생성된 ErrorMeta를 포함한 ApiResponse
+ */
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity> handleAccessDenied(
AccessDeniedException ex,
@@ -125,6 +172,16 @@ public ResponseEntity> handleAccessDenied(
.body(ApiResponse.error(FORBIDDEN_USER, meta));
}
+ /**
+ * 매핑되지 않은 요청(등록된 핸들러가 없음)을 처리하고 404 Not Found 응답을 반환합니다.
+ *
+ * 요청된 경로에 대응하는 핸들러를 찾지 못했을 때 호출됩니다. 요청 정보로부터 생성한 ErrorMeta를 포함한
+ * ApiResponse를 바디로 하여 HTTP 404 상태 코드를 반환합니다.
+ *
+ * @param ex 처리된 NoHandlerFoundException 인스턴스
+ * @param request 요청 URI를 사용해 ErrorMeta를 생성하는 HttpServletRequest
+ * @return HTTP 404 상태와 RESOURCE_NOT_FOUND 코드 및 생성된 ErrorMeta를 포함한 ApiResponse
+ */
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity> handleNotFound(
NoHandlerFoundException ex,
@@ -138,6 +195,14 @@ public ResponseEntity> handleNotFound(
.body(ApiResponse.error(RESOURCE_NOT_FOUND, meta));
}
+ /**
+ * 모든 예외의 최상위 캐치핸들러로, 처리되지 않은 예외 발생 시 500 Internal Server Error 응답을 반환합니다.
+ *
+ * 상세: 발생한 예외로부터 에러 메시지를 기록하고 요청 정보를 바탕으로 ErrorMeta를 생성한 뒤,
+ * ErrorCode.INTERNAL_SERVER_ERROR와 함께 ApiResponse 형태의 ResponseEntity를 상태 코드 500으로 반환합니다.
+ *
+ * @return 500 상태와 INTERNAL_SERVER_ERROR 코드 및 요청 메타 정보가 포함된 ApiResponse
+ */
@ExceptionHandler(Exception.class)
public ResponseEntity> handleUnhandledException(
Exception ex,
@@ -151,6 +216,12 @@ public ResponseEntity> handleUnhandledException(
.body(ApiResponse.error(INTERNAL_SERVER_ERROR, meta));
}
+ /**
+ * 요청 정보를 바탕으로 ErrorMeta 객체를 생성합니다.
+ *
+ * @param request 메타를 만들 때 사용할 요청; ErrorMeta에는 요청의 URI와 현재 시스템 타임스탬프(밀리초)가 들어갑니다.
+ * @return 요청 URI와 생성 시각을 담은 ErrorMeta 인스턴스
+ */
private ErrorMeta createMeta(HttpServletRequest request) {
return new ErrorMeta(request.getRequestURI(), System.currentTimeMillis());
}
diff --git a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java
index e38bac4..cca1215 100644
--- a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java
+++ b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java
@@ -29,6 +29,24 @@ public class SecurityConfig {
private final TokenAuthenticationFilter tokenAuthenticationFilter;
+ /**
+ * 애플리케이션의 Spring Security 필터 체인을 구성하고 반환한다.
+ *
+ * 구성 내용:
+ * - CSRF 비활성화
+ * - CORS 설정은 corsConfigurationSource() 사용
+ * - form 로그인 및 HTTP Basic 인증 비활성화
+ * - 다음 경로들은 인증 없이 접근 허용:
+ * /swagger-ui/**, /v3/api-docs/**, /swagger-ui.html,
+ * /api/v1/auth/**, /api/v1/game/**, /api/v1/apply/**, /api/v1/check/**, /api/v1/password-reset/**
+ * - 그 외 모든 요청은 인증 필요
+ * - 세션 정책: STATELESS
+ * - TokenAuthenticationFilter를 UsernamePasswordAuthenticationFilter 앞에 추가
+ * - 인증 실패 시 401 상태와 GlobalErrorCode.UNAUTHORIZED_USER 기반의 JSON ErrorResponse 반환
+ * - 권한 부족 시 403 상태와 GlobalErrorCode.FORBIDDEN_USER 기반의 JSON ErrorResponse 반환
+ *
+ * @return 구성된 SecurityFilterChain 빈
+ */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http