diff --git a/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java b/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java index 33491f363..a7a04fbd6 100644 --- a/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java +++ b/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java @@ -23,8 +23,8 @@ import kr.allcll.backend.domain.graduation.certification.GraduationCertRuleRepository; import kr.allcll.backend.domain.graduation.certification.GraduationCertRuleType; import kr.allcll.backend.domain.graduation.credit.CategoryType; -import kr.allcll.backend.domain.graduation.credit.CourseReplacement; -import kr.allcll.backend.domain.graduation.credit.CourseReplacementRepository; +import kr.allcll.backend.domain.graduation.credit.CourseEquivalence; +import kr.allcll.backend.domain.graduation.credit.CourseEquivalenceRepository; import kr.allcll.backend.domain.graduation.credit.CreditCriterion; import kr.allcll.backend.domain.graduation.credit.CreditCriterionRepository; import kr.allcll.backend.domain.graduation.credit.DoubleCreditCriterion; @@ -42,7 +42,7 @@ import kr.allcll.backend.support.sheet.validation.BalanceRequiredRulesSheetValidator; import kr.allcll.backend.support.sheet.validation.ClassicCertCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.CodingCertCriteriaSheetValidator; -import kr.allcll.backend.support.sheet.validation.CourseReplacementsSheetValidator; +import kr.allcll.backend.support.sheet.validation.CourseEquivalencesSheetValidator; import kr.allcll.backend.support.sheet.validation.CreditCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.DoubleCreditCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.EnglishCertCriteriaSheetValidator; @@ -64,7 +64,7 @@ public class AdminGraduationSyncService { private final RequiredCourseRepository requiredCourseRepository; private final CreditCriterionRepository creditCriterionRepository; private final GraduationSheetProperties graduationSheetProperties; - private final CourseReplacementRepository courseReplacementRepository; + private final CourseEquivalenceRepository courseEquivalenceRepository; private final GraduationCertRuleRepository graduationCertRuleRepository; private final BalanceRequiredRuleRepository balanceRequiredRuleRepository; private final CodingCertCriterionRepository codingCertCriterionRepository; @@ -82,7 +82,7 @@ public void syncGraduationRules() { syncCreditCriteria(); syncDoubleCreditCriteria(); syncRequiredCourses(); - syncCourseReplacements(); + syncCourseEquivalences(); syncBalanceRequiredRule(); syncBalanceRequiredCourseAreaMap(); @@ -179,6 +179,7 @@ private void syncRequiredCourses() { graduationSheetTable.getString(row, "curi_no"), graduationSheetTable.getString(row, "curi_nm"), graduationSheetTable.getString(row, "alt_group"), + graduationSheetTable.getString(row, "group_code"), graduationSheetTable.getBoolean(row, "required"), graduationSheetTable.getString(row, "note") ); @@ -191,31 +192,26 @@ private void syncRequiredCourses() { log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, requiredCourseList.size()); } - private void syncCourseReplacements() { - String tabKey = CourseReplacementsSheetValidator.TAB_KEY; + private void syncCourseEquivalences() { + String tabKey = CourseEquivalencesSheetValidator.TAB_KEY; GraduationSheetTable graduationSheetTable = fetchAndValidate(tabKey); - List courseReplacementList = new ArrayList<>(); + List courseEquivalenceList = new ArrayList<>(); for (List row : graduationSheetTable.getDataRows()) { - CourseReplacement courseReplacement = new CourseReplacement( - graduationSheetTable.getInt(row, "admission_year"), - graduationSheetTable.getInt(row, "admission_year_short"), - graduationSheetTable.getString(row, "legacy_curi_nm"), - graduationSheetTable.getString(row, "current_curi_no"), - graduationSheetTable.getString(row, "current_curi_nm"), - graduationSheetTable.getBoolean(row, "enabled"), - graduationSheetTable.getString(row, "note") + CourseEquivalence courseEquivalence = new CourseEquivalence( + graduationSheetTable.getString(row, "group_code"), + graduationSheetTable.getString(row, "curi_no"), + graduationSheetTable.getString(row, "curi_nm") ); - courseReplacementList.add(courseReplacement); + courseEquivalenceList.add(courseEquivalence); } - courseReplacementRepository.deleteAllInBatch(); - courseReplacementRepository.saveAll(courseReplacementList); + courseEquivalenceRepository.deleteAllInBatch(); + courseEquivalenceRepository.saveAll(courseEquivalenceList); - log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, courseReplacementList.size()); + log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, courseEquivalenceList.size()); } - private void syncBalanceRequiredRule() { String tabKey = BalanceRequiredRulesSheetValidator.TAB_KEY; GraduationSheetTable graduationSheetTable = fetchAndValidate(tabKey); diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicy.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicy.java index 309a3990d..39d8d41f2 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicy.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicy.java @@ -10,13 +10,14 @@ public class AcademicBasicPolicy { private final RequiredCourseResolver requiredCourseResolver; - private final CourseReplacementRepository courseReplacementRepository; + private final CourseEquivalenceRepository courseEquivalenceRepository; public boolean isRecentMajorAcademicBasic(CompletedCourse course, CreditCriterion criterion) { if (isNotAcademicBasic(course)) { return true; } - String courseName = course.getCuriNm(); + String curiNm = course.getCuriNm(); + String curiNo = course.getCuriNo(); Integer admissionYear = criterion.getAdmissionYear(); String departmentName = criterion.getDeptNm(); List academicBasicRequiredCourseNames = requiredCourseResolver.findRequiredCourseNames( @@ -25,44 +26,33 @@ public boolean isRecentMajorAcademicBasic(CompletedCourse course, CreditCriterio CategoryType.ACADEMIC_BASIC ); - if (isExistAcademicBasicCourse(academicBasicRequiredCourseNames, courseName)) { + if (isExistAcademicBasicCourse(academicBasicRequiredCourseNames, curiNm)) { return true; } - return isHaveReplaceCourse(academicBasicRequiredCourseNames, admissionYear, courseName); + return isHaveReplaceOrEquivalenceCourse(admissionYear, departmentName, curiNo); } private boolean isExistAcademicBasicCourse(List academicBasicRequiredCourseNames, String courseName) { return academicBasicRequiredCourseNames.contains(courseName); } - /* - 대체된 최신 과목을 들었을 경우를 판별한다. - 대체된 최신 과목이 없는 경우 false를 반환한다. - 대체 과목의 예전 과목 명이, 학생의 이수 요건에 없으면 false를 반환한다. - */ - private boolean isHaveReplaceCourse( - List academicBasicRequiredCourseNames, - Integer admissionYear, - String courseName - ) { - List recentCourse = courseReplacementRepository.findRecentCourse(admissionYear, courseName); - if (recentCourse.isEmpty()) { - return false; - } - for (CourseReplacement courseReplacement : recentCourse) { - if (academicBasicRequiredCourseNames.contains(courseReplacement.getLegacyCuriNm())) { - return true; - } - } - return false; - } - private boolean isNotAcademicBasic(CompletedCourse course) { - return !isAcademicBasic(course); + return !CategoryType.ACADEMIC_BASIC.equals(course.getCategoryType()); } - private boolean isAcademicBasic(CompletedCourse course) { - return CategoryType.ACADEMIC_BASIC.equals(course.getCategoryType()); + private boolean isHaveReplaceOrEquivalenceCourse( + Integer admissionYear, + String departmentName, + String curiNo + ) { + return courseEquivalenceRepository.findGroupCodeByCuriNo(curiNo) + .map(groupCode -> requiredCourseResolver.findRequiredCourseInGroup( + departmentName, + admissionYear, + CategoryType.ACADEMIC_BASIC, + groupCode + )) + .orElse(false); } } diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalence.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalence.java new file mode 100644 index 000000000..b252c9d7a --- /dev/null +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalence.java @@ -0,0 +1,38 @@ +package kr.allcll.backend.domain.graduation.credit; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import kr.allcll.backend.support.entity.BaseEntity; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Table(name = "course_equivalences") +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CourseEquivalence extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "group_code", nullable = false) + private String groupCode; // 동일과목 그룹 번호 + + @Column(name = "curi_no", nullable = false) + private String curiNo; // 학수번호 + + @Column(name = "curi_nm", nullable = false, length = 255) + private String curiNm; // 과목명 + + public CourseEquivalence(String groupCode, String curiNo, String curiNm) { + this.groupCode = groupCode; + this.curiNo = curiNo; + this.curiNm = curiNm; + } +} diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java new file mode 100644 index 000000000..3da14f27c --- /dev/null +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java @@ -0,0 +1,23 @@ +package kr.allcll.backend.domain.graduation.credit; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface CourseEquivalenceRepository extends JpaRepository { + + @Query(""" + select e.curiNo from CourseEquivalence e + join CourseEquivalence e2 on e.groupCode = e2.groupCode + where e2.curiNo in :curiNos + """) + List findSameGroupCuriNos(Set curiNos); + + @Query(""" + select e.groupCode from CourseEquivalence e + where e.curiNo = :curiNo + """) + Optional findGroupCodeByCuriNo(String curiNo); +} diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacement.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacement.java deleted file mode 100644 index df5c40491..000000000 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacement.java +++ /dev/null @@ -1,57 +0,0 @@ -package kr.allcll.backend.domain.graduation.credit; - -import jakarta.persistence.*; -import kr.allcll.backend.support.entity.BaseEntity; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Table(name = "course_replacements") -@Entity -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -public class CourseReplacement extends BaseEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "admission_year", nullable = false) - private Integer admissionYear; // 입학년도 - - @Column(name = "admission_year_short", nullable = false) - private Integer admissionYearShort; - - @Column(name = "legacy_curi_nm", nullable = false) - private String legacyCuriNm; // 과거 과목명 - - @Column(name = "current_curi_no", nullable = false) - private String currentCuriNo; // 현재 과목 코드 - - @Column(name = "current_curi_nm", nullable = false) - private String currentCuriNm; // 현재 과목명 - - @Column(name = "enabled", nullable = false) - private Boolean enabled; // 활성화 여부 - - @Column(name = "note") - private String note; // 비고 - - public CourseReplacement( - Integer admissionYear, - Integer admissionYearShort, - String legacyCuriNm, - String currentCuriNo, - String currentCuriNm, - Boolean enabled, - String note - ) { - this.admissionYear = admissionYear; - this.admissionYearShort = admissionYearShort; - this.legacyCuriNm = legacyCuriNm; - this.currentCuriNo = currentCuriNo; - this.currentCuriNm = currentCuriNm; - this.enabled = enabled; - this.note = note; - } -} diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementRepository.java deleted file mode 100644 index 475207e7f..000000000 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementRepository.java +++ /dev/null @@ -1,28 +0,0 @@ -package kr.allcll.backend.domain.graduation.credit; - -import java.util.Collection; -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface CourseReplacementRepository extends JpaRepository { - - @Query(""" - select c from CourseReplacement c - where c.admissionYear = :admissionYear - and c.legacyCuriNm in :legacyNames - and c.enabled = true - """) - List findByAdmissionYearAndLegacyCuriNmIn( - Integer admissionYear, - Collection legacyNames - ); - - @Query(""" - select c from CourseReplacement c - where c.admissionYear = :admissionYear - and c.currentCuriNm = :currentCuriNm - and c.enabled = true - """) - List findRecentCourse(Integer admissionYear, String currentCuriNm); -} diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementResolver.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementResolver.java deleted file mode 100644 index 7170e102d..000000000 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementResolver.java +++ /dev/null @@ -1,81 +0,0 @@ -package kr.allcll.backend.domain.graduation.credit; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import kr.allcll.backend.domain.graduation.credit.dto.RequiredCourseResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -@Slf4j -@Component -@RequiredArgsConstructor -public class CourseReplacementResolver { - - private static final String DEPRECATED = "DEPRECATED"; - - private final CourseReplacementRepository courseReplacementRepository; - - public List replaceDeprecatedSubject(Integer admissionYear, - List requiredCourses) { - Set legacyNamesToReplace = requiredCourses.stream() - .filter(this::isDeprecated) - .map(RequiredCourse::getCuriNm) - .collect(Collectors.toSet()); - - Map replacementByLegacyName = loadReplacementMap(admissionYear, legacyNamesToReplace); - return requiredCourses.stream() - .map(requiredCourse -> toResponse(admissionYear, requiredCourse, replacementByLegacyName)) - .toList(); - } - - public List resolveCurrentCuriNos(Integer admissionYear, Set legacyNames) { - return loadReplacementMap(admissionYear, legacyNames).values().stream() - .map(CourseReplacement::getCurrentCuriNo) - .toList(); - } - - private Map loadReplacementMap( - Integer admissionYear, - Set legacyNamesToReplace - ) { - if (legacyNamesToReplace.isEmpty()) { - return new HashMap<>(); - } - - return courseReplacementRepository - .findByAdmissionYearAndLegacyCuriNmIn(admissionYear, legacyNamesToReplace) - .stream() - .collect(Collectors.toMap( - CourseReplacement::getLegacyCuriNm, - courseReplacement -> courseReplacement - )); - } - - private RequiredCourseResponse toResponse( - Integer admissionYear, - RequiredCourse requiredCourse, - Map replacementByLegacyName - ) { - if (!isDeprecated(requiredCourse)) { - return RequiredCourseResponse.of(requiredCourse.getCuriNo(), requiredCourse.getCuriNm()); - } - CourseReplacement courseReplacement = replacementByLegacyName.get(requiredCourse.getCuriNm()); - if (courseReplacement == null) { - log.error( - "[졸업요건] 대체 과목 매핑에 실패했습니다. admissionYear={}, legacyCuriNm={}", - admissionYear, - requiredCourse.getCuriNm() - ); - return RequiredCourseResponse.of(requiredCourse.getCuriNo(), requiredCourse.getCuriNm()); - } - return RequiredCourseResponse.of(courseReplacement.getCurrentCuriNo(), courseReplacement.getCurrentCuriNm()); - } - - private boolean isDeprecated(RequiredCourse course) { - return DEPRECATED.equals(course.getCuriNo()); - } -} diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/GraduationCategoryService.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/GraduationCategoryService.java index 227e145ae..505b16467 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/GraduationCategoryService.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/GraduationCategoryService.java @@ -71,7 +71,6 @@ private List filterUncompletedCourses( ) { List earnedCourses = completedCourseRepository.findEarnedCourses(user.getId()); return uncompletedCourseFilter.filterUncompletedCourses( - user.getAdmissionYear(), categories, earnedCourses ); diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/NonMajorCategoryResolver.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/NonMajorCategoryResolver.java index d84a6d94b..6eedbb9f5 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/NonMajorCategoryResolver.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/NonMajorCategoryResolver.java @@ -29,7 +29,7 @@ public class NonMajorCategoryResolver { private static final String ALL_DEPT = "0"; - private final CourseReplacementResolver courseReplacementResolver; + private final RequiredCourseResolver requiredCourseResolver; private final BalanceRequiredResolver balanceRequiredResolver; private final RequiredCourseRepository requiredCourseRepository; private final CreditCriterionRepository creditCriterionRepository; @@ -89,7 +89,7 @@ private Map> loadRequiredCourses( RequiredCourse::getCategoryType, collectingAndThen( toList(), - requiredCourses -> courseReplacementResolver.replaceDeprecatedSubject(admissionYear, requiredCourses) + requiredCourseResolver::resolveDeprecatedCourses ) )); } diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourse.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourse.java index 735cdfd26..baca1c169 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourse.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourse.java @@ -48,6 +48,9 @@ public class RequiredCourse extends BaseEntity { @Column(name = "alt_group") private String altGroup; // 선택 과목 그룹 키 + @Column(name = "group_code") + private String groupCode; // 동일 및 대체 과목 그룹 코드 + @Column(name = "required", nullable = false) private Boolean required; // 검사 대상 여부 @@ -63,6 +66,7 @@ public RequiredCourse( String curiNo, String curiNm, String altGroup, + String groupCode, Boolean required, String note ) { @@ -74,6 +78,7 @@ public RequiredCourse( this.curiNo = curiNo; this.curiNm = curiNm; this.altGroup = altGroup; + this.groupCode = groupCode; this.required = required; this.note = note; } diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java index acb26dfd0..5b6998174 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java @@ -20,4 +20,25 @@ public interface RequiredCourseRepository extends JpaRepository findRequiredCourses(List deptNms, Integer admissionYear, CategoryType categoryType); + + @Query(""" + select r from RequiredCourse r + where r.deptNm in :deptNms + and r.admissionYear = :admissionYear + and r.categoryType = :categoryType + and r.groupCode = :groupCode + """) + List findRequiredCoursesByGroupCode( + List deptNms, + Integer admissionYear, + CategoryType categoryType, + String groupCode + ); + + @Query(""" + select r from RequiredCourse r + where r.groupCode = :groupCode + and r.curiNo != :deprecatedCuriNo + """) + List findCurrentCourseByGroupCode(String groupCode, String deprecatedCuriNo); } diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseResolver.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseResolver.java index 1ad118c55..80ff55e2e 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseResolver.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseResolver.java @@ -3,16 +3,41 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import kr.allcll.backend.domain.graduation.credit.dto.RequiredCourseResponse; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +@Slf4j @Component @RequiredArgsConstructor public class RequiredCourseResolver { + private static final String DEPRECATED = "DEPRECATED"; private static final String WILD_CARD_DEPT_NM = "ALL"; private final RequiredCourseRepository requiredCourseRepository; + public boolean findRequiredCourseInGroup( + String departmentName, + Integer admissionYear, + CategoryType categoryType, + String groupCode + ) { + List requiredCourseCandidatesWithWildCard = + requiredCourseRepository.findRequiredCoursesByGroupCode( + List.of(WILD_CARD_DEPT_NM, departmentName), + admissionYear, + categoryType, + groupCode + ); + + List requiredCoursesWithStatus + = getDepartmentRequiredCourses(requiredCourseCandidatesWithWildCard, departmentName); + + return requiredCoursesWithStatus.stream() + .anyMatch(RequiredCourse::getRequired); + } + public List findRequiredCourseNames( String departmentName, Integer admissionYear, @@ -54,6 +79,12 @@ private List getDepartmentRequiredCourses( return requiredCourseCandidates.values().stream().toList(); } + public List resolveDeprecatedCourses(List requiredCourses) { + return requiredCourses.stream() + .map(this::mapToCurrentCourse) + .toList(); + } + private boolean isWildCardRule(RequiredCourse requiredCourse, String departmentName) { return !isSpecificRule(requiredCourse, departmentName) && requiredCourse.getDeptNm().equals(WILD_CARD_DEPT_NM); } @@ -61,4 +92,27 @@ private boolean isWildCardRule(RequiredCourse requiredCourse, String departmentN private boolean isSpecificRule(RequiredCourse requiredCourse, String departmentName) { return requiredCourse.getDeptNm().equals(departmentName); } + + private RequiredCourseResponse mapToCurrentCourse(RequiredCourse requiredCourse) { + if (isNotDeprecated(requiredCourse.getCuriNo())) { + return RequiredCourseResponse.of(requiredCourse.getCuriNo(), requiredCourse.getCuriNm()); + } + return requiredCourseRepository.findCurrentCourseByGroupCode(requiredCourse.getGroupCode(), DEPRECATED) + .stream().findFirst() + .map(currentCourse -> RequiredCourseResponse.of(currentCourse.getCuriNo(), currentCourse.getCuriNm())) + .orElseGet(() -> { + log.error( + "[졸업요건] DEPRECATED된 과목의 현재 과목 조회에 실패했습니다. groupCode={}", requiredCourse.getGroupCode() + ); + return RequiredCourseResponse.of(requiredCourse.getCuriNo(), requiredCourse.getCuriNm()); + }); + } + + private boolean isNotDeprecated(String curiNo) { + return !isDeprecated(curiNo); + } + + private boolean isDeprecated(String curiNo) { + return DEPRECATED.equals(curiNo); + } } diff --git a/src/main/java/kr/allcll/backend/domain/graduation/credit/UncompletedCourseFilter.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/UncompletedCourseFilter.java index 5db031dd8..3d697c616 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/UncompletedCourseFilter.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/UncompletedCourseFilter.java @@ -1,8 +1,8 @@ package kr.allcll.backend.domain.graduation.credit; -import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; import kr.allcll.backend.domain.graduation.balance.dto.BalanceAreaCoursesResponse; import kr.allcll.backend.domain.graduation.check.excel.CompletedCourse; import kr.allcll.backend.domain.graduation.credit.dto.GraduationCategoryResponse; @@ -14,36 +14,25 @@ @RequiredArgsConstructor public class UncompletedCourseFilter { - private final CourseReplacementResolver courseReplacementResolver; + private final CourseEquivalenceRepository courseEquivalenceRepository; public List filterUncompletedCourses( - Integer admissionYear, List categories, List earnedCourses ) { - Set earnedCuriNos = buildEarnedCuriNos(admissionYear, earnedCourses); + Set earnedCuriNos = buildEarnedCuriNos(earnedCourses); return categories.stream() .map(category -> filterCategory(category, earnedCuriNos)) .toList(); } - private Set buildEarnedCuriNos(Integer admissionYear, List earnedCourses) { - Set earnedCuriNos = new HashSet<>(); - Set earnedCuriNms = new HashSet<>(); + private Set buildEarnedCuriNos(List earnedCourses) { + Set earnedCuriNos = earnedCourses.stream() + .map(CompletedCourse::getCuriNo) + .collect(Collectors.toSet()); - for (CompletedCourse completedCourse : earnedCourses) { - String curiNo = completedCourse.getCuriNo(); - earnedCuriNos.add(curiNo); - - String curiNm = completedCourse.getCuriNm(); - String trimmedCuriNm = curiNm.trim(); - if (!trimmedCuriNm.isBlank()) { - earnedCuriNms.add(trimmedCuriNm); - } - } - - earnedCuriNos.addAll(courseReplacementResolver.resolveCurrentCuriNos(admissionYear, earnedCuriNms)); + earnedCuriNos.addAll(courseEquivalenceRepository.findSameGroupCuriNos(earnedCuriNos)); return earnedCuriNos; } diff --git a/src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java b/src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java similarity index 59% rename from src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java rename to src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java index 337c34b43..05ec26e9c 100644 --- a/src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java +++ b/src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java @@ -7,17 +7,14 @@ @Component @RequiredArgsConstructor -public class CourseReplacementsSheetValidator implements GraduationSheetValidator { +public class CourseEquivalencesSheetValidator implements GraduationSheetValidator { - public static final String TAB_KEY = "course-replacements"; + public static final String TAB_KEY = "course-equivalences"; private static final List REQUIRED_HEADERS = List.of( - "admission_year", - "admission_year_short", - "legacy_curi_nm", - "current_curi_no", - "current_curi_nm", - "enabled" + "group_code", + "curi_no", + "curi_nm" ); private final GraduationSheetValidationSupport graduationSheetValidationSupport; @@ -36,12 +33,9 @@ public void validate(GraduationSheetTable sheetTable) { for (int rowIndex = 0; rowIndex < dataRows.size(); rowIndex++) { List dataRow = dataRows.get(rowIndex); - graduationSheetValidationSupport.requireInt(TAB_KEY, sheetTable, dataRow, rowIndex, "admission_year"); - graduationSheetValidationSupport.requireInt(TAB_KEY, sheetTable, dataRow, rowIndex, "admission_year_short"); - graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "legacy_curi_nm"); - graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "current_curi_no"); - graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "current_curi_nm"); - graduationSheetValidationSupport.requireBoolean(TAB_KEY, sheetTable, dataRow, rowIndex, "enabled"); + graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "group_code"); + graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "curi_no"); + graduationSheetValidationSupport.requireString(TAB_KEY, sheetTable, dataRow, rowIndex, "curi_nm"); } } } diff --git a/src/test/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicyTest.java b/src/test/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicyTest.java index 3ebc3764c..0aba1b99a 100644 --- a/src/test/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicyTest.java +++ b/src/test/java/kr/allcll/backend/domain/graduation/credit/AcademicBasicPolicyTest.java @@ -1,11 +1,12 @@ package kr.allcll.backend.domain.graduation.credit; +import static kr.allcll.backend.fixture.CompletedCourseFixture.createCompletedCourse; +import static kr.allcll.backend.fixture.CreditCriterionFixture.createAcademicBasicCriterion; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import java.util.List; -import kr.allcll.backend.domain.graduation.MajorScope; -import kr.allcll.backend.domain.graduation.MajorType; +import java.util.Optional; import kr.allcll.backend.domain.graduation.check.excel.CompletedCourse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -23,26 +24,14 @@ class AcademicBasicPolicyTest { private RequiredCourseResolver requiredCourseResolver; @MockitoBean - private CourseReplacementRepository courseReplacementRepository; + private CourseEquivalenceRepository courseEquivalenceRepository; @Test @DisplayName("ACADEMIC_BASIC이 아닌 과목은 검사 없이 통과한다") void returnTrueWhenNotAcademicBasic() { // given - String departmentName = "컴퓨터공학과"; - Integer admissionYear = 2021; - CompletedCourse course = new CompletedCourse( - 1L, - "123456", - "알고리즘및실습", - CategoryType.MAJOR_BASIC, - "", - 3.0, - "A+", - MajorScope.PRIMARY, - true - ); - CreditCriterion criterion = createAcademicBasicCreditCriterion(departmentName, admissionYear); + CompletedCourse course = createCompletedCourse("123456", "알고리즘및실습", CategoryType.MAJOR_BASIC); + CreditCriterion criterion = createAcademicBasicCriterion("컴퓨터공학과", 2021); // when boolean result = academicBasicPolicy.isRecentMajorAcademicBasic(course, criterion); @@ -58,18 +47,8 @@ void returnTrueWhenDirectMatch() { String courseName = "공학설계기초"; String departmentName = "컴퓨터공학과"; Integer admissionYear = 2021; - CompletedCourse course = new CompletedCourse( - 1L, - "123456", - courseName, - CategoryType.ACADEMIC_BASIC, - "", - 3.0, - "A+", - MajorScope.PRIMARY, - true - ); - CreditCriterion criterion = createAcademicBasicCreditCriterion(departmentName, admissionYear); + CompletedCourse course = createCompletedCourse("123456", courseName, CategoryType.ACADEMIC_BASIC); + CreditCriterion criterion = createAcademicBasicCriterion(departmentName, admissionYear); given( requiredCourseResolver.findRequiredCourseNames( @@ -86,45 +65,33 @@ void returnTrueWhenDirectMatch() { } @Test - @DisplayName("필수 과목엔 없지만, 대체 과목의 레거시 이름이 필수 목록에 있으면 true를 반환한다") - void returnTrueWhenReplacementMatch() { + @DisplayName("필수 과목에 없지만 동일과목 그룹에 속하면 true를 반환한다") + void returnTrueWhenGroupCodeMatchesRequiredCourse() { // given - String attendCourseName = "새로운 공학설계기초"; - String oldCourseName = "공학설계기초"; + String curiNo = "123456"; + String groupCode = "1"; String departmentName = "컴퓨터공학과"; Integer admissionYear = 2021; - - CompletedCourse course = new CompletedCourse( - 1L, - "123456", - attendCourseName, - CategoryType.ACADEMIC_BASIC, - "", - 3.0, - "A+", - MajorScope.PRIMARY, - true - ); - CreditCriterion criterion = createAcademicBasicCreditCriterion(departmentName, admissionYear); + CompletedCourse course = createCompletedCourse(curiNo, "옛날공학설계기초", CategoryType.ACADEMIC_BASIC); + CreditCriterion criterion = createAcademicBasicCriterion(departmentName, admissionYear); given( requiredCourseResolver.findRequiredCourseNames( departmentName, admissionYear, - CategoryType.ACADEMIC_BASIC) - ).willReturn(List.of(oldCourseName)); - - CourseReplacement replacement = new CourseReplacement( - 2021, - 21, - oldCourseName, - "currentCuriNo", - attendCourseName, - true, - null - ); - given(courseReplacementRepository.findRecentCourse(2021, attendCourseName)) - .willReturn(List.of(replacement)); + CategoryType.ACADEMIC_BASIC + ) + ).willReturn(List.of()); + given(courseEquivalenceRepository.findGroupCodeByCuriNo(curiNo)) + .willReturn(Optional.of(groupCode)); + given( + requiredCourseResolver.findRequiredCourseInGroup( + departmentName, + admissionYear, + CategoryType.ACADEMIC_BASIC, + groupCode + ) + ).willReturn(true); // when boolean result = academicBasicPolicy.isRecentMajorAcademicBasic(course, criterion); @@ -134,24 +101,14 @@ void returnTrueWhenReplacementMatch() { } @Test - @DisplayName("필수 과목에도 없고, 대체 과목 정보도 없으면 false를 반환한다") - void returnFalseWhenNoMatchNoReplacement() { - // given + @DisplayName("과목명이 필수과목에 없고 동일과목 그룹에 속하지만, 해당 과의 이수 요건에 해당하지 않으면 false를 반환한다") + void returnFalseWhenGroupCodeNotMatchesRequiredCourse() { + String curiNo = "123456"; + String groupCode = "1"; String departmentName = "컴퓨터공학과"; Integer admissionYear = 2021; - String courseName = "건환공기초창의설계"; - CompletedCourse course = new CompletedCourse( - 1L, - "123456", - courseName, - CategoryType.ACADEMIC_BASIC, - "", - 3.0, - "A+", - MajorScope.PRIMARY, - true - ); - CreditCriterion criterion = createAcademicBasicCreditCriterion(departmentName, admissionYear); + CompletedCourse course = createCompletedCourse(curiNo, "옛날공학설계기초", CategoryType.ACADEMIC_BASIC); + CreditCriterion criterion = createAcademicBasicCriterion(departmentName, admissionYear); given( requiredCourseResolver.findRequiredCourseNames( @@ -159,10 +116,16 @@ void returnFalseWhenNoMatchNoReplacement() { admissionYear, CategoryType.ACADEMIC_BASIC ) - ).willReturn(List.of("공학설계기초")); - - given(courseReplacementRepository.findRecentCourse(admissionYear, courseName)) - .willReturn(List.of()); + ).willReturn(List.of()); + given(courseEquivalenceRepository.findGroupCodeByCuriNo(curiNo)) + .willReturn(Optional.of(groupCode)); + given( + requiredCourseResolver.findRequiredCourseInGroup( + departmentName, + admissionYear, + CategoryType.ACADEMIC_BASIC, + groupCode) + ).willReturn(false); //해당 학과의 지정 과목이 required=false인 경우 // when boolean result = academicBasicPolicy.isRecentMajorAcademicBasic(course, criterion); @@ -171,18 +134,25 @@ void returnFalseWhenNoMatchNoReplacement() { assertThat(result).isFalse(); } - private CreditCriterion createAcademicBasicCreditCriterion(String departmentName, Integer admissionYear) { - return new CreditCriterion( - admissionYear, - 21, - MajorType.SINGLE, - "3210", - departmentName, - MajorScope.PRIMARY, - CategoryType.ACADEMIC_BASIC, - 9, - true, - "" - ); + @Test + @DisplayName("과목명이 필수과목에도 없고 동일과목 그룹에도 속하지 않으면 false를 반환한다") + void returnFalseWhenCuriNoNotInEquivalence() { + // given + String curiNo = "123456"; + String departmentName = "컴퓨터공학과"; + Integer admissionYear = 2021; + CompletedCourse course = createCompletedCourse(curiNo, "존재하지않는과목", CategoryType.ACADEMIC_BASIC); + CreditCriterion criterion = createAcademicBasicCriterion(departmentName, admissionYear); + + given(requiredCourseResolver.findRequiredCourseNames(departmentName, admissionYear, CategoryType.ACADEMIC_BASIC)) + .willReturn(List.of()); + given(courseEquivalenceRepository.findGroupCodeByCuriNo(curiNo)) + .willReturn(Optional.empty()); + + // when + boolean result = academicBasicPolicy.isRecentMajorAcademicBasic(course, criterion); + + // then + assertThat(result).isFalse(); } } diff --git a/src/test/java/kr/allcll/backend/fixture/CompletedCourseFixture.java b/src/test/java/kr/allcll/backend/fixture/CompletedCourseFixture.java index 9f55f8351..234529af1 100644 --- a/src/test/java/kr/allcll/backend/fixture/CompletedCourseFixture.java +++ b/src/test/java/kr/allcll/backend/fixture/CompletedCourseFixture.java @@ -33,4 +33,18 @@ public static CompletedCourse createCompletedCourse(CategoryType categoryType) { true ); } + + public static CompletedCourse createCompletedCourse(String curiNo, String curiNm, CategoryType categoryType) { + return new CompletedCourse( + 1L, + curiNo, + curiNm, + categoryType, + "", + 3.0, + "A+", + MajorScope.PRIMARY, + true + ); + } } diff --git a/src/test/java/kr/allcll/backend/fixture/CreditCriterionFixture.java b/src/test/java/kr/allcll/backend/fixture/CreditCriterionFixture.java index 258c844b1..3b11fcd33 100644 --- a/src/test/java/kr/allcll/backend/fixture/CreditCriterionFixture.java +++ b/src/test/java/kr/allcll/backend/fixture/CreditCriterionFixture.java @@ -7,15 +7,15 @@ public class CreditCriterionFixture { - public static CreditCriterion createCriterion(CategoryType categoryType) { + public static CreditCriterion createAcademicBasicCriterion(String deptNm, Integer admissionYear) { return new CreditCriterion( - 2023, - 23, + admissionYear, + admissionYear % 100, MajorType.SINGLE, - "소프트웨어학과", - "3220", + "3210", + deptNm, MajorScope.PRIMARY, - categoryType, + CategoryType.ACADEMIC_BASIC, 9, true, ""