Skip to content

Commit d3d4ae1

Browse files
committed
feat: RoadmapNode의 description 필드 세분화
1 parent 4a34112 commit d3d4ae1

File tree

10 files changed

+226
-66
lines changed

10 files changed

+226
-66
lines changed

back/src/main/java/com/back/domain/roadmap/roadmap/dto/request/RoadmapNodeRequest.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.roadmap.roadmap.dto.request;
22

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

14-
@NotBlank(message = "노드 설명은 필수입니다.")
15-
@Size(max = 2000, message = "노드 설명은 2000자를 초과할 수 없습니다.")
16-
String description,
15+
@Size(max = 2000, message = "학습 조언은 2000자를 초과할 수 없습니다.")
16+
String learningAdvice, // 학습 조언/방법
17+
18+
@Size(max = 2000, message = "추천 자료는 2000자를 초과할 수 없습니다.")
19+
String recommendedResources, // 추천 자료
20+
21+
@Size(max = 1000, message = "학습 목표는 1000자를 초과할 수 없습니다.")
22+
String learningGoals, // 학습 목표
23+
24+
@Min(value = 1, message = "난이도는 1 이상이어야 합니다.")
25+
@Max(value = 5, message = "난이도는 5 이하이어야 합니다.")
26+
Integer difficulty, // 난이도 (1-5)
27+
28+
@Min(value = 1, message = "중요도는 1 이상이어야 합니다.")
29+
@Max(value = 5, message = "중요도는 5 이하이어야 합니다.")
30+
Integer importance, // 중요도 (1-5)
31+
32+
@Min(value = 1, message = "하루 학습 시간은 1 이상이어야 합니다.")
33+
Integer hoursPerDay, // 하루 학습 시간 (시간 단위)
34+
35+
@Min(value = 1, message = "학습 주차는 1 이상이어야 합니다.")
36+
Integer weeks, // 학습 주차
1737

1838
@Min(value = 1, message = "단계 순서는 1 이상이어야 합니다.")
1939
int stepOrder

