Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bada53c
feat: 고전독서인증 진행도 계산 시 기준 초과 도서 제외
2Jin1031 Mar 4, 2026
47963a6
refactor: [이수구분 문자열 <-> ENUM 매핑] 책임 및 [23학번 이하의 전기 -> 기필 보정] 책임 `Cate…
goldm0ng Mar 5, 2026
1a642c5
refactor: `CompletedCourseDto` -> `CompletedCourse` Entity로 변환하는 과정에서…
goldm0ng Mar 5, 2026
1e0efc5
refactor: 기존 `MajorBasicPolicy`에서 보정하던 흐름 삭제
goldm0ng Mar 5, 2026
c0b263f
test: CategoryType 단위 테스트 작성
goldm0ng Mar 5, 2026
9f57c1f
refactor: EOF 처리
2Jin1031 Mar 5, 2026
fdd6394
refactor: 고전독서 총 인증 권수 계산에 영역별 최대값 적용
2Jin1031 Mar 5, 2026
32f503b
refactor: 고전인증 합격 여부 파싱 로직 개선 및 테스트 추가
2Jin1031 Mar 5, 2026
5bf6acc
Merge pull request #302 from allcll/2Jin1031/TSK-56-149
2Jin1031 Mar 6, 2026
f0c4ac5
refactor: 엑셀 파싱 단계에서 이미 검증하므로 중복된 null 검증 제거
goldm0ng Mar 6, 2026
4580e85
refactor: 엑셀 파싱 단계에서 이미 검증하므로 중복된 null 검증 제거
goldm0ng Mar 6, 2026
55f8ec0
test: 중복된 null 검증 제거로 인해 깨지는 테스트 제거
goldm0ng Mar 6, 2026
c0059ce
refactor: `CategoryType` 매핑 로직 개선
goldm0ng Mar 6, 2026
aac699c
test: `CategoryType` 매핑 로직 개선에 따른 테스트 수정
goldm0ng Mar 6, 2026
d2c0d3e
Merge pull request #300 from allcll/goldm0ng/TSK-56-148
goldm0ng Mar 6, 2026
170fd15
refactor: 고전독서 기준 권수를 enum으로 관리
2Jin1031 Mar 7, 2026
77935c4
refactor: 파싱 형식 수정
2Jin1031 Mar 7, 2026
ddfa0f5
fix: main과 merge 충돌 해결
2Jin1031 Mar 7, 2026
78e741d
test: 기존 수정 사항에 맞춰 테스트 코드 수정
2Jin1031 Mar 7, 2026
d668567
test: 기존 수정 사항에 맞춰 테스트 코드 수정
2Jin1031 Mar 7, 2026
74a542d
Merge branch '2Jin1031/TSK-56-147' of https://github.com/allcll/allcl…
2Jin1031 Mar 7, 2026
ed5aec4
test: 불필요한 테스트 제거
2Jin1031 Mar 8, 2026
93c602e
fix: GraduationCertCriteriaResponse에 enum을 활용한 ClassicCertCriteriaRes…
2Jin1031 Mar 8, 2026
ad925e6
Merge pull request #299 from allcll/2Jin1031/TSK-56-147
goldm0ng Mar 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ public class GraduationCertCriteriaService {
private final GraduationCertRuleRepository graduationCertRuleRepository;
private final CodingCertCriterionRepository codingCertCriterionRepository;
private final GraduationDepartmentInfoRepository departmentInfoRepository;
private final ClassicCertCriterionRepository classicCertCriterionRepository;
private final EnglishCertCriterionRepository englishCertCriterionRepository;


Expand All @@ -49,11 +48,11 @@ public GraduationCertCriteriaResponse getGraduationCertCriteria(Long userId) {
GraduationCertPolicyResponse certPolicy =
buildCertPolicy(graduationCertRule.getGraduationCertRuleType(), englishTargetType, codingTargetType);
EnglishCertCriteriaResponse englishCriteria = buildEnglishCriteria(admissionYear, englishTargetType);
ClassicCertCriteriaResponse classicCriteria = buildClassicCriteria(admissionYear);
ClassicCertCriteriaResponse classicCriteria = buildClassicCriteria();
CodingCertCriteriaResponse codingCriteria = buildCodingCriteria(admissionYear, codingTargetType);

return GraduationCertCriteriaResponse.of(criteriaTarget, certPolicy, englishCriteria, classicCriteria,
codingCriteria);
return GraduationCertCriteriaResponse.of(criteriaTarget, certPolicy, englishCriteria,
classicCriteria, codingCriteria);
}

private GraduationDepartmentInfo findDepartment(int admissionYear, String deptCd) {
Expand Down Expand Up @@ -151,17 +150,8 @@ private EnglishCertCriteriaResponse buildEnglishCriteria(int admissionYear, Engl
);
}

private ClassicCertCriteriaResponse buildClassicCriteria(int admissionYear) {
ClassicCertCriterion classicCertCriterion = classicCertCriterionRepository.findByAdmissionYear(admissionYear)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.CLASSIC_CERT_CRITERIA_NOT_FOUND));

return ClassicCertCriteriaResponse.of(
classicCertCriterion.getTotalRequiredCount(),
classicCertCriterion.getRequiredCountWestern(),
classicCertCriterion.getRequiredCountEastern(),
classicCertCriterion.getRequiredCountEasternAndWestern(),
classicCertCriterion.getRequiredCountScience()
);
private ClassicCertCriteriaResponse buildClassicCriteria() {
return ClassicCertCriteriaResponse.fromEnum();
}

private CodingCertCriteriaResponse buildCodingCriteria(int admissionYear, CodingTargetType codingTargetType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package kr.allcll.backend.domain.graduation.certification.dto;

import kr.allcll.backend.domain.graduation.check.cert.ClassicsArea;

public record ClassicCertCriteriaResponse(
Integer totalRequiredCount,
Integer requiredCountWestern,
Expand All @@ -23,4 +25,14 @@ public static ClassicCertCriteriaResponse of(
requiredCountScience
);
}

public static ClassicCertCriteriaResponse fromEnum() {
return new ClassicCertCriteriaResponse(
ClassicsArea.getTotalRequiredCount(),
ClassicsArea.WESTERN.getMaxRecognizedCount(),
ClassicsArea.EASTERN.getMaxRecognizedCount(),
ClassicsArea.EASTERN_AND_WESTERN.getMaxRecognizedCount(),
ClassicsArea.SCIENCE.getMaxRecognizedCount()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package kr.allcll.backend.domain.graduation.check.cert;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

import java.util.Arrays;
import java.util.Optional;

@Getter
@RequiredArgsConstructor
public enum ClassicsArea {
WESTERN("서양의 역사와 사상", 4),
EASTERN("동양의 역사와 사상", 2),
EASTERN_AND_WESTERN("동·서양의 문학", 3),
SCIENCE("과학 사상", 1);

private final String koreanName;
private final int maxRecognizedCount;

public static Optional<ClassicsArea> findByLabel(String label) {
return Arrays.stream(values())
.filter(area -> label.contains(area.koreanName))
.findFirst();
}

public static int getTotalRequiredCount() {
return Arrays.stream(values())
.mapToInt(ClassicsArea::getMaxRecognizedCount)
.sum();
}

public int getRecognizedCount(int actualCount) {
return Math.min(actualCount, maxRecognizedCount);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package kr.allcll.backend.domain.graduation.check.cert;

import jakarta.persistence.EntityManager;
import kr.allcll.backend.domain.graduation.certification.ClassicCertCriterion;
import kr.allcll.backend.domain.graduation.certification.ClassicCertCriterionRepository;
import kr.allcll.backend.domain.graduation.certification.GraduationCertRule;
import kr.allcll.backend.domain.graduation.certification.GraduationCertRuleRepository;
import kr.allcll.backend.domain.graduation.certification.GraduationCertRuleType;
Expand All @@ -21,7 +19,6 @@ public class GraduationCertService {

private final EntityManager entityManager;
private final GraduationCertRuleRepository graduationCertRuleRepository;
private final ClassicCertCriterionRepository classicCertCriterionRepository;
private final GraduationCheckCertResultRepository graduationCheckCertResultRepository;

@Transactional
Expand All @@ -39,17 +36,12 @@ public void createOrUpdate(User user, GraduationCertInfo certInfo) {
int requiredPassCount = certRuleType.getRequiredPassCount();
boolean isSatisfied = certRuleType.isSatisfied(passedCount);

// 고전독서 기준 데이터 DB에서 조회
ClassicCertCriterion classicCriteria = classicCertCriterionRepository
.findByAdmissionYear(user.getAdmissionYear())
.orElseThrow(
() -> new AllcllException(AllcllErrorCode.GRADUATION_CERT_RULE_NOT_FOUND, user.getAdmissionYear()));

int requiredCountWestern = classicCriteria.getRequiredCountWestern();
int requiredCountEastern = classicCriteria.getRequiredCountEastern();
int requiredCountEasternAndWestern = classicCriteria.getRequiredCountEasternAndWestern();
int requiredCountScience = classicCriteria.getRequiredCountScience();
int classicsTotalRequiredCount = classicCriteria.getTotalRequiredCount();
// 고전독서 기준 데이터 enum에서 조회
int classicsTotalRequiredCount = ClassicsArea.getTotalRequiredCount();
int requiredCountWestern = ClassicsArea.WESTERN.getMaxRecognizedCount();
int requiredCountEastern = ClassicsArea.EASTERN.getMaxRecognizedCount();
int requiredCountEasternAndWestern = ClassicsArea.EASTERN_AND_WESTERN.getMaxRecognizedCount();
int requiredCountScience = ClassicsArea.SCIENCE.getMaxRecognizedCount();

boolean isWesternSatisfied = certInfo.myCountWestern() >= requiredCountWestern;
boolean isEasternSatisfied = certInfo.myCountEastern() >= requiredCountEastern;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@
import org.jsoup.select.Elements;
import org.springframework.stereotype.Service;

import java.util.EnumMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class GraduationClassicsCertFetcher {

private static final int PASS_STATUS_INDEX = 0;

private final LoginProperties properties;
private final GraduationHtmlParser parser;
private final GraduationCertDocumentFetcher documentFetcher;
Expand All @@ -33,8 +38,9 @@ public ClassicsResult fetchClassics(OkHttpClient client) {
}

private boolean parsePass(Document document) {
String approvalText = parser.selectClassicsPassText(document).trim();
return !approvalText.equals("아니오");
String[] passTextParts = parser.selectClassicsPassText(document);
String passText = passTextParts[PASS_STATUS_INDEX];
return !passText.equals("아니오");
}

private ClassicsCounts parseCounts(Document document) {
Expand All @@ -43,10 +49,7 @@ private ClassicsCounts parseCounts(Document document) {
throw new AllcllException(AllcllErrorCode.CLASSIC_DETAIL_INFO_FETCH_FAIL);
}

int westernCompleted = 0;
int easternCompleted = 0;
int literatureCompleted = 0;
int scienceCompleted = 0;
Map<ClassicsArea, Integer> countMap = new EnumMap<>(ClassicsArea.class);

for (Element row : table.select("tbody tr")) {
Element th = row.selectFirst("th");
Expand All @@ -58,28 +61,24 @@ private ClassicsCounts parseCounts(Document document) {
Elements tds = row.select("td");
if (tds.isEmpty()) continue;

int completedCount = extractCount(tds.get(0));

if (label.contains("서양의 역사와 사상")) {
westernCompleted = completedCount;
}
else if (label.contains("동양의 역사와 사상")) {
easternCompleted = completedCount;
}
else if (label.contains("동·서양의 문학")) {
literatureCompleted = completedCount;
}
else if (label.contains("과학 사상")) {
scienceCompleted = completedCount;
}
ClassicsArea.findByLabel(label).ifPresent(area -> {
int actualCount = extractCompletedCount(tds.getFirst());
countMap.put(area, actualCount);
});
}

return new ClassicsCounts(westernCompleted, easternCompleted, literatureCompleted, scienceCompleted);
return new ClassicsCounts(
countMap.getOrDefault(ClassicsArea.WESTERN, 0),
countMap.getOrDefault(ClassicsArea.EASTERN, 0),
countMap.getOrDefault(ClassicsArea.EASTERN_AND_WESTERN, 0),
countMap.getOrDefault(ClassicsArea.SCIENCE, 0)
);
}

private int extractCount(Element value) {
private int extractCompletedCount(Element value) {
try {
return Integer.parseInt(value.text().replace("권", "").trim());
String text = value.text().replace("권", "").trim();
return Integer.parseInt(text);
} catch (NumberFormatException e) {
return 0;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.allcll.backend.domain.graduation.check.cert.dto;

import kr.allcll.backend.domain.graduation.check.cert.ClassicsArea;
import kr.allcll.backend.domain.graduation.check.cert.GraduationCheckCertResult;

public record ClassicsCounts(
Expand All @@ -10,7 +11,10 @@ public record ClassicsCounts(
) {

public int totalMyCount() {
return myCountWestern + myCountEastern + myCountEasternAndWestern + myCountScience;
return ClassicsArea.WESTERN.getRecognizedCount(myCountWestern)
+ ClassicsArea.EASTERN.getRecognizedCount(myCountEastern)
+ ClassicsArea.EASTERN_AND_WESTERN.getRecognizedCount(myCountEasternAndWestern)
+ ClassicsArea.SCIENCE.getRecognizedCount(myCountScience);
}

public static ClassicsCounts fallback(GraduationCheckCertResult certResult) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
public record CompletedCourseDto(
String curiNo, // 학수번호
String curiNm, // 교과목명
CategoryType categoryType, // 이수구분 (변환됨)
String categoryTypeRaw, // 이수구분 (변환됨)
String selectedArea, // 선택영역 (균형교양 영역)
Double credits, // 학점
String grade, // 등급 (성적)
Expand All @@ -29,20 +29,20 @@ public static CompletedCourseDto of(
return new CompletedCourseDto(
curiNo,
curiNm,
convertCategoryType(categoryTypeRaw),
categoryTypeRaw,
selectedArea,
credits,
grade,
determineMajorScope(categoryTypeRaw)
);
}

public CompletedCourse toEntity(Long userId) {
public CompletedCourse toEntity(Long userId, int admissionYear) {
return new CompletedCourse(
userId,
curiNo,
curiNm,
categoryType,
CategoryType.fromRaw(categoryTypeRaw, admissionYear),
selectedArea,
credits,
grade,
Expand All @@ -51,44 +51,11 @@ public CompletedCourse toEntity(Long userId) {
);
}

public CompletedCourseDto toAcademicBasic() {
return new CompletedCourseDto(
this.curiNo,
this.curiNm,
CategoryType.ACADEMIC_BASIC,
this.selectedArea,
this.credits,
this.grade,
this.majorScope
);
}

// grade 기준 학점 인정 판별 메서드
public boolean isCreditEarned() {
if (grade == null || grade.isEmpty()) {
return true;
}
return !NOT_EARNED_GRADES.contains(grade);
}

private static CategoryType convertCategoryType(String raw) {
if (raw == null) {
return null;
}
String stripped = raw.strip();
return switch (stripped) {
case "교필", "공필" -> CategoryType.COMMON_REQUIRED;
case "균필" -> CategoryType.BALANCE_REQUIRED;
case "기교", "기필" -> CategoryType.ACADEMIC_BASIC;
case "교선", "교선1", "교선2" -> CategoryType.GENERAL_ELECTIVE;
case "교양" -> CategoryType.GENERAL;
case "전필", "복필" -> CategoryType.MAJOR_REQUIRED;
case "전선", "복선" -> CategoryType.MAJOR_ELECTIVE;
case "전기" -> CategoryType.MAJOR_BASIC;
default -> null;
};
}

private static MajorScope determineMajorScope(String categoryTypeRaw) {
if (categoryTypeRaw == null) {
throw new AllcllException(AllcllErrorCode.EMPTY_REQUIRED_COLUMN);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package kr.allcll.backend.domain.graduation.check.excel;

import java.util.List;
import kr.allcll.backend.domain.user.User;
import kr.allcll.backend.domain.user.UserRepository;
import kr.allcll.backend.support.exception.AllcllErrorCode;
import kr.allcll.backend.support.exception.AllcllException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand All @@ -10,16 +14,21 @@
@RequiredArgsConstructor
public class CompletedCoursePersistenceService {

private final UserRepository userRepository;
private final CompletedCourseRepository completedCourseRepository;

@Transactional
public List<CompletedCourse> saveAllCompletedCourse(Long userId, List<CompletedCourseDto> parsedCourses) {
completedCourseRepository.deleteByUserId(userId);
public List<CompletedCourse> saveAllCompletedCourse(
Long userId,
List<CompletedCourseDto> parsedCourses
) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new AllcllException(AllcllErrorCode.USER_NOT_FOUND));

completedCourseRepository.deleteByUserId(userId);
List<CompletedCourse> completedCourses = parsedCourses.stream()
.map(parsedCourse -> parsedCourse.toEntity(userId))
.map(parsedCourse -> parsedCourse.toEntity(user.getId(), user.getAdmissionYear()))
.toList();

return completedCourseRepository.saveAll(completedCourses);
}

Expand Down
Loading