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,5 +1,6 @@
package com.back.domain.roadmap.roadmap.dto.request;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
Expand All @@ -11,9 +12,28 @@ public record RoadmapNodeRequest(
@Size(max = 100, message = "Task 이름은 100자를 초과할 수 없습니다.")
String taskName, // 표시용 Task 이름 (rawTaskName으로 저장)

@NotBlank(message = "노드 설명은 필수입니다.")
@Size(max = 2000, message = "노드 설명은 2000자를 초과할 수 없습니다.")
String description,
@Size(max = 2000, message = "학습 조언은 2000자를 초과할 수 없습니다.")
String learningAdvice, // 학습 조언/방법

@Size(max = 2000, message = "추천 자료는 2000자를 초과할 수 없습니다.")
String recommendedResources, // 추천 자료

@Size(max = 1000, message = "학습 목표는 1000자를 초과할 수 없습니다.")
String learningGoals, // 학습 목표

@Min(value = 1, message = "난이도는 1 이상이어야 합니다.")
@Max(value = 5, message = "난이도는 5 이하이어야 합니다.")
Integer difficulty, // 난이도 (1-5)

@Min(value = 1, message = "중요도는 1 이상이어야 합니다.")
@Max(value = 5, message = "중요도는 5 이하이어야 합니다.")
Integer importance, // 중요도 (1-5)

@Min(value = 1, message = "하루 학습 시간은 1 이상이어야 합니다.")
Integer hoursPerDay, // 하루 학습 시간 (시간 단위)

@Min(value = 1, message = "학습 주차는 1 이상이어야 합니다.")
Integer weeks, // 학습 주차

@Min(value = 1, message = "단계 순서는 1 이상이어야 합니다.")
int stepOrder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ public record JobRoadmapNodeResponse(
List<Long> childIds, // 자식 노드 ID 목록 (프론트엔드 렌더링용)
Long taskId, // Task와 연결된 경우의 표준 Task ID
String taskName, // 표시용 Task 이름
String description,
String learningAdvice, // 학습 조언/방법 (통합)
String recommendedResources, // 추천 자료 (통합)
String learningGoals, // 학습 목표 (통합)
Double difficulty, // 난이도 (1-5, 평균)
Double importance, // 중요도 (1-5, 평균)
Integer estimatedHours, // 총 예상 학습 시간 (평균)
int stepOrder,
int level, // 트리 깊이 (0: 루트, 1: 1단계 자식...)
boolean isLinkedToTask,
Expand Down Expand Up @@ -46,7 +51,12 @@ public static JobRoadmapNodeResponse from(RoadmapNode node, List<JobRoadmapNodeR
childIds,
node.getTask() != null ? node.getTask().getId() : null,
node.getTask() != null ? node.getTask().getName() : node.getTaskName(),
node.getDescription(),
node.getLearningAdvice(),
node.getRecommendedResources(),
node.getLearningGoals(),
node.getDifficulty() != null ? node.getDifficulty().doubleValue() : null,
node.getImportance() != null ? node.getImportance().doubleValue() : null,
node.getEstimatedHours(),
node.getStepOrder(),
node.getLevel(),
node.getTask() != null,
Expand All @@ -63,7 +73,9 @@ public static JobRoadmapNodeResponse from(RoadmapNode node, List<JobRoadmapNodeR
// 가중치 설정 헬퍼 메서드 (불변 객체이므로 새 인스턴스 반환)
public JobRoadmapNodeResponse withWeight(Double weight) {
return new JobRoadmapNodeResponse(
this.id, this.parentId, this.childIds, this.taskId, this.taskName, this.description,
this.id, this.parentId, this.childIds, this.taskId, this.taskName,
this.learningAdvice, this.recommendedResources, this.learningGoals,
this.difficulty, this.importance, this.estimatedHours,
this.stepOrder, this.level, this.isLinkedToTask, weight,
this.mentorCount, this.totalMentorCount, this.mentorCoverageRatio,
this.isEssential, this.essentialLevel,
Expand All @@ -78,7 +90,9 @@ public JobRoadmapNodeResponse withStats(Integer mentorCount, Integer totalMentor
String essentialLevel = calculateEssentialLevel(mentorCoverageRatio);

return new JobRoadmapNodeResponse(
this.id, this.parentId, this.childIds, this.taskId, this.taskName, this.description,
this.id, this.parentId, this.childIds, this.taskId, this.taskName,
this.learningAdvice, this.recommendedResources, this.learningGoals,
this.difficulty, this.importance, this.estimatedHours,
this.stepOrder, this.level, this.isLinkedToTask, this.weight,
mentorCount, totalMentorCount, mentorCoverageRatio,
isEssential, essentialLevel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ public record RoadmapNodeResponse(
Long id,
Long taskId, // Task와 연결된 경우의 표준 Task ID
String taskName, // 표시용 Task 이름(Task와 연결된 경우 해당 Task 이름, 자유 입력시 입력값)
String description,
String learningAdvice, // 학습 조언/방법
String recommendedResources, // 추천 자료
String learningGoals, // 학습 목표
Integer difficulty, // 난이도 (1-5)
Integer importance, // 중요도 (1-5)
Integer hoursPerDay, // 하루 학습 시간
Integer weeks, // 학습 주차
Integer estimatedHours, // 총 예상 학습 시간
int stepOrder,
boolean isLinkedToTask // Task와 연결 여부
) {
Expand All @@ -17,7 +24,14 @@ public static RoadmapNodeResponse from(RoadmapNode node) {
node.getId(),
node.getTask() != null ? node.getTask().getId() : null,
node.getTaskName(), // taskName 필드 직접 사용 (Task 엔티티 접근 불필요)
node.getDescription(),
node.getLearningAdvice(),
node.getRecommendedResources(),
node.getLearningGoals(),
node.getDifficulty(),
node.getImportance(),
node.getHoursPerDay(),
node.getWeeks(),
node.getEstimatedHours(),
node.getStepOrder(),
node.getTask() != null
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,29 @@ public class RoadmapNode extends BaseEntity {
@Column(name = "raw_task_name")
private String taskName; // Task 이름 표시값(DB에 없는 Task 입력시 입력값 그대로 출력)

@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "learning_advice", columnDefinition = "TEXT")
private String learningAdvice; // 학습 조언/방법

@Column(name = "recommended_resources", columnDefinition = "TEXT")
private String recommendedResources; // 추천 자료

@Column(name = "learning_goals", columnDefinition = "TEXT")
private String learningGoals; // 학습 목표

@Column(name = "difficulty")
private Integer difficulty; // 난이도 (1-5)

@Column(name = "importance")
private Integer importance; // 중요도 (1-5)

@Column(name = "hours_per_day")
private Integer hoursPerDay; // 하루 학습 시간 (시간 단위)

@Column(name = "weeks")
private Integer weeks; // 학습 주차

@Column(name = "estimated_hours")
private Integer estimatedHours; // 총 예상 학습 시간 (시간 단위)

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "task_id")
Expand All @@ -55,9 +76,19 @@ public enum RoadmapType {

// Builder 패턴 적용된 생성자
@Builder
public RoadmapNode(String taskName, String description, Task task, int stepOrder, int level, long roadmapId, RoadmapType roadmapType) {
public RoadmapNode(String taskName, String learningAdvice, String recommendedResources,
String learningGoals, Integer difficulty, Integer importance,
Integer hoursPerDay, Integer weeks, Integer estimatedHours, Task task,
int stepOrder, int level, long roadmapId, RoadmapType roadmapType) {
this.taskName = taskName;
this.description = description;
this.learningAdvice = learningAdvice;
this.recommendedResources = recommendedResources;
this.learningGoals = learningGoals;
this.difficulty = difficulty;
this.importance = importance;
this.hoursPerDay = hoursPerDay;
this.weeks = weeks;
this.estimatedHours = estimatedHours;
this.task = task;
this.stepOrder = stepOrder;
this.level = level;
Expand Down Expand Up @@ -98,10 +129,6 @@ public void setRoadmapType(RoadmapType roadmapType) {
this.roadmapType = roadmapType;
}

public void setDescription(String description) {
this.description = description;
}

public void setTask(Task task) {
this.task = task;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
Map<String, Integer> rootCount = new HashMap<>();
Map<String, Set<Long>> mentorAppearSet = new HashMap<>(); // unique mentors per key
Map<String, List<Integer>> positions = new HashMap<>(); // to compute average position
Map<String, List<String>> descs = new HashMap<>();
Map<String, List<String>> learningAdvices = new HashMap<>();
Map<String, List<String>> recommendedResourcesList = new HashMap<>();
Map<String, List<String>> learningGoalsList = new HashMap<>();
Map<String, List<Integer>> difficulties = new HashMap<>();
Map<String, List<Integer>> importances = new HashMap<>();
Map<String, List<Integer>> estimatedHoursList = new HashMap<>();

// 집계 루프(모든 멘토 로드맵의 모든 노드 순회하며 필요한 통계 자료 수집)
for (MentorRoadmap mr : mentorRoadmaps) {
Expand Down Expand Up @@ -109,9 +114,34 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
// metorAppearSet: 고유 멘토 수
mentorAppearSet.computeIfAbsent(k, kk -> new HashSet<>()).add(mentorId);

// descs: description 모으기
if (rn.getDescription() != null && !rn.getDescription().isBlank()) {
descs.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getDescription());
// learningAdvice 모으기
if (rn.getLearningAdvice() != null && !rn.getLearningAdvice().isBlank()) {
learningAdvices.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getLearningAdvice());
}

// recommendedResources 모으기
if (rn.getRecommendedResources() != null && !rn.getRecommendedResources().isBlank()) {
recommendedResourcesList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getRecommendedResources());
}

// learningGoals 모으기
if (rn.getLearningGoals() != null && !rn.getLearningGoals().isBlank()) {
learningGoalsList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getLearningGoals());
}

// difficulty 모으기
if (rn.getDifficulty() != null) {
difficulties.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getDifficulty());
}

// importance 모으기
if (rn.getImportance() != null) {
importances.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getImportance());
}

// estimatedHours 모으기
if (rn.getEstimatedHours() != null) {
estimatedHoursList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getEstimatedHours());
}

// transitions: 다음 노드로의 이동 기록 (A->B가 몇 번 나왔는지)
Expand Down Expand Up @@ -154,16 +184,28 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
Map<String, RoadmapNode> keyToNode = new HashMap<>();
for (Map.Entry<String, AggregatedNode> e : agg.entrySet()) {
AggregatedNode a = e.getValue();
String key = e.getKey();

// 숫자 필드 평균 계산
Double avgDifficulty = calculateAverage(difficulties.get(key));
Double avgImportance = calculateAverage(importances.get(key));
Integer avgEstimatedHours = calculateIntegerAverage(estimatedHoursList.get(key));

RoadmapNode node = RoadmapNode.builder()
.taskName(a.displayName)
.description(mergeTopDescriptions(descs.get(e.getKey())))
.learningAdvice(mergeTopDescriptions(learningAdvices.get(key)))
.recommendedResources(mergeTopDescriptions(recommendedResourcesList.get(key)))
.learningGoals(mergeTopDescriptions(learningGoalsList.get(key)))
.difficulty(avgDifficulty != null ? avgDifficulty.intValue() : null)
.importance(avgImportance != null ? avgImportance.intValue() : null)
.estimatedHours(avgEstimatedHours)
.task(a.task != null ? taskMap.get(a.task.getId()) : null)
.stepOrder(0) // assign later
.level(0) // assign later via addChild/setLevel
.roadmapId(0L)
.roadmapType(RoadmapType.JOB)
.build();
keyToNode.put(e.getKey(), node);
keyToNode.put(key, node);
}

// Transition 빈도 기반 자식 선택 + 부모 우선순위 계산
Expand Down Expand Up @@ -251,7 +293,6 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
AggregatedNode a = agg.get(rootKey);
RoadmapNode rootNode = RoadmapNode.builder()
.taskName(a != null ? a.displayName : "root")
.description(mergeTopDescriptions(descs.get(rootKey)))
.task(a != null ? a.task : null)
.stepOrder(0)
.level(0)
Expand Down Expand Up @@ -574,6 +615,25 @@ private String mergeTopDescriptions(List<String> list) {
return String.join("\n\n", set);
}

// Integer 리스트의 평균을 Double로 반환
private Double calculateAverage(List<Integer> list) {
if (list == null || list.isEmpty()) return null;
return list.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
}

// Integer 리스트의 평균을 반올림하여 Integer로 반환
private Integer calculateIntegerAverage(List<Integer> list) {
if (list == null || list.isEmpty()) return null;
double avg = list.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0.0);
return (int) Math.round(avg);
}

// RoadmapNode를 고유한 키로 변환
private String generateKey(RoadmapNode rn) {
// Task가 연결된 경우: "T:{taskId}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,22 @@ private List<RoadmapNode> createAllNodesWithRoadmapId(
Task task = nodeRequest.taskId() != null ? tasksMap.get(nodeRequest.taskId()) : null;
String taskName = (task != null) ? task.getName() : nodeRequest.taskName();

// estimatedHours 자동 계산 (hoursPerDay * weeks * 7)
Integer estimatedHours = null;
if (nodeRequest.hoursPerDay() != null && nodeRequest.weeks() != null) {
estimatedHours = nodeRequest.hoursPerDay() * nodeRequest.weeks() * 7;
}

RoadmapNode node = RoadmapNode.builder()
.taskName(taskName)
.description(nodeRequest.description())
.learningAdvice(nodeRequest.learningAdvice())
.recommendedResources(nodeRequest.recommendedResources())
.learningGoals(nodeRequest.learningGoals())
.difficulty(nodeRequest.difficulty())
.importance(nodeRequest.importance())
.hoursPerDay(nodeRequest.hoursPerDay())
.weeks(nodeRequest.weeks())
.estimatedHours(estimatedHours)
.task(task)
.stepOrder(nodeRequest.stepOrder())
.roadmapId(roadmapId)
Expand Down
Loading