Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,9 +1,6 @@
package kr.allcll.backend.domain.graduation.check.cert;

import java.util.List;
import kr.allcll.backend.domain.graduation.certification.ClassicAltCoursePolicy;
import kr.allcll.backend.domain.graduation.certification.GraduationCertificationAltCoursePolicy;
import kr.allcll.backend.domain.graduation.check.cert.dto.ClassicsCounts;
import kr.allcll.backend.domain.graduation.check.cert.dto.ClassicsResult;
import kr.allcll.backend.domain.graduation.check.cert.dto.GraduationCertInfo;
import kr.allcll.backend.domain.graduation.check.excel.CompletedCourse;
Expand All @@ -24,14 +21,11 @@
public class GraduationCertResolver {

private final CompletedCourseRepository completedCourseRepository;
private final GraduationCodingCertFetcher graduationCodingCertFetcher;
private final GraduationEnglishCertFetcher graduationEnglishCertFetcher;
private final GraduationClassicsCertFetcher graduationClassicsCertFetcher;
private final GraduationCertificationAltCoursePolicy codingAltCoursePolicy;
private final GraduationCertificationAltCoursePolicy englishAltCoursePolicy;
private final ClassicAltCoursePolicy classicAltCoursePolicy;
private final GraduationDepartmentInfoRepository graduationDepartmentInfoRepository;
private final GraduationCheckCertResultRepository graduationCheckCertResultRepository;
private final GraduationCodingCertResolver graduationCodingCertResolver;
private final GraduationEnglishCertResolver graduationEnglishCertResolver;
private final GraduationClassicsCertResolver graduationClassicsCertResolver;

public GraduationCertInfo resolve(User user, OkHttpClient client) {
GraduationDepartmentInfo userDept = graduationDepartmentInfoRepository
Expand All @@ -41,11 +35,23 @@ public GraduationCertInfo resolve(User user, OkHttpClient client) {
List<CompletedCourse> completedCourses = completedCourseRepository.findAllByUserId(user.getId());

GraduationCheckCertResult certResult = graduationCheckCertResultRepository.findByUserId(user.getId())
.orElse(null);

boolean englishPassed = resolveEnglishPassed(user, client, userDept, completedCourses, certResult);
boolean codingPassed = resolveCodingPassed(user, client, userDept, completedCourses, certResult);
ClassicsResult classicsResult = resolveClassics(user, client, certResult);
.orElseGet(GraduationCheckCertResult::empty);

boolean englishPassed = graduationEnglishCertResolver.resolve(
user,
client,
userDept,
completedCourses,
certResult
);
boolean codingPassed = graduationCodingCertResolver.resolve(
user,
client,
userDept,
completedCourses,
certResult
);
ClassicsResult classicsResult = graduationClassicsCertResolver.resolve(user, client, certResult);

return GraduationCertInfo.of(
englishPassed,
Expand All @@ -54,103 +60,4 @@ public GraduationCertInfo resolve(User user, OkHttpClient client) {
classicsResult.counts()
);
}

private boolean resolveEnglishPassed(
User user,
OkHttpClient client,
GraduationDepartmentInfo userDept,
List<CompletedCourse> completedCourses,
GraduationCheckCertResult certResult
) {
if (isEnglishAlreadyPassed(certResult)) {
return true;
}

if (englishAltCoursePolicy.isSatisfiedByAltCourse(user, userDept, completedCourses)) {
return true;
}

try {
return graduationEnglishCertFetcher.fetchEnglishPass(client);
} catch (Exception e) {
log.error("[졸업요건검사] 영어인증 여부를 불러오지 못했습니다.", e);
return false;
}
}

private boolean resolveCodingPassed(
User user,
OkHttpClient client,
GraduationDepartmentInfo userDept,
List<CompletedCourse> completedCourses,
GraduationCheckCertResult certResult
) {
if (isCodingAlreadyPassed(certResult)) {
return true;
}

if (codingAltCoursePolicy.isSatisfiedByAltCourse(user, userDept, completedCourses)) {
return true;
}

try {
return graduationCodingCertFetcher.fetchCodingPass(client);
} catch (Exception e) {
log.error("[졸업요건검사] 코딩인증 정보를 불러오지 못했습니다.", e);
return false;
}
}

private ClassicsResult resolveClassics(
User user,
OkHttpClient client,
GraduationCheckCertResult certResult
) {
ClassicsCounts fallbackCounts = ClassicsCounts.fallback(certResult);

if (isClassicsAlreadyPassed(certResult)) {
return ClassicsResult.passedWith(fallbackCounts);
}

ClassicsResult classicsResult = fetchClassicsResultFromExternal(client, fallbackCounts);
if (classicsResult.passed()) {
return classicsResult;
}
boolean satisfiedByAltCourse = classicAltCoursePolicy.isSatisfiedByAltCourse(user);
return classicsResult.passedWith(satisfiedByAltCourse, classicsResult.counts());
}

private ClassicsResult fetchClassicsResultFromExternal(OkHttpClient client, ClassicsCounts fallbackCounts) {
try {
ClassicsResult classicsResult = graduationClassicsCertFetcher.fetchClassics(client);
if (classicsResult == null) {
return ClassicsResult.empty();
}
return classicsResult.withFallbackCounts(fallbackCounts);
} catch (Exception e) {
log.error("[졸업요건검사] 고전인증 여부를 불러오지 못했습니다.", e);
return ClassicsResult.failedWith(fallbackCounts);
}
}

private boolean isEnglishAlreadyPassed(GraduationCheckCertResult certResult) {
if (certResult == null) {
return false;
}
return Boolean.TRUE.equals(certResult.getIsEnglishCertPassed());
}

private boolean isCodingAlreadyPassed(GraduationCheckCertResult certResult) {
if (certResult == null) {
return false;
}
return Boolean.TRUE.equals(certResult.getIsCodingCertPassed());
}

private boolean isClassicsAlreadyPassed(GraduationCheckCertResult certResult) {
if (certResult == null) {
return false;
}
return Boolean.TRUE.equals(certResult.getIsClassicsCertPassed());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,35 @@ public void reCalculate() {
this.requiredPassCount = this.graduationCertRuleType.getRequiredPassCount();
this.isSatisfied = this.graduationCertRuleType.isSatisfied(newPassedCount);
}

public boolean isClassicsPassed() {
return Boolean.TRUE.equals(this.isClassicsCertPassed);
}

public static GraduationCheckCertResult empty() {
return new GraduationCheckCertResult(
null,
null,
0,
0,
false,
false,
false,
false,
0,
0,
0,
0,
false,
0,
0,
false,
0,
0,
false,
0,
0,
false
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package kr.allcll.backend.domain.graduation.check.cert;

import kr.allcll.backend.domain.graduation.certification.ClassicAltCoursePolicy;
import kr.allcll.backend.domain.graduation.check.cert.dto.ClassicsCounts;
import kr.allcll.backend.domain.graduation.check.cert.dto.ClassicsResult;
import kr.allcll.backend.domain.user.User;
import kr.allcll.backend.support.exception.AllcllException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class GraduationClassicsCertResolver {

private final GraduationClassicsCertFetcher graduationClassicsCertFetcher;
private final ClassicAltCoursePolicy classicAltCoursePolicy;

public ClassicsResult resolve(
User user,
OkHttpClient client,
GraduationCheckCertResult certResult
) {
if (certResult.isClassicsPassed()) {
return ClassicsResult.passedWith(ClassicsCounts.fallback(certResult));
}
ClassicsCounts fallbackCounts = ClassicsCounts.fallback(certResult);

ClassicsResult classicsResult = fetchClassicsResultFromExternal(client, fallbackCounts);
boolean isSatisfiedByCrawledResult = classicsResult.isSatisfiedByCrawledResult();
boolean satisfiedByAltCourse = classicAltCoursePolicy.isSatisfiedByAltCourse(user);
if (isSatisfiedByCrawledResult || satisfiedByAltCourse) {
return ClassicsResult.passedWith(classicsResult.counts());
Comment thread
2Jin1031 marked this conversation as resolved.
}

return ClassicsResult.failedWith(classicsResult.counts());
}

private ClassicsResult fetchClassicsResultFromExternal(OkHttpClient client, ClassicsCounts fallbackCounts) {
try {
ClassicsResult classicsResult = graduationClassicsCertFetcher.fetchClassics(client);
return classicsResult.withFallbackCounts(fallbackCounts);
} catch (AllcllException exception) {
log.error("[졸업요건검사] 고전인증 여부를 불러오지 못했습니다.", exception);
return ClassicsResult.failedWith(fallbackCounts);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package kr.allcll.backend.domain.graduation.check.cert;

import java.util.List;
import kr.allcll.backend.domain.graduation.certification.GraduationCertificationAltCoursePolicy;
import kr.allcll.backend.domain.graduation.check.excel.CompletedCourse;
import kr.allcll.backend.domain.graduation.department.GraduationDepartmentInfo;
import kr.allcll.backend.domain.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class GraduationCodingCertResolver {

private final GraduationCodingCertFetcher graduationCodingCertFetcher;
private final GraduationCertificationAltCoursePolicy codingAltCoursePolicy;

public boolean resolve(
User user,
OkHttpClient client,
GraduationDepartmentInfo userDept,
List<CompletedCourse> completedCourses,
GraduationCheckCertResult certResult
) {
if (isCodingAlreadyPassed(certResult)) {
return true;
}

if (codingAltCoursePolicy.isSatisfiedByAltCourse(user, userDept, completedCourses)) {
return true;
}

try {
return graduationCodingCertFetcher.fetchCodingPass(client);
} catch (Exception e) {
log.error("[졸업요건검사] 코딩인증 정보를 불러오지 못했습니다.", e);
return false;
}
}

private boolean isCodingAlreadyPassed(GraduationCheckCertResult certResult) {
return Boolean.TRUE.equals(certResult.getIsCodingCertPassed());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package kr.allcll.backend.domain.graduation.check.cert;

import java.util.List;
import kr.allcll.backend.domain.graduation.certification.GraduationCertificationAltCoursePolicy;
import kr.allcll.backend.domain.graduation.check.excel.CompletedCourse;
import kr.allcll.backend.domain.graduation.department.GraduationDepartmentInfo;
import kr.allcll.backend.domain.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.OkHttpClient;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class GraduationEnglishCertResolver {

private final GraduationEnglishCertFetcher graduationEnglishCertFetcher;
private final GraduationCertificationAltCoursePolicy englishAltCoursePolicy;

public boolean resolve(
User user,
OkHttpClient client,
GraduationDepartmentInfo userDept,
List<CompletedCourse> completedCourses,
GraduationCheckCertResult certResult
) {
if (isEnglishAlreadyPassed(certResult)) {
return true;
}

if (englishAltCoursePolicy.isSatisfiedByAltCourse(user, userDept, completedCourses)) {
return true;
}

try {
return graduationEnglishCertFetcher.fetchEnglishPass(client);
} catch (Exception e) {
log.error("[졸업요건검사] 영어인증 여부를 불러오지 못했습니다.", e);
return false;
}
}

private boolean isEnglishAlreadyPassed(GraduationCheckCertResult certResult) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 동일한 코멘트 입니당!!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위 코멘트와 같은 이유로, 검증이 필요한 것 같은데 확인부탁드려요 ...!🤔

Copy link
Copy Markdown
Member Author

@haeyoon1 haeyoon1 Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

가 아니라 이건 또 다른 케이스군요!

다만 GraduationCertResolverresolve에서

        GraduationCheckCertResult certResult = graduationCheckCertResultRepository.findByUserId(user.getId())
            .orElse(null);

        boolean englishPassed = graduationEnglishCertResolver.resolve(
            user,
            client,
            userDept,
            completedCourses,
            certResult
        );
        boolean codingPassed = graduationCodingCertResolver.resolve(
            user,
            client,
            userDept,
            completedCourses,
            certResult
        );

이렇게 db의 기존 certResult를 가져오고있어 certResult에 대한 null 검증은 필요해보입니다!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그러네요..! 제가 놓쳤습니다.

처음 검사하는 사용자 판별을 위한 분기문으로 필요한 코드로 보여요! 다만 null 가드를 매번 검사해야 하는 구조는 휴먼 에러가 생길 가능성이 클 것 같아서요. (실제로 GraduationClassicsCertResolver.java:27에서 null 가드가 빠져 있기도 하고요!)

그래서 Null Object 패턴을 제안드려 봅니다. GraduationCheckCertResult.empty() 같은 정적 팩토리로 "이력 없음"을 표현하는 빈 인스턴스를 만들고, 리졸버에서는 이걸 기본값으로 넘기는 방식으로,

GraduationCheckCertResult certResult = graduationCheckCertResultRepository
    .findByUserId(user.getId())
    .orElseGet(GraduationCheckCertResult::empty);

이렇게 하면 하위 resolver들의 null 분기문이 전부 사라지고, "상태"가 타입으로 표현될 것 같아서요. 해윤님은 어떻게 생각하시나요?


직접 적용한번 해보니까 여러 파일에서 변경이 필요해서 변경 사항이 포함된 patch 파일로 추출해봤습니다.
아래 파일을 다운받아 적용해보실 수 있습니다!

graduation-cert-null-object.patch

적용 방법

git apply graduation-cert-null-object.patch

Copy link
Copy Markdown
Member Author

@haeyoon1 haeyoon1 Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하위 resolver들의 null 분기문이 전부 사라지고, "상태"가 타입으로 표현될 것 같아서요. 해윤님은 어떻게 생각하시나요?

매우 좋은 것 같습니다 👍 단순 null이 아닌 empty라는 상태로 분기처리를 하면 의미 전달이 명확해질 것 같아요!

직접 적용한번 해보니까 여러 파일에서 변경이 필요해서 변경 사항이 포함된 patch 파일로 추출해봤습니다.

대박..... 이런 방법이 있다니 신기하네요! 변경 범위가 꽤 크던데 감사합니다 ㅎㅎ😇

return Boolean.TRUE.equals(certResult.getIsEnglishCertPassed());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ public int totalMyCount() {
}

public static ClassicsCounts fallback(GraduationCheckCertResult certResult) {
if (certResult == null) {
return ClassicsCounts.empty();
}
return new ClassicsCounts(
certResult.getMyCountWestern(),
certResult.getMyCountEastern(),
Expand All @@ -32,4 +29,11 @@ public static ClassicsCounts fallback(GraduationCheckCertResult certResult) {
public static ClassicsCounts empty() {
return new ClassicsCounts(0, 0, 0, 0);
}

public boolean isPassed() {
return myCountWestern >= ClassicsArea.WESTERN.getMaxRecognizedCount() &&
myCountEastern >= ClassicsArea.EASTERN.getMaxRecognizedCount() &&
myCountEasternAndWestern >= ClassicsArea.EASTERN_AND_WESTERN.getMaxRecognizedCount() &&
myCountScience >= ClassicsArea.SCIENCE.getMaxRecognizedCount();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,6 @@ public static ClassicsResult passedWith(ClassicsCounts fallbackCounts) {
return new ClassicsResult(true, fallbackCounts);
}

public ClassicsResult passedWith(boolean passed, ClassicsCounts fallbackCounts) {
return new ClassicsResult(passed, fallbackCounts);
}

public static ClassicsResult failedWith(ClassicsCounts fallbackCounts) {
return new ClassicsResult(false, fallbackCounts);
}
Expand All @@ -27,4 +23,8 @@ public ClassicsResult withFallbackCounts(ClassicsCounts fallbackCounts) {
}
return this;
}

public boolean isSatisfiedByCrawledResult() {
return passed || counts.isPassed();
}
}
Loading
Loading