back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapNodeResponse.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ public record JobRoadmapNodeResponse(
1111
List<Long> childIds, // 자식 노드 ID 목록 (프론트엔드 렌더링용)
1212
Long taskId, // Task와 연결된 경우의 표준 Task ID
1313
String taskName, // 표시용 Task 이름
14-
String description,
14+
String learningAdvice, // 학습 조언/방법 (통합)
15+
String recommendedResources, // 추천 자료 (통합)
16+
String learningGoals, // 학습 목표 (통합)
17+
Double difficulty, // 난이도 (1-5, 평균)
18+
Double importance, // 중요도 (1-5, 평균)
19+
Integer estimatedHours, // 총 예상 학습 시간 (평균)
1520
int stepOrder,
1621
int level, // 트리 깊이 (0: 루트, 1: 1단계 자식...)
1722
boolean isLinkedToTask,
@@ -46,7 +51,12 @@ public static JobRoadmapNodeResponse from(RoadmapNode node, List<JobRoadmapNodeR
4651
childIds,
4752
node.getTask() != null ? node.getTask().getId() : null,
4853
node.getTask() != null ? node.getTask().getName() : node.getTaskName(),
49-
node.getDescription(),
54+
node.getLearningAdvice(),
55+
node.getRecommendedResources(),
56+
node.getLearningGoals(),
57+
node.getDifficulty() != null ? node.getDifficulty().doubleValue() : null,
58+
node.getImportance() != null ? node.getImportance().doubleValue() : null,
59+
node.getEstimatedHours(),
5060
node.getStepOrder(),
5161
node.getLevel(),
5262
node.getTask() != null,
@@ -63,7 +73,9 @@ public static JobRoadmapNodeResponse from(RoadmapNode node, List<JobRoadmapNodeR
6373
// 가중치 설정 헬퍼 메서드 (불변 객체이므로 새 인스턴스 반환)
6474
public JobRoadmapNodeResponse withWeight(Double weight) {
6575
return new JobRoadmapNodeResponse(
66-
this.id, this.parentId, this.childIds, this.taskId, this.taskName, this.description,
76+
this.id, this.parentId, this.childIds, this.taskId, this.taskName,
77+
this.learningAdvice, this.recommendedResources, this.learningGoals,
78+
this.difficulty, this.importance, this.estimatedHours,
6779
this.stepOrder, this.level, this.isLinkedToTask, weight,
6880
this.mentorCount, this.totalMentorCount, this.mentorCoverageRatio,
6981
this.isEssential, this.essentialLevel,
@@ -78,7 +90,9 @@ public JobRoadmapNodeResponse withStats(Integer mentorCount, Integer totalMentor
7890
String essentialLevel = calculateEssentialLevel(mentorCoverageRatio);
7991

8092
return new JobRoadmapNodeResponse(
81-
this.id, this.parentId, this.childIds, this.taskId, this.taskName, this.description,
93+
this.id, this.parentId, this.childIds, this.taskId, this.taskName,
94+
this.learningAdvice, this.recommendedResources, this.learningGoals,
95+
this.difficulty, this.importance, this.estimatedHours,
8296
this.stepOrder, this.level, this.isLinkedToTask, this.weight,
8397
mentorCount, totalMentorCount, mentorCoverageRatio,
8498
isEssential, essentialLevel,

back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/RoadmapNodeResponse.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ public record RoadmapNodeResponse(
66
Long id,
77
Long taskId, // Task와 연결된 경우의 표준 Task ID
88
String taskName, // 표시용 Task 이름(Task와 연결된 경우 해당 Task 이름, 자유 입력시 입력값)
9-
String description,
9+
String learningAdvice, // 학습 조언/방법
10+
String recommendedResources, // 추천 자료
11+
String learningGoals, // 학습 목표
12+
Integer difficulty, // 난이도 (1-5)
13+
Integer importance, // 중요도 (1-5)
14+
Integer hoursPerDay, // 하루 학습 시간
15+
Integer weeks, // 학습 주차
16+
Integer estimatedHours, // 총 예상 학습 시간
1017
int stepOrder,
1118
boolean isLinkedToTask // Task와 연결 여부
1219
) {
@@ -17,7 +24,14 @@ public static RoadmapNodeResponse from(RoadmapNode node) {
1724
node.getId(),
1825
node.getTask() != null ? node.getTask().getId() : null,
1926
node.getTaskName(), // taskName 필드 직접 사용 (Task 엔티티 접근 불필요)
20-
node.getDescription(),
27+
node.getLearningAdvice(),
28+
node.getRecommendedResources(),
29+
node.getLearningGoals(),
30+
node.getDifficulty(),
31+
node.getImportance(),
32+
node.getHoursPerDay(),
33+
node.getWeeks(),
34+
node.getEstimatedHours(),
2135
node.getStepOrder(),
2236
node.getTask() != null
2337
);

back/src/main/java/com/back/domain/roadmap/roadmap/entity/RoadmapNode.java

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,29 @@ public class RoadmapNode extends BaseEntity {
4242
@Column(name = "raw_task_name")
4343
private String taskName; // Task 이름 표시값(DB에 없는 Task 입력시 입력값 그대로 출력)
4444

45-
@Column(name = "description", columnDefinition = "TEXT")
46-
private String description;
45+
@Column(name = "learning_advice", columnDefinition = "TEXT")
46+
private String learningAdvice; // 학습 조언/방법
47+
48+
@Column(name = "recommended_resources", columnDefinition = "TEXT")
49+
private String recommendedResources; // 추천 자료
50+
51+
@Column(name = "learning_goals", columnDefinition = "TEXT")
52+
private String learningGoals; // 학습 목표
53+
54+
@Column(name = "difficulty")
55+
private Integer difficulty; // 난이도 (1-5)
56+
57+
@Column(name = "importance")
58+
private Integer importance; // 중요도 (1-5)
59+
60+
@Column(name = "hours_per_day")
61+
private Integer hoursPerDay; // 하루 학습 시간 (시간 단위)
62+
63+
@Column(name = "weeks")
64+
private Integer weeks; // 학습 주차
65+
66+
@Column(name = "estimated_hours")
67+
private Integer estimatedHours; // 총 예상 학습 시간 (시간 단위)
4768

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

5677
// Builder 패턴 적용된 생성자
5778
@Builder
58-
public RoadmapNode(String taskName, String description, Task task, int stepOrder, int level, long roadmapId, RoadmapType roadmapType) {
79+
public RoadmapNode(String taskName, String learningAdvice, String recommendedResources,
80+
String learningGoals, Integer difficulty, Integer importance,
81+
Integer hoursPerDay, Integer weeks, Integer estimatedHours, Task task,
82+
int stepOrder, int level, long roadmapId, RoadmapType roadmapType) {
5983
this.taskName = taskName;
60-
this.description = description;
84+
this.learningAdvice = learningAdvice;
85+
this.recommendedResources = recommendedResources;
86+
this.learningGoals = learningGoals;
87+
this.difficulty = difficulty;
88+
this.importance = importance;
89+
this.hoursPerDay = hoursPerDay;
90+
this.weeks = weeks;
91+
this.estimatedHours = estimatedHours;
6192
this.task = task;
6293
this.stepOrder = stepOrder;
6394
this.level = level;
@@ -98,10 +129,6 @@ public void setRoadmapType(RoadmapType roadmapType) {
98129
this.roadmapType = roadmapType;
99130
}
100131

101-
public void setDescription(String description) {
102-
this.description = description;
103-
}
104-
105132
public void setTask(Task task) {
106133
this.task = task;
107134
}

back/src/main/java/com/back/domain/roadmap/roadmap/service/JobRoadmapIntegrationService.java

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
7575
Map<String, Integer> rootCount = new HashMap<>();
7676
Map<String, Set<Long>> mentorAppearSet = new HashMap<>(); // unique mentors per key
7777
Map<String, List<Integer>> positions = new HashMap<>(); // to compute average position
78-
Map<String, List<String>> descs = new HashMap<>();
78+
Map<String, List<String>> learningAdvices = new HashMap<>();
79+
Map<String, List<String>> recommendedResourcesList = new HashMap<>();
80+
Map<String, List<String>> learningGoalsList = new HashMap<>();
81+
Map<String, List<Integer>> difficulties = new HashMap<>();
82+
Map<String, List<Integer>> importances = new HashMap<>();
83+
Map<String, List<Integer>> estimatedHoursList = new HashMap<>();
7984

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

112-
// descs: description 모으기
113-
if (rn.getDescription() != null && !rn.getDescription().isBlank()) {
114-
descs.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getDescription());
117+
// learningAdvice 모으기
118+
if (rn.getLearningAdvice() != null && !rn.getLearningAdvice().isBlank()) {
119+
learningAdvices.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getLearningAdvice());
120+
}
121+
122+
// recommendedResources 모으기
123+
if (rn.getRecommendedResources() != null && !rn.getRecommendedResources().isBlank()) {
124+
recommendedResourcesList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getRecommendedResources());
125+
}
126+
127+
// learningGoals 모으기
128+
if (rn.getLearningGoals() != null && !rn.getLearningGoals().isBlank()) {
129+
learningGoalsList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getLearningGoals());
130+
}
131+
132+
// difficulty 모으기
133+
if (rn.getDifficulty() != null) {
134+
difficulties.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getDifficulty());
135+
}
136+
137+
// importance 모으기
138+
if (rn.getImportance() != null) {
139+
importances.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getImportance());
140+
}
141+
142+
// estimatedHours 모으기
143+
if (rn.getEstimatedHours() != null) {
144+
estimatedHoursList.computeIfAbsent(k, kk -> new ArrayList<>()).add(rn.getEstimatedHours());
115145
}
116146

117147
// transitions: 다음 노드로의 이동 기록 (A->B가 몇 번 나왔는지)
@@ -154,16 +184,28 @@ public JobRoadmap integrateJobRoadmap(Long jobId) {
154184
Map<String, RoadmapNode> keyToNode = new HashMap<>();
155185
for (Map.Entry<String, AggregatedNode> e : agg.entrySet()) {
156186
AggregatedNode a = e.getValue();
187+
String key = e.getKey();
188+
189+
// 숫자 필드 평균 계산
190+
Double avgDifficulty = calculateAverage(difficulties.get(key));
191+
Double avgImportance = calculateAverage(importances.get(key));
192+
Integer avgEstimatedHours = calculateIntegerAverage(estimatedHoursList.get(key));
193+
157194
RoadmapNode node = RoadmapNode.builder()
158195
.taskName(a.displayName)
159-
.description(mergeTopDescriptions(descs.get(e.getKey())))
196+
.learningAdvice(mergeTopDescriptions(learningAdvices.get(key)))
197+
.recommendedResources(mergeTopDescriptions(recommendedResourcesList.get(key)))
198+
.learningGoals(mergeTopDescriptions(learningGoalsList.get(key)))
199+
.difficulty(avgDifficulty != null ? avgDifficulty.intValue() : null)
200+
.importance(avgImportance != null ? avgImportance.intValue() : null)
201+
.estimatedHours(avgEstimatedHours)
160202
.task(a.task != null ? taskMap.get(a.task.getId()) : null)
161203
.stepOrder(0) // assign later
162204
.level(0) // assign later via addChild/setLevel
163205
.roadmapId(0L)
164206
.roadmapType(RoadmapType.JOB)
165207
.build();
166-
keyToNode.put(e.getKey(), node);
208+
keyToNode.put(key, node);
167209
}
168210

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

618+
// Integer 리스트의 평균을 Double로 반환
619+
private Double calculateAverage(List<Integer> list) {
620+
if (list == null || list.isEmpty()) return null;
621+
return list.stream()
622+
.mapToInt(Integer::intValue)
623+
.average()
624+
.orElse(0.0);
625+
}
626+
627+
// Integer 리스트의 평균을 반올림하여 Integer로 반환
628+
private Integer calculateIntegerAverage(List<Integer> list) {
629+
if (list == null || list.isEmpty()) return null;
630+
double avg = list.stream()
631+
.mapToInt(Integer::intValue)
632+
.average()
633+
.orElse(0.0);
634+
return (int) Math.round(avg);
635+
}
636+
577637
// RoadmapNode를 고유한 키로 변환
578638
private String generateKey(RoadmapNode rn) {
579639
// Task가 연결된 경우: "T:{taskId}"

back/src/main/java/com/back/domain/roadmap/roadmap/service/MentorRoadmapService.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,22 @@ private List<RoadmapNode> createAllNodesWithRoadmapId(
256256
Task task = nodeRequest.taskId() != null ? tasksMap.get(nodeRequest.taskId()) : null;
257257
String taskName = (task != null) ? task.getName() : nodeRequest.taskName();
258258

259+
// estimatedHours 자동 계산 (hoursPerDay * weeks * 7)
260+
Integer estimatedHours = null;
261+
if (nodeRequest.hoursPerDay() != null && nodeRequest.weeks() != null) {
262+
estimatedHours = nodeRequest.hoursPerDay() * nodeRequest.weeks() * 7;
263+
}
264+
259265
RoadmapNode node = RoadmapNode.builder()
260266
.taskName(taskName)
261-
.description(nodeRequest.description())
267+
.learningAdvice(nodeRequest.learningAdvice())
268+
.recommendedResources(nodeRequest.recommendedResources())
269+
.learningGoals(nodeRequest.learningGoals())
270+
.difficulty(nodeRequest.difficulty())
271+
.importance(nodeRequest.importance())
272+
.hoursPerDay(nodeRequest.hoursPerDay())
273+
.weeks(nodeRequest.weeks())
274+
.estimatedHours(estimatedHours)
262275
.task(task)
263276
.stepOrder(nodeRequest.stepOrder())
264277
.roadmapId(roadmapId)

0 commit comments

Comments
 (0)