diff --git a/src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java b/src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java index 1e81c0f..f054597 100644 --- a/src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/auth/exception/AuthErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.auth.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import org.springframework.http.HttpStatus; public enum AuthErrorCode implements ErrorCode { diff --git a/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java b/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java index 236985e..33debae 100644 --- a/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java +++ b/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.auth.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class AuthException extends BusinessException { diff --git a/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java b/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java index 4b3f18e..048cff2 100644 --- a/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java @@ -91,7 +91,7 @@ public Map processOAuthLogin(String code, HttpServletResponse re Optional foundUser = userRepository.findByEmail(email); if (foundUser.isEmpty()) { return Map.of( - "exists", false, + "isExists", false, "email", email, "name", name ); @@ -117,7 +117,7 @@ public Map processOAuthLogin(String code, HttpServletResponse re response.addHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); return Map.of( - "exists", true, + "isExists", true, "access_token", jwtAccessToken ); } diff --git a/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java b/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java index ddbe949..9637e21 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java +++ b/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java @@ -6,16 +6,20 @@ import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.STUDENT_ID_DUPLICATION_CHECK_SUCCESS; import inha.gdgoc.domain.recruit.dto.request.ApplicationRequest; -import inha.gdgoc.domain.recruit.dto.request.CheckPhoneNumberRequest; -import inha.gdgoc.domain.recruit.dto.request.CheckStudentIdRequest; +import inha.gdgoc.domain.recruit.dto.response.CheckPhoneNumberResponse; +import inha.gdgoc.domain.recruit.dto.response.CheckStudentIdResponse; import inha.gdgoc.domain.recruit.dto.response.SpecifiedMemberResponse; import inha.gdgoc.domain.recruit.service.RecruitMemberService; import inha.gdgoc.global.dto.response.ApiResponse; -import jakarta.validation.Valid; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,34 +42,45 @@ public ResponseEntity> recruitMemberAdd( return ResponseEntity.ok(ApiResponse.ok(MEMBER_SAVE_SUCCESS)); } - // TODO valid 핸들러 추가 - // TODO DTO로 응답 리팩토링, requestparam으로 변경하기 - @GetMapping("/check/studentId") - public ResponseEntity> duplicatedStudentIdDetails( - @Valid @ModelAttribute CheckStudentIdRequest studentIdRequest + @GetMapping("/studentId") + public ResponseEntity> 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)); } - // TODO DTO로 응답 리팩토링 @GetMapping("/check/phoneNumber") - public ResponseEntity> duplicatedPhoneNumberDetails( - @Valid @ModelAttribute CheckPhoneNumberRequest phoneNumberRequest + public ResponseEntity> duplicatedPhoneNumberDetails( + @RequestParam + @NotBlank(message = "전화번호는 필수 입력 값입니다.") + @Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "전화번호 형식은 010-XXXX-XXXX 이어야 합니다.") + String phoneNumber ) { - boolean exists = recruitMemberService.isRegisteredPhoneNumber(phoneNumberRequest.getPhoneNumber()); + CheckPhoneNumberResponse response = recruitMemberService + .isRegisteredPhoneNumber(phoneNumber); - return ResponseEntity.ok(ApiResponse.ok(PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS, exists)); + return ResponseEntity.ok(ApiResponse.ok(PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS, response)); } - // TODO 코어 멤버 인증 리팩토링 (Authentication), requestparam으로 변경하기 - @GetMapping("/recruit/member") - public ResponseEntity> getSpecifiedMember ( - @RequestParam Long userId + @Operation(summary = "특정 멤버 가입 신청서 조회", security = { @SecurityRequirement(name = "BearerAuth") }) + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/recruit/members/{memberId}") + public ResponseEntity> getSpecifiedMember( + @PathVariable Long memberId ) { - SpecifiedMemberResponse response = recruitMemberService.findSpecifiedMember(userId); + SpecifiedMemberResponse response = recruitMemberService.findSpecifiedMember(memberId); return ResponseEntity.ok(ApiResponse.ok(MEMBER_RETRIEVED_SUCCESS, response)); } + + // TODO 전체 응답 조회 및 검색 + + // TODO 입금 완료 + + // TODO 입금 미완료 } diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java b/src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java deleted file mode 100644 index 6f7fb80..0000000 --- a/src/main/java/inha/gdgoc/domain/recruit/dto/request/CheckStudentIdRequest.java +++ /dev/null @@ -1,14 +0,0 @@ -package inha.gdgoc.domain.recruit.dto.request; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Pattern; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class CheckStudentIdRequest { - @NotBlank(message = "학번은 필수 입력 값입니다.") - @Pattern(regexp = "^12[0-9]{6}$", message = "유효하지 않은 학번 값입니다.") - private String studentId; -} diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java b/src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java index be1151f..3baa07a 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java @@ -40,7 +40,7 @@ public RecruitMember toEntity() { .birth(birth) .major(major) .doubleMajor(doubleMajor) - .isPayed(isPayed) + .isPayed(false) .build(); } } diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java b/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java new file mode 100644 index 0000000..3ac3d17 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswerResponse.java @@ -0,0 +1,49 @@ +package inha.gdgoc.domain.recruit.dto.response; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import inha.gdgoc.domain.recruit.entity.Answer; +import inha.gdgoc.domain.recruit.enums.InputType; +import java.util.List; +import java.util.Map; + +public record AnswerResponse( + Long id, + InputType inputType, + Object responseValue +) { + public static AnswerResponse from(Answer answer, ObjectMapper om) { + return new AnswerResponse( + answer.getId(), + answer.getInputType(), + toFriendlyValue(answer.getResponseValue(), om) + ); + } + + 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>() {}); + } + if (node.isObject()) { + return om.convertValue(node, new TypeReference>() {}); + } + if (node.isNumber()) { + return node.numberValue(); + } + if (node.isBoolean()) { + return node.booleanValue(); + } + return null; + } catch (Exception e) { + return json; + } + } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java b/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java new file mode 100644 index 0000000..896c8cc --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/response/AnswersResponse.java @@ -0,0 +1,17 @@ +package inha.gdgoc.domain.recruit.dto.response; + +import com.fasterxml.jackson.databind.ObjectMapper; +import inha.gdgoc.domain.recruit.entity.Answer; +import java.util.List; + +public record AnswersResponse( + List answers +) { + public static AnswersResponse from(List entities, ObjectMapper objectMapper) { + return new AnswersResponse( + entities.stream() + .map(a -> AnswerResponse.from(a, objectMapper)) + .toList() + ); + } +} \ No newline at end of file diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java b/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java new file mode 100644 index 0000000..759f42f --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckPhoneNumberResponse.java @@ -0,0 +1,5 @@ +package inha.gdgoc.domain.recruit.dto.response; + +public record CheckPhoneNumberResponse(boolean isExists) { + +} diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java b/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java new file mode 100644 index 0000000..8537486 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/response/CheckStudentIdResponse.java @@ -0,0 +1,5 @@ +package inha.gdgoc.domain.recruit.dto.response; + +public record CheckStudentIdResponse(boolean isExists) { + +} diff --git a/src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java b/src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java index 669793e..838bd30 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java +++ b/src/main/java/inha/gdgoc/domain/recruit/dto/response/SpecifiedMemberResponse.java @@ -1,20 +1,29 @@ package inha.gdgoc.domain.recruit.dto.response; +import com.fasterxml.jackson.databind.ObjectMapper; +import inha.gdgoc.domain.recruit.entity.Answer; import inha.gdgoc.domain.recruit.entity.RecruitMember; +import java.util.List; public record SpecifiedMemberResponse( String name, String major, String studentId, - boolean isPayed + boolean isPayed, + AnswersResponse answers ) { - public static SpecifiedMemberResponse from(RecruitMember member) { + public static SpecifiedMemberResponse from( + RecruitMember member, + List answers, + ObjectMapper objectMapper + ) { return new SpecifiedMemberResponse( member.getName(), member.getMajor(), member.getStudentId(), - Boolean.TRUE.equals(member.getIsPayed()) + Boolean.TRUE.equals(member.getIsPayed()), + AnswersResponse.from(answers, objectMapper) ); } } diff --git a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java index 20c1d6b..e78520a 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.recruit.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java index 591d656..8d07d43 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java +++ b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.recruit.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class RecruitMemberException extends BusinessException { diff --git a/src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java b/src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java index e53831d..3ae036d 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java +++ b/src/main/java/inha/gdgoc/domain/recruit/repository/AnswerRepository.java @@ -1,7 +1,15 @@ package inha.gdgoc.domain.recruit.repository; import inha.gdgoc.domain.recruit.entity.Answer; +import inha.gdgoc.domain.recruit.entity.RecruitMember; +import inha.gdgoc.domain.recruit.enums.SurveyType; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface AnswerRepository extends JpaRepository { + + List 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 ee9910e..f6f2255 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java +++ b/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import inha.gdgoc.domain.recruit.dto.request.ApplicationRequest; +import inha.gdgoc.domain.recruit.dto.response.CheckPhoneNumberResponse; +import inha.gdgoc.domain.recruit.dto.response.CheckStudentIdResponse; import inha.gdgoc.domain.recruit.dto.response.SpecifiedMemberResponse; import inha.gdgoc.domain.recruit.entity.Answer; import inha.gdgoc.domain.recruit.entity.RecruitMember; @@ -45,18 +47,24 @@ public void addRecruitMember(ApplicationRequest applicationRequest) { answerRepository.saveAll(answers); } - public boolean isRegisteredStudentId(String studentId) { - return recruitMemberRepository.existsByStudentId(studentId); + public CheckStudentIdResponse isRegisteredStudentId(String studentId) { + boolean exists = recruitMemberRepository.existsByStudentId(studentId); + + return new CheckStudentIdResponse(exists); } - public boolean isRegisteredPhoneNumber(String phoneNumber) { - return recruitMemberRepository.existsByPhoneNumber(phoneNumber); + public CheckPhoneNumberResponse isRegisteredPhoneNumber(String phoneNumber) { + boolean exists = recruitMemberRepository.existsByPhoneNumber(phoneNumber); + + return new CheckPhoneNumberResponse(exists); } public SpecifiedMemberResponse findSpecifiedMember(Long id) { RecruitMember member = recruitMemberRepository.findById(id) .orElseThrow(() -> new RecruitMemberException(RECRUIT_MEMBER_NOT_FOUND)); + List answers = answerRepository + .findByRecruitMemberAndSurveyType(member, SurveyType.RECRUIT); - return SpecifiedMemberResponse.from(member); + return SpecifiedMemberResponse.from(member, answers, objectMapper); } } diff --git a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java index 7ab7469..00a93d6 100644 --- a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.resource.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import org.springframework.http.HttpStatus; public enum ResourceErrorCode implements ErrorCode { diff --git a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java index d87c713..fc39a49 100644 --- a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java +++ b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.resource.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class ResourceException extends BusinessException { diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java index 4f30573..4c02f76 100644 --- a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.study.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import org.springframework.http.HttpStatus; public enum StudyAttendeeErrorCode implements ErrorCode { diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java index 9085522..bf8f506 100644 --- a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.study.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class StudyAttendeeException extends BusinessException { diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java index 5c3f36d..cd05434 100644 --- a/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.study.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import org.springframework.http.HttpStatus; public enum StudyErrorCode implements ErrorCode { diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java index 501b734..0d6e0de 100644 --- a/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.study.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class StudyException extends BusinessException { diff --git a/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java b/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java index a73ac6a..6a2db9b 100644 --- a/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java +++ b/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java @@ -1,6 +1,6 @@ package inha.gdgoc.domain.user.exception; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; diff --git a/src/main/java/inha/gdgoc/domain/user/exception/UserException.java b/src/main/java/inha/gdgoc/domain/user/exception/UserException.java index 1da69d3..1feff66 100644 --- a/src/main/java/inha/gdgoc/domain/user/exception/UserException.java +++ b/src/main/java/inha/gdgoc/domain/user/exception/UserException.java @@ -1,7 +1,7 @@ package inha.gdgoc.domain.user.exception; -import inha.gdgoc.global.error.BusinessException; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.BusinessException; +import inha.gdgoc.global.exception.ErrorCode; public class UserException extends BusinessException { 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 e41fd91..ae7cb26 100644 --- a/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java +++ b/src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java @@ -1,8 +1,11 @@ package inha.gdgoc.global.config.jwt; +import static inha.gdgoc.global.exception.GlobalErrorCode.INVALID_JWT_REQUEST; + import inha.gdgoc.domain.auth.enums.LoginType; import inha.gdgoc.domain.user.entity.User; import inha.gdgoc.domain.user.enums.UserRole; +import inha.gdgoc.global.exception.BusinessException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; @@ -11,6 +14,12 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.UnsupportedJwtException; +import java.time.Duration; +import java.util.Base64; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Set; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -19,13 +28,6 @@ import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; -import java.time.Duration; -import java.util.Base64; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.Set; - @RequiredArgsConstructor @Service public class TokenProvider { @@ -67,19 +69,23 @@ public Claims validToken(String token) throws ExpiredJwtException, UnsupportedJw public Authentication getAuthentication(String token) { Claims claims = getClaims(token); - UserRole userRole = UserRole.valueOf(claims.get("role", String.class)); - Long userId = claims.get("id", Integer.class).longValue(); - String username = claims.getSubject(); + 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 authorities = Collections.singleton( - new SimpleGrantedAuthority(userRole.getRole()) + new SimpleGrantedAuthority(roleName) ); CustomUserDetails userDetails = new CustomUserDetails(userId, username, "", authorities); return new UsernamePasswordAuthenticationToken( userDetails, - token, + null, authorities ); } 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 14ac53b..85d88d1 100644 --- a/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java +++ b/src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java @@ -1,6 +1,10 @@ package inha.gdgoc.global.config.openapi; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; import java.util.List; import org.springdoc.core.customizers.OpenApiCustomizer; @@ -11,12 +15,28 @@ @Configuration public class OpenApiConfig { + @Bean + public OpenAPI openAPI() { + String schemeName = "BearerAuth"; + 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") + ) + ); + } + @Bean public GroupedOpenApi all() { return GroupedOpenApi.builder() - .group("all") - .pathsToMatch("/**") - .build(); + .group("all") + .pathsToMatch("/**") + .build(); } @Bean @@ -24,29 +44,34 @@ public GroupedOpenApi v1Api() { return groupedApi("v1", "/api/v1"); } - @Bean - public GroupedOpenApi v2Api() { - return groupedApi("v2", "/api/v2"); - } + @Bean + public GroupedOpenApi v2Api() { + return groupedApi("v2", "/api/v2"); + } private GroupedOpenApi groupedApi(String group, String fullPrefix) { return GroupedOpenApi.builder() - .group(group) - .pathsToMatch(fullPrefix + "/**") - .addOpenApiCustomizer(stripPrefixAndSetServer(fullPrefix)) - .build(); + .group(group) + .pathsToMatch(fullPrefix + "/**") + .addOpenApiCustomizer(stripPrefixAndSetServer(fullPrefix)) + .build(); } private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) { return openApi -> { Paths src = openApi.getPaths(); - if (src == null || src.isEmpty()) return; + if (src == null || src.isEmpty()) { + return; + } Paths dst = new Paths(); src.forEach((path, item) -> { String p = path; - if (p.equals(fullPrefix)) p = "/"; - else if (p.startsWith(fullPrefix + "/")) p = p.substring(fullPrefix.length()); + if (p.equals(fullPrefix)) { + p = "/"; + } else if (p.startsWith(fullPrefix + "/")) { + p = p.substring(fullPrefix.length()); + } dst.addPathItem(p, item); }); diff --git a/src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java b/src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java index 6c38760..ebab670 100644 --- a/src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java +++ b/src/main/java/inha/gdgoc/global/dto/response/ApiResponse.java @@ -1,7 +1,7 @@ package inha.gdgoc.global.dto.response; import com.fasterxml.jackson.annotation.JsonInclude; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; @JsonInclude(JsonInclude.Include.NON_NULL) public record ApiResponse(int code, String message, T data, M meta) { diff --git a/src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java b/src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java index dd0d1e7..52469a5 100644 --- a/src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java +++ b/src/main/java/inha/gdgoc/global/dto/response/ErrorResponse.java @@ -1,6 +1,6 @@ package inha.gdgoc.global.dto.response; -import inha.gdgoc.global.error.ErrorCode; +import inha.gdgoc.global.exception.ErrorCode; import lombok.Getter; import org.springframework.http.HttpStatus; diff --git a/src/main/java/inha/gdgoc/global/error/BusinessException.java b/src/main/java/inha/gdgoc/global/exception/BusinessException.java similarity index 87% rename from src/main/java/inha/gdgoc/global/error/BusinessException.java rename to src/main/java/inha/gdgoc/global/exception/BusinessException.java index ec0cf9f..e45c264 100644 --- a/src/main/java/inha/gdgoc/global/error/BusinessException.java +++ b/src/main/java/inha/gdgoc/global/exception/BusinessException.java @@ -1,4 +1,4 @@ -package inha.gdgoc.global.error; +package inha.gdgoc.global.exception; import lombok.Getter; diff --git a/src/main/java/inha/gdgoc/global/error/ErrorCode.java b/src/main/java/inha/gdgoc/global/exception/ErrorCode.java similarity index 78% rename from src/main/java/inha/gdgoc/global/error/ErrorCode.java rename to src/main/java/inha/gdgoc/global/exception/ErrorCode.java index a3d879a..74de75a 100644 --- a/src/main/java/inha/gdgoc/global/error/ErrorCode.java +++ b/src/main/java/inha/gdgoc/global/exception/ErrorCode.java @@ -1,4 +1,4 @@ -package inha.gdgoc.global.error; +package inha.gdgoc.global.exception; import org.springframework.http.HttpStatus; diff --git a/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java b/src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java similarity index 86% rename from src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java rename to src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java index bb4951a..5cf5675 100644 --- a/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java +++ b/src/main/java/inha/gdgoc/global/exception/GlobalErrorCode.java @@ -1,9 +1,12 @@ -package inha.gdgoc.global.error; +package inha.gdgoc.global.exception; import org.springframework.http.HttpStatus; public enum GlobalErrorCode implements ErrorCode { + // 401 Unauthorized + UNAUTHORIZED_USER(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."), + // 400 Bad Request BAD_REQUEST(HttpStatus.BAD_REQUEST, "요청 경로의 파라미터는 올바른 형식이 아닙니다."), INVALID_JSON_REQUEST(HttpStatus.BAD_REQUEST, "JSON 형식이 올바르지 않습니다."), @@ -11,6 +14,7 @@ public enum GlobalErrorCode implements ErrorCode { // 403 FORBIDDEN INVALID_JWT_REQUEST(HttpStatus.FORBIDDEN, "잘못된 JWT 토큰입니다."), + FORBIDDEN_USER(HttpStatus.NOT_FOUND, "권한이 부족합니다."), // 404 Not Found RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 리소스입니다."), diff --git a/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java b/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java similarity index 60% rename from src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java rename to src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java index b8399b7..a139c84 100644 --- a/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java +++ b/src/main/java/inha/gdgoc/global/exception/GlobalExceptionHandler.java @@ -1,11 +1,18 @@ -package inha.gdgoc.global.error; +package inha.gdgoc.global.exception; + +import static inha.gdgoc.global.exception.GlobalErrorCode.*; +import static inha.gdgoc.global.exception.GlobalErrorCode.FORBIDDEN_USER; import inha.gdgoc.global.dto.response.ApiResponse; import inha.gdgoc.global.dto.response.ErrorMeta; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.core.AuthenticationException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -38,7 +45,7 @@ public ResponseEntity> handleMissingRequestExceptio ) { log.error("요청 헤더 {}가 누락되었습니다.", ex.getHeaderName()); - String message = GlobalErrorCode.MISSING_HEADER.format(ex.getHeaderName()); + String message = MISSING_HEADER.format(ex.getHeaderName()); ErrorMeta meta = createMeta(request); return ResponseEntity.status(HttpStatus.BAD_REQUEST) @@ -68,14 +75,56 @@ public ResponseEntity> handleTypeMismatch( HttpServletRequest request ) { log.error("MethodArgumentTypeMismatchException 발생: {}", ex.getMessage()); - String message = GlobalErrorCode.BAD_REQUEST.format(ex.getName()); + String message = BAD_REQUEST.format(ex.getName()); ErrorMeta meta = createMeta(request); - return ResponseEntity.status(GlobalErrorCode.BAD_REQUEST.getStatus()) + return ResponseEntity.status(BAD_REQUEST.getStatus()) .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta)); } + @ExceptionHandler(jakarta.validation.ConstraintViolationException.class) + public ResponseEntity> 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)); + } + + @ExceptionHandler({ AuthenticationCredentialsNotFoundException.class, AuthenticationException.class }) + public ResponseEntity> handleAuthentication( + AuthenticationException ex, + HttpServletRequest request + ) { + log.warn("AuthenticationException: {}", ex.getMessage()); + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(HttpStatus.UNAUTHORIZED) + .body(ApiResponse.error(UNAUTHORIZED_USER, meta)); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity> handleAccessDenied( + AccessDeniedException ex, + HttpServletRequest request + ) { + log.warn("AccessDeniedException: {}", ex.getMessage()); + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(HttpStatus.FORBIDDEN) + .body(ApiResponse.error(FORBIDDEN_USER, meta)); + } + @ExceptionHandler(NoHandlerFoundException.class) public ResponseEntity> handleNotFound( NoHandlerFoundException ex, @@ -86,7 +135,7 @@ public ResponseEntity> handleNotFound( ErrorMeta meta = createMeta(request); return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ApiResponse.error(GlobalErrorCode.RESOURCE_NOT_FOUND, meta)); + .body(ApiResponse.error(RESOURCE_NOT_FOUND, meta)); } @ExceptionHandler(Exception.class) @@ -99,7 +148,7 @@ public ResponseEntity> handleUnhandledException( ErrorMeta meta = createMeta(request); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ApiResponse.error(GlobalErrorCode.INTERNAL_SERVER_ERROR, meta)); + .body(ApiResponse.error(INTERNAL_SERVER_ERROR, meta)); } private ErrorMeta createMeta(HttpServletRequest request) { diff --git a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java index 3a7c6c9..e38bac4 100644 --- a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java +++ b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java @@ -2,12 +2,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import inha.gdgoc.global.dto.response.ErrorResponse; -import inha.gdgoc.global.error.GlobalErrorCode; +import inha.gdgoc.global.exception.GlobalErrorCode; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -20,6 +21,7 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +@EnableMethodSecurity @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -42,7 +44,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/v1/auth/**", "/api/v1/game/**", "/api/v1/apply/**", - "/api/v1/check/**") + "/api/v1/check/**", + "/api/v1/password-reset/**") .permitAll() .anyRequest() .authenticated() @@ -53,17 +56,27 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { UsernamePasswordAuthenticationFilter.class) .exceptionHandling(ex -> ex .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(); + }) + .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpStatus.FORBIDDEN.value()); response.setContentType("application/json; charset=UTF-8"); - // ErrorResponse 생성 ErrorResponse errorResponse = new ErrorResponse( - GlobalErrorCode.INVALID_JWT_REQUEST); + GlobalErrorCode.FORBIDDEN_USER + ); - // JSON 직렬화 후 응답에 쓰기 ObjectMapper objectMapper = new ObjectMapper(); - response.getWriter() - .write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); response.getWriter().flush(); }) );