diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapController.java b/back/src/main/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapController.java index 64433b52..be0f9086 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapController.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapController.java @@ -20,8 +20,36 @@ public class JobRoadmapController { @GetMapping @Operation( - summary = "직업 로드맵 다건 조회", - description = "직업 로드맵 목록을 페이징과 키워드 검색으로 조회합니다." + summary = "직업 로드맵 목록 조회", + description = """ + ### 개요 + 모든 직업의 로드맵 목록을 페이징과 키워드 검색으로 조회합니다. + + ### 쿼리 파라미터 + - `page`: 페이지 번호 (0부터 시작, 기본값: 0) + - `size`: 페이지 크기 (기본값: 10) + - `keyword`: 검색 키워드 (선택, 직업명으로 검색) + + ### 반환 정보 + 각 직업 로드맵의 요약 정보: + - id: 직업 로드맵 ID + - jobName: 직업명 + - jobDescription: 직업 설명 + + ### 응답 형식 + - content: 직업 로드맵 목록 + - totalElements: 전체 개수 + - totalPages: 전체 페이지 수 + - number: 현재 페이지 번호 + - size: 페이지 크기 + + ### 응답 코드 + - **200**: 조회 성공 + + ### 참고 + - 인증 불필요 (누구나 조회 가능) + - 키워드는 직업명(jobName)에 대해 부분 일치 검색 + """ ) public RsData getJobRoadmaps( @RequestParam(defaultValue = "0") int page, @@ -37,7 +65,71 @@ public RsData getJobRoadmaps( @GetMapping("/{id}") @Operation( summary = "직업 로드맵 상세 조회", - description = "특정 직업 로드맵의 상세 정보(직업 정보 + 모든 노드)를 조회합니다." + description = """ + ### 개요 + 특정 직업의 통합 로드맵을 트리 구조로 조회합니다. + 여러 멘토의 멘토 로드맵을 통합하여 생성된 로드맵입니다. + + ### 직업 로드맵이란? + - 동일 직업의 멘토들의 로드맵을 통합한 결과 + - 비선형 구조 (트리형) - 다양한 학습 경로 표현 + - 멘토들의 빈도, 순서 패턴, 연결 관계를 분석하여 생성 + - 주기적으로 자동 업데이트 (멘토 로드맵 변경 시) + + ### 반환 정보 + + **로드맵 기본 정보:** + - id, jobId, jobName: 직업 정보 + - totalNodeCount: 전체 노드 개수 + - createdDate, modifiedDate: 생성/수정일 + + **노드 정보 (트리 구조):** + + 각 노드는 다음 정보를 포함합니다: + + *기본 정보:* + - id, parentId, childIds: 트리 구조 정보 + - taskId, taskName: Task 정보 + - level: 트리 깊이 (0: 루트, 1: 1단계 자식...) + - stepOrder: 같은 부모 내 순서 + + *학습 정보 (여러 멘토의 정보 통합):* + - learningAdvice: 학습 조언 통합 + - recommendedResources: 추천 자료 통합 + - learningGoals: 학습 목표 통합 + - difficulty: 평균 난이도 (1-5) + - importance: 평균 중요도 (1-5) + - estimatedHours: 평균 예상 학습 시간 + + *통계 정보:* + - weight: 노드 가중치 (빈도, 위치, 연결성 기반) + - mentorCount: 이 노드를 사용한 멘토 수 + - totalMentorCount: 해당 직업의 전체 멘토 수 + - mentorCoverageRatio: 커버리지 비율 (0.0 ~ 1.0) + - isEssential: 필수 노드 여부 (50% 이상) + - essentialLevel: + - "CORE": 80% 이상의 멘토가 선택 (핵심 필수) + - "COMMON": 50% 이상의 멘토가 선택 (일반 필수) + - "OPTIONAL": 50% 미만의 멘토가 선택 (선택) + + *자식 노드:* + - children: 자식 노드 목록 (재귀 구조) + + ### 응답 구조 + - 루트 노드들만 최상위에 반환 + - 각 노드의 children 필드에 자식 노드들이 재귀적으로 포함 + - 전체 트리 구조를 한 번에 조회 가능 + + ### 응답 코드 + - **200**: 조회 성공 + - **404**: 직업 로드맵을 찾을 수 없음 + + ### 참고 + - 로그인한 사용자만 조회 가능 + - 통합 알고리즘: 빈도수(40%), 멘토 커버리지(30%), 위치(20%), 연결성(10%) + - essentialLevel로 필수/선택 경로 구분 가능 + - 통계 정보를 활용해 학습 우선순위 판단 가능 + """ ) public RsData getJobRoadmapById(@PathVariable Long id) { JobRoadmapResponse roadmap = jobRoadmapService.getJobRoadmapById(id); diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/controller/MentorRoadmapController.java b/back/src/main/java/com/back/domain/roadmap/roadmap/controller/MentorRoadmapController.java index 3109e872..8c07c6f2 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/controller/MentorRoadmapController.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/controller/MentorRoadmapController.java @@ -27,18 +27,51 @@ public class MentorRoadmapController { @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. description(Task에 대한 멘토의 경험, 조언, 학습 방법 등) 입력 + ### 개요 + 멘토가 자신의 커리어 여정을 단계별로 기록한 로드맵을 생성합니다. + + ### 제약 사항 + - **멘토당 1개의 로드맵만 생성 가능** (중복 시 409 에러) + - **최소 1개 이상의 노드** 필요 + - **stepOrder는 1부터 시작하는 연속된 숫자** (예: 1,2,3,...) + - **멘토 권한(MENTOR)** 필요 + + ### 노드(RoadmapNode) 구성 + + **필수 필드:** + - `taskName`: 기술/단계 이름 (최대 100자) + - `stepOrder`: 로드맵 상 순서 (1부터 시작, 중복 불가) + + **선택 필드:** + - `taskId`: 표준 Task ID (nullable) + - Task 검색(`/tasks/search`)으로 선택 가능 + - null이면 자동으로 pending alias 등록 (관리자 승인 대기) + - `learningAdvice`: 학습 조언/방법 (최대 2000자) + - `recommendedResources`: 추천 자료 (최대 2000자) + - `learningGoals`: 학습 목표 (최대 1000자) + - `difficulty`: 난이도 (1-5) + - `importance`: 중요도 (1-5) + - `hoursPerDay`: 하루 학습 시간 + - `weeks`: 학습 주차 + - `estimatedHours`: 자동 계산 (hoursPerDay × weeks × 7) + + ### 사용 시나리오 + 1. `/tasks/search`로 Task 검색 + 2. 원하는 Task 선택 → taskId, taskName 획득 + 3. 원하는 Task 없으면 → taskId는 null, taskName 직접 입력 + 4. 각 단계별 학습 정보 입력 (조언, 자료, 목표 등) + 5. 로드맵 생성 요청 + + ### 응답 코드 + - **201**: 생성 성공 + - **400**: 유효성 검증 실패 (stepOrder 불연속/중복 등) + - **403**: 멘토 권한 없음 + - **404**: 존재하지 않는 taskId 참조 + - **409**: 이미 로드맵 존재 + + ### 참고 + - 멘토 로드맵은 선형 구조 (순차적 학습 경로) + - 로드맵 생성 후 직업 로드맵 통합 이벤트 발행 (비동기) """ ) @PostMapping @@ -58,12 +91,29 @@ public RsData create(@Valid @RequestBody MentorRoadma @Operation( summary = "멘토 로드맵 상세 조회 (로드맵 ID)", description = """ - 로드맵 ID로 멘토 로드맵 상세 정보를 조회합니다. - 로그인한 사용자만 조회할 수 있습니다. + ### 개요 + 로드맵 ID로 멘토 로드맵의 전체 정보를 조회합니다. - 반환 정보: - - 로드맵 기본 정보 (로드맵 ID, 멘토 ID, 제목, 설명, 생성일, 수정일 등) + ### 권한 + - 로그인한 모든 사용자 조회 가능 + + ### 반환 정보 + - 로드맵 기본 정보 (id, mentorId, title, description) - 모든 노드 정보 (stepOrder 순으로 정렬) + - 생성일, 수정일 + + ### 노드 정보 구조 + 각 노드는 다음 정보를 포함합니다: + - taskId, taskName: Task 정보 + - learningAdvice, recommendedResources, learningGoals: 학습 가이드 + - difficulty, importance: 난이도/중요도 (1-5) + - hoursPerDay, weeks, estimatedHours: 학습 시간 정보 + - stepOrder: 로드맵 상 순서 + + ### 응답 코드 + - **200**: 조회 성공 + - **401**: 인증 필요 + - **404**: 로드맵을 찾을 수 없음 """ ) @GetMapping("/{id}") @@ -81,14 +131,25 @@ public RsData getById(@PathVariable Long id) { @Operation( summary = "멘토 로드맵 상세 조회 (멘토 ID)", description = """ - 멘토 ID로 해당 멘토의 로드맵 상세 정보를 조회합니다. - 로그인한 사용자만 조회할 수 있습니다. + ### 개요 + 멘토 ID로 해당 멘토의 로드맵 전체 정보를 조회합니다. + + ### 권한 + - 로그인한 모든 사용자 조회 가능 - 반환 정보: - - 로드맵 기본 정보 (로드맵 ID, 멘토 ID, 제목, 설명, 생성일, 수정일 등) + ### 반환 정보 + - 로드맵 기본 정보 (id, mentorId, title, description) - 모든 노드 정보 (stepOrder 순으로 정렬) + - 생성일, 수정일 - 주의: 멘토가 로드맵을 생성하지 않았다면 404 에러가 발생합니다. + ### 응답 코드 + - **200**: 조회 성공 + - **401**: 인증 필요 + - **404**: 해당 멘토의 로드맵을 찾을 수 없음 + + ### 참고 + - 멘토가 로드맵을 생성하지 않았으면 404 에러 발생 + - 멘토 ID는 Member ID가 아닌 Mentor 엔티티의 ID입니다 """ ) @GetMapping("/mentor/{mentorId}") @@ -103,7 +164,36 @@ public RsData getByMentorId(@PathVariable Long mentorId) ); } - @Operation(summary = "멘토 로드맵 수정", description = "로드맵 ID로 로드맵을 찾아 수정합니다. 본인이 생성한 로드맵만 수정할 수 있습니다.") + @Operation( + summary = "멘토 로드맵 수정", + description = """ + ### 개요 + 로드맵 ID로 로드맵을 찾아 전체 내용을 수정합니다. + + ### 권한 + - **본인이 생성한 로드맵만 수정 가능** (타인 수정 시 403 에러) + - 멘토 권한(MENTOR) 필요 + + ### 수정 방식 + - **전체 교체 방식**: 기존 노드를 모두 삭제하고 새 노드로 교체 + - 로드맵 제목, 설명도 함께 수정 + - 생성 API와 동일한 유효성 검증 적용 + + ### 요청 형식 + - 생성 API와 동일한 Request Body 사용 + - 모든 노드를 다시 전송해야 함 (부분 수정 불가) + + ### 응답 코드 + - **200**: 수정 성공 + - **400**: 유효성 검증 실패 + - **403**: 본인의 로드맵이 아님 + - **404**: 로드맵을 찾을 수 없음 + + ### 참고 + - 수정 후 직업 로드맵 통합 이벤트 발행 (비동기) + - taskId가 null인 노드는 자동으로 pending alias 등록 + """ + ) @PutMapping("/{id}") @PreAuthorize("hasRole('MENTOR')") public RsData update(@PathVariable Long id, @Valid @RequestBody MentorRoadmapSaveRequest request) { @@ -118,7 +208,30 @@ public RsData update(@PathVariable Long id, @Valid @R ); } - @Operation(summary = "멘토 로드맵 삭제", description = "로드맵 ID로 로드맵을 삭제합니다. 본인이 생성한 로드맵만 삭제할 수 있습니다.") + @Operation( + summary = "멘토 로드맵 삭제", + description = """ + ### 개요 + 로드맵 ID로 로드맵을 삭제합니다. + + ### 권한 + - **본인이 생성한 로드맵만 삭제 가능** (타인 삭제 시 403 에러) + - 멘토 권한(MENTOR) 필요 + + ### 삭제 범위 + - 로드맵과 모든 노드가 함께 삭제됩니다 (Cascade) + - 참조하던 Task는 삭제되지 않습니다 (Task는 공유 자원) + + ### 응답 코드 + - **200**: 삭제 성공 + - **403**: 본인의 로드맵이 아님 + - **404**: 로드맵을 찾을 수 없음 + + ### 참고 + - 삭제 후 직업 로드맵 통합 이벤트 발행 (비동기) + - 삭제된 로드맵은 복구할 수 없습니다 + """ + ) @DeleteMapping("/{id}") @PreAuthorize("hasRole('MENTOR')") public RsData delete(@PathVariable Long id) { diff --git a/back/src/main/java/com/back/domain/roadmap/task/controller/TaskController.java b/back/src/main/java/com/back/domain/roadmap/task/controller/TaskController.java index 27e0c108..ddff2420 100644 --- a/back/src/main/java/com/back/domain/roadmap/task/controller/TaskController.java +++ b/back/src/main/java/com/back/domain/roadmap/task/controller/TaskController.java @@ -29,7 +29,39 @@ public class TaskController { private final TaskService taskService; private final Rq rq; - @Operation(summary = "키워드로 task 검색", description = "사용자가 입력한 키워드로 Task를 검색합니다.") + @Operation( + summary = "키워드로 Task 검색", + description = """ + ### 개요 + 사용자가 입력한 키워드로 표준 Task를 검색합니다. + + ### 검색 범위 + - Task 이름 (표준 기술명) + - TaskAlias 이름 (별칭) + - 대소문자 구분 없이 부분 일치 검색 + + ### 쿼리 파라미터 + - `keyword`: 검색 키워드 (필수) + + ### 반환 정보 + - id: Task ID + - name: 표준 Task 이름 + - 연결된 TaskAlias는 반환되지 않음 (Task 정보만) + + ### 사용 시나리오 + 1. 멘토 로드맵 생성 전 Task 검색 + 2. 원하는 기술/단계 선택 + 3. 없으면 pending alias 생성 제안 + + ### 응답 코드 + - **200**: 검색 성공 (결과 없어도 200, 빈 배열 반환) + + ### 참고 + - 인증 불필요 + - 키워드가 비어있으면 빈 배열 반환 + - 중복 제거된 Task 목록 반환 + """ + ) @GetMapping("/search") public RsData> searchTasks(@RequestParam String keyword) { // 입력값 검증 @@ -53,8 +85,41 @@ public RsData> searchTasks(@RequestParam String keyword) { ); } - // 사용자가 새로운 기술 제안 (pending alias 생성) - @Operation(summary = "사용자가 새로운 Task 제안", description = "사용자가 입력한 Task 이름으로 pending 상태의(표준 Task와 연결되지 않은) TaskAlias를 생성합니다.") + @Operation( + summary = "사용자가 새로운 Task 제안", + description = """ + ### 개요 + 사용자가 원하는 Task가 검색되지 않을 때, + 새로운 Task 이름을 제안합니다 (pending alias 생성). + + ### Pending Alias란? + - 표준 Task와 아직 연결되지 않은 별칭 + - 관리자 검토 대기 상태 + - 관리자가 기존 Task와 연결하거나 새 Task로 승격 + + ### 요청 형식 + - `taskName`: 제안할 Task 이름 (필수) + + ### 검증 로직 + 1. Task 테이블에서 중복 확인 (이미 표준 Task 존재 시 400) + 2. TaskAlias 테이블에서 중복 확인: + - 연결된 alias 존재 시: "이미 등록된 Task의 별칭입니다" (400) + - Pending alias 존재 시: "이미 제안된 Task명입니다" (400) + + ### 응답 정보 + - id: 생성된 TaskAlias ID + - name: 제안한 이름 + - isPending: true (항상) + + ### 응답 코드 + - **201**: 제안 성공 + - **400**: 이미 존재하는 이름 + + ### 참고 + - 인증 불필요 + - 멘토 로드맵 생성 시 taskId가 null인 경우 자동으로 pending alias 등록 + """ + ) @PostMapping("/aliases/pending") public RsData createPendingAlias(@Valid @RequestBody CreatePendingAliasRequest request) { TaskAlias pendingAlias = taskService.createPendingAlias(request.taskName().trim()); @@ -68,8 +133,41 @@ public RsData createPendingAlias(@Valid @RequestBody //=== 관리자용 API === - // 나중에 CORS 설정 - @Operation(summary = "pending 상태의 TaskAlias 목록 조회", description = "관리자가 pending 상태의 (아직 매칭되지 않은) TaskAlias 목록을 페이징하여 조회합니다.") + @Operation( + summary = "pending 상태의 TaskAlias 목록 조회 (관리자)", + description = """ + ### 개요 + 관리자가 아직 표준 Task와 연결되지 않은 + pending 상태의 TaskAlias 목록을 조회합니다. + + ### 권한 + - **관리자 권한(ADMIN) 필수** + - 401: 미인증 + - 403: 권한 없음 + + ### 쿼리 파라미터 + - `page`: 페이지 번호 (기본값: 0) + - `size`: 페이지 크기 (기본값: 10) + - `sort`: 정렬 기준 (기본값: createdDate,DESC) + + ### 반환 정보 + - id: TaskAlias ID + - name: 제안된 이름 + - 페이징 정보 포함 + + ### 관리자 작업 플로우 + 1. pending alias 목록 조회 + 2. 각 alias 검토: + - 기존 Task와 유사 → linkPendingAlias (연결) + - 새로운 Task 필요 → createTaskFromPending (승격) + - 부적절 → deletePendingAlias (삭제) + + ### 응답 코드 + - **200**: 조회 성공 + - **401**: 인증 필요 + - **403**: 관리자 권한 없음 + """ + ) @GetMapping("/aliases/pending") public RsData> getPendingTaskAliases( @PageableDefault(size = 10, sort = "createdDate", direction = Sort.Direction.DESC) Pageable pageable @@ -85,8 +183,43 @@ public RsData> getPendingTaskAliases( ); } - // Pending alias를 기존 Task와 연결 - @Operation(summary = "pending 상태의 alias를 기존 표준 Task와 연결", description = "관리자가 pending 상태의 (아직 매칭되지 않은) TaskAlias를 기존에 존재하는 표준 Task와 연결합니다.") + @Operation( + summary = "pending alias를 기존 표준 Task와 연결 (관리자)", + description = """ + ### 개요 + 관리자가 pending alias를 검토하여 + 기존의 표준 Task와 연결합니다. + + ### 권한 + - **관리자 권한(ADMIN) 필수** + + ### 요청 형식 + - `taskId`: 연결할 표준 Task의 ID (필수) + + ### 처리 로직 + 1. aliasId로 TaskAlias 조회 및 pending 상태 확인 + 2. taskId로 Task 조회 + 3. TaskAlias.task 필드에 Task 연결 + + ### 응답 정보 + - id: TaskAlias ID + - name: Alias 이름 + - taskId: 연결된 Task ID + - taskName: 연결된 Task 이름 + + ### 응답 코드 + - **200**: 연결 성공 + - **400**: 이미 연결된 alias + - **401**: 인증 필요 + - **403**: 관리자 권한 없음 + - **404**: alias 또는 Task를 찾을 수 없음 + + ### 사용 예시 + - 사용자가 "리액트"를 제안 + - 관리자가 기존 Task "React"와 연결 + - 이후 "리액트" 검색 시 "React" Task 반환 + """ + ) @PutMapping("/aliases/pending/{aliasId}/link") public RsData linkPendingAlias( @PathVariable Long aliasId, @@ -103,8 +236,39 @@ public RsData linkPendingAlias( ); } - // Pending alias를 새로운 Task로 등록(생성) - @Operation(summary = "pending 상태의 alias를 새로운 표준 Task로 등록", description = "관리자가 pending 상태의 (아직 매칭되지 않은) TaskAlias를 표준 Task로 생성하고, 해당 alias를 새 Task와 연결합니다.") + @Operation( + summary = "pending alias를 새로운 표준 Task로 승격 (관리자)", + description = """ + ### 개요 + 관리자가 pending alias를 검토하여 + 새로운 표준 Task로 생성하고 연결합니다. + + ### 권한 + - **관리자 권한(ADMIN) 필수** + + ### 처리 로직 + 1. aliasId로 TaskAlias 조회 및 pending 상태 확인 + 2. 동일 이름의 Task가 이미 존재하는지 확인 + 3. 새 Task 생성 (alias 이름으로) + 4. TaskAlias를 새 Task와 연결 + + ### 응답 정보 + - id: 생성된 Task ID + - name: Task 이름 (alias 이름과 동일) + + ### 응답 코드 + - **201**: Task 생성 및 연결 성공 + - **400**: 이미 연결된 alias 또는 동일 이름 Task 존재 + - **401**: 인증 필요 + - **403**: 관리자 권한 없음 + - **404**: alias를 찾을 수 없음 + + ### 사용 예시 + - 사용자가 "Bun"을 제안 + - 관리자가 새로운 기술로 판단하여 Task 승격 + - 이후 "Bun"은 표준 Task로 검색 가능 + """ + ) @PostMapping("/aliases/pending/{aliasId}") public RsData createTaskFromPending( @PathVariable Long aliasId @@ -120,7 +284,33 @@ public RsData createTaskFromPending( ); } - @Operation(summary = "pending 상태의 alias 삭제", description = "관리자가 pending 상태의 (아직 매칭되지 않은) TaskAlias를 삭제합니다. 더 이상 필요하지 않은 제안이거나, 부적절한 제안인 경우에 사용됩니다.") + @Operation( + summary = "pending alias 삭제 (관리자)", + description = """ + ### 개요 + 관리자가 pending alias를 검토하여 + 부적절하거나 불필요한 제안을 삭제합니다. + + ### 권한 + - **관리자 권한(ADMIN) 필수** + + ### 삭제 사유 예시 + - 오타나 의미 없는 제안 + - 중복 제안 + - 부적절한 내용 + + ### 응답 코드 + - **200**: 삭제 성공 + - **400**: 이미 연결된 alias (pending 아님) + - **401**: 인증 필요 + - **403**: 관리자 권한 없음 + - **404**: alias를 찾을 수 없음 + + ### 참고 + - pending 상태가 아닌 alias는 삭제 불가 + - 이미 Task와 연결된 alias는 Task 자체를 삭제해야 함 + """ + ) @DeleteMapping("/aliases/pending/{aliasId}") public RsData deletePendingAlias(@PathVariable Long aliasId) { validateAdminRole(); diff --git a/back/src/main/java/com/back/global/initData/RoadmapInitData.java b/back/src/main/java/com/back/global/initData/RoadmapInitData.java index db43ec38..0a86e2a5 100644 --- a/back/src/main/java/com/back/global/initData/RoadmapInitData.java +++ b/back/src/main/java/com/back/global/initData/RoadmapInitData.java @@ -10,7 +10,9 @@ import com.back.domain.roadmap.roadmap.dto.request.MentorRoadmapSaveRequest; import com.back.domain.roadmap.roadmap.dto.request.RoadmapNodeRequest; import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import com.back.domain.roadmap.roadmap.entity.JobRoadmapNodeStat; import com.back.domain.roadmap.roadmap.entity.RoadmapNode; +import com.back.domain.roadmap.roadmap.repository.JobRoadmapNodeStatRepository; import com.back.domain.roadmap.roadmap.repository.JobRoadmapRepository; import com.back.domain.roadmap.roadmap.repository.MentorRoadmapRepository; import com.back.domain.roadmap.roadmap.service.JobRoadmapIntegrationService; @@ -28,6 +30,7 @@ import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; @Slf4j @Configuration @@ -43,6 +46,7 @@ public class RoadmapInitData { private final MentorRoadmapService mentorRoadmapService; private final MentorRoadmapRepository mentorRoadmapRepository; private final JobRoadmapRepository jobRoadmapRepository; + private final JobRoadmapNodeStatRepository jobRoadmapNodeStatRepository; private final JobRoadmapIntegrationService jobRoadmapIntegrationService; private final JobRoadmapIntegrationServiceV2 jobRoadmapIntegrationServiceV2; @@ -56,7 +60,7 @@ public void runInitData() { initJobData(); initTaskData(); // 보강된 Task 목록 //initSampleMentorRoadmaps(); // 활성화: 다양한 멘토 로드맵 생성 - //initSampleJobRoadmap(); // 직업 로드맵 조회 API 테스트용 샘플 데이터 + initSampleJobRoadmap(); // 직업 로드맵 조회 API 테스트용 샘플 데이터 // 통합 로직 테스트 //initTestMentorRoadmaps(); // 테스트용 멘토 로드맵 10개 생성 @@ -68,20 +72,76 @@ public void runInitData() { public void initJobData() { if (jobService.count() > 0) return; - Job job1 = jobService.create("백엔드 개발자", "서버 사이드 로직 구현과 데이터베이스를 담당하는 개발자"); + // 백엔드 개발자 + Job job1 = jobService.create("백엔드 개발자", "서버 사이드 로직을 구현하고, 데이터베이스 및 API를 설계·운영하는 개발자입니다."); jobService.createAlias(job1, "백엔드"); jobService.createAlias(job1, "BE 개발자"); jobService.createAlias(job1, "Backend 개발자"); jobService.createAlias(job1, "서버 개발자"); jobService.createAlias(job1, "API 개발자"); - Job job2 = jobService.create("프론트엔드 개발자", "사용자 인터페이스(UI)와 사용자 경험(UX)을 담당하는 개발자"); + // 프론트엔드 개발자 + Job job2 = jobService.create("프론트엔드 개발자", "웹 또는 앱의 사용자 인터페이스(UI)와 사용자 경험(UX)을 담당하며, 사용자가 직접 보는 화면을 구현하는 개발자입니다."); jobService.createAlias(job2, "프론트엔드"); jobService.createAlias(job2, "FE 개발자"); jobService.createAlias(job2, "Frontend 개발자"); jobService.createAlias(job2, "웹 퍼블리셔"); jobService.createAlias(job2, "UI 개발자"); jobService.createAlias(job2, "클라이언트 개발자"); + + // 모바일 앱 개발자 + Job job3 = jobService.create( + "모바일 앱 개발자", + "스마트폰과 태블릿 환경에서 동작하는 iOS 또는 Android 애플리케이션을 개발하는 직군으로, 플랫폼별 네이티브 또는 크로스플랫폼 기술을 활용합니다." + ); + + // 데이터 엔지니어 + Job job4 = jobService.create( + "데이터 엔지니어", + "데이터를 수집·저장·처리할 수 있는 파이프라인을 설계하고 구축하는 전문가로, 데이터 분석과 AI 모델링의 기반을 마련합니다." + ); + + // 데이터 분석가 + Job job5 = jobService.create( + "데이터 분석가", + "데이터를 기반으로 비즈니스 인사이트를 도출하고, 통계 분석과 시각화를 통해 의사결정을 지원하는 직군입니다." + ); + + // AI / 머신러닝 엔지니어 + Job job6 = jobService.create( + "AI/ML 엔지니어", + "머신러닝과 딥러닝 알고리즘을 활용해 예측 모델과 인공지능 서비스를 개발하는 직군으로, 데이터 처리와 모델 학습에 대한 이해가 필요합니다." + ); + + // DevOps 엔지니어 + Job job7 = jobService.create( + "DevOps 엔지니어", + "개발(Development)과 운영(Operations)을 연결하여 CI/CD 파이프라인, 인프라 자동화, 배포 환경을 최적화하는 엔지니어입니다." + ); + + // 클라우드 엔지니어 + Job job8 = jobService.create( + "클라우드 엔지니어", + "AWS, GCP, Azure 등 클라우드 환경에서 인프라를 설계·배포·운영하며, 서비스의 안정성과 확장성을 책임지는 직군입니다." + ); + + // 사이버 보안 전문가 + Job job9 = jobService.create( + "보안 엔지니어", + "시스템과 네트워크의 보안 취약점을 점검하고, 공격 방어 및 보안 정책을 설계하는 역할을 수행합니다." + ); + + // 게임 서버/클라이언트 개발자 + Job job10 = jobService.create( + "게임 개발자", + "게임 클라이언트 또는 서버를 개발하는 직군으로, 그래픽·물리 엔진·네트워크 프로그래밍 등 다양한 기술을 다룹니다." + ); + + // QA / 테스트 엔지니어 + Job job11 = jobService.create( + "QA 엔지니어", + "소프트웨어 품질을 보증하기 위해 테스트를 설계·자동화·수행하는 직군으로, 버그 탐지와 품질 관리 프로세스를 담당합니다." + ); } // --- Task 초기화 (기존 + 기초 보강) --- @@ -568,130 +628,505 @@ public void initSampleJobRoadmap() { Job frontendJob = jobRepository.findByName("프론트엔드 개발자") .orElseThrow(() -> new RuntimeException("프론트엔드 개발자 직업을 찾을 수 없습니다.")); + Job mobileJob = jobRepository.findByName("모바일 앱 개발자") + .orElseThrow(() -> new RuntimeException("모바일 개발자 직업을 찾을 수 없습니다.")); + + Job dataJob = jobRepository.findByName("데이터 엔지니어") + .orElseThrow(() -> new RuntimeException("데이터 엔지니어 직업을 찾을 수 없습니다.")); + + Job aiJob = jobRepository.findByName("AI/ML 엔지니어") + .orElseThrow(() -> new RuntimeException("AI 엔지니어 직업을 찾을 수 없습니다.")); + + // 조회용 멘토 및 직업 설정 + Member member = memberService.joinMentor("testmentor@test.com", "멘토", "mentor", "1234", "백엔드 개발자", 6); + Mentor mentor = updateMentorJob(member, backendJob); + // 백엔드 개발자 직업 로드맵 생성 (트리 구조로 구성) - JobRoadmap jobRoadmap = JobRoadmap.builder() + JobRoadmap backendRoadmap = JobRoadmap.builder() .job(backendJob) .build(); - jobRoadmap = jobRoadmapRepository.save(jobRoadmap); + backendRoadmap = jobRoadmapRepository.save(backendRoadmap); - // 다건 조회 확인용 프론트엔드 개발자 직업 로드맵 생성 (빈 로드맵) + // 다건 조회 확인용 다른 직업 로드맵 생성 (빈 로드맵) JobRoadmap frontendRoadmap = JobRoadmap.builder() .job(frontendJob) .build(); + jobRoadmapRepository.save(frontendRoadmap); + + JobRoadmap mobileRoadmap = JobRoadmap.builder() + .job(mobileJob) + .build(); + jobRoadmapRepository.save(mobileRoadmap); + + JobRoadmap dataRoadmap = JobRoadmap.builder() + .job(dataJob) + .build(); + jobRoadmapRepository.save(dataRoadmap); + + JobRoadmap aiRoadmap = JobRoadmap.builder() + .job(aiJob) + .build(); + jobRoadmapRepository.save(aiRoadmap); // Task 조회 (이미 생성된 Task들 사용) Task programmingFundamentals = taskRepository.findByNameIgnoreCase("Programming Fundamentals").orElse(null); - Task git = taskRepository.findByNameIgnoreCase("Git").orElse(null); Task java = taskRepository.findByNameIgnoreCase("Java").orElse(null); + Task git = taskRepository.findByNameIgnoreCase("Git").orElse(null); + Task nodejs = taskRepository.findByNameIgnoreCase("Node.js").orElse(null); + Task python = taskRepository.findByNameIgnoreCase("Python").orElse(null); Task springBoot = taskRepository.findByNameIgnoreCase("Spring Boot").orElse(null); - Task mysql = taskRepository.findByNameIgnoreCase("MySQL").orElse(null); + Task expressjs = taskRepository.findByNameIgnoreCase("Express.js").orElse(null); + Task django = taskRepository.findByNameIgnoreCase("Django").orElse(null); Task jpa = taskRepository.findByNameIgnoreCase("JPA").orElse(null); + Task mysql = taskRepository.findByNameIgnoreCase("MySQL").orElse(null); + Task postgresql = taskRepository.findByNameIgnoreCase("PostgreSQL").orElse(null); + Task http = taskRepository.findByNameIgnoreCase("HTTP").orElse(null); + Task restApi = taskRepository.findByNameIgnoreCase("REST API").orElse(null); + Task springSecurity = taskRepository.findByNameIgnoreCase("Spring Security").orElse(null); + Task testing = taskRepository.findByNameIgnoreCase("Testing").orElse(null); + Task ciCd = taskRepository.findByNameIgnoreCase("CI/CD").orElse(null); Task docker = taskRepository.findByNameIgnoreCase("Docker").orElse(null); - Task aws = taskRepository.findByNameIgnoreCase("AWS").orElse(null); + Task caching = taskRepository.findByNameIgnoreCase("Caching").orElse(null); - // 트리 구조로 노드 생성 (루트 노드들과 자식 노드들) - - // 루트 노드 1: Programming Fundamentals (level=0, stepOrder=1) + // ===== Level 0: 루트 노드 ===== RoadmapNode fundamentalsNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) .task(programmingFundamentals) .taskName("Programming Fundamentals") - .learningAdvice("프로그래밍의 기초 개념: 변수, 조건문, 반복문, 함수 등을 이해하고 활용할 수 있습니다.") + .learningAdvice("프로그래밍의 기초 개념(변수, 조건문, 반복문, 함수 등)을 이해하고, 간단한 알고리즘 문제를 해결할 수 있는 능력을 키웁니다.") + .recommendedResources("생활코딩, 코드잇 프로그래밍 입문 강의, 프로그래머스 Lv.0~1 문제") + .learningGoals("기본 자료구조 이해, 간단한 알고리즘 문제 풀이, 함수형 프로그래밍 개념 습득") + .difficulty(1) + .importance(5) + .hoursPerDay(2) + .weeks(4) + .estimatedHours(2 * 4 * 7) // hoursPerDay * weeks * 7 .stepOrder(1) .level(0) .build(); - // 루트 노드 2: Git (level=0, stepOrder=2) + // ===== Level 1: 언어 선택 (Java, Git, Node.js, Python) ===== + RoadmapNode javaNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(java) + .taskName("Java") + .learningAdvice("객체지향 프로그래밍의 핵심 개념을 익히고, Java의 컬렉션 프레임워크와 스트림 API를 활용합니다.") + .recommendedResources("Java의 정석, Effective Java, 백기선 자바 스터디") + .learningGoals("OOP 4대 원칙 이해, 컬렉션/스트림 API 활용, 예외 처리 및 멀티스레딩 기초") + .difficulty(3) + .importance(5) + .hoursPerDay(3) + .weeks(6) + .estimatedHours(3 * 6 * 7) + .stepOrder(1) + .level(1) + .build(); + RoadmapNode gitNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) .task(git) .taskName("Git") - .learningAdvice("버전 관리 시스템으로 코드 히스토리 관리 및 협업을 위한 필수 도구입니다.") + .learningAdvice("버전 관리 시스템의 핵심 개념을 이해하고, 브랜치 전략과 협업 워크플로를 익힙니다.") + .recommendedResources("Pro Git(무료 e-book), Learn Git Branching, GitHub 공식 문서") + .learningGoals("기본 명령어 숙달, 브랜치 전략 이해, 충돌 해결 능력, 협업 워크플로 실습") + .difficulty(2) + .importance(5) + .hoursPerDay(1) + .weeks(2) + .estimatedHours(1 * 2 * 7) .stepOrder(2) - .level(0) + .level(1) .build(); - // Fundamentals의 자식 노드들 - RoadmapNode javaNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + RoadmapNode nodejsNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) - .task(java) - .taskName("Java") - .learningAdvice("객체지향 프로그래밍 언어로 백엔드 개발의 기초가 되는 언어입니다.") - .stepOrder(1) + .task(nodejs) + .taskName("Node.js") + .learningAdvice("JavaScript 기반의 비동기 I/O 환경에서 이벤트 루프와 콜백 패턴을 이해합니다.") + .recommendedResources("Node.js 공식 문서, The Node.js Handbook") + .learningGoals("비동기 프로그래밍 이해, 이벤트 루프 동작 원리, NPM 패키지 관리") + .difficulty(3) + .importance(4) + .hoursPerDay(2) + .weeks(4) + .estimatedHours(2 * 4 * 7) + .stepOrder(3) + .level(1) + .build(); + + RoadmapNode pythonNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(python) + .taskName("Python") + .learningAdvice("간결하고 읽기 쉬운 문법으로 빠른 개발이 가능하며, 데이터 처리에 강점이 있습니다.") + .recommendedResources("점프 투 파이썬, Python 공식 튜토리얼") + .learningGoals("파이썬 문법 완성, 리스트 컴프리헨션/제네레이터 이해, 데코레이터 활용") + .difficulty(2) + .importance(4) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) + .stepOrder(4) .level(1) .build(); + // ===== Level 2: 프레임워크 선택 ===== RoadmapNode springBootNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) .task(springBoot) .taskName("Spring Boot") - .learningAdvice("Java 기반의 웹 애플리케이션 프레임워크로 REST API 개발에 필수입니다.") - .stepOrder(2) - .level(1) + .learningAdvice("Java 생태계의 표준 프레임워크로, 의존성 주입과 관점 지향 프로그래밍 개념을 익힙니다.") + .recommendedResources("스프링 부트와 AWS로 혼자 구현하는 웹 서비스, 백기선 스프링 부트 강의") + .learningGoals("DI/IoC 개념 이해, REST API 구현, Actuator를 통한 모니터링") + .difficulty(4) + .importance(5) + .hoursPerDay(3) + .weeks(8) + .estimatedHours(3 * 8 * 7) + .stepOrder(1) + .level(2) .build(); - // Java의 자식 노드들 - RoadmapNode mysqlNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + RoadmapNode expressjsNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) - .task(mysql) - .taskName("MySQL") - .learningAdvice("관계형 데이터베이스로 데이터 저장 및 관리를 위한 기본 기술입니다.") + .task(expressjs) + .taskName("Express.js") + .learningAdvice("Node.js의 대표 웹 프레임워크로, 미들웨어 패턴과 라우팅을 학습합니다.") + .recommendedResources("Express 공식 문서, Node.js 디자인 패턴 바이블") + .learningGoals("미들웨어 체인 이해, RESTful API 설계, 에러 핸들링 전략") + .difficulty(3) + .importance(4) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) .stepOrder(1) .level(2) .build(); + RoadmapNode djangoNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(django) + .taskName("Django") + .learningAdvice("Python 기반의 풀스택 프레임워크로, MVT 패턴과 강력한 ORM을 제공합니다.") + .recommendedResources("Django 공식 튜토리얼, Two Scoops of Django") + .learningGoals("MVT 아키텍처 이해, Django ORM 활용, Admin 페이지 커스터마이징") + .difficulty(3) + .importance(4) + .hoursPerDay(2) + .weeks(4) + .estimatedHours(2 * 4 * 7) + .stepOrder(1) + .level(2) + .build(); + + // ===== Level 3: Spring Boot 경로 - DB/ORM 및 HTTP ===== RoadmapNode jpaNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) .task(jpa) .taskName("JPA") - .learningAdvice("Java 진영의 ORM 기술로 객체와 관계형 데이터베이스를 매핑합니다.") + .learningAdvice("객체와 관계형 데이터베이스를 매핑하는 ORM 기술로, 엔티티 설계와 연관관계 관리를 학습합니다.") + .recommendedResources("자바 ORM 표준 JPA 프로그래밍(김영한), Hibernate 공식 문서") + .learningGoals("엔티티 매핑, 연관관계 관리, 지연 로딩/즉시 로딩, 영속성 컨텍스트 이해") + .difficulty(4) + .importance(5) + .hoursPerDay(3) + .weeks(5) + .estimatedHours(3 * 5 * 7) + .stepOrder(1) + .level(3) + .build(); + + RoadmapNode mysqlNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(mysql) + .taskName("MySQL") + .learningAdvice("가장 널리 사용되는 오픈소스 관계형 데이터베이스로, SQL 기본 문법과 인덱싱을 익힙니다.") + .recommendedResources("Real MySQL, SQL 첫걸음") + .learningGoals("CRUD 쿼리 작성, 인덱스 최적화, 트랜잭션 격리 수준 이해") + .difficulty(3) + .importance(5) + .hoursPerDay(2) + .weeks(4) + .estimatedHours(2 * 4 * 7) .stepOrder(2) - .level(2) + .level(3) + .build(); + + RoadmapNode postgresqlNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(postgresql) + .taskName("PostgreSQL") + .learningAdvice("표준 SQL 준수와 확장성이 뛰어난 오픈소스 RDBMS로, JSONB 등 고급 기능을 지원합니다.") + .recommendedResources("PostgreSQL 공식 문서, The Art of PostgreSQL") + .learningGoals("고급 쿼리 작성, JSONB 활용, 파티셔닝") + .difficulty(4) + .importance(4) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) + .stepOrder(3) + .level(3) + .build(); + + RoadmapNode httpNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(http) + .taskName("HTTP") + .learningAdvice("웹의 기반 프로토콜로, 요청/응답 구조, 메서드, 상태 코드, 헤더 등의 개념을 이해합니다.") + .recommendedResources("HTTP 완벽 가이드, MDN Web Docs") + .learningGoals("HTTP 메서드 이해, 상태 코드 활용, 헤더/쿠키 메커니즘") + .difficulty(2) + .importance(5) + .hoursPerDay(2) + .weeks(2) + .estimatedHours(2 * 2 * 7) + .stepOrder(4) + .level(3) + .build(); + + // ===== Level 4: REST API ===== + RoadmapNode restApiNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(restApi) + .taskName("REST API") + .learningAdvice("RESTful 설계 원칙에 따라 리소스 기반 API를 설계하고, 적절한 HTTP 메서드와 상태 코드를 활용합니다.") + .recommendedResources("RESTful Web API Patterns and Practices, Swagger/OpenAPI 문서") + .learningGoals("REST 제약 조건 이해, 리소스 URI 설계, API 버전 관리") + .difficulty(3) + .importance(5) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) + .stepOrder(1) + .level(4) + .build(); + + // ===== Level 5: Spring Security ===== + RoadmapNode springSecurityNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(springSecurity) + .taskName("Spring Security") + .learningAdvice("인증과 인가를 구현하고, JWT, OAuth2 등의 보안 패턴을 학습합니다.") + .recommendedResources("스프링 시큐리티 인 액션, Baeldung Spring Security 튜토리얼") + .learningGoals("인증/인가 메커니즘 이해, JWT 토큰 기반 인증 구현, OAuth2/OIDC 통합") + .difficulty(4) + .importance(5) + .hoursPerDay(3) + .weeks(4) + .estimatedHours(3 * 4 * 7) + .stepOrder(1) + .level(5) + .build(); + + // ===== Level 6: Testing ===== + RoadmapNode testingNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(testing) + .taskName("Testing") + .learningAdvice("단위 테스트, 통합 테스트, E2E 테스트를 작성하고, TDD 방법론을 실천합니다.") + .recommendedResources("Test Driven Development(켄트 벡), JUnit 5 User Guide") + .learningGoals("JUnit/Mockito 활용, 테스트 더블 패턴, 통합 테스트 전략") + .difficulty(4) + .importance(5) + .hoursPerDay(2) + .weeks(4) + .estimatedHours(2 * 4 * 7) + .stepOrder(1) + .level(6) + .build(); + + // ===== Level 7: CI/CD, Docker, Caching ===== + RoadmapNode ciCdNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(ciCd) + .taskName("CI/CD") + .learningAdvice("지속적 통합과 지속적 배포 파이프라인을 구축하고, 자동화된 테스트와 배포 프로세스를 익힙니다.") + .recommendedResources("The DevOps Handbook, GitHub Actions 공식 문서") + .learningGoals("CI/CD 파이프라인 구축, 자동화된 테스트/빌드/배포, 롤백 전략") + .difficulty(4) + .importance(4) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) + .stepOrder(1) + .level(7) .build(); - // Spring Boot의 자식 노드들 RoadmapNode dockerNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) .task(docker) .taskName("Docker") - .learningAdvice("컨테이너 기술로 애플리케이션 배포 및 환경 관리를 간소화합니다.") - .stepOrder(1) - .level(2) + .learningAdvice("컨테이너 기술을 활용하여 일관된 개발 환경을 구성하고, 이미지 빌드와 배포를 자동화합니다.") + .recommendedResources("Docker 공식 문서, Docker Deep Dive") + .learningGoals("Dockerfile 작성, 이미지 빌드 최적화, Docker Compose 활용") + .difficulty(3) + .importance(5) + .hoursPerDay(2) + .weeks(3) + .estimatedHours(2 * 3 * 7) + .stepOrder(2) + .level(7) .build(); - RoadmapNode awsNode = RoadmapNode.builder() - .roadmapId(jobRoadmap.getId()) + RoadmapNode cachingNode = RoadmapNode.builder() + .roadmapId(backendRoadmap.getId()) .roadmapType(RoadmapNode.RoadmapType.JOB) - .task(aws) - .taskName("AWS") - .learningAdvice("클라우드 서비스로 애플리케이션을 확장 가능하게 배포하고 운영합니다.") - .stepOrder(2) - .level(2) + .task(caching) + .taskName("Caching") + .learningAdvice("Redis, Memcached 등을 활용한 캐싱 전략으로 응답 속도를 개선하고 DB 부하를 줄입니다.") + .recommendedResources("Redis 공식 문서, Caching Strategies and How to Choose the Right One") + .learningGoals("캐싱 전략 이해, Redis 활용, 캐시 무효화 전략") + .difficulty(4) + .importance(4) + .hoursPerDay(2) + .weeks(2) + .estimatedHours(2 * 2 * 7) + .stepOrder(3) + .level(7) .build(); - // 트리 구조 연결 (addChild 메서드 사용) + // ===== 트리 구조 연결 ===== + // Level 0 -> Level 1 fundamentalsNode.addChild(javaNode); - fundamentalsNode.addChild(springBootNode); - javaNode.addChild(mysqlNode); - javaNode.addChild(jpaNode); - springBootNode.addChild(dockerNode); - springBootNode.addChild(awsNode); + fundamentalsNode.addChild(gitNode); + fundamentalsNode.addChild(nodejsNode); + fundamentalsNode.addChild(pythonNode); + + // Level 1 -> Level 2 + javaNode.addChild(springBootNode); + nodejsNode.addChild(expressjsNode); + pythonNode.addChild(djangoNode); + + // Level 2 (Spring Boot) -> Level 3 + springBootNode.addChild(jpaNode); + springBootNode.addChild(mysqlNode); + springBootNode.addChild(postgresqlNode); + springBootNode.addChild(httpNode); + + // Level 3 (HTTP) -> Level 4 + httpNode.addChild(restApiNode); + + // Level 4 (REST API) -> Level 5 + restApiNode.addChild(springSecurityNode); + + // Level 5 (Spring Security) -> Level 6 + springSecurityNode.addChild(testingNode); + + // Level 6 (Testing) -> Level 7 + testingNode.addChild(ciCdNode); + testingNode.addChild(dockerNode); + testingNode.addChild(cachingNode); // 모든 노드를 JobRoadmap에 추가 - jobRoadmap.getNodes().addAll(List.of( - fundamentalsNode, gitNode, javaNode, springBootNode, - mysqlNode, jpaNode, dockerNode, awsNode + backendRoadmap.getNodes().addAll(List.of( + fundamentalsNode, javaNode, gitNode, nodejsNode, pythonNode, + springBootNode, expressjsNode, djangoNode, + jpaNode, mysqlNode, postgresqlNode, httpNode, + restApiNode, + springSecurityNode, + testingNode, + ciCdNode, dockerNode, cachingNode )); - jobRoadmapRepository.save(jobRoadmap); + // JobRoadmap 저장 (cascade로 RoadmapNode들도 함께 저장됨) + JobRoadmap savedJobRoadmap = jobRoadmapRepository.save(backendRoadmap); jobRoadmapRepository.save(frontendRoadmap); // 빈 로드맵 저장 + + // ===== JobRoadmapNodeStat 샘플 데이터 생성 ===== + // 저장된 노드들을 다시 조회하여 영속화된 객체 사용 + JobRoadmap reloadedJobRoadmap = jobRoadmapRepository.findByIdWithJobAndNodes(savedJobRoadmap.getId()) + .orElseThrow(() -> new RuntimeException("저장된 JobRoadmap을 찾을 수 없습니다.")); + + // 저장된 노드들을 taskName으로 매핑 (영속화된 노드 사용) + java.util.Map nodeMap = reloadedJobRoadmap.getNodes().stream() + .collect(Collectors.toMap(RoadmapNode::getTaskName, node -> node)); + + // totalMentorCount = 10 (가상의 멘토 10명 기준) + int totalMentorCount = 10; + + // Level 0: Root (모든 멘토가 시작) + createNodeStat(nodeMap.get("Programming Fundamentals"), 1, 1.0, 1.0, 10, totalMentorCount); + + // Level 1: 언어 선택 + createNodeStat(nodeMap.get("Java"), 1, 0.9, 1.5, 9, totalMentorCount); // Java 경로가 주류 + createNodeStat(nodeMap.get("Git"), 2, 0.95, 1.8, 10, totalMentorCount); // Git은 필수 도구 + createNodeStat(nodeMap.get("Node.js"), 3, 0.3, 2.2, 3, totalMentorCount); // Node.js는 소수 + createNodeStat(nodeMap.get("Python"), 4, 0.2, 2.5, 2, totalMentorCount); // Python은 더 소수 + + // Level 2: 프레임워크 + createNodeStat(nodeMap.get("Spring Boot"), 1, 0.85, 2.8, 9, totalMentorCount); // Spring Boot 주류 + createNodeStat(nodeMap.get("Express.js"), 1, 0.28, 3.2, 3, totalMentorCount); // Express.js 소수 + createNodeStat(nodeMap.get("Django"), 1, 0.18, 3.5, 2, totalMentorCount); // Django 소수 + + // Level 3: DB/ORM/HTTP + createNodeStat(nodeMap.get("JPA"), 1, 0.65, 3.8, 7, totalMentorCount); // JPA는 Spring Boot 사용자의 대부분 + createNodeStat(nodeMap.get("MySQL"), 2, 0.68, 4.2, 7, totalMentorCount); // MySQL 인기 + createNodeStat(nodeMap.get("PostgreSQL"), 3, 0.48, 4.5, 5, totalMentorCount); // PostgreSQL은 중간 + createNodeStat(nodeMap.get("HTTP"), 4, 0.75, 4.0, 10, totalMentorCount); // HTTP는 모두 학습 + + // Level 4: REST API + createNodeStat(nodeMap.get("REST API"), 1, 0.78, 5.2, 9, totalMentorCount); // REST API는 거의 필수 + + // Level 5: Spring Security + createNodeStat(nodeMap.get("Spring Security"), 1, 0.62, 6.0, 7, totalMentorCount); // 보안은 중요하지만 진입 장벽 + + // Level 6: Testing + createNodeStat(nodeMap.get("Testing"), 1, 0.7, 6.8, 8, totalMentorCount); // 테스팅은 대부분 학습 + + // Level 7: CI/CD, Docker, Caching + createNodeStat(nodeMap.get("CI/CD"), 1, 0.52, 7.5, 6, totalMentorCount); // CI/CD는 중간 수준 + createNodeStat(nodeMap.get("Docker"), 2, 0.72, 7.2, 8, totalMentorCount); // Docker는 인기 + createNodeStat(nodeMap.get("Caching"), 3, 0.45, 7.8, 5, totalMentorCount); // Caching은 고급 주제 + + log.info("백엔드 개발자 직업 로드맵 샘플 데이터 생성 완료 (노드 18개, 통계 18개)"); + } + + /** + * JobRoadmapNodeStat 샘플 데이터 생성 헬퍼 메서드 + * + * @param node RoadmapNode + * @param stepOrder 노드의 stepOrder + * @param weight 가중치 (0.0 ~ 1.0) + * @param averagePosition 평균 등장 위치 (1.0부터 시작) + * @param mentorCount 사용한 멘토 수 + * @param totalMentorCount 전체 멘토 수 + */ + private void createNodeStat(RoadmapNode node, int stepOrder, double weight, + double averagePosition, int mentorCount, int totalMentorCount) { + double mentorCoverageRatio = (double) mentorCount / totalMentorCount; + + JobRoadmapNodeStat stat = JobRoadmapNodeStat.builder() + .node(node) + .stepOrder(stepOrder) + .weight(weight) + .averagePosition(averagePosition) + .mentorCount(mentorCount) + .totalMentorCount(totalMentorCount) + .mentorCoverageRatio(mentorCoverageRatio) + .outgoingTransitions(mentorCount) // 간단히 mentorCount와 동일하게 설정 + .incomingTransitions(mentorCount) // 간단히 mentorCount와 동일하게 설정 + .transitionCounts(null) // 샘플 데이터에서는 null + .alternativeParents(null) // 샘플 데이터에서는 null + .build(); + + jobRoadmapNodeStatRepository.save(stat); } // --- 통합 로직 테스트용 멘토 로드맵 10개 생성 --- diff --git a/back/src/test/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapControllerTest.java b/back/src/test/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapControllerTest.java index 215085ee..51ec97e1 100644 --- a/back/src/test/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapControllerTest.java +++ b/back/src/test/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapControllerTest.java @@ -129,8 +129,8 @@ void getJobRoadmaps_DefaultPaging() throws Exception { .andExpect(jsonPath("$.resultCode").value("200")) .andExpect(jsonPath("$.msg").value("직업 로드맵 목록 조회 성공")) .andExpect(jsonPath("$.data.jobRoadmaps").isArray()) - .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(2)) - .andExpect(jsonPath("$.data.totalElements").value(2)) + .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(7)) + .andExpect(jsonPath("$.data.totalElements").value(7)) .andExpect(jsonPath("$.data.totalPage").value(1)) .andExpect(jsonPath("$.data.currentPage").value(0)) .andExpect(jsonPath("$.data.hasNext").value(false)); @@ -147,8 +147,8 @@ void getJobRoadmaps_CustomPageSize() throws Exception { .andDo(print()) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(1)) - .andExpect(jsonPath("$.data.totalElements").value(2)) - .andExpect(jsonPath("$.data.totalPage").value(2)) + .andExpect(jsonPath("$.data.totalElements").value(7)) + .andExpect(jsonPath("$.data.totalPage").value(7)) .andExpect(jsonPath("$.data.currentPage").value(0)); } @@ -157,11 +157,11 @@ void getJobRoadmaps_CustomPageSize() throws Exception { void getJobRoadmaps_WithKeyword() throws Exception { // when & then mvc.perform(get("/job-roadmaps") - .param("keyword", "백엔드") + .param("keyword", "테스트") .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(1)) + .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(2)) .andExpect(jsonPath("$.data.jobRoadmaps[0].jobName").value(testJob1.getName())); } diff --git a/back/src/test/java/com/back/domain/roadmap/roadmap/service/JobRoadmapServiceTest.java b/back/src/test/java/com/back/domain/roadmap/roadmap/service/JobRoadmapServiceTest.java index 4e96b493..cd6b150c 100644 --- a/back/src/test/java/com/back/domain/roadmap/roadmap/service/JobRoadmapServiceTest.java +++ b/back/src/test/java/com/back/domain/roadmap/roadmap/service/JobRoadmapServiceTest.java @@ -107,9 +107,9 @@ void getAllJobRoadmaps() { // then assertThat(result).isNotEmpty(); - assertThat(result).hasSize(1); + assertThat(result).hasSize(6); - JobRoadmapListResponse response = result.get(0); + JobRoadmapListResponse response = result.get(5); // 마지막에 추가된 테스트 데이터 assertThat(response.id()).isEqualTo(testJobRoadmap.getId()); assertThat(response.jobName()).isEqualTo(testJob.getName()); assertThat(response.jobDescription()).isEqualTo(testJob.getDescription()); @@ -119,7 +119,7 @@ void getAllJobRoadmaps() { @DisplayName("직업 로드맵 다건 조회 - 페이징 및 키워드 검색") void getJobRoadmaps_WithPagingAndKeyword() { // given - String keyword = "백엔드"; + String keyword = "테스트"; int page = 0; int size = 10; @@ -165,8 +165,8 @@ void getJobRoadmaps_NullKeyword() { Page result = jobRoadmapService.getJobRoadmaps(keyword, page, size); // then - assertThat(result.getContent()).hasSize(1); - assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getContent()).hasSize(6); + assertThat(result.getTotalElements()).isEqualTo(6); } @Test