From e473e34fda7bb3126044cb079154880955f46639 Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:55:17 +0900 Subject: [PATCH 1/7] =?UTF-8?q?134=20feat:=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=EC=97=90=20=ED=95=84=EC=9A=94=ED=95=9C=20Err?= =?UTF-8?q?orCode=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exception/code/ErrorCode.java | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/chukchuk/haksa/global/exception/code/ErrorCode.java b/src/main/java/com/chukchuk/haksa/global/exception/code/ErrorCode.java index 96e79873..3417a5dd 100644 --- a/src/main/java/com/chukchuk/haksa/global/exception/code/ErrorCode.java +++ b/src/main/java/com/chukchuk/haksa/global/exception/code/ErrorCode.java @@ -4,6 +4,20 @@ public enum ErrorCode { + // 공통(Common) + INVALID_ARGUMENT("C01", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST), + NOT_FOUND("C05", "요청한 API를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + + // 인증 및 세션 관련 + SESSION_EXPIRED("A04", "로그인 세션이 만료되었습니다.", HttpStatus.UNAUTHORIZED), + // 인증 및 세션 관련 + AUTHENTICATION_REQUIRED("A05", "인증이 필요한 요청입니다.", HttpStatus.UNAUTHORIZED), + + // 서버 오류 관련 + SCRAPING_FAILED("C02", "포털 크롤링 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + REFRESH_FAILED("C03", "포털 정보 재연동 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), + FORBIDDEN("C04", "접근 권한이 없습니다.", HttpStatus.FORBIDDEN), + // Token 관련 TOKEN_INVALID_FORMAT("T01", "ID 토큰 형식이 올바르지 않습니다.", HttpStatus.UNAUTHORIZED), TOKEN_NO_MATCHING_KEY("T02", "일치하는 공개키가 없습니다.", HttpStatus.UNAUTHORIZED), @@ -23,36 +37,27 @@ public enum ErrorCode { STUDENT_ACADEMIC_RECORD_NOT_FOUND("U02", "해당 학생의 학적 정보가 존재하지 않습니다.", HttpStatus.NOT_FOUND), USER_ALREADY_CONNECTED("U03", "이미 포털과 연동된 사용자입니다.", HttpStatus.BAD_REQUEST), USER_NOT_CONNECTED("U04", "아직 포털과 연동되지 않은 사용자입니다.", HttpStatus.BAD_REQUEST), + + // Student 관련 STUDENT_NOT_FOUND("S01", "해당 학생이 존재하지 않습니다.", HttpStatus.NOT_FOUND), INVALID_TARGET_GPA("S02", "유효하지 않은 목표 학점입니다.", HttpStatus.BAD_REQUEST), STUDENT_ID_REQUIRED("S03", "Student ID는 필수입니다.", HttpStatus.BAD_REQUEST), + TRANSFER_STUDENT_UNSUPPORTED("T13", "편입생 학적 정보는 현재 지원되지 않습니다.", HttpStatus.UNPROCESSABLE_ENTITY), // 학업 관련 SEMESTER_RECORD_NOT_FOUND("A01", "해당 학기의 성적 데이터를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), SEMESTER_RECORD_EMPTY("A02", "학기 성적 데이터를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), FRESHMAN_NO_SEMESTER("A03", "신입생은 학기 기록이 없습니다.", HttpStatus.BAD_REQUEST), - GRADUATION_REQUIREMENTS_NOT_FOUND("G01", "졸업 요건 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), - // 학업 관련 INVALID_GRADE_TYPE("A06", "존재하지 않는 성적 등급입니다.", HttpStatus.BAD_REQUEST), + // 졸업 요건 관련 + GRADUATION_REQUIREMENTS_NOT_FOUND("G01", "졸업 요건 정보를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), + GRADUATION_REQUIREMENTS_DATA_NOT_FOUND("G02", "사용자에게 맞는 졸업 요건 데이터가 존재하지 않습니다.", HttpStatus.NOT_FOUND), + // 포털 관련 PORTAL_LOGIN_FAILED("P01", "아이디나 비밀번호가 일치하지 않습니다.\n학교 홈페이지에서 확인해주세요.", HttpStatus.UNAUTHORIZED), PORTAL_SCRAPE_FAILED("P02", "포털 크롤링 실패", HttpStatus.INTERNAL_SERVER_ERROR), - PORTAL_ACCOUNT_LOCKED("P03", "계정이 잠겼습니다. 포털사이트로 돌아가서 학번/사번 찾기 및 비밀번호 재발급을 진행해주세요", HttpStatus.LOCKED), - - // 공통(Common) - INVALID_ARGUMENT("C01", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST), - NOT_FOUND("C05", "요청한 API를 찾을 수 없습니다.", HttpStatus.NOT_FOUND), - - // 인증 및 세션 관련 - SESSION_EXPIRED("A04", "로그인 세션이 만료되었습니다.", HttpStatus.UNAUTHORIZED), - // 인증 및 세션 관련 - AUTHENTICATION_REQUIRED("A05", "인증이 필요한 요청입니다.", HttpStatus.UNAUTHORIZED), - - // 서버 오류 관련 - SCRAPING_FAILED("C02", "포털 크롤링 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), - REFRESH_FAILED("C03", "포털 정보 재연동 중 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR), - FORBIDDEN("C04", "접근 권한이 없습니다.", HttpStatus.FORBIDDEN); + PORTAL_ACCOUNT_LOCKED("P03", "계정이 잠겼습니다. 포털사이트로 돌아가서 학번/사번 찾기 및 비밀번호 재발급을 진행해주세요", HttpStatus.LOCKED); private final String code; private final String message; From 6667d246d1b519dd2df871b76a2999ae84e5a24f Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:16:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?134=20feat:=20=EC=A1=B8=EC=97=85=20?= =?UTF-8?q?=EC=9A=94=EA=B1=B4=20=EB=B6=80=EC=9E=AC=20=EC=8B=9C=20404=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/GraduationQueryRepository.java | 16 +++- .../graduation/service/GraduationService.java | 74 +++++++++++++++++-- 2 files changed, 82 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java b/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java index 158c7792..c4b4c621 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java @@ -5,6 +5,8 @@ import com.chukchuk.haksa.domain.graduation.dto.AreaRequirementDto; import com.chukchuk.haksa.domain.graduation.dto.CourseDto; import com.chukchuk.haksa.domain.graduation.dto.CourseInternalDto; +import com.chukchuk.haksa.global.exception.code.ErrorCode; +import com.chukchuk.haksa.global.exception.type.CommonException; import com.chukchuk.haksa.global.logging.annotation.LogTime; import com.chukchuk.haksa.infrastructure.redis.RedisCacheStore; import jakarta.persistence.EntityManager; @@ -144,15 +146,25 @@ public List getDualMajorAreaProgress(UUID studentId, Long prima // 주전공 졸업 요건 조회 List primaryReqs = getAreaRequirementsWithCache(primaryMajorId, admissionYear); - // 주전공 졸업 요건 중 '전선' 제외 + // 주전공 졸업 요건 데이터 부재 시 404 예외 처리 + if (primaryReqs == null || primaryReqs.isEmpty()) { + throw new CommonException(ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND); + } + + // 주전공 졸업 요건 중 전선/일선 제외 List primaryFiltered = primaryReqs.stream() .filter(req -> !req.areaType().equalsIgnoreCase(AREA_MAJOR_ELECTIVE)) .filter(req -> !req.areaType().equalsIgnoreCase(AREA_GENERAL_ELECTIVE)) .toList(); - // 복수전공 및 주전공 전선1 졸업 요건 조회 + // 복수전공 졸업 요건 조회 List dualMajorReqs = getDualMajorRequirementsWithCache(primaryMajorId, secondaryMajorId, admissionYear); + // 복수 전공 졸업 요건 데이터 부재 시 404 예외 처리 + if (dualMajorReqs == null || dualMajorReqs.isEmpty()) { + throw new CommonException(ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND); + } + // 전체 병합 List mergedRequirements = new ArrayList<>(); mergedRequirements.addAll(primaryFiltered); // 주전공 졸업 요건 (전선 제외) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java index b72db30f..02ecbbf3 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java @@ -6,9 +6,12 @@ import com.chukchuk.haksa.domain.graduation.repository.GraduationQueryRepository; import com.chukchuk.haksa.domain.student.model.Student; import com.chukchuk.haksa.domain.student.service.StudentService; +import com.chukchuk.haksa.global.exception.code.ErrorCode; +import com.chukchuk.haksa.global.exception.type.CommonException; import com.chukchuk.haksa.infrastructure.redis.RedisCacheStore; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -50,13 +53,16 @@ public GraduationProgressResponse getGraduationProgress(UUID studentId) { Long primaryMajorId = student.getMajor() != null ? student.getMajor().getId() : dept.getId(); int admissionYear = student.getAcademicInfo().getAdmissionYear(); - List areaProgress = null; - if (student.getSecondaryMajor() != null) { // 복수전공 존재 - Long secondaryMajorId = student.getSecondaryMajor().getId(); - areaProgress = graduationQueryRepository.getDualMajorAreaProgress(studentId, primaryMajorId, secondaryMajorId, admissionYear); + List areaProgress; + + if (student.getSecondaryMajor() != null) { + areaProgress = getDualMajorProgressOrThrow( + student, studentId, primaryMajorId, admissionYear + ); } else { - // 졸업 요건 충족 여부 조회 - areaProgress = graduationQueryRepository.getStudentAreaProgress(studentId, primaryMajorId, admissionYear); + areaProgress = getSingleMajorProgressOrThrow( + student, studentId, primaryMajorId, admissionYear + ); } GraduationProgressResponse response = new GraduationProgressResponse(areaProgress); @@ -78,4 +84,60 @@ public GraduationProgressResponse getGraduationProgress(UUID studentId) { private boolean isDifferentGradRequirement(Long departmentId, int admissionYear) { return admissionYear == SPECIAL_YEAR && departmentId != null && SPECIAL_DEPTS.contains(departmentId); } + + // 단일 전공 처리 메서드 + private List getSingleMajorProgressOrThrow( + Student student, + UUID studentId, + Long departmentId, + int admissionYear + ) { + List result = + graduationQueryRepository.getStudentAreaProgress( + studentId, departmentId, admissionYear + ); + + if (result == null || result.isEmpty()) { + throwGraduationRequirementNotFound(student, departmentId, admissionYear); + } + + return result; + } + + // 복수 전공 처리 메서드 + private List getDualMajorProgressOrThrow( + Student student, + UUID studentId, + Long primaryMajorId, + int admissionYear + ) { + Long secondaryMajorId = student.getSecondaryMajor().getId(); + + try { + return graduationQueryRepository.getDualMajorAreaProgress( + studentId, primaryMajorId, secondaryMajorId, admissionYear + ); + } catch (CommonException e) { + if (ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND.code().equals(e.getCode())) { + throwGraduationRequirementNotFound(student, primaryMajorId, admissionYear); + } + throw e; + } + } + + /** + * 졸업 요건 부재 예외 처리 메서드 + * MDC 정리는 GlobalExceptionHandler에서 수행됨 + */ + private void throwGraduationRequirementNotFound( + Student student, + Long departmentId, + int admissionYear + ) { + MDC.put("department_id", String.valueOf(departmentId)); + MDC.put("admission_year", String.valueOf(admissionYear)); + MDC.put("student_code", student.getStudentCode()); + + throw new CommonException(ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND); + } } From 2ffa5694a33bc7e3893e4fe3cb70bea22fe90ba2 Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 14:32:38 +0900 Subject: [PATCH 3/7] =?UTF-8?q?134=20feat:=20=EA=B2=BD=EC=9A=B0=EB=A5=BC?= =?UTF-8?q?=20=EB=8C=80=EB=B9=84=ED=95=9C=20=EB=B0=A9=EC=96=B4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/repository/GraduationQueryRepository.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java b/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java index c4b4c621..414e5552 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/repository/GraduationQueryRepository.java @@ -157,6 +157,11 @@ public List getDualMajorAreaProgress(UUID studentId, Long prima .filter(req -> !req.areaType().equalsIgnoreCase(AREA_GENERAL_ELECTIVE)) .toList(); + // 필터링 후 빈 리스트가 되는 경우를 대비한 방어 코드 + if (primaryFiltered.isEmpty()) { + throw new CommonException(ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND); + } + // 복수전공 졸업 요건 조회 List dualMajorReqs = getDualMajorRequirementsWithCache(primaryMajorId, secondaryMajorId, admissionYear); From 570b80d17412755264a9a19e19cbddae3e6776b0 Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:00:27 +0900 Subject: [PATCH 4/7] =?UTF-8?q?134=20feat:=20=EC=97=90=EC=99=B8=20Sentry?= =?UTF-8?q?=20=EC=A0=84=EC=86=A1=20=EC=8B=9C=20MDC=EC=97=90=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC/=EB=B3=B5=EC=88=98=20=EC=A0=84=EA=B3=B5=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/service/GraduationService.java | 31 +++++++++++++++---- .../handler/GlobalExceptionHandler.java | 17 ++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java index 02ecbbf3..23b90698 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java @@ -97,13 +97,18 @@ private List getSingleMajorProgressOrThrow( studentId, departmentId, admissionYear ); - if (result == null || result.isEmpty()) { - throwGraduationRequirementNotFound(student, departmentId, admissionYear); + if (result.isEmpty()) { + throwGraduationRequirementNotFound( + student, + departmentId, + null, + admissionYear); } return result; } + // 복수 전공 처리 메서드 // 복수 전공 처리 메서드 private List getDualMajorProgressOrThrow( Student student, @@ -119,7 +124,12 @@ private List getDualMajorProgressOrThrow( ); } catch (CommonException e) { if (ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND.code().equals(e.getCode())) { - throwGraduationRequirementNotFound(student, primaryMajorId, admissionYear); + throwGraduationRequirementNotFound( + student, + primaryMajorId, + secondaryMajorId, + admissionYear + ); } throw e; } @@ -131,12 +141,21 @@ private List getDualMajorProgressOrThrow( */ private void throwGraduationRequirementNotFound( Student student, - Long departmentId, + Long primaryMajorId, + Long secondaryMajorId, int admissionYear ) { - MDC.put("department_id", String.valueOf(departmentId)); - MDC.put("admission_year", String.valueOf(admissionYear)); MDC.put("student_code", student.getStudentCode()); + MDC.put("admission_year", String.valueOf(admissionYear)); + MDC.put("primary_department_id", String.valueOf(primaryMajorId)); + + if (secondaryMajorId == null) { + MDC.put("major_type", "SINGLE"); + MDC.put("secondary_department_id", "NONE"); + } else { + MDC.put("major_type", "DUAL"); + MDC.put("secondary_department_id", String.valueOf(secondaryMajorId)); + } throw new CommonException(ErrorCode.GRADUATION_REQUIREMENTS_DATA_NOT_FOUND); } diff --git a/src/main/java/com/chukchuk/haksa/global/exception/handler/GlobalExceptionHandler.java b/src/main/java/com/chukchuk/haksa/global/exception/handler/GlobalExceptionHandler.java index 521c9dc8..8338d634 100644 --- a/src/main/java/com/chukchuk/haksa/global/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/chukchuk/haksa/global/exception/handler/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.chukchuk.haksa.global.exception.type.BaseException; import com.chukchuk.haksa.global.exception.type.EntityNotFoundException; import com.chukchuk.haksa.global.logging.sanitize.LogSanitizer; +import io.sentry.IScope; import io.sentry.Sentry; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; @@ -34,12 +35,28 @@ public ResponseEntity handleBase(BaseException ex, HttpServletReq scope.setTag("error.code", ex.getCode()); scope.setFingerprint(List.of("BASE_EXCEPTION", ex.getCode())); scope.setLevel(io.sentry.SentryLevel.WARNING); // 4xx 의미 유지 + + // MDC → Sentry Tag 승격 (있을 때만) + putIfPresent(scope, "student_code"); + putIfPresent(scope, "admission_year"); + putIfPresent(scope, "primary_department_id"); + putIfPresent(scope, "secondary_department_id"); + putIfPresent(scope, "major_type"); + Sentry.captureException(ex); }); return ResponseEntity.status(ex.getStatus()) .body(ErrorResponse.of(ex.getCode(), ex.getMessage(), null)); } + + private void putIfPresent(IScope scope, String key) { + String value = MDC.get(key); + if (value != null) { + scope.setTag(key, value); + } + } + /** 404 */ @ExceptionHandler(org.springframework.web.servlet.NoHandlerFoundException.class) public ResponseEntity handleNoHandler( From 57376986633229250a6f1bc450028eb4333edeca Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:02:40 +0900 Subject: [PATCH 5/7] =?UTF-8?q?134=20feat:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C=20=EC=8B=9C=20MDC=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80=EB=A1=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=BB=A8=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EB=88=84?= =?UTF-8?q?=EC=88=98=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logging/filter/MdcCleanupFilter.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/com/chukchuk/haksa/global/logging/filter/MdcCleanupFilter.java diff --git a/src/main/java/com/chukchuk/haksa/global/logging/filter/MdcCleanupFilter.java b/src/main/java/com/chukchuk/haksa/global/logging/filter/MdcCleanupFilter.java new file mode 100644 index 00000000..37761562 --- /dev/null +++ b/src/main/java/com/chukchuk/haksa/global/logging/filter/MdcCleanupFilter.java @@ -0,0 +1,31 @@ +package com.chukchuk.haksa.global.logging.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.LOWEST_PRECEDENCE) +public class MdcCleanupFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } finally { + MDC.clear(); // 요청 종료 지점 + } + } +} From 53fa6ea5aae344f0585eac846339e8c59f6088d7 Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:08:04 +0900 Subject: [PATCH 6/7] =?UTF-8?q?134=20feat:=20=ED=8E=B8=EC=9E=85=EC=83=9D?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20-=20=ED=8E=B8=EC=9E=85=EC=83=9D=20=ED=8C=90=EB=B3=84?= =?UTF-8?q?=EB=B2=95:=20=ED=95=99=EB=B2=95=20=EC=95=9E=202=EC=9E=90?= =?UTF-8?q?=EB=A6=AC,=20=EC=9E=85=ED=95=99=EB=85=84=EB=8F=84=20=EB=92=B7?= =?UTF-8?q?=202=EC=9E=90=EB=A6=AC=20=EB=B9=84=EA=B5=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/service/GraduationService.java | 10 ++++++++++ .../haksa/domain/student/model/Student.java | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java index 23b90698..c77e2514 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java @@ -48,6 +48,9 @@ public GraduationProgressResponse getGraduationProgress(UUID studentId) { } Student student = studentService.getStudentById(studentId); + // 편입생인 경우 예외 처리, TODO: 편입생 졸업 요건 추가 후 삭제 + validateTransferStudent(student); + Department dept = student.getDepartment(); // 전공 코드가 없는 학과도 있으므로 majorId가 없으면 departmentId를 사용 Long primaryMajorId = student.getMajor() != null ? student.getMajor().getId() : dept.getId(); @@ -81,6 +84,13 @@ public GraduationProgressResponse getGraduationProgress(UUID studentId) { return response; } + // 편입생인 경우 예외 처리, TODO: 편입생 졸업 요건 추가 후 삭제 + private void validateTransferStudent(Student student) { + if (student.isTransferStudent()) { + throw new CommonException(ErrorCode.TRANSFER_STUDENT_UNSUPPORTED); + } + } + private boolean isDifferentGradRequirement(Long departmentId, int admissionYear) { return admissionYear == SPECIAL_YEAR && departmentId != null && SPECIAL_DEPTS.contains(departmentId); } diff --git a/src/main/java/com/chukchuk/haksa/domain/student/model/Student.java b/src/main/java/com/chukchuk/haksa/domain/student/model/Student.java index 00c4836a..deca6928 100644 --- a/src/main/java/com/chukchuk/haksa/domain/student/model/Student.java +++ b/src/main/java/com/chukchuk/haksa/domain/student/model/Student.java @@ -137,6 +137,21 @@ public void updateInfo(String name, Department department, Department major, Dep .build(); } + public boolean isTransferStudent() { + if (this.studentCode == null || this.academicInfo == null || this.academicInfo.getAdmissionYear() == null) { + return false; + } + + if (this.studentCode.length() < 2) { + return false; + } + + String codePrefix = this.studentCode.substring(0, 2); // 학번 앞 2자리 + String yearSuffix = String.valueOf(this.academicInfo.getAdmissionYear()).substring(2); // 입학년도 뒤 2자리 + + return !codePrefix.equals(yearSuffix); + } + public void addStudentCourse(StudentCourse course) { this.studentCourses.add(course); course.setStudent(this); From 1825451e126c0f12fa9a7b76e5d736785fb60311 Mon Sep 17 00:00:00 2001 From: SANGMIN PARK <71458064+pp8817@users.noreply.github.com> Date: Fri, 9 Jan 2026 23:18:09 +0900 Subject: [PATCH 7/7] =?UTF-8?q?134=20refactor:=20=EC=A1=B8=EC=97=85=20?= =?UTF-8?q?=EC=9A=94=EA=B1=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=8B=A8=EC=9C=84?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/service/GraduationService.java | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java index c77e2514..9b058d82 100644 --- a/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java +++ b/src/main/java/com/chukchuk/haksa/domain/graduation/service/GraduationService.java @@ -1,6 +1,5 @@ package com.chukchuk.haksa.domain.graduation.service; -import com.chukchuk.haksa.domain.department.model.Department; import com.chukchuk.haksa.domain.graduation.dto.AreaProgressDto; import com.chukchuk.haksa.domain.graduation.dto.GraduationProgressResponse; import com.chukchuk.haksa.domain.graduation.repository.GraduationQueryRepository; @@ -48,31 +47,18 @@ public GraduationProgressResponse getGraduationProgress(UUID studentId) { } Student student = studentService.getStudentById(studentId); - // 편입생인 경우 예외 처리, TODO: 편입생 졸업 요건 추가 후 삭제 validateTransferStudent(student); - Department dept = student.getDepartment(); - // 전공 코드가 없는 학과도 있으므로 majorId가 없으면 departmentId를 사용 - Long primaryMajorId = student.getMajor() != null ? student.getMajor().getId() : dept.getId(); + Long primaryMajorId = resolvePrimaryMajorId(student); int admissionYear = student.getAcademicInfo().getAdmissionYear(); - List areaProgress; - - if (student.getSecondaryMajor() != null) { - areaProgress = getDualMajorProgressOrThrow( - student, studentId, primaryMajorId, admissionYear - ); - } else { - areaProgress = getSingleMajorProgressOrThrow( - student, studentId, primaryMajorId, admissionYear - ); - } + List areaProgress = + resolveAreaProgress(student, studentId, primaryMajorId, admissionYear); GraduationProgressResponse response = new GraduationProgressResponse(areaProgress); if (isDifferentGradRequirement(primaryMajorId, admissionYear)) { response.setHasDifferentGraduationRequirement(); - log.info("[BIZ] graduation.progress.flag.set studentId={} deptId={} year={}", studentId, primaryMajorId, admissionYear); } try { @@ -91,10 +77,32 @@ private void validateTransferStudent(Student student) { } } + private Long resolvePrimaryMajorId(Student student) { + return student.getMajor() != null + ? student.getMajor().getId() + : student.getDepartment().getId(); + } + private boolean isDifferentGradRequirement(Long departmentId, int admissionYear) { return admissionYear == SPECIAL_YEAR && departmentId != null && SPECIAL_DEPTS.contains(departmentId); } + private List resolveAreaProgress( + Student student, + UUID studentId, + Long primaryMajorId, + int admissionYear + ) { + if (student.getSecondaryMajor() != null) { + return getDualMajorProgressOrThrow( + student, studentId, primaryMajorId, admissionYear + ); + } + return getSingleMajorProgressOrThrow( + student, studentId, primaryMajorId, admissionYear + ); + } + // 단일 전공 처리 메서드 private List getSingleMajorProgressOrThrow( Student student, @@ -118,7 +126,6 @@ private List getSingleMajorProgressOrThrow( return result; } - // 복수 전공 처리 메서드 // 복수 전공 처리 메서드 private List getDualMajorProgressOrThrow( Student student,