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
@@ -0,0 +1,116 @@
package com.back.domain.roadmap.roadmap.controller;

import com.back.domain.member.member.entity.Member;
import com.back.domain.roadmap.roadmap.dto.request.MentorRoadmapSaveRequest;
import com.back.domain.roadmap.roadmap.dto.response.MentorRoadmapSaveResponse;
import com.back.domain.roadmap.roadmap.dto.response.MentorRoadmapResponse;
import com.back.domain.roadmap.roadmap.service.MentorRoadmapService;
import com.back.global.exception.ServiceException;
import com.back.global.rq.Rq;
import com.back.global.rsData.RsData;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/mentor-roadmaps")
@RequiredArgsConstructor
@Tag(name = "MentorRoadmap", description = "멘토 로드맵 관리 API")
public class MentorRoadmapController {
private final MentorRoadmapService mentorRoadmapService;
private final Rq rq;

@Operation(
summary = "멘토 로드맵 생성",
description = """
멘토가 자신의 커리어 로드맵을 생성합니다.
- 멘토는 하나의 로드맵만 생성 가능
- TaskId는 nullable (DB에 있는 Task 중 선택하게 하고, 원하는 Task 없는 경우 null 가능)
- TaskName은 필수 (표시용 이름. DB에 있는 Task 선택시 해당 taskName으로 저장, 없는 경우 입력한 이름으로 저장)
- stepOrder는 1부터 시작하는 연속된 숫자로, 로드맵 상 노드의 순서를 나타냄
- 노드들은 stepOrder 순으로 자동 정렬(멘토 로드맵은 선형으로만 구성)

사용 시나리오:
1. TaskController로 Task 검색
2. Task 선택 시 TaskId와 TaskName 획득
3. Task 없는 경우 TaskId null, TaskName 직접 입력
4. 노드 설명과 입력
"""
)
@PostMapping
public RsData<MentorRoadmapSaveResponse> create(@Valid @RequestBody MentorRoadmapSaveRequest request) {
Member member = validateMentorAuth();

MentorRoadmapSaveResponse response = mentorRoadmapService.create(member.getId(), request);

return new RsData<>(
"201",
"멘토 로드맵이 성공적으로 생성되었습니다.",
response
);
}

@Operation(
summary = "멘토 로드맵 상세 조회",
description = """
로드맵 ID로 멘토 로드맵 상세 정보를 조회합니다.

반환 정보:
- 로드맵 기본 정보 (로드맵 ID, 멘토 ID, 제목, 설명, 생성일, 수정일 등)
- 모든 노드 정보 (stepOrder 순으로 정렬)
"""
)
@GetMapping("/{id}")
public RsData<MentorRoadmapResponse> getByMentorId(@PathVariable Long id) {
MentorRoadmapResponse response = mentorRoadmapService.getById(id);

return new RsData<>(
"200",
"멘토 로드맵 조회 성공",
response
);
}

@Operation(summary = "멘토 로드맵 수정", description = "로드맵 ID로 로드맵을 찾아 수정합니다. 본인이 생성한 로드맵만 수정할 수 있습니다.")
@PutMapping("/{id}")
public RsData<MentorRoadmapSaveResponse> update(@PathVariable Long id, @Valid @RequestBody MentorRoadmapSaveRequest request) {
Member member = validateMentorAuth();

MentorRoadmapSaveResponse response = mentorRoadmapService.update(id, member.getId(), request);

return new RsData<>(
"200",
"멘토 로드맵이 성공적으로 수정되었습니다.",
response
);
}

@Operation(summary = "멘토 로드맵 삭제", description = "로드맵 ID로 로드맵을 삭제합니다. 본인이 생성한 로드맵만 삭제할 수 있습니다.")
@DeleteMapping("/{id}")
public RsData<Void> delete( @PathVariable Long id) {

Member member = validateMentorAuth();

mentorRoadmapService.delete(id, member.getId());

return new RsData<>(
"200",
"멘토 로드맵이 성공적으로 삭제되었습니다.",
null
);
}

// 멘토 권한 검증
private Member validateMentorAuth() {
Member member = rq.getActor();
if (member == null) {
throw new ServiceException("401", "로그인이 필요합니다.");
}
if (member.getRole() != Member.Role.MENTOR) {
throw new ServiceException("403", "멘토만 접근 가능합니다.");
}
return member;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.back.domain.roadmap.roadmap.dto.request;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;

import java.util.List;

public record MentorRoadmapSaveRequest(
@NotBlank(message = "로드맵 제목은 필수입니다.")
@Size(max = 100, message = "로드맵 제목은 100자를 초과할 수 없습니다.")
String title,

@Size(max = 1000, message = "로드맵 설명은 1000자를 초과할 수 없습니다.")
String description,

@NotEmpty(message = "로드맵 노드는 최소 1개 이상 필요합니다.")
@Valid
List<RoadmapNodeRequest> nodes
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.back.domain.roadmap.roadmap.dto.request;

import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record RoadmapNodeRequest(
Long taskId, // nullable - Task와 연결되지 않은 경우 null

@NotBlank(message = "Task 이름은 필수입니다.")
@Size(max = 100, message = "Task 이름은 100자를 초과할 수 없습니다.")
String taskName, // 표시용 Task 이름 (rawTaskName으로 저장)

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

@Min(value = 1, message = "단계 순서는 1 이상이어야 합니다.")
int stepOrder
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.back.domain.roadmap.roadmap.dto.response;

import com.back.domain.roadmap.roadmap.entity.MentorRoadmap;
import java.time.LocalDateTime;
import java.util.List;

public record MentorRoadmapResponse(
Long id,
Long mentorId,
String title,
String description,
List<RoadmapNodeResponse> nodes,
LocalDateTime createdDate,
LocalDateTime modifiedDate
) {

// 정적 팩터리 메서드 - MentorRoadmap로부터 Response DTO 생성
public static MentorRoadmapResponse from(MentorRoadmap mentorRoadmap) {
List<RoadmapNodeResponse> nodeResponses = mentorRoadmap.getNodes().stream()
.map(RoadmapNodeResponse::from)
.toList();

return new MentorRoadmapResponse(
mentorRoadmap.getId(),
mentorRoadmap.getMentorId(),
mentorRoadmap.getTitle(),
mentorRoadmap.getDescription(),
nodeResponses,
mentorRoadmap.getCreateDate(),
mentorRoadmap.getModifyDate()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.back.domain.roadmap.roadmap.dto.response;

import java.time.LocalDateTime;

// 순수 데이터 전송 객체 - 엔티티에 의존하지 않음
public record MentorRoadmapSaveResponse(
Long id,
Long mentorId,
String title,
String description,
int nodeCount,
LocalDateTime createDate
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.back.domain.roadmap.roadmap.dto.response;

import com.back.domain.roadmap.roadmap.entity.RoadmapNode;

public record RoadmapNodeResponse(
Long id,
Long taskId, // Task와 연결된 경우의 표준 Task ID
String taskName, // 표시용 Task 이름(Task와 연결된 경우 해당 Task 이름, 자유 입력시 입력값)
String description,
int stepOrder,
boolean isLinkedToTask // Task와 연결 여부
) {

// 정적 팩터리 메서드 - RoadmapNode로부터 Response DTO 생성
public static RoadmapNodeResponse from(RoadmapNode node) {
return new RoadmapNodeResponse(
node.getId(),
node.getTask() != null ? node.getTask().getId() : null,
node.getTask() != null ? node.getTask().getName() : node.getRawTaskName(),
node.getDescription(),
node.getStepOrder(),
node.getTask() != null
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "job_roadmap")
Expand All @@ -14,12 +18,19 @@ public class JobRoadmap extends BaseEntity {
@Column(name = "job_id", nullable = false)
private Long jobId; // Job FK

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "root_node_id", nullable = false)
private RoadmapNode rootNode;
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "roadmap_id") // RoadmapNode.roadmapId 참조
@SQLRestriction("roadmap_type = 'JOB'")
@OrderBy("stepOrder ASC")
private List<RoadmapNode> nodes;

public JobRoadmap(Long jobId, RoadmapNode rootNode) {
public JobRoadmap(Long jobId) {
this.jobId = jobId;
this.rootNode = rootNode;
this.nodes = new ArrayList<>();
}

public RoadmapNode getRootNode() {
return nodes.isEmpty() ? null : nodes.get(0);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.SQLRestriction;

import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "mentor_roadmap")
@Getter @Setter
@Getter
@NoArgsConstructor
public class MentorRoadmap extends BaseEntity {
@Column(name = "title", nullable = false)
Expand All @@ -20,14 +23,53 @@ public class MentorRoadmap extends BaseEntity {
@Column(name = "mentor_id", nullable = false)
private Long mentorId; // Mentor 엔티티 FK

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "root_node_id", nullable = false)
private RoadmapNode rootNode;
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "roadmap_id")
@SQLRestriction("roadmap_type = 'MENTOR'")
@OrderBy("stepOrder ASC")
private List<RoadmapNode> nodes;


public MentorRoadmap(Long mentorId, String title, String description, RoadmapNode rootNode) {
public MentorRoadmap(Long mentorId, String title, String description) {
this.mentorId = mentorId;
this.title = title;
this.description = description;
this.rootNode = rootNode;
this.nodes = new ArrayList<>();
}

public RoadmapNode getRootNode() {
return nodes.isEmpty() ? null : nodes.get(0);
}

// 노드 추가 헬퍼 메서드 (이미 완전히 초기화된 노드 추가)
public void addNode(RoadmapNode node) {
if (node == null) {
throw new IllegalArgumentException("추가할 노드는 null일 수 없습니다.");
}
// 노드는 이미 생성자에서 완전히 초기화되어 전달됨
this.nodes.add(node);
}

// 여러 노드 일괄 추가
public void addNodes(List<RoadmapNode> nodes) {
nodes.forEach(this::addNode);
}


// 제목 수정 (비즈니스 로직)
public void updateTitle(String newTitle) {
if (newTitle == null || newTitle.trim().isEmpty()) {
throw new IllegalArgumentException("로드맵 제목은 필수입니다.");
}
this.title = newTitle.trim();
}

// 설명 수정 (비즈니스 로직)
public void updateDescription(String newDescription) {
this.description = newDescription;
}

public void clearNodes() {
this.nodes.clear();
}
}
Loading