Skip to content

Commit c63d62f

Browse files
committed
feat: 멘토 로드맵 수정
1 parent 566b195 commit c63d62f

File tree

9 files changed

+160
-51
lines changed

9 files changed

+160
-51
lines changed

back/src/main/java/com/back/domain/roadmap/roadmap/controller/MentorRoadmapController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
@RestController
1818
@RequestMapping("/mentor-roadmaps")
1919
@RequiredArgsConstructor
20-
@Tag(name = "MentorRoadmap", description = "멘토 로드맵 관리 API")
20+
@Tag(name = "MentorRoadmapController", description = "멘토 로드맵 API")
2121
public class MentorRoadmapController {
2222
private final MentorRoadmapService mentorRoadmapService;
2323
private final Rq rq;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public record MentorRoadmapResponse(
1717
// 정적 팩터리 메서드 - MentorRoadmap로부터 Response DTO 생성
1818
public static MentorRoadmapResponse from(MentorRoadmap mentorRoadmap) {
1919
List<RoadmapNodeResponse> nodeResponses = mentorRoadmap.getNodes().stream()
20+
.sorted((n1, n2) -> Integer.compare(n1.getStepOrder(), n2.getStepOrder())) // stepOrder로 정렬
2021
.map(RoadmapNodeResponse::from)
2122
.toList();
2223

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static RoadmapNodeResponse from(RoadmapNode node) {
1616
return new RoadmapNodeResponse(
1717
node.getId(),
1818
node.getTask() != null ? node.getTask().getId() : null,
19-
node.getTask() != null ? node.getTask().getName() : node.getRawTaskName(),
19+
node.getTaskName(), // taskName 필드 직접 사용 (Task 엔티티 접근 불필요)
2020
node.getDescription(),
2121
node.getStepOrder(),
2222
node.getTask() != null

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ public MentorRoadmap(Long mentorId, String title, String description) {
3838
}
3939

4040
public RoadmapNode getRootNode() {
41-
return nodes.isEmpty() ? null : nodes.get(0);
41+
return nodes.stream()
42+
.filter(node -> node.getStepOrder() == 1)
43+
.findFirst()
44+
.orElse(null);
4245
}
4346

4447
// 노드 추가 헬퍼 메서드 (이미 완전히 초기화된 노드 추가)

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.roadmap.task.entity.Task;
44
import com.back.global.jpa.BaseEntity;
55
import jakarta.persistence.*;
6+
import lombok.Builder;
67
import lombok.Getter;
78
import lombok.NoArgsConstructor;
89

@@ -36,7 +37,7 @@ public class RoadmapNode extends BaseEntity {
3637
private int stepOrder = 0;
3738

3839
@Column(name = "raw_task_name")
39-
private String rawTaskName; // 원본 Task 입력값
40+
private String taskName; // Task 이름 표시값(DB에 없는 Task 입력시 입력값 그대로 출력)
4041

4142
@Column(name = "description", columnDefinition = "TEXT")
4243
private String description;
@@ -49,15 +50,18 @@ public enum RoadmapType {
4950
MENTOR, JOB
5051
}
5152

52-
public RoadmapNode(String rawTaskName, String description, Task task, int stepOrder, long roadmapId, RoadmapType roadmapType) {
53-
this.rawTaskName = rawTaskName;
53+
// Builder 패턴 적용된 생성자
54+
@Builder
55+
public RoadmapNode(String taskName, String description, Task task, int stepOrder, long roadmapId, RoadmapType roadmapType) {
56+
this.taskName = taskName;
5457
this.description = description;
5558
this.task = task;
5659
this.stepOrder = stepOrder;
5760
this.roadmapId = roadmapId;
5861
this.roadmapType = roadmapType;
5962
}
6063

64+
6165
public void addChild(RoadmapNode child) {
6266
if (child == null) {
6367
throw new IllegalArgumentException("자식 노드는 null일 수 없습니다.");

back/src/main/java/com/back/domain/roadmap/roadmap/repository/MentorRoadmapRepository.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,20 @@ public interface MentorRoadmapRepository extends JpaRepository<MentorRoadmap, Lo
1212
@Query("""
1313
SELECT mr FROM MentorRoadmap mr
1414
LEFT JOIN FETCH mr.nodes n
15-
LEFT JOIN FETCH n.task
1615
WHERE mr.id = :id
16+
ORDER BY n.stepOrder ASC
1717
""")
1818
Optional<MentorRoadmap> findByIdWithNodes(@Param("id") Long id);
1919

2020
@Query("""
2121
SELECT mr FROM MentorRoadmap mr
2222
LEFT JOIN FETCH mr.nodes n
23-
LEFT JOIN FETCH n.task
2423
WHERE mr.mentorId = :mentorId
24+
ORDER BY n.stepOrder ASC
2525
""")
2626
Optional<MentorRoadmap> findByMentorIdWithNodes(@Param("mentorId") Long mentorId);
2727

28-
// 기본 정보만 조회하는 메서드도 유지
28+
// 기본 정보만 조회하는 메서드
2929
Optional<MentorRoadmap> findByMentorId(Long mentorId);
3030

3131
boolean existsByMentorId(Long mentorId);

back/src/main/java/com/back/domain/roadmap/roadmap/repository/RoadmapNodeRepository.java

Lines changed: 0 additions & 41 deletions
This file was deleted.

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,33 @@ private void validateRequest(MentorRoadmapSaveRequest request) {
135135
if (request.nodes().isEmpty()) {
136136
throw new ServiceException("400", "로드맵은 적어도 하나 이상의 노드를 포함해야 합니다.");
137137
}
138+
validateStepOrderSequence(request.nodes());
139+
}
140+
141+
// stepOrder 연속성 검증 (멘토 로드맵은 선형 구조)
142+
private void validateStepOrderSequence(List<RoadmapNodeRequest> nodes) {
143+
List<Integer> stepOrders = nodes.stream()
144+
.map(RoadmapNodeRequest::stepOrder)
145+
.toList();
146+
147+
// 중복 검증 먼저 수행
148+
long distinctCount = stepOrders.stream().distinct().count();
149+
if (distinctCount != stepOrders.size()) {
150+
throw new ServiceException("400", "stepOrder에 중복된 값이 있습니다.");
151+
}
152+
153+
// 정렬 후 연속성 검증
154+
List<Integer> sortedStepOrders = stepOrders.stream().sorted().toList();
155+
156+
// 1부터 시작하는 연속된 숫자인지 검증
157+
for (int i = 0; i < sortedStepOrders.size(); i++) {
158+
int expectedOrder = i + 1;
159+
if (!sortedStepOrders.get(i).equals(expectedOrder)) {
160+
throw new ServiceException("400",
161+
String.format("stepOrder는 1부터 시작하는 연속된 숫자여야 합니다. 현재: %s, 기대값: %d",
162+
sortedStepOrders, expectedOrder));
163+
}
164+
}
138165
}
139166

140167
// 로드맵 저장 (노드들도 cascade로 함께 저장)
@@ -199,7 +226,14 @@ private List<RoadmapNode> createAllNodesWithRoadmapId(
199226
Task task = nodeRequest.taskId() != null ? tasksMap.get(nodeRequest.taskId()) : null;
200227
String taskName = (task != null) ? task.getName() : nodeRequest.taskName();
201228

202-
RoadmapNode node = new RoadmapNode(taskName, nodeRequest.description(), task, nodeRequest.stepOrder(), roadmapId, RoadmapNode.RoadmapType.MENTOR);
229+
RoadmapNode node = RoadmapNode.builder()
230+
.taskName(taskName)
231+
.description(nodeRequest.description())
232+
.task(task)
233+
.stepOrder(nodeRequest.stepOrder())
234+
.roadmapId(roadmapId)
235+
.roadmapType(RoadmapNode.RoadmapType.MENTOR)
236+
.build();
203237

204238
nodes.add(node);
205239
}

back/src/test/java/com/back/domain/roadmap/roadmap/service/MentorRoadmapServiceTest.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,114 @@ void t12() {
303303
assertThat(retrieved.nodes().get(1).taskName()).isEqualTo("직접 입력 노드");
304304
}
305305

306+
@Test
307+
@DisplayName("stepOrder 검증 실패 - 비연속 숫자")
308+
void t13() {
309+
// Given
310+
MentorRoadmapSaveRequest request = new MentorRoadmapSaveRequest(
311+
"잘못된 로드맵", "stepOrder가 비연속",
312+
List.of(
313+
new RoadmapNodeRequest(null, "Java", "1단계", 1),
314+
new RoadmapNodeRequest(null, "Spring", "3단계", 3), // 2가 빠짐
315+
new RoadmapNodeRequest(null, "Database", "5단계", 5) // 4가 빠짐
316+
)
317+
);
318+
319+
// When & Then
320+
assertThatThrownBy(() -> mentorRoadmapService.create(mentorId, request))
321+
.isInstanceOf(ServiceException.class)
322+
.hasMessageContaining("stepOrder는 1부터 시작하는 연속된 숫자여야 합니다");
323+
}
324+
325+
@Test
326+
@DisplayName("stepOrder 검증 실패 - 중복된 값")
327+
void t14() {
328+
// Given
329+
MentorRoadmapSaveRequest request = new MentorRoadmapSaveRequest(
330+
"잘못된 로드맵", "stepOrder가 중복",
331+
List.of(
332+
new RoadmapNodeRequest(null, "Java", "1단계", 1),
333+
new RoadmapNodeRequest(null, "Spring", "중복 1단계", 1), // 중복
334+
new RoadmapNodeRequest(null, "Database", "2단계", 2)
335+
)
336+
);
337+
338+
// When & Then
339+
assertThatThrownBy(() -> mentorRoadmapService.create(mentorId, request))
340+
.isInstanceOf(ServiceException.class)
341+
.hasMessageContaining("stepOrder에 중복된 값이 있습니다");
342+
}
343+
344+
@Test
345+
@DisplayName("stepOrder 검증 실패 - 2부터 시작하는 연속 숫자")
346+
void t15() {
347+
// Given - 1부터 시작하지 않는 경우
348+
MentorRoadmapSaveRequest request = new MentorRoadmapSaveRequest(
349+
"잘못된 로드맵", "stepOrder가 2부터 시작",
350+
List.of(
351+
new RoadmapNodeRequest(null, "Java", "2단계", 2), // 2부터 시작
352+
new RoadmapNodeRequest(null, "Spring", "3단계", 3)
353+
)
354+
);
355+
356+
// When & Then
357+
assertThatThrownBy(() -> mentorRoadmapService.create(mentorId, request))
358+
.isInstanceOf(ServiceException.class)
359+
.hasMessageContaining("stepOrder는 1부터 시작하는 연속된 숫자여야 합니다");
360+
}
361+
362+
@Test
363+
@DisplayName("stepOrder 검증 성공 - 순서 무관한 입력")
364+
void t16() {
365+
// Given - 입력 순서와 stepOrder가 다름 (정렬 후 검증)
366+
MentorRoadmapSaveRequest request = new MentorRoadmapSaveRequest(
367+
"순서 무관 로드맵", "입력 순서와 stepOrder가 달라도 성공",
368+
List.of(
369+
new RoadmapNodeRequest(null, "Database", "3단계", 3),
370+
new RoadmapNodeRequest(null, "Java", "1단계", 1),
371+
new RoadmapNodeRequest(null, "Spring", "2단계", 2)
372+
)
373+
);
374+
375+
// When
376+
MentorRoadmapSaveResponse response = mentorRoadmapService.create(mentorId, request);
377+
378+
// Then
379+
assertThat(response.nodeCount()).isEqualTo(3);
380+
381+
MentorRoadmapResponse retrieved = mentorRoadmapService.getById(response.id());
382+
assertThat(retrieved.nodes()).hasSize(3);
383+
// 결과는 stepOrder 순으로 정렬되어 반환
384+
assertThat(retrieved.nodes().get(0).stepOrder()).isEqualTo(1);
385+
assertThat(retrieved.nodes().get(0).taskName()).isEqualTo("Java");
386+
assertThat(retrieved.nodes().get(1).stepOrder()).isEqualTo(2);
387+
assertThat(retrieved.nodes().get(1).taskName()).isEqualTo("Spring");
388+
assertThat(retrieved.nodes().get(2).stepOrder()).isEqualTo(3);
389+
assertThat(retrieved.nodes().get(2).taskName()).isEqualTo("Database");
390+
}
391+
392+
@Test
393+
@DisplayName("stepOrder 검증 - 수정 시에도 동일하게 적용")
394+
void t17() {
395+
// Given
396+
MentorRoadmapSaveRequest originalRequest = createSampleRequest();
397+
MentorRoadmapSaveResponse created = mentorRoadmapService.create(mentorId, originalRequest);
398+
399+
// 잘못된 stepOrder로 수정 시도
400+
MentorRoadmapSaveRequest updateRequest = new MentorRoadmapSaveRequest(
401+
"수정된 로드맵", "잘못된 stepOrder",
402+
List.of(
403+
new RoadmapNodeRequest(null, "Java", "1단계", 1),
404+
new RoadmapNodeRequest(null, "Spring", "3단계", 3) // 2가 빠짐
405+
)
406+
);
407+
408+
// When & Then
409+
assertThatThrownBy(() -> mentorRoadmapService.update(created.id(), mentorId, updateRequest))
410+
.isInstanceOf(ServiceException.class)
411+
.hasMessageContaining("stepOrder는 1부터 시작하는 연속된 숫자여야 합니다");
412+
}
413+
306414
private MentorRoadmapSaveRequest createSampleRequest() {
307415
return new MentorRoadmapSaveRequest(
308416
"백엔드 개발자 로드맵",

0 commit comments

Comments
 (0)