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
Expand Up @@ -34,30 +34,33 @@ public class CourseRankingService {
private static final List<String> LIBERAL_EXCLUDE_KEYWORDS =
List.of("공동체리더십훈련", "채플");

public CourseRankingService(CourseRankingRepository repository, MajorRoadmapIndex majorRoadmapIndex) {
public CourseRankingService(
CourseRankingRepository repository,
MajorRoadmapIndex majorRoadmapIndex
) {
this.repository = repository;
this.majorRoadmapIndex = majorRoadmapIndex;
}

public RankingResponse getCourseRanking() {
var major = loadMajorByTerms(); // ✅ 변경
var major = loadMajorByTerms();

var liberal = new LiberalRanking(
loadTop10(LIB_FAITH, true),
loadTop10(LIB_GENERAL_EDU, true),
loadTop10(LIB_BSM, true),
loadTop10(LIB_FREE_ELECTIVE, true)
loadTop10Merged(LIB_FAITH, true),
loadTop10Merged(LIB_GENERAL_EDU, true),
loadTop10Merged(LIB_BSM, true),
loadTop10Merged(LIB_FREE_ELECTIVE, true)
);

return new RankingResponse(major, liberal);
}

/**
* ✅ 전공: MAJOR에서 많이 가져온 뒤 로드맵(term)으로 분류해서 각 term Top10 생성
* 1-1은 없음 → y1s2부터 생성
* - 1-1은 없음 → y1s2부터 생성
* - 같은 과목(공백 차이)은 term 버킷 안에서 합산
*/
private MajorRanking loadMajorByTerms() {
// term별 버킷 (순서 고정)
Map<String, List<CourseRankingRepository.CourseCountRow>> bucket = new LinkedHashMap<>();
bucket.put("y1s2", new ArrayList<>());
bucket.put("y2s1", new ArrayList<>());
Expand All @@ -79,59 +82,82 @@ private MajorRanking loadMajorByTerms() {

var key = termOpt.get().toBucketKey();
var list = bucket.get(key);
if (list != null) list.add(r); // 혹시 1-1 같은게 들어오면 null이라 무시
if (list != null) list.add(r);
}

return new MajorRanking(
toTop10(bucket.get("y1s2")),
toTop10(bucket.get("y2s1")),
toTop10(bucket.get("y2s2")),
toTop10(bucket.get("y3s1")),
toTop10(bucket.get("y3s2")),
toTop10(bucket.get("y4s1")),
toTop10(bucket.get("y4s2"))
toTop10Merged(bucket.get("y1s2")),
toTop10Merged(bucket.get("y2s1")),
toTop10Merged(bucket.get("y2s2")),
toTop10Merged(bucket.get("y3s1")),
toTop10Merged(bucket.get("y3s2")),
toTop10Merged(bucket.get("y4s1")),
toTop10Merged(bucket.get("y4s2"))
);
}

private List<RankingItem> toTop10(List<CourseRankingRepository.CourseCountRow> rows) {
if (rows == null) return List.of();
var finalRows = rows.stream().limit(10).toList();
/**
* ✅ 교양/전공 공통: rows를 "공백 제거 키"로 합산 → takenCount desc 정렬 → top10 → rank 부여
*/
private List<RankingItem> toTop10Merged(List<CourseRankingRepository.CourseCountRow> rows) {
if (rows == null || rows.isEmpty()) return List.of();

// normKey -> (sumCount, displayName)
Map<String, Agg> map = new HashMap<>();

for (var r : rows) {
String raw = r.getName();
String key = norm(raw);
if (key.isBlank()) continue;

Agg prev = map.get(key);
long nextCount = (prev == null ? 0L : prev.takenCount) + r.getTakenCount();

// ✅ 대표 표기명: 사람이 읽기 편한 쪽(공백 포함, 더 긴 것) 우선
String nextDisplay = (prev == null)
? safe(raw)
: chooseDisplay(prev.displayName, safe(raw));

map.put(key, new Agg(nextCount, nextDisplay));
}
Comment on lines +108 to +122

Choose a reason for hiding this comment

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

low

for 루프 내의 집계 로직을 Map.merge() 메서드를 사용하여 더 간결하게 개선할 수 있습니다. merge를 사용하면 키 존재 여부를 확인하고 값을 계산하여 업데이트하는 과정을 하나의 메서드 호출로 처리할 수 있어 코드 가독성이 향상됩니다. (이 제안은 Aggrecord라고 가정합니다.)

        for (var r : rows) {
            String raw = r.getName();
            String key = norm(raw);
            if (key.isBlank()) continue;

            var newAgg = new Agg(r.getTakenCount(), safe(raw));
            map.merge(key, newAgg, (oldAgg, agg) -> new Agg(
                    oldAgg.takenCount() + agg.takenCount(),
                    chooseDisplay(oldAgg.displayName(), agg.displayName())
            ));
        }


return IntStream.range(0, finalRows.size())
var merged = map.values().stream()
.sorted((a, b) -> {
int c = Long.compare(b.takenCount, a.takenCount);
if (c != 0) return c;
return a.displayName.compareTo(a.displayName);
})
Comment on lines +125 to +129

Choose a reason for hiding this comment

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

low

Comparator 체이닝을 사용하면 정렬 로직을 더 간결하고 읽기 쉽게 표현할 수 있습니다. Comparator.comparingLong()thenComparing()을 활용하여 다중 조건 정렬을 구현하는 것을 권장합니다. (이 제안을 적용하려면 Agg 클래스를 record로 변경하거나 getter를 추가해야 합니다.)

                .sorted(Comparator.comparingLong(Agg::takenCount).reversed()
                        .thenComparing(Agg::displayName))

.limit(10)
.toList();

return IntStream.range(0, merged.size())
.mapToObj(i -> new RankingItem(
i + 1,
finalRows.get(i).getName(),
finalRows.get(i).getTakenCount(),
merged.get(i).displayName,
merged.get(i).takenCount,
0
))
.toList();
}

private List<RankingItem> loadTop10(Set<Category> categories, boolean applyFilter) {
/**
* ✅ 교양: 카테고리별로 가져온 뒤(필요시 필터) → 합산 top10
*/
private List<RankingItem> loadTop10Merged(Set<Category> categories, boolean applyFilter) {
int fetchSize = applyFilter ? 30 : 10;

var fetchedRows = repository.findTopCoursesByCategories(
categories,
PageRequest.of(0, fetchSize)
);

final var finalRows = applyFilter
var filtered = applyFilter
? fetchedRows.stream()
.filter(r -> !containsAny(r.getName(), LIBERAL_EXCLUDE_KEYWORDS))
.limit(10)
.toList()
: fetchedRows.stream()
.limit(10)
.toList();
: fetchedRows;

return IntStream.range(0, finalRows.size())
.mapToObj(i -> new RankingItem(
i + 1,
finalRows.get(i).getName(),
finalRows.get(i).getTakenCount(),
0
))
.toList();
return toTop10Merged(filtered);
}

private boolean containsAny(String text, List<String> keywords) {
Expand All @@ -141,4 +167,36 @@ private boolean containsAny(String text, List<String> keywords) {
}
return false;
}

// =======================
// normalize / display util
// =======================

// ✅ 공백만 제거 + trim (웹서비스개발 / 웹 서비스 개발 동일 키)
private static String norm(String s) {
if (s == null) return "";
return s.trim().replaceAll("\\s+", "");
}
Comment on lines +176 to +179

Choose a reason for hiding this comment

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

medium

norm() 유틸리티 메서드가 MajorRoadmapIndex 클래스에도 동일하게 존재합니다. 코드 중복을 피하고 재사용성을 높이기 위해, 이 메서드를 공통 유틸리티 클래스로 추출하는 것을 고려해 보세요.
스타일 가이드 25번 라인에 따라 com.gradu.common 패키지 아래에 StringUtil 같은 클래스를 만들어 관리하면 좋을 것 같습니다.

예시:

// in com.gradu.common.util.StringUtil.java
public final class StringUtil {
    private StringUtil() {}

    public static String normalize(String s) {
        if (s == null) return "";
        return s.trim().replaceAll("\\s+", "");
    }
}
References
  1. 공통 유틸리티는 com.gradu/common/ 패키지에 위치시켜야 합니다. norm() 메서드는 여러 곳에서 사용되므로 공통 유틸리티로 분리하는 것이 좋습니다. (link)


private static String safe(String s) {
return s == null ? "" : s;
}

// 더 읽기 좋은 표기(대개 공백 포함이 더 길다) 우선
private static String chooseDisplay(String a, String b) {
if (a == null || a.isBlank()) return safe(b);
if (b == null || b.isBlank()) return safe(a);
if (b.length() > a.length()) return b;
return a;
}

private static final class Agg {
final long takenCount;
final String displayName;

private Agg(long takenCount, String displayName) {
this.takenCount = takenCount;
this.displayName = displayName;
}
}
Comment on lines +193 to +201

Choose a reason for hiding this comment

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

low

Agg 클래스는 불변 데이터 컨테이너(immutable data carrier)의 역할을 합니다. Java record를 사용하면 보일러플레이트 코드(생성자, getter, equals, hashCode, toString) 없이 클래스를 더 간결하게 정의할 수 있습니다.
프로젝트 스타일 가이드 62번 라인("가독성 향상에 도움이 되면 record ... 사용")에서도 record 사용을 권장하고 있습니다.

    private record Agg(long takenCount, String displayName) {}
References
  1. 가독성 향상에 도움이 된다면 Java 21의 record와 같은 최신 기능을 사용하는 것을 권장합니다. Agg 클래스는 record로 변환하기에 좋은 후보입니다. (link)

}
25 changes: 12 additions & 13 deletions src/main/resources/catalog/major_roadmap.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
[
{ "year": 1, "semester": 2, "courseCode": "ECE10002", "nameKo": "C 프로그래밍(전산전공)", "nameEn": "C-Programming (CSEE)" },
{ "year": 1, "semester": 2, "courseCode": "ECE10002", "nameKo": "C 프로그래밍", "nameEn": "C-Programming" },
{ "year": 1, "semester": 2, "courseCode": "ECE10005", "nameKo": "코딩 스튜디오", "nameEn": "Coding Studio" },
{ "year": 1, "semester": 2, "courseCode": "ECE10020", "nameKo": "공학설계입문", "nameEn": "Introduction to Engineering Design" },

{ "year": 1, "semester": 2, "courseCode": "ECE20010", "nameKo": "데이터구조", "nameEn": "Data Structure" },
{ "year": 1, "semester": 2, "courseCode": "ECE20016", "nameKo": "자바프로그래밍언어", "nameEn": "Introduction to JAVA Programming" },
{ "year": 1, "semester": 2, "courseCode": "ECE20025", "nameKo": "프로그래밍 스튜디오", "nameEn": "Programming Studio" },
{ "year": 1, "semester": 2, "courseCode": "ECE20057", "nameKo": "논리설계", "nameEn": "Logic Design" },
{ "year": 1, "semester": 2, "courseCode": "ECE20064", "nameKo": "회로이론", "nameEn": "Circuit Theory" },
{ "year": 1, "semester": 2, "courseCode": "ECE20065", "nameKo": "기초회로 및 논리실습", "nameEn": "Basic Circuit and Logic Laboratory" },

{ "year": 2, "semester": 1, "courseCode": "ECE20006", "nameKo": "신호및시스템", "nameEn": "Signal and System" },
{ "year": 2, "semester": 1, "courseCode": "ECE20009", "nameKo": "웹서비스개발", "nameEn": "Web Service Development" },
{ "year": 2, "semester": 1, "courseCode": "ECE20010", "nameKo": "데이터구조", "nameEn": "Data Structure" },
{ "year": 2, "semester": 1, "courseCode": "ECE20016", "nameKo": "자바프로그래밍언어", "nameEn": "Introduction to JAVA Programming" },
{ "year": 2, "semester": 1, "courseCode": "ECE20025", "nameKo": "프로그래밍 스튜디오", "nameEn": "Programming Studio" },
{ "year": 2, "semester": 1, "courseCode": "ECE20057", "nameKo": "논리설계", "nameEn": "Logic Design" },
{ "year": 2, "semester": 1, "courseCode": "ECE20064", "nameKo": "회로이론", "nameEn": "Circuit Theory" },
{ "year": 2, "semester": 1, "courseCode": "ECE20065", "nameKo": "기초회로 및 논리실습", "nameEn": "Basic Circuit and Logic Laboratory" },

{ "year": 2, "semester": 2, "courseCode": "ECE20006", "nameKo": "신호및시스템", "nameEn": "Signal and System" },
{ "year": 2, "semester": 2, "courseCode": "ECE20009", "nameKo": "웹서비스개발", "nameEn": "Web Service Development" },
{ "year": 2, "semester": 2, "courseCode": "ECE20021", "nameKo": "컴퓨터구조", "nameEn": "Computer Architecture and Organization" },
{ "year": 2, "semester": 2, "courseCode": "ECE20022", "nameKo": "컴퓨터비전", "nameEn": "Computer Vision" },
{ "year": 2, "semester": 2, "courseCode": "ECE20026", "nameKo": "오픈소스 스튜디오", "nameEn": "Open Source Studio" },
Expand All @@ -26,7 +25,7 @@
{ "year": 3, "semester": 1, "courseCode": "ECE30012", "nameKo": "객체지향설계패턴", "nameEn": "Object-Oriented Design Pattern" },
{ "year": 3, "semester": 1, "courseCode": "ECE30021", "nameKo": "운영체제", "nameEn": "Operating System" },
{ "year": 3, "semester": 1, "courseCode": "ECE30030", "nameKo": "데이터베이스", "nameEn": "Data Base" },
{ "year": 3, "semester": 1, "courseCode": "ECE30039", "nameKo": "직업과진로설계(전산전지)", "nameEn": "Vocation and Career Planning (CSEE)" },
{ "year": 3, "semester": 1, "courseCode": "ECE30039", "nameKo": "직업과진로설계", "nameEn": "Vocation and Career Planning" },
{ "year": 3, "semester": 1, "courseCode": "ECE30051", "nameKo": "전자회로1", "nameEn": "Electronic Circuits 1" },
{ "year": 3, "semester": 1, "courseCode": "ECE30070", "nameKo": "마이크로프로세서응용", "nameEn": "Microprocessor Application" },

Expand All @@ -41,15 +40,15 @@

{ "year": 4, "semester": 1, "courseCode": "ECE40010", "nameKo": "소프트웨어공학", "nameEn": "Software Engineering" },
{ "year": 4, "semester": 1, "courseCode": "ECE40012", "nameKo": "컴파일러이론", "nameEn": "Compiler Theory" },
{ "year": 4, "semester": 1, "courseCode": "ECE40027", "nameKo": "포스트캡스톤 연구", "nameEn": "Post-capstone Research" },
{ "year": 4, "semester": 1, "courseCode": "ECE40035", "nameKo": "딥러닝개론", "nameEn": "Introduction to Deep Learning" },
{ "year": 4, "semester": 1, "courseCode": "ECE40042", "nameKo": "컴퓨터그래픽스", "nameEn": "Computer Graphics" },
{ "year": 4, "semester": 1, "courseCode": "ECE40066", "nameKo": "IoT 실습", "nameEn": "IoT Laboratories" },
{ "year": 4, "semester": 1, "courseCode": "ECE40079", "nameKo": "캡스톤디자인2", "nameEn": "Capstone Design 2" },
{ "year": 4, "semester": 1, "courseCode": "ECE40097", "nameKo": "특론1", "nameEn": "Special Topic 1" },

{ "year": 4, "semester": 2, "courseCode": "ECE40027", "nameKo": "포스트캡스톤 연구", "nameEn": "Post-capstone Research" },
{ "year": 4, "semester": 2, "courseCode": "ECE40013", "nameKo": "지능형 신호처리", "nameEn": "Intelligent Signal Processing" },
{ "year": 4, "semester": 2, "courseCode": "ECE40014", "nameKo": "실전 AI 스튜디오", "nameEn": "Applied Project Studio" },
{ "year": 4, "semester": 2, "courseCode": "ECE40014", "nameKo": "실전 스튜디오", "nameEn": "Applied Project Studio" },
{ "year": 4, "semester": 2, "courseCode": "ECE40033", "nameKo": "게임개발", "nameEn": "Game Development" },
{ "year": 4, "semester": 2, "courseCode": "ECE40034", "nameKo": "클라우드 컴퓨팅", "nameEn": "Cloud Computing" },
{ "year": 4, "semester": 2, "courseCode": "ECE40044", "nameKo": "컴퓨터보안", "nameEn": "Computer Security" },
Expand Down
Loading
Loading