From f8a2644f591d65c1799cb218c0087db28c265ed0 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 16:26:14 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EB=8F=99=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4=20=EA=B3=BC=EB=AA=A9=20=EA=B4=80=EB=A6=AC=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/credit/CourseEquivalence.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalence.java 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 00000000..b252c9d7 --- /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; + } +} From 3059cc8edd961bb9d1b82e4bed9509215f5b44b6 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 16:28:47 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20=EB=8F=99=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4=20=EA=B3=BC=EB=AA=A9=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=ED=8C=90=EB=8B=A8=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EC=B9=BC=EB=9F=BC(`group=5Fcode`)=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/domain/graduation/credit/RequiredCourse.java | 5 +++++ 1 file changed, 5 insertions(+) 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 735cdfd2..baca1c16 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; } From 014d591dba3e9c6a713a0f611ec51df2a2819bff Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 16:29:38 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat:=20=EB=8F=99=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4=20=EA=B3=BC=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B2=80=EC=A6=9D=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CourseEquivalencesSheetValidator.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java diff --git a/src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java b/src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java new file mode 100644 index 00000000..05ec26e9 --- /dev/null +++ b/src/main/java/kr/allcll/backend/support/sheet/validation/CourseEquivalencesSheetValidator.java @@ -0,0 +1,41 @@ +package kr.allcll.backend.support.sheet.validation; + +import java.util.List; +import kr.allcll.backend.support.sheet.GraduationSheetTable; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class CourseEquivalencesSheetValidator implements GraduationSheetValidator { + + public static final String TAB_KEY = "course-equivalences"; + + private static final List REQUIRED_HEADERS = List.of( + "group_code", + "curi_no", + "curi_nm" + ); + + private final GraduationSheetValidationSupport graduationSheetValidationSupport; + + @Override + public String tabKey() { + return TAB_KEY; + } + + @Override + public void validate(GraduationSheetTable sheetTable) { + graduationSheetValidationSupport.validateNotEmpty(TAB_KEY, sheetTable); + graduationSheetValidationSupport.validateRequiredHeaders(TAB_KEY, sheetTable, REQUIRED_HEADERS); + + List> dataRows = sheetTable.getDataRows(); + for (int rowIndex = 0; rowIndex < dataRows.size(); rowIndex++) { + List dataRow = dataRows.get(rowIndex); + + 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"); + } + } +} From d496bf29262570663048615d847bf25eb8e3d2ac Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 16:32:16 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat:=20=EB=8F=99=EC=9D=BC=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8C=80=EC=B2=B4=20=EA=B3=BC=EB=AA=A9=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0(`course=5Fequivalences`)=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=95=84?= =?UTF-8?q?=EC=88=98=20=EA=B3=BC=EB=AA=A9=20=EB=8D=B0=EC=9D=B4=ED=84=B0(`r?= =?UTF-8?q?equired=5Fcourses`)=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminGraduationSyncService.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) 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 33491f36..a0238a6e 100644 --- a/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java +++ b/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java @@ -23,6 +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.CourseEquivalence; +import kr.allcll.backend.domain.graduation.credit.CourseEquivalenceRepository; import kr.allcll.backend.domain.graduation.credit.CourseReplacement; import kr.allcll.backend.domain.graduation.credit.CourseReplacementRepository; import kr.allcll.backend.domain.graduation.credit.CreditCriterion; @@ -42,6 +44,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.CourseEquivalencesSheetValidator; import kr.allcll.backend.support.sheet.validation.CourseReplacementsSheetValidator; import kr.allcll.backend.support.sheet.validation.CreditCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.DoubleCreditCriteriaSheetValidator; @@ -64,6 +67,7 @@ public class AdminGraduationSyncService { private final RequiredCourseRepository requiredCourseRepository; private final CreditCriterionRepository creditCriterionRepository; private final GraduationSheetProperties graduationSheetProperties; + private final CourseEquivalenceRepository courseEquivalenceRepository; private final CourseReplacementRepository courseReplacementRepository; private final GraduationCertRuleRepository graduationCertRuleRepository; private final BalanceRequiredRuleRepository balanceRequiredRuleRepository; @@ -83,6 +87,7 @@ public void syncGraduationRules() { syncDoubleCreditCriteria(); syncRequiredCourses(); syncCourseReplacements(); + syncCourseEquivalences(); syncBalanceRequiredRule(); syncBalanceRequiredCourseAreaMap(); @@ -179,6 +184,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") ); @@ -215,6 +221,25 @@ private void syncCourseReplacements() { log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, courseReplacementList.size()); } + private void syncCourseEquivalences() { + String tabKey = CourseEquivalencesSheetValidator.TAB_KEY; + GraduationSheetTable graduationSheetTable = fetchAndValidate(tabKey); + + List courseEquivalenceList = new ArrayList<>(); + for (List row : graduationSheetTable.getDataRows()) { + CourseEquivalence courseEquivalence = new CourseEquivalence( + graduationSheetTable.getString(row, "group_code"), + graduationSheetTable.getString(row, "curi_no"), + graduationSheetTable.getString(row, "curi_nm") + ); + courseEquivalenceList.add(courseEquivalence); + } + + courseEquivalenceRepository.deleteAllInBatch(); + courseEquivalenceRepository.saveAll(courseEquivalenceList); + + log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, courseEquivalenceList.size()); + } private void syncBalanceRequiredRule() { String tabKey = BalanceRequiredRulesSheetValidator.TAB_KEY; From bb9bd4afdb664ce0f061279035d61565cee851a0 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 17:10:23 +0900 Subject: [PATCH 05/10] =?UTF-8?q?fix:=20=EA=B8=B0=EC=A1=B4=20=EA=B8=B0?= =?UTF-8?q?=ED=95=84=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4=EA=B3=BC=EB=AA=A9=20=EB=B3=B4=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95=ED=95=98=EC=97=AC=20=EB=8F=99?= =?UTF-8?q?=EC=9D=BC=EA=B3=BC=EB=AA=A9=20=ED=95=99=EC=A0=90=20=EB=AF=B8?= =?UTF-8?q?=EC=9D=B8=EC=A0=95=20=EC=9D=B4=EC=8A=88=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credit/AcademicBasicPolicy.java | 48 ++++++++----------- .../credit/CourseEquivalenceRepository.java | 14 ++++++ .../credit/RequiredCourseRepository.java | 14 ++++++ .../credit/RequiredCourseResolver.java | 21 ++++++++ 4 files changed, 68 insertions(+), 29 deletions(-) create mode 100644 src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java 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 309a3990..39d8d41f 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/CourseEquivalenceRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java new file mode 100644 index 00000000..b5339454 --- /dev/null +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java @@ -0,0 +1,14 @@ +package kr.allcll.backend.domain.graduation.credit; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface CourseEquivalenceRepository extends JpaRepository { + + @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/RequiredCourseRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java index acb26dfd..86f9ee4f 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,18 @@ 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 + ); } 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 1ad118c5..7c40c28e 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 @@ -13,6 +13,27 @@ public class RequiredCourseResolver { 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, From 4e272f14ca89c02e59512a14286454be345f8fa1 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 19:02:48 +0900 Subject: [PATCH 06/10] =?UTF-8?q?test:=20=EB=8C=80=EC=B2=B4=20=EB=B0=8F=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=EA=B3=BC=EB=AA=A9=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credit/AcademicBasicPolicyTest.java | 156 +++++++----------- .../fixture/CompletedCourseFixture.java | 14 ++ .../fixture/CreditCriterionFixture.java | 12 +- 3 files changed, 83 insertions(+), 99 deletions(-) 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 3ebc3764..0aba1b99 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 9f55f835..234529af 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 258c844b..3b11fcd3 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, "" From 62267b3c5ba36510c9308f733882defdf2e92d7e Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 21:33:10 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=84=20=EC=A1=B8=EC=97=85=EC=9A=94=EA=B1=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20-=20deprecated=20=EB=90=9C=20=EA=B3=BC?= =?UTF-8?q?=EB=AA=A9=EC=9D=98=20=EC=B5=9C=EC=8B=A0=20=EA=B3=BC=EB=AA=A9=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=B1=85=EC=9E=84=20=EC=9D=B4=EB=8F=99(`C?= =?UTF-8?q?ourseReplacement`=EA=B8=B0=EB=B0=98=20=ED=81=B4=EB=9E=98?= =?UTF-8?q?=EC=8A=A4=20->=20`CourseEquivalence`=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=81=B4=EB=9E=98=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credit/NonMajorCategoryResolver.java | 4 +-- .../credit/RequiredCourseRepository.java | 7 ++++ .../credit/RequiredCourseResolver.java | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) 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 d84a6d94..6eedbb9f 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/RequiredCourseRepository.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/RequiredCourseRepository.java index 86f9ee4f..5b699817 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 @@ -34,4 +34,11 @@ List findRequiredCoursesByGroupCode( 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 7c40c28e..80ff55e2 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,13 +3,17 @@ 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; @@ -75,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); } @@ -82,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); + } } From 531505fc8fbcd89c7fd1562bb90670459ccacbaa Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 21:38:44 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=84=20=EC=A1=B8=EC=97=85=EC=9A=94=EA=B1=B4=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20API=20-=20=20=EA=B8=B0=EC=A1=B4=20`CourseReplacemen?= =?UTF-8?q?t`=EA=B0=80=20=EB=8B=B4=EB=8B=B9=ED=96=88=EB=8D=98=20=EB=AF=B8?= =?UTF-8?q?=EC=9D=B4=EC=88=98=20=EA=B3=BC=EB=AA=A9=20=ED=8C=90=EB=8B=A8=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=EC=9D=84=20`CourseEquivalence`=EB=A1=9C=20?= =?UTF-8?q?=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../credit/CourseEquivalenceRepository.java | 9 +++++++++ .../credit/UncompletedCourseFilter.java | 18 +++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) 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 index b5339454..3da14f27 100644 --- a/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java +++ b/src/main/java/kr/allcll/backend/domain/graduation/credit/CourseEquivalenceRepository.java @@ -1,11 +1,20 @@ 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 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 5db031dd..85a28203 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 @@ -14,36 +14,28 @@ @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) { + private Set buildEarnedCuriNos(List earnedCourses) { Set earnedCuriNos = new HashSet<>(); - Set earnedCuriNms = new HashSet<>(); 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.add(completedCourse.getCuriNo()); } - earnedCuriNos.addAll(courseReplacementResolver.resolveCurrentCuriNos(admissionYear, earnedCuriNms)); + earnedCuriNos.addAll(courseEquivalenceRepository.findSameGroupCuriNos(earnedCuriNos)); return earnedCuriNos; } From 76eb9abcf92fdcf6e5c6ada3d65504bba86a31d0 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Mon, 30 Mar 2026 21:42:02 +0900 Subject: [PATCH 09/10] =?UTF-8?q?fix:=20`CourseEquivalence`=EB=A1=9C?= =?UTF-8?q?=EC=9D=98=20=EB=8C=80=EC=B2=B4=20=EB=B0=8F=20=EB=8F=99=EC=9D=BC?= =?UTF-8?q?=EA=B3=BC=EB=AA=A9=20=EA=B4=80=EB=A6=AC=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=8A=B8=20=ED=86=B5=ED=95=A9=EC=97=90=20=EB=94=B0=EB=9D=BC=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=B4=EC=A7=84=20`CourseReplacem?= =?UTF-8?q?ent`=20=EA=B4=80=EB=A0=A8=20=ED=81=B4=EB=9E=98=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminGraduationSyncService.java | 29 ------- .../graduation/credit/CourseReplacement.java | 57 ------------- .../credit/CourseReplacementRepository.java | 28 ------- .../credit/CourseReplacementResolver.java | 81 ------------------- .../CourseReplacementsSheetValidator.java | 47 ----------- 5 files changed, 242 deletions(-) delete mode 100644 src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacement.java delete mode 100644 src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementRepository.java delete mode 100644 src/main/java/kr/allcll/backend/domain/graduation/credit/CourseReplacementResolver.java delete mode 100644 src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java 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 a0238a6e..a7a04fbd 100644 --- a/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java +++ b/src/main/java/kr/allcll/backend/admin/graduation/AdminGraduationSyncService.java @@ -25,8 +25,6 @@ import kr.allcll.backend.domain.graduation.credit.CategoryType; import kr.allcll.backend.domain.graduation.credit.CourseEquivalence; import kr.allcll.backend.domain.graduation.credit.CourseEquivalenceRepository; -import kr.allcll.backend.domain.graduation.credit.CourseReplacement; -import kr.allcll.backend.domain.graduation.credit.CourseReplacementRepository; import kr.allcll.backend.domain.graduation.credit.CreditCriterion; import kr.allcll.backend.domain.graduation.credit.CreditCriterionRepository; import kr.allcll.backend.domain.graduation.credit.DoubleCreditCriterion; @@ -45,7 +43,6 @@ import kr.allcll.backend.support.sheet.validation.ClassicCertCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.CodingCertCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.CourseEquivalencesSheetValidator; -import kr.allcll.backend.support.sheet.validation.CourseReplacementsSheetValidator; import kr.allcll.backend.support.sheet.validation.CreditCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.DoubleCreditCriteriaSheetValidator; import kr.allcll.backend.support.sheet.validation.EnglishCertCriteriaSheetValidator; @@ -68,7 +65,6 @@ public class AdminGraduationSyncService { private final CreditCriterionRepository creditCriterionRepository; private final GraduationSheetProperties graduationSheetProperties; private final CourseEquivalenceRepository courseEquivalenceRepository; - private final CourseReplacementRepository courseReplacementRepository; private final GraduationCertRuleRepository graduationCertRuleRepository; private final BalanceRequiredRuleRepository balanceRequiredRuleRepository; private final CodingCertCriterionRepository codingCertCriterionRepository; @@ -86,7 +82,6 @@ public void syncGraduationRules() { syncCreditCriteria(); syncDoubleCreditCriteria(); syncRequiredCourses(); - syncCourseReplacements(); syncCourseEquivalences(); syncBalanceRequiredRule(); @@ -197,30 +192,6 @@ private void syncRequiredCourses() { log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, requiredCourseList.size()); } - private void syncCourseReplacements() { - String tabKey = CourseReplacementsSheetValidator.TAB_KEY; - GraduationSheetTable graduationSheetTable = fetchAndValidate(tabKey); - - List courseReplacementList = 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") - ); - courseReplacementList.add(courseReplacement); - } - - courseReplacementRepository.deleteAllInBatch(); - courseReplacementRepository.saveAll(courseReplacementList); - - log.info("[졸업요건 데이터 동기화] 탭 이름={}, {}개 저장 완료", tabKey, courseReplacementList.size()); - } - private void syncCourseEquivalences() { String tabKey = CourseEquivalencesSheetValidator.TAB_KEY; GraduationSheetTable graduationSheetTable = fetchAndValidate(tabKey); 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 df5c4049..00000000 --- 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 475207e7..00000000 --- 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 7170e102..00000000 --- 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/support/sheet/validation/CourseReplacementsSheetValidator.java b/src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java deleted file mode 100644 index 337c34b4..00000000 --- a/src/main/java/kr/allcll/backend/support/sheet/validation/CourseReplacementsSheetValidator.java +++ /dev/null @@ -1,47 +0,0 @@ -package kr.allcll.backend.support.sheet.validation; - -import java.util.List; -import kr.allcll.backend.support.sheet.GraduationSheetTable; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Component; - -@Component -@RequiredArgsConstructor -public class CourseReplacementsSheetValidator implements GraduationSheetValidator { - - public static final String TAB_KEY = "course-replacements"; - - private static final List REQUIRED_HEADERS = List.of( - "admission_year", - "admission_year_short", - "legacy_curi_nm", - "current_curi_no", - "current_curi_nm", - "enabled" - ); - - private final GraduationSheetValidationSupport graduationSheetValidationSupport; - - @Override - public String tabKey() { - return TAB_KEY; - } - - @Override - public void validate(GraduationSheetTable sheetTable) { - graduationSheetValidationSupport.validateNotEmpty(TAB_KEY, sheetTable); - graduationSheetValidationSupport.validateRequiredHeaders(TAB_KEY, sheetTable, REQUIRED_HEADERS); - - List> dataRows = sheetTable.getDataRows(); - 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"); - } - } -} From de8ae848d3f7944686f15f124ccb888ca8bc0d61 Mon Sep 17 00:00:00 2001 From: goldm0ng Date: Wed, 1 Apr 2026 14:00:27 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refactor:=20=EC=95=88=20=EC=93=B4?= =?UTF-8?q?=EB=8A=90=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=B0=8F=20=EB=A1=9C=EC=A7=81=20=EA=B0=80=EB=8F=85?= =?UTF-8?q?=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../graduation/credit/GraduationCategoryService.java | 1 - .../graduation/credit/UncompletedCourseFilter.java | 11 ++++------- 2 files changed, 4 insertions(+), 8 deletions(-) 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 227e145a..505b1646 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/UncompletedCourseFilter.java b/src/main/java/kr/allcll/backend/domain/graduation/credit/UncompletedCourseFilter.java index 85a28203..3d697c61 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; @@ -17,7 +17,6 @@ public class UncompletedCourseFilter { private final CourseEquivalenceRepository courseEquivalenceRepository; public List filterUncompletedCourses( - Integer admissionYear, List categories, List earnedCourses ) { @@ -29,11 +28,9 @@ public List filterUncompletedCourses( } private Set buildEarnedCuriNos(List earnedCourses) { - Set earnedCuriNos = new HashSet<>(); - - for (CompletedCourse completedCourse : earnedCourses) { - earnedCuriNos.add(completedCourse.getCuriNo()); - } + Set earnedCuriNos = earnedCourses.stream() + .map(CompletedCourse::getCuriNo) + .collect(Collectors.toSet()); earnedCuriNos.addAll(courseEquivalenceRepository.findSameGroupCuriNos(earnedCuriNos));