diff --git a/src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java b/src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java index de8ec19..7ad9e32 100644 --- a/src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java +++ b/src/main/java/inha/gdgoc/domain/auth/controller/AuthController.java @@ -18,6 +18,7 @@ import inha.gdgoc.domain.auth.dto.response.CodeVerificationResponse; import inha.gdgoc.domain.auth.dto.response.LoginResponse; import inha.gdgoc.domain.auth.exception.AuthErrorCode; +import inha.gdgoc.domain.auth.exception.AuthException; import inha.gdgoc.domain.auth.service.AuthCodeService; import inha.gdgoc.domain.auth.service.AuthService; import inha.gdgoc.domain.auth.service.MailService; @@ -25,7 +26,6 @@ import inha.gdgoc.domain.user.entity.User; import inha.gdgoc.domain.user.repository.UserRepository; import inha.gdgoc.global.dto.response.ApiResponse; -import inha.gdgoc.global.error.BusinessException; import jakarta.servlet.http.HttpServletResponse; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; @@ -62,6 +62,7 @@ public ResponseEntity, Void>> handleGoogleCallba HttpServletResponse response ) { Map data = authService.processOAuthLogin(code, response); + return ResponseEntity.ok(ApiResponse.ok(OAUTH_LOGIN_SIGNUP_SUCCESS, data)); } @@ -72,11 +73,9 @@ public ResponseEntity refreshAccessToken( log.info("리프레시 토큰 요청 받음. 토큰 존재 여부: {}", refreshToken != null); if (refreshToken == null) { - throw new BusinessException(AuthErrorCode.INVALID_COOKIE); + throw new AuthException(AuthErrorCode.INVALID_COOKIE); } - log.info("리프레시 토큰 값: {}", refreshToken); - try { String newAccessToken = refreshTokenService.refreshAccessToken(refreshToken); AccessTokenResponse accessTokenResponse = new AccessTokenResponse(newAccessToken); @@ -85,7 +84,7 @@ public ResponseEntity refreshAccessToken( ApiResponse.ok(ACCESS_TOKEN_REFRESH_SUCCESS, accessTokenResponse, null)); } catch (Exception e) { log.error("리프레시 토큰 처리 중 오류: {}", e.getMessage(), e); - throw new BusinessException(AuthErrorCode.INVALID_REFRESH_TOKEN); + throw new AuthException(AuthErrorCode.INVALID_REFRESH_TOKEN); } } @@ -105,12 +104,12 @@ public ResponseEntity> logout() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { - throw new BusinessException(UNAUTHORIZED_USER); + throw new AuthException(UNAUTHORIZED_USER); } String email = authentication.getName(); User user = userRepository.findByEmail(email) - .orElseThrow(() -> new BusinessException(USER_NOT_FOUND)); + .orElseThrow(() -> new AuthException(USER_NOT_FOUND)); Long userId = user.getId(); log.info("로그아웃 시도: 사용자 ID: {}, 이메일: {}", userId, email); @@ -142,7 +141,7 @@ public ResponseEntity> responseResponseEntity( return ResponseEntity.ok(ApiResponse.ok(CODE_CREATION_SUCCESS)); } - throw new BusinessException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } @PostMapping("/password-reset/verify") @@ -163,7 +162,7 @@ public ResponseEntity> resetPassword( // TODO 서비스 단으로 Optional user = userRepository.findByEmail(passwordResetRequest.email()); if (user.isEmpty()) { - throw new BusinessException(USER_NOT_FOUND); + throw new AuthException(USER_NOT_FOUND); } User foundUser = user.get(); diff --git a/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java b/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java new file mode 100644 index 0000000..236985e --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/auth/exception/AuthException.java @@ -0,0 +1,11 @@ +package inha.gdgoc.domain.auth.exception; + +import inha.gdgoc.global.error.BusinessException; +import inha.gdgoc.global.error.ErrorCode; + +public class AuthException extends BusinessException { + + public AuthException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/inha/gdgoc/domain/game/service/GameUserService.java b/src/main/java/inha/gdgoc/domain/game/service/GameUserService.java index 835c5b9..39acedd 100644 --- a/src/main/java/inha/gdgoc/domain/game/service/GameUserService.java +++ b/src/main/java/inha/gdgoc/domain/game/service/GameUserService.java @@ -25,17 +25,7 @@ public List saveGameResultAndGetRanking(GameUserRequest gameUs GameUser gameUser = gameUserRequest.toEntity(); gameUserRepository.save(gameUser); - LocalDate today = LocalDate.now(ZoneId.of("Asia/Seoul")); - LocalDateTime startOfDay = today.atStartOfDay(); // 00:00:00 - LocalDateTime endOfDay = today.atTime(23, 59, 59); // 23:59:59 - - // 전체 유저 순위 리스트 가져오기 - List results = gameUserRepository.findAllByCreatedAtBetweenOrderByTypingSpeedAsc(startOfDay, - endOfDay); - - return results.stream() - .map(user -> new GameUserResponse(results.indexOf(user) + 1, user)) - .collect(Collectors.toList()); + return findUserRankings(); } public List findUserRankings() { 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 07ed3db..ddbe949 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java +++ b/src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java @@ -65,6 +65,7 @@ public ResponseEntity> getSpecifiedMe @RequestParam Long userId ) { SpecifiedMemberResponse response = recruitMemberService.findSpecifiedMember(userId); + return ResponseEntity.ok(ApiResponse.ok(MEMBER_RETRIEVED_SUCCESS, response)); } } 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 2093bda..669793e 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,17 +1,20 @@ package inha.gdgoc.domain.recruit.dto.response; -import inha.gdgoc.global.entity.BaseEntity; +import inha.gdgoc.domain.recruit.entity.RecruitMember; -public class SpecifiedMemberResponse extends BaseEntity { - private String name; - private String major; - private String studentId; - private boolean isPayed; +public record SpecifiedMemberResponse( + String name, + String major, + String studentId, + boolean isPayed +) { - public SpecifiedMemberResponse(String name, String major, String studentId, boolean isPayed) { - this.name = name; - this.major = major; - this.studentId = studentId; - this.isPayed = isPayed; + public static SpecifiedMemberResponse from(RecruitMember member) { + return new SpecifiedMemberResponse( + member.getName(), + member.getMajor(), + member.getStudentId(), + Boolean.TRUE.equals(member.getIsPayed()) + ); } } diff --git a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java new file mode 100644 index 0000000..20c1d6b --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberErrorCode.java @@ -0,0 +1,25 @@ +package inha.gdgoc.domain.recruit.exception; + +import inha.gdgoc.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +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; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java new file mode 100644 index 0000000..591d656 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/recruit/exception/RecruitMemberException.java @@ -0,0 +1,11 @@ +package inha.gdgoc.domain.recruit.exception; + +import inha.gdgoc.global.error.BusinessException; +import inha.gdgoc.global.error.ErrorCode; + +public class RecruitMemberException extends BusinessException { + + public RecruitMemberException(ErrorCode errorCode) { + super(errorCode); + } +} 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 f2778ae..ee9910e 100644 --- a/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java +++ b/src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java @@ -1,5 +1,7 @@ package inha.gdgoc.domain.recruit.service; +import static inha.gdgoc.domain.recruit.exception.RecruitMemberErrorCode.RECRUIT_MEMBER_NOT_FOUND; + import com.fasterxml.jackson.databind.ObjectMapper; import inha.gdgoc.domain.recruit.dto.request.ApplicationRequest; import inha.gdgoc.domain.recruit.dto.response.SpecifiedMemberResponse; @@ -7,11 +9,11 @@ import inha.gdgoc.domain.recruit.entity.RecruitMember; import inha.gdgoc.domain.recruit.enums.InputType; import inha.gdgoc.domain.recruit.enums.SurveyType; +import inha.gdgoc.domain.recruit.exception.RecruitMemberException; import inha.gdgoc.domain.recruit.repository.AnswerRepository; import inha.gdgoc.domain.recruit.repository.RecruitMemberRepository; import jakarta.transaction.Transactional; import java.util.List; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -52,9 +54,9 @@ public boolean isRegisteredPhoneNumber(String phoneNumber) { } public SpecifiedMemberResponse findSpecifiedMember(Long id) { - Optional foundMember = recruitMemberRepository.findById(id); - RecruitMember member = foundMember.get(); - return new SpecifiedMemberResponse(member.getName(), member.getMajor(), member.getStudentId(), - member.getIsPayed()); + RecruitMember member = recruitMemberRepository.findById(id) + .orElseThrow(() -> new RecruitMemberException(RECRUIT_MEMBER_NOT_FOUND)); + + return SpecifiedMemberResponse.from(member); } } diff --git a/src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java b/src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java index 99af090..208a629 100644 --- a/src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java +++ b/src/main/java/inha/gdgoc/domain/resource/controller/ResourceController.java @@ -5,10 +5,11 @@ import inha.gdgoc.domain.auth.service.AuthService; import inha.gdgoc.domain.resource.dto.response.S3ResultResponse; import inha.gdgoc.domain.resource.enums.S3KeyType; +import inha.gdgoc.domain.resource.exception.ResourceErrorCode; import inha.gdgoc.domain.resource.exception.ResourceException; import inha.gdgoc.domain.resource.service.S3Service; import inha.gdgoc.global.dto.response.ApiResponse; -import inha.gdgoc.global.error.BusinessException; +import java.io.IOException; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; @@ -18,8 +19,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.io.IOException; - @RestController @RequestMapping("/api/v1/resource") @RequiredArgsConstructor @@ -38,7 +37,7 @@ public ResponseEntity> uploadImage( @RequestParam("s3key") S3KeyType s3key ) { if (file.getSize() > MAX_FILE_SIZE) { - throw new BusinessException(ResourceException.INVALID_BIG_FILE); + throw new ResourceException(ResourceErrorCode.INVALID_BIG_FILE); } Long userId = authService.getAuthenticationUserId(authentication); diff --git a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java new file mode 100644 index 0000000..7ab7469 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceErrorCode.java @@ -0,0 +1,28 @@ +package inha.gdgoc.domain.resource.exception; + +import inha.gdgoc.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum ResourceErrorCode implements ErrorCode { + + // 413 + INVALID_BIG_FILE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10Mb를 넘을 수 없습니다."); + + private final HttpStatus status; + private final String message; + + ResourceErrorCode(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} 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 2fe4300..d87c713 100644 --- a/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java +++ b/src/main/java/inha/gdgoc/domain/resource/exception/ResourceException.java @@ -1,28 +1,11 @@ package inha.gdgoc.domain.resource.exception; +import inha.gdgoc.global.error.BusinessException; import inha.gdgoc.global.error.ErrorCode; -import org.springframework.http.HttpStatus; -public enum ResourceException implements ErrorCode { +public class ResourceException extends BusinessException { - // 413 - INVALID_BIG_FILE(HttpStatus.PAYLOAD_TOO_LARGE, "파일 크기는 10Mb를 넘을 수 없습니다."); - - private final HttpStatus status; - private final String message; - - ResourceException(HttpStatus status, String message) { - this.status = status; - this.message = message; - } - - @Override - public HttpStatus getStatus() { - return status; - } - - @Override - public String getMessage() { - return message; + public ResourceException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java new file mode 100644 index 0000000..4f30573 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeErrorCode.java @@ -0,0 +1,34 @@ +package inha.gdgoc.domain.study.exception; + +import inha.gdgoc.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum StudyAttendeeErrorCode implements ErrorCode { + + // 400 Bad Request + INVALID_PAGE(HttpStatus.BAD_REQUEST, "page가 1보다 작을 수 없습니다."), + + // 404 Not Found + STUDY_ATTENDEE_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 스터디에 지원한 지원자 정보가 없습니다."), + + // 409 Conflict + STUDY_ALREADY_APPLIED(HttpStatus.CONFLICT, "이미 가입한 스터디입니다."); + + private final HttpStatus status; + private final String message; + + StudyAttendeeErrorCode(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java new file mode 100644 index 0000000..9085522 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyAttendeeException.java @@ -0,0 +1,11 @@ +package inha.gdgoc.domain.study.exception; + +import inha.gdgoc.global.error.BusinessException; +import inha.gdgoc.global.error.ErrorCode; + +public class StudyAttendeeException extends BusinessException { + + public StudyAttendeeException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java new file mode 100644 index 0000000..5c3f36d --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyErrorCode.java @@ -0,0 +1,34 @@ +package inha.gdgoc.domain.study.exception; + +import inha.gdgoc.global.error.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum StudyErrorCode implements ErrorCode { + + // 400 Bad Request + INVALID_PAGE(HttpStatus.BAD_REQUEST, "page가 1보다 작을 수 없습니다."), + + // 403 FORBIDDEN + STUDY_APPLICANT_ACCESS_DENIED(HttpStatus.FORBIDDEN, "본인이 만든 스터디의 지원자 정보만 확인할 수 있습니다."), + + // 404 NOT FOUND + STUDY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 스터디입니다."); + + private final HttpStatus status; + private final String message; + + StudyErrorCode(HttpStatus status, String message) { + this.status = status; + this.message = message; + } + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java b/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java new file mode 100644 index 0000000..501b734 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/study/exception/StudyException.java @@ -0,0 +1,11 @@ +package inha.gdgoc.domain.study.exception; + +import inha.gdgoc.global.error.BusinessException; +import inha.gdgoc.global.error.ErrorCode; + +public class StudyException extends BusinessException { + + public StudyException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java b/src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java index 40f1280..a1077d1 100644 --- a/src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java +++ b/src/main/java/inha/gdgoc/domain/study/service/StudyAttendeeService.java @@ -1,5 +1,12 @@ package inha.gdgoc.domain.study.service; +import static inha.gdgoc.domain.study.exception.StudyAttendeeErrorCode.STUDY_ALREADY_APPLIED; +import static inha.gdgoc.domain.study.exception.StudyAttendeeErrorCode.STUDY_ATTENDEE_NOT_FOUND; +import static inha.gdgoc.domain.study.exception.StudyErrorCode.STUDY_APPLICANT_ACCESS_DENIED; +import static inha.gdgoc.domain.study.exception.StudyAttendeeErrorCode.INVALID_PAGE; +import static inha.gdgoc.domain.study.exception.StudyErrorCode.STUDY_NOT_FOUND; +import static inha.gdgoc.domain.user.exception.UserErrorCode.USER_NOT_FOUND; + import inha.gdgoc.domain.study.dto.AttendeeUpdateDto; import inha.gdgoc.domain.study.dto.StudyAttendeeDto; import inha.gdgoc.domain.study.dto.StudyAttendeeListWithMetaDto; @@ -10,10 +17,13 @@ import inha.gdgoc.domain.study.entity.Study; import inha.gdgoc.domain.study.entity.StudyAttendee; import inha.gdgoc.domain.study.enums.AttendeeStatus; +import inha.gdgoc.domain.study.exception.StudyAttendeeException; +import inha.gdgoc.domain.study.exception.StudyException; import inha.gdgoc.domain.study.repository.StudyAttendeeRepository; import inha.gdgoc.domain.study.repository.StudyRepository; import inha.gdgoc.domain.study.validator.CreateStudyAttendeeValidator; import inha.gdgoc.domain.user.entity.User; +import inha.gdgoc.domain.user.exception.UserException; import inha.gdgoc.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -43,14 +53,16 @@ public class StudyAttendeeService { public StudyAttendeeListWithMetaDto getStudyAttendeeList(Long studyId, Optional _page) { Long page = _page.orElse(1L); if (page < 1) { - throw new RuntimeException("page가 1보다 작을 수 없습니다."); + throw new StudyAttendeeException(INVALID_PAGE); } Long limit = STUDY_ATTENDEE_PAGE_COUNT; Long offset = (page - 1) * limit; - Long StudyAttendeeCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount(studyId); - List attendees = studyAttendeeRepository.pageAllByStudyId(studyId, limit, offset).stream() + Long StudyAttendeeCount = studyAttendeeRepository.findAllByStudyIdStudyAttendeeCount( + studyId); + List attendees = studyAttendeeRepository.pageAllByStudyId(studyId, limit, + offset).stream() .map(this::studyAttendeeEntityToDto) .toList(); @@ -61,19 +73,22 @@ public StudyAttendeeListWithMetaDto getStudyAttendeeList(Long studyId, Optional< .build(); } - public GetStudyAttendeeResponse getStudyAttendee(Long authenticatedUser, Long studyId, Long attendeeId) { + public GetStudyAttendeeResponse getStudyAttendee(Long authenticatedUser, Long studyId, + Long attendeeId) { Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new RuntimeException("존재하지 않는 스터디입니다.")); - if (!userRepository.existsById(attendeeId)) { - throw new RuntimeException("존재하지 않는 유저입니다."); - } + .orElseThrow(() -> new StudyException(STUDY_NOT_FOUND)); if (!study.isCreatedBy(authenticatedUser)) { - throw new RuntimeException("본인이 만든 스터디의 지원자 정보만 확인할 수 있습니다."); + throw new StudyException(STUDY_APPLICANT_ACCESS_DENIED); + } + if (!userRepository.existsById(attendeeId)) { + throw new UserException(USER_NOT_FOUND); } - StudyAttendee studyAttendee = studyAttendeeRepository.findStudyAttendeeByStudyIdAndUserId(studyId, attendeeId) - .orElseThrow(() -> new IllegalArgumentException("해당 스터디에 지원한 지원자 정보가 없습니다.")); + StudyAttendee studyAttendee = studyAttendeeRepository.findStudyAttendeeByStudyIdAndUserId( + studyId, attendeeId) + .orElseThrow(() -> new StudyAttendeeException(STUDY_ATTENDEE_NOT_FOUND)); User stuatAttendeeUser = studyAttendee.getUser(); + return GetStudyAttendeeResponse.builder() .name(stuatAttendeeUser.getName()) .phone(stuatAttendeeUser.getPhoneNumber()) @@ -88,6 +103,7 @@ public List getStudyAttendeeResultListByUserId( Long userId ) { List studyAttendeeList = studyAttendeeRepository.findAllByUserId(userId); + return studyAttendeeList.stream().map(attendee -> StudyAttendeeResultDto.builder() .studyId(attendee.getStudy().getId()) .title(attendee.getStudy().getTitle()) @@ -104,9 +120,9 @@ public GetStudyAttendeeResponse createAttendee( AttendeeCreateRequest attendeeCreateRequest ) { User user = userRepository.findById(userId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 유저입니다.")); + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); Study study = studyRepository.findById(studyId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 스터디입니다.")); + .orElseThrow(() -> new StudyException(STUDY_NOT_FOUND)); validateIsApplied(studyId, userId); createStudyAttendeeValidator.validateAll(user, study); @@ -138,11 +154,12 @@ public void updateAttendee( List attendees = request.getAttendees(); List attendeeIds = attendees.stream().map(AttendeeUpdateDto::getAttendeeId).toList(); Study study = studyRepository.findOneWithUserById(studyId) - .orElseThrow(() -> new IllegalArgumentException("해당 스터디가 존재하지 않습니다.")); - List studyAttendeeList = studyAttendeeRepository.findAllByIdsAndStudyId(attendeeIds, studyId); + .orElseThrow(() -> new StudyException(STUDY_NOT_FOUND)); + List studyAttendeeList = studyAttendeeRepository.findAllByIdsAndStudyId( + attendeeIds, studyId); if (!Objects.equals(userId, study.getUser().getId())) { - throw new IllegalArgumentException("해당 study creator 가 아닙니다. userId: " + userId + " studyId: " + studyId); + throw new StudyException(STUDY_APPLICANT_ACCESS_DENIED); } Map studyAttendeeMap = studyAttendeeList.stream() @@ -166,7 +183,7 @@ private StudyAttendeeDto studyAttendeeEntityToDto(StudyAttendee studyAttendee) { private void validateIsApplied(Long studyId, Long userId) { if (studyAttendeeRepository.existsByStudyIdAndUserId(studyId, userId)) { - throw new IllegalArgumentException("이미 가입한 스터디입니다."); + throw new StudyAttendeeException(STUDY_ALREADY_APPLIED); } } } diff --git a/src/main/java/inha/gdgoc/domain/study/service/StudyService.java b/src/main/java/inha/gdgoc/domain/study/service/StudyService.java index d97a198..2d225e4 100644 --- a/src/main/java/inha/gdgoc/domain/study/service/StudyService.java +++ b/src/main/java/inha/gdgoc/domain/study/service/StudyService.java @@ -1,5 +1,8 @@ package inha.gdgoc.domain.study.service; +import static inha.gdgoc.domain.study.exception.StudyErrorCode.INVALID_PAGE; +import static inha.gdgoc.domain.study.exception.StudyErrorCode.STUDY_NOT_FOUND; + import inha.gdgoc.domain.resource.service.S3Service; import inha.gdgoc.domain.study.dto.MyStudyRecruitDto; import inha.gdgoc.domain.study.dto.StudyDto; @@ -11,6 +14,7 @@ import inha.gdgoc.domain.study.entity.Study; import inha.gdgoc.domain.study.enums.CreatorType; import inha.gdgoc.domain.study.enums.StudyStatus; +import inha.gdgoc.domain.study.exception.StudyException; import inha.gdgoc.domain.study.repository.StudyRepository; import inha.gdgoc.domain.user.entity.User; import inha.gdgoc.domain.user.service.UserService; @@ -41,7 +45,7 @@ public StudyListWithMetaDto getStudyList( Long page = _page.orElse(1L); if (page < 1) { - throw new RuntimeException("page가 1보다 작을 수 없습니다."); + throw new StudyException(INVALID_PAGE); } Long limit = STUDY_PAGE_COUNT; @@ -92,7 +96,7 @@ public MyStudyRecruitResponse getMyStudyList(Long userId) { public GetDetailedStudyResponse getStudyById(Long studyId) { Optional study = studyRepository.findById(studyId); if (study.isEmpty()) { - throw new RuntimeException("해당 스터디가 존재하지 않습니다."); + throw new StudyException(STUDY_NOT_FOUND); } return detailedStudyResponse(study.orElse(null), study.get().getUser()); } @@ -143,9 +147,11 @@ private StudyDto studyEntityToDto(Study study) { } private GetDetailedStudyResponse detailedStudyResponse(Study study, User user) { - return new GetDetailedStudyResponse(study.getId(), GetCreatorResponse.from(user), study.getTitle(), + return new GetDetailedStudyResponse(study.getId(), GetCreatorResponse.from(user), + study.getTitle(), study.getSimpleIntroduce(), study.getActivityIntroduce(), study.getStatus(), - study.getRecruitStartDate(), study.getRecruitEndDate(), study.getActivityStartDate(), + study.getRecruitStartDate(), study.getRecruitEndDate(), + study.getActivityStartDate(), study.getActivityEndDate(), study.getExpectedTime(), study.getExpectedPlace(), s3Service.getS3FileUrl(study.getImagePath())); } diff --git a/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java b/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java new file mode 100644 index 0000000..a73ac6a --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/user/exception/UserErrorCode.java @@ -0,0 +1,25 @@ +package inha.gdgoc.domain.user.exception; + +import inha.gdgoc.global.error.ErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +public enum UserErrorCode implements ErrorCode { + + // 404 NOT FOUND + USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."); + + private final HttpStatus status; + private final String message; + + @Override + public HttpStatus getStatus() { + return status; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/inha/gdgoc/domain/user/exception/UserException.java b/src/main/java/inha/gdgoc/domain/user/exception/UserException.java new file mode 100644 index 0000000..1da69d3 --- /dev/null +++ b/src/main/java/inha/gdgoc/domain/user/exception/UserException.java @@ -0,0 +1,11 @@ +package inha.gdgoc.domain.user.exception; + +import inha.gdgoc.global.error.BusinessException; +import inha.gdgoc.global.error.ErrorCode; + +public class UserException extends BusinessException { + + public UserException(ErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/inha/gdgoc/domain/user/service/UserService.java b/src/main/java/inha/gdgoc/domain/user/service/UserService.java index b4f0485..b7fcc04 100644 --- a/src/main/java/inha/gdgoc/domain/user/service/UserService.java +++ b/src/main/java/inha/gdgoc/domain/user/service/UserService.java @@ -1,17 +1,17 @@ package inha.gdgoc.domain.user.service; +import static inha.gdgoc.domain.user.exception.UserErrorCode.USER_NOT_FOUND; import static inha.gdgoc.global.util.EncryptUtil.encrypt; import static inha.gdgoc.global.util.EncryptUtil.generateSalt; import inha.gdgoc.domain.auth.dto.request.FindIdRequest; +import inha.gdgoc.domain.auth.dto.response.FindIdResponse; import inha.gdgoc.domain.user.dto.request.CheckDuplicatedEmailRequest; import inha.gdgoc.domain.user.dto.request.UserSignupRequest; -import inha.gdgoc.domain.auth.dto.response.FindIdResponse; import inha.gdgoc.domain.user.dto.response.CheckDuplicatedEmailResponse; import inha.gdgoc.domain.user.entity.User; +import inha.gdgoc.domain.user.exception.UserException; import inha.gdgoc.domain.user.repository.UserRepository; -import inha.gdgoc.global.error.NotFoundException; - import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Optional; @@ -20,8 +20,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.List; - @Slf4j @Service @Transactional @@ -30,19 +28,13 @@ public class UserService { private final UserRepository userRepository; - public List getAllUserIds() { - return userRepository.findAllUsers().stream() - .map((User::getId)) - .toList(); - } - public CheckDuplicatedEmailResponse isExistsByEmail(CheckDuplicatedEmailRequest request) { return new CheckDuplicatedEmailResponse(userRepository.existsByEmail(request.email())); } public User findUserById(Long userId) { return userRepository.findByUserId(userId) - .orElseThrow(() -> new NotFoundException("User not found user id: " + userId)); + .orElseThrow(() -> new UserException(USER_NOT_FOUND)); } public FindIdResponse findId(FindIdRequest findIdRequest) { @@ -53,7 +45,7 @@ public FindIdResponse findId(FindIdRequest findIdRequest) { ); if (user.isEmpty()) { - throw new IllegalArgumentException("해당 정보를 가진 사용자를 찾을 수 없습니다."); + throw new UserException(USER_NOT_FOUND); } String email = user.get().getEmail(); diff --git a/src/main/java/inha/gdgoc/global/error/BusinessException.java b/src/main/java/inha/gdgoc/global/error/BusinessException.java index 21d1a4a..ec0cf9f 100644 --- a/src/main/java/inha/gdgoc/global/error/BusinessException.java +++ b/src/main/java/inha/gdgoc/global/error/BusinessException.java @@ -1,5 +1,8 @@ package inha.gdgoc.global.error; +import lombok.Getter; + +@Getter public class BusinessException extends RuntimeException { private final ErrorCode errorCode; @@ -9,7 +12,4 @@ public BusinessException(ErrorCode errorCode) { this.errorCode = errorCode; } - public ErrorCode getErrorCode() { - return errorCode; - } } diff --git a/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java b/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java index 299624d..bb4951a 100644 --- a/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java +++ b/src/main/java/inha/gdgoc/global/error/GlobalErrorCode.java @@ -7,6 +7,7 @@ public enum GlobalErrorCode implements ErrorCode { // 400 Bad Request BAD_REQUEST(HttpStatus.BAD_REQUEST, "요청 경로의 파라미터는 올바른 형식이 아닙니다."), INVALID_JSON_REQUEST(HttpStatus.BAD_REQUEST, "JSON 형식이 올바르지 않습니다."), + MISSING_HEADER(HttpStatus.BAD_REQUEST, "요청 헤더 '%s'가 누락되었습니다."), // 403 FORBIDDEN INVALID_JWT_REQUEST(HttpStatus.FORBIDDEN, "잘못된 JWT 토큰입니다."), @@ -37,4 +38,8 @@ public HttpStatus getStatus() { public String getMessage() { return message; } + + public String format(Object... args) { + return String.format(this.message, args); + } } diff --git a/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java b/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java index 56799f8..b8399b7 100644 --- a/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java +++ b/src/main/java/inha/gdgoc/global/error/GlobalExceptionHandler.java @@ -1,67 +1,108 @@ package inha.gdgoc.global.error; -import com.fasterxml.jackson.core.JsonParseException; -import inha.gdgoc.global.dto.response.ErrorResponse; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.MalformedJwtException; +import inha.gdgoc.global.dto.response.ApiResponse; +import inha.gdgoc.global.dto.response.ErrorMeta; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.servlet.resource.NoResourceFoundException; +import org.springframework.web.servlet.NoHandlerFoundException; +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(MalformedJwtException.class) - public ResponseEntity handleMalformedJwtException(MalformedJwtException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.INVALID_JWT_REQUEST); - return new ResponseEntity<>(response, HttpStatus.UNAUTHORIZED); - } + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusinessException( + BusinessException ex, + HttpServletRequest request + ) { + log.error("BusinessException 발생: {}", ex.getMessage()); - @ExceptionHandler(JsonParseException.class) - public ResponseEntity handleJsonParseException(JsonParseException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.INVALID_JSON_REQUEST); - return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + ErrorCode errorCode = ex.getErrorCode(); + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(errorCode.getStatus()) + .body(ApiResponse.error(errorCode, meta)); } + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity> handleMissingRequestException( + MissingRequestHeaderException ex, + HttpServletRequest request + ) { + log.error("요청 헤더 {}가 누락되었습니다.", ex.getHeaderName()); - @ExceptionHandler(HttpRequestMethodNotSupportedException.class) - public ResponseEntity handleMethodNotSupported(HttpRequestMethodNotSupportedException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.METHOD_NOT_ALLOWED); - return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED).body(response); - } + String message = GlobalErrorCode.MISSING_HEADER.format(ex.getHeaderName()); + ErrorMeta meta = createMeta(request); - @ExceptionHandler(JwtException.class) - public ResponseEntity handleJwtException(JwtException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.INVALID_JWT_REQUEST); - return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta)); } - @ExceptionHandler(NoResourceFoundException.class) - public ResponseEntity handleNotFound(NoResourceFoundException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.RESOURCE_NOT_FOUND); - return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); - } + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException( + MethodArgumentNotValidException ex, + HttpServletRequest request + ) { + String message = ex.getBindingResult() + .getAllErrors() + .get(0) + .getDefaultMessage(); + log.error("MethodArgumentNotValidException 발생: {}", message); - @ExceptionHandler(BusinessException.class) - public ResponseEntity handleBusinessException(BusinessException ex) { - ErrorCode errorCode = ex.getErrorCode(); - ErrorResponse response = new ErrorResponse(errorCode); - return new ResponseEntity<>(response, errorCode.getStatus()); + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta)); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) - public ResponseEntity handleTypeMismatch(MethodArgumentTypeMismatchException ex) { - ErrorResponse response = new ErrorResponse(GlobalErrorCode.BAD_REQUEST); - return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + public ResponseEntity> handleTypeMismatch( + MethodArgumentTypeMismatchException ex, + HttpServletRequest request + ) { + log.error("MethodArgumentTypeMismatchException 발생: {}", ex.getMessage()); + String message = GlobalErrorCode.BAD_REQUEST.format(ex.getName()); + + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(GlobalErrorCode.BAD_REQUEST.getStatus()) + .body(ApiResponse.error(HttpStatus.BAD_REQUEST.value(), message, meta)); + } + + @ExceptionHandler(NoHandlerFoundException.class) + public ResponseEntity> handleNotFound( + NoHandlerFoundException ex, + HttpServletRequest request + ) { + log.error("NoHandlerFoundException 발생: {}", ex.getMessage()); + + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(ApiResponse.error(GlobalErrorCode.RESOURCE_NOT_FOUND, meta)); } @ExceptionHandler(Exception.class) - public ResponseEntity handleUnhandledException(Exception ex) { - ErrorResponse response = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); - return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); + public ResponseEntity> handleUnhandledException( + Exception ex, + HttpServletRequest request + ) { + log.error("서버 내부 오류 발생: {}", ex.getMessage()); + + ErrorMeta meta = createMeta(request); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(GlobalErrorCode.INTERNAL_SERVER_ERROR, meta)); + } + + private ErrorMeta createMeta(HttpServletRequest request) { + return new ErrorMeta(request.getRequestURI(), System.currentTimeMillis()); } } diff --git a/src/main/java/inha/gdgoc/global/error/NotFoundException.java b/src/main/java/inha/gdgoc/global/error/NotFoundException.java deleted file mode 100644 index 267199b..0000000 --- a/src/main/java/inha/gdgoc/global/error/NotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package inha.gdgoc.global.error; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(HttpStatus.NOT_FOUND) -public class NotFoundException extends RuntimeException { - public NotFoundException(String message) { - super(message); - } -} diff --git a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java index 5b01620..3a7c6c9 100644 --- a/src/main/java/inha/gdgoc/global/security/SecurityConfig.java +++ b/src/main/java/inha/gdgoc/global/security/SecurityConfig.java @@ -30,33 +30,43 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http - .csrf(AbstractHttpConfigurer::disable) - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .formLogin(AbstractHttpConfigurer::disable) - .httpBasic(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", - "/swagger-ui.html", "/auth/**", "/test/**", "/game/**", "/apply/**", - "/check/**").permitAll() - .anyRequest().authenticated() - ) - .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(ex -> ex - .authenticationEntryPoint((request, response, authException) -> { - response.setStatus(HttpStatus.FORBIDDEN.value()); - response.setContentType("application/json; charset=UTF-8"); + .csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers( + "/swagger-ui/**", + "/v3/api-docs/**", + "/swagger-ui.html", + "/api/v1/auth/**", + "/api/v1/game/**", + "/api/v1/apply/**", + "/api/v1/check/**") + .permitAll() + .anyRequest() + .authenticated() + ) + .sessionManagement( + sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(tokenAuthenticationFilter, + UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(ex -> ex + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setContentType("application/json; charset=UTF-8"); - // ErrorResponse 생성 - ErrorResponse errorResponse = new ErrorResponse( - GlobalErrorCode.INVALID_JWT_REQUEST); + // ErrorResponse 생성 + ErrorResponse errorResponse = new ErrorResponse( + GlobalErrorCode.INVALID_JWT_REQUEST); - // JSON 직렬화 후 응답에 쓰기 - ObjectMapper objectMapper = new ObjectMapper(); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); - response.getWriter().flush(); - }) - ); + // JSON 직렬화 후 응답에 쓰기 + ObjectMapper objectMapper = new ObjectMapper(); + response.getWriter() + .write(objectMapper.writeValueAsString(errorResponse)); + response.getWriter().flush(); + }) + ); return http.build(); } @@ -65,15 +75,14 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of( - "http://localhost:3000", - "http://gdgocinha.com", - "https://gdgocinha.com", - "https://www.gdgocinha.com", - "https://typing-game-alpha-umber.vercel.app" + "http://localhost:3000", + "https://gdgocinha.com", + "https://www.gdgocinha.com", + "https://typing-game-alpha-umber.vercel.app" )); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders( - List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization")); + List.of("Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 7453a2c..47d075e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -2,6 +2,9 @@ server: forward-headers-strategy: framework spring: + web: + resources: + add-mappings: false config: import: optional:file:.env[.properties] jackson: diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 3bdb7fa..5a665bf 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -2,6 +2,9 @@ server: forward-headers-strategy: framework spring: + web: + resources: + add-mappings: false config: import: optional:file:.env[.properties] jackson: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 39ea37c..cdefe07 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -2,6 +2,9 @@ server: forward-headers-strategy: framework spring: + web: + resources: + add-mappings: false config: import: optional:file:.env[.properties] jackson: