diff --git a/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java b/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java index 76b73197..c7d44a1c 100644 --- a/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java +++ b/back/src/main/java/com/back/domain/member/member/service/MemberStorage.java @@ -10,8 +10,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import java.util.Optional; - @Component @RequiredArgsConstructor public class MemberStorage { @@ -29,9 +27,6 @@ public Mentor findMentorByMemberId(Long memberId) { return mentorRepository.findByMemberIdWithMember(memberId) .orElseThrow(() -> new ServiceException(MemberErrorCode.NOT_FOUND_MENTOR)); } - public Optional findMentorByMemberOptional(Member member) { - return mentorRepository.findByMemberIdWithMember(member.getId()); - } public Mentee findMenteeByMember(Member member) { return menteeRepository.findByMemberIdWithMember(member.getId()) diff --git a/back/src/main/java/com/back/domain/member/mentee/entity/Mentee.java b/back/src/main/java/com/back/domain/member/mentee/entity/Mentee.java index 64b65bc5..c2a87c85 100644 --- a/back/src/main/java/com/back/domain/member/mentee/entity/Mentee.java +++ b/back/src/main/java/com/back/domain/member/mentee/entity/Mentee.java @@ -31,4 +31,8 @@ public Mentee(Member member, Long jobId) { public void delete() { this.isDeleted = true; } + + public boolean isMember(Member member) { + return this.member.equals(member); + } } diff --git a/back/src/main/java/com/back/domain/member/mentor/entity/Mentor.java b/back/src/main/java/com/back/domain/member/mentor/entity/Mentor.java index fe961834..144b3775 100644 --- a/back/src/main/java/com/back/domain/member/mentor/entity/Mentor.java +++ b/back/src/main/java/com/back/domain/member/mentor/entity/Mentor.java @@ -43,4 +43,8 @@ public void updateCareerYears(Integer careerYears) { public void delete() { this.isDeleted = true; } + + public boolean isMember(Member member) { + return this.member.equals(member); + } } diff --git a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java index 9a4abd88..bdc44f91 100644 --- a/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java +++ b/back/src/main/java/com/back/domain/mentoring/mentoring/dto/MentoringWithTagsDto.java @@ -11,13 +11,19 @@ public record MentoringWithTagsDto( @Schema(description = "멘토링 제목") String title, @Schema(description = "멘토링 태그") - List tags + List tags, + @Schema(description = "멘토 ID") + Long mentorId, + @Schema(description = "멘토 닉네임") + String nickname ) { public static MentoringWithTagsDto from(Mentoring mentoring) { return new MentoringWithTagsDto( mentoring.getId(), mentoring.getTitle(), - mentoring.getTags() + mentoring.getTags(), + mentoring.getMentor().getId(), + mentoring.getMentor().getMember().getNickname() ); } } diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java b/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java index 6fd2b52f..4053018f 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/controller/ReservationController.java @@ -4,7 +4,9 @@ import com.back.domain.member.member.service.MemberStorage; import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; +import com.back.domain.mentoring.reservation.dto.ReservationDto; import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; +import com.back.domain.mentoring.reservation.dto.response.ReservationPagingResponse; import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; import com.back.domain.mentoring.reservation.service.ReservationService; import com.back.global.rq.Rq; @@ -13,11 +15,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import java.util.Optional; - @RestController @RequestMapping("/reservations") @RequiredArgsConstructor @@ -28,6 +29,39 @@ public class ReservationController { private final ReservationService reservationService; private final MemberStorage memberStorage; + @GetMapping + @Operation(summary = "나의 예약 목록 조회", description = "본인의 예약 목록을 조회합니다. 로그인 후 조회할 수 있습니다.") + public RsData getReservations( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size + ) { + Member member = rq.getActor(); + Page reservationPage = reservationService.getReservations(member, page, size); + ReservationPagingResponse resDto = ReservationPagingResponse.from(reservationPage); + + return new RsData<>( + "200", + "예약 목록을 조회하였습니다.", + resDto + ); + } + + + @GetMapping("/{reservationId}") + @Operation(summary = "예약 조회", description = "특정 예약을 조회합니다. 로그인 후 예약 조회할 수 있습니다.") + public RsData getReservation( + @PathVariable Long reservationId + ) { + Member member = rq.getActor(); + ReservationResponse resDto = reservationService.getReservation(member, reservationId); + + return new RsData<>( + "200", + "예약을 조회하였습니다.", + resDto + ); + } + @PostMapping @PreAuthorize("hasRole('MENTEE')") @Operation(summary = "예약 신청", description = "멘티가 멘토의 슬롯을 선택해 예약 신청을 합니다. 로그인한 멘티만 예약 신청할 수 있습니다.") @@ -35,7 +69,6 @@ public RsData createReservation( @RequestBody @Valid ReservationRequest reqDto ) { Mentee mentee = memberStorage.findMenteeByMember(rq.getActor()); - ReservationResponse resDto = reservationService.createReservation(mentee, reqDto); return new RsData<>( @@ -52,7 +85,6 @@ public RsData approveReservation( @PathVariable Long reservationId ) { Mentor mentor = memberStorage.findMentorByMember(rq.getActor()); - ReservationResponse resDto = reservationService.approveReservation(mentor, reservationId); return new RsData<>( @@ -69,7 +101,6 @@ public RsData rejectReservation( @PathVariable Long reservationId ) { Mentor mentor = memberStorage.findMentorByMember(rq.getActor()); - ReservationResponse resDto = reservationService.rejectReservation(mentor, reservationId); return new RsData<>( @@ -85,15 +116,7 @@ public RsData cancelReservation( @PathVariable Long reservationId ) { Member member = rq.getActor(); - ReservationResponse resDto; - - Optional mentor = memberStorage.findMentorByMemberOptional(member); - if (mentor.isPresent()) { - resDto = reservationService.cancelReservation(mentor.get(), reservationId); - } else { - Mentee mentee = memberStorage.findMenteeByMember(member); - resDto = reservationService.cancelReservation(mentee, reservationId); - } + ReservationResponse resDto = reservationService.cancelReservation(member, reservationId); return new RsData<>( "200", diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDto.java b/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDto.java new file mode 100644 index 00000000..8da541c5 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/dto/ReservationDto.java @@ -0,0 +1,38 @@ +package com.back.domain.mentoring.reservation.dto; + +import com.back.domain.mentoring.reservation.constant.ReservationStatus; +import com.back.domain.mentoring.reservation.entity.Reservation; +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDateTime; + +public record ReservationDto( + @Schema(description = "예약 ID") + Long reservationId, + @Schema(description = "예약 상태") + ReservationStatus status, + + @Schema(description = "멘토링 ID") + Long mentoringId, + @Schema(description = "멘토링 제목") + String title, + + @Schema(description = "멘토 슬롯 ID") + Long mentorSlotId, + @Schema(description = "시작 일시") + LocalDateTime startDateTime, + @Schema(description = "종료 일시") + LocalDateTime endDateTime +) { + public static ReservationDto from(Reservation reservation) { + return new ReservationDto( + reservation.getId(), + reservation.getStatus(), + reservation.getMentoring().getId(), + reservation.getMentoring().getTitle(), + reservation.getMentorSlot().getId(), + reservation.getMentorSlot().getStartDateTime(), + reservation.getMentorSlot().getEndDateTime() + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationPagingResponse.java b/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationPagingResponse.java new file mode 100644 index 00000000..6e04d525 --- /dev/null +++ b/back/src/main/java/com/back/domain/mentoring/reservation/dto/response/ReservationPagingResponse.java @@ -0,0 +1,30 @@ +package com.back.domain.mentoring.reservation.dto.response; + +import com.back.domain.mentoring.reservation.dto.ReservationDto; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.data.domain.Page; + +import java.util.List; + +public record ReservationPagingResponse( + @Schema(description = "예약 목록") + List reservations, + @Schema(description = "현재 페이지 (0부터 시작)") + int currentPage, + @Schema(description = "총 페이지") + int totalPage, + @Schema(description = "총 개수") + long totalElements, + @Schema(description = "다음 페이지 존재 여부") + boolean hasNext +) { + public static ReservationPagingResponse from(Page page) { + return new ReservationPagingResponse( + page.getContent(), + page.getNumber(), + page.getTotalPages(), + page.getTotalElements(), + page.hasNext() + ); + } +} diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java b/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java index 858b335b..773ec2fd 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java @@ -1,5 +1,6 @@ package com.back.domain.mentoring.reservation.entity; +import com.back.domain.member.member.entity.Member; import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; import com.back.domain.mentoring.mentoring.entity.Mentoring; @@ -14,6 +15,13 @@ import lombok.NoArgsConstructor; @Entity +@Table( + name = "reservation", + indexes = { + @Index(name = "idx_reservation_mentor", columnList = "mentor_id"), + @Index(name = "idx_reservation_mentee", columnList = "mentee_id") + } +) @Getter @NoArgsConstructor public class Reservation extends BaseEntity { @@ -86,15 +94,10 @@ public void reject(Mentor mentor) { updateStatus(ReservationStatus.REJECTED); } - public void cancel(Mentor mentor) { - ensureMentor(mentor); - ensureCanCancel(); - ensureNotPast(); - updateStatus(ReservationStatus.CANCELED); - } - - public void cancel(Mentee mentee) { - ensureMentee(mentee); + public void cancel(Member member) { + if (!mentor.isMember(member) && !mentee.isMember(member) ) { + throw new ServiceException(ReservationErrorCode.FORBIDDEN_NOT_PARTICIPANT); + } ensureCanCancel(); ensureNotPast(); updateStatus(ReservationStatus.CANCELED); @@ -114,12 +117,6 @@ private void ensureMentor(Mentor mentor) { } } - private void ensureMentee(Mentee mentee) { - if (!isMentee(mentee)) { - throw new ServiceException(ReservationErrorCode.FORBIDDEN_NOT_MENTEE); - } - } - private void ensureCanApprove() { if(!this.status.canApprove()) { throw new ServiceException(ReservationErrorCode.CANNOT_APPROVE); diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java b/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java index dd54c5b4..f97e753e 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java @@ -20,6 +20,8 @@ public enum ReservationErrorCode implements ErrorCode { // 403 FORBIDDEN_NOT_MENTOR("403-1", "해당 예약에 대한 멘토 권한이 없습니다."), FORBIDDEN_NOT_MENTEE("403-2", "해당 예약에 대한 멘티 권한이 없습니다."), + FORBIDDEN_NOT_PARTICIPANT("403-3", "해당 예약에 대한 권한이 없습니다"), + RESERVATION_NOT_ACCESSIBLE("403-4", "예약을 찾을 수 없거나 권한이 없습니다"), // 404 NOT_FOUND_RESERVATION("404-1", "예약이 존재하지 않습니다."), diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java b/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java index a0b9ad50..39fb5bfe 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java @@ -1,8 +1,13 @@ package com.back.domain.mentoring.reservation.repository; +import com.back.domain.member.member.entity.Member; import com.back.domain.mentoring.reservation.constant.ReservationStatus; import com.back.domain.mentoring.reservation.entity.Reservation; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -11,6 +16,40 @@ public interface ReservationRepository extends JpaRepository Optional findTopByOrderByIdDesc(); Optional findByMentorSlotIdAndStatusIn(Long mentorSlotId, List statuses); + @Query(""" + SELECT r + FROM Reservation r + WHERE r.id = :reservationId + AND (r.mentee.member = :member + OR r.mentor.member = :member) + """) + Optional findByIdAndMember( + @Param("reservationId") Long reservationId, + @Param("member") Member member + ); + + @Query(""" + SELECT r + FROM Reservation r + WHERE r.mentor.member = :member + ORDER BY r.mentorSlot.startDateTime DESC + """) + Page findAllByMentorMember( + @Param("member") Member member, + Pageable pageable + ); + + @Query(""" + SELECT r + FROM Reservation r + WHERE r.mentee.member = :member + ORDER BY r.mentorSlot.startDateTime DESC + """) + Page findAllByMenteeMember( + @Param("member") Member member, + Pageable pageable + ); + boolean existsByMentoringId(Long mentoringId); /** diff --git a/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java b/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java index 03e310f8..86d68c2b 100644 --- a/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java +++ b/back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java @@ -1,10 +1,12 @@ package com.back.domain.mentoring.reservation.service; +import com.back.domain.member.member.entity.Member; import com.back.domain.member.mentee.entity.Mentee; import com.back.domain.member.mentor.entity.Mentor; import com.back.domain.mentoring.mentoring.entity.Mentoring; import com.back.domain.mentoring.mentoring.service.MentoringStorage; import com.back.domain.mentoring.reservation.constant.ReservationStatus; +import com.back.domain.mentoring.reservation.dto.ReservationDto; import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; import com.back.domain.mentoring.reservation.entity.Reservation; @@ -15,6 +17,9 @@ import com.back.global.exception.ServiceException; import jakarta.persistence.OptimisticLockException; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,6 +33,28 @@ public class ReservationService { private final ReservationRepository reservationRepository; private final MentoringStorage mentoringStorage; + @Transactional(readOnly = true) + public Page getReservations(Member member, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + + Page reservations; + + if (member.getRole() == Member.Role.MENTOR) { + reservations = reservationRepository.findAllByMentorMember(member, pageable); + } else { + reservations = reservationRepository.findAllByMenteeMember(member, pageable); + } + return reservations.map(ReservationDto::from); + } + + @Transactional(readOnly = true) + public ReservationResponse getReservation(Member member, Long reservationId) { + Reservation reservation = reservationRepository.findByIdAndMember(reservationId, member) + .orElseThrow(() -> new ServiceException(ReservationErrorCode.RESERVATION_NOT_ACCESSIBLE)); + + return ReservationResponse.from(reservation); + } + @Transactional public ReservationResponse createReservation(Mentee mentee, ReservationRequest reqDto) { try { @@ -78,16 +105,9 @@ public ReservationResponse rejectReservation(Mentor mentor, Long reservationId) } @Transactional - public ReservationResponse cancelReservation(Mentor mentor, Long reservationId) { - Reservation reservation = mentoringStorage.findReservation(reservationId); - reservation.cancel(mentor); - return ReservationResponse.from(reservation); - } - - @Transactional - public ReservationResponse cancelReservation(Mentee mentee, Long reservationId) { + public ReservationResponse cancelReservation(Member member, Long reservationId) { Reservation reservation = mentoringStorage.findReservation(reservationId); - reservation.cancel(mentee); + reservation.cancel(member); return ReservationResponse.from(reservation); } diff --git a/back/src/main/java/com/back/domain/post/comment/dto/CommentAllResponse.java b/back/src/main/java/com/back/domain/post/comment/dto/CommentAllResponse.java index 9fcf8aa7..b03e1901 100644 --- a/back/src/main/java/com/back/domain/post/comment/dto/CommentAllResponse.java +++ b/back/src/main/java/com/back/domain/post/comment/dto/CommentAllResponse.java @@ -5,19 +5,21 @@ import java.time.LocalDateTime; -@Data -public class CommentAllResponse { - Long id; - String content; - String authorName; - LocalDateTime createdAt; + +public record CommentAllResponse( + Long id, + String content, + String authorName, + LocalDateTime createdAt +) { + public static CommentAllResponse from(PostComment comment) { - CommentAllResponse response = new CommentAllResponse(); - response.setId(comment.getId()); - response.setContent(comment.getContent()); - response.setAuthorName(comment.getAuthorName()); // Member에서 이름 가져오기 - response.setCreatedAt(comment.getCreateDate()); - return response; + return new CommentAllResponse( + comment.getId(), + comment.getContent(), + comment.getAuthorName(), + comment.getCreateDate() + ); } } diff --git a/back/src/main/java/com/back/domain/post/comment/dto/CommentCreateRequest.java b/back/src/main/java/com/back/domain/post/comment/dto/CommentCreateRequest.java index 96529d0a..4cb485be 100644 --- a/back/src/main/java/com/back/domain/post/comment/dto/CommentCreateRequest.java +++ b/back/src/main/java/com/back/domain/post/comment/dto/CommentCreateRequest.java @@ -1,11 +1,13 @@ package com.back.domain.post.comment.dto; import jakarta.validation.constraints.NotBlank; -import lombok.Data; -@Data -public class CommentCreateRequest { - private String role; - @NotBlank(message = "댓글을 입력해주세요") - private String comment; + +public record CommentCreateRequest( + String role, + + @NotBlank(message = "댓글을 입력해주세요") + String comment +) { + } diff --git a/back/src/main/java/com/back/domain/post/comment/dto/CommentDeleteRequest.java b/back/src/main/java/com/back/domain/post/comment/dto/CommentDeleteRequest.java index 4f95ea0d..8c553ea7 100644 --- a/back/src/main/java/com/back/domain/post/comment/dto/CommentDeleteRequest.java +++ b/back/src/main/java/com/back/domain/post/comment/dto/CommentDeleteRequest.java @@ -1,10 +1,9 @@ package com.back.domain.post.comment.dto; import jakarta.validation.constraints.NotNull; -import lombok.Data; -@Data -public class CommentDeleteRequest { - @NotNull - private Long CommentId; +public record CommentDeleteRequest(@NotNull + Long commentId +) { + } diff --git a/back/src/main/java/com/back/domain/post/comment/dto/CommentModifyRequest.java b/back/src/main/java/com/back/domain/post/comment/dto/CommentModifyRequest.java index ee3d79ac..ffe5d4f2 100644 --- a/back/src/main/java/com/back/domain/post/comment/dto/CommentModifyRequest.java +++ b/back/src/main/java/com/back/domain/post/comment/dto/CommentModifyRequest.java @@ -2,12 +2,12 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import lombok.Data; -@Data -public class CommentModifyRequest { - @NotNull - private Long commentId; - @NotBlank(message = "공백일 수 없습니다.") - private String content; +public record CommentModifyRequest( + @NotNull + Long commentId, + + @NotBlank(message = "공백일 수 없습니다.") + String content +) { } diff --git a/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java b/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java index fdb5d9cb..9664240b 100644 --- a/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java +++ b/back/src/main/java/com/back/domain/post/comment/service/PostCommentService.java @@ -29,7 +29,7 @@ public void createComment(Member member, Long postId, CommentCreateRequest comme PostComment postComment = PostComment.builder() .post(post) - .content(commentCreateRequest.getComment()) + .content(commentCreateRequest.comment()) .member(member) .role(member.getRole().name()) .build(); @@ -55,7 +55,7 @@ public List getAllPostCommentResponse(Long postId) { public void removePostComment(Long postId, CommentDeleteRequest commentDeleteRequest, Member member) { validatePostExists(postId); - PostComment postComment = getPostCommentById(commentDeleteRequest.getCommentId()); + PostComment postComment = getPostCommentById(commentDeleteRequest.commentId()); Member author = postComment.getMember(); @@ -75,7 +75,7 @@ public void removePostComment(Long postId, CommentDeleteRequest commentDeleteReq public void updatePostComment(Long postId, CommentModifyRequest commentModifyRequest, Member member) { validatePostExists(postId); - PostComment postComment = getPostCommentById(commentModifyRequest.getCommentId()); + PostComment postComment = getPostCommentById(commentModifyRequest.commentId()); Member author = postComment.getMember(); @@ -83,11 +83,11 @@ public void updatePostComment(Long postId, CommentModifyRequest commentModifyReq throw new ServiceException("400", "수정 권한이 없습니다."); } - if ( commentModifyRequest.getContent() == null || commentModifyRequest.getContent().isEmpty()) { + if ( commentModifyRequest.content() == null || commentModifyRequest.content().isEmpty()) { throw new ServiceException("400", "댓글은 비어 있을 수 없습니다."); } - postComment.updateContent(commentModifyRequest.getContent()); + postComment.updateContent(commentModifyRequest.content()); } diff --git a/back/src/main/java/com/back/domain/post/post/controller/PostController.java b/back/src/main/java/com/back/domain/post/post/controller/PostController.java index 0ccb2e57..b1d27ddf 100644 --- a/back/src/main/java/com/back/domain/post/post/controller/PostController.java +++ b/back/src/main/java/com/back/domain/post/post/controller/PostController.java @@ -40,7 +40,7 @@ public RsData getPostWithPage( return new RsData<>("200", "게시글이 조회 되었습니다.", resDto); } - @Operation(summary = "게시글 생성") + @Operation(summary = "게시글 생성 ") @PostMapping public RsData createPost( @Valid @RequestBody PostCreateRequest postCreateRequest @@ -93,28 +93,13 @@ public RsData updatePost(@PathVariable Long post_id @Operation(summary = "게시글 좋아요") @PostMapping("/{post_id}/liked") - public RsData likePost(@PathVariable Long post_id) { + public RsData likePost(@PathVariable Long post_id) { postLikeService.likePost(post_id); - return new RsData<>("200", "게시글 좋아요 성공", null); - } - - @Operation(summary = "게시글 좋아요 (Show)") - @GetMapping("/{post_id}/liked") - public RsData getLike(@PathVariable Long post_id) { int likeCount = postLikeService.getLikeCount(post_id); PostLikedResponse postLikedResponse = new PostLikedResponse(likeCount); - return new RsData<>("200", "게시글 좋아요 조회 성공", postLikedResponse); - } - - @Operation(summary = "게시글 싫어요 (Show)") - @GetMapping("/{post_id}/disliked") - public RsData getDisLike(@PathVariable Long post_id) { - int likeCount = postLikeService.getDisLikeCount(post_id); - PostLikedResponse postLikedResponse = new PostLikedResponse(likeCount); - - return new RsData<>("200", "게시글 싫어요 조회 성공", postLikedResponse); + return new RsData<>("200", "게시글 좋아요 성공", postLikedResponse); } @Operation(summary = "게시글 싫어요") @@ -122,12 +107,15 @@ public RsData getDisLike(@PathVariable Long post_id) { public RsData disLikePost(@PathVariable Long post_id) { postLikeService.disLikePost(post_id); - return new RsData<>("200", "게시글 싫어요 성공", null); + int likeCount = postLikeService.getDisLikeCount(post_id); + PostLikedResponse postLikedResponse = new PostLikedResponse(likeCount); + + return new RsData<>("200", "게시글 싫어요 성공", postLikedResponse); } @Operation(summary = "게시글 상세페이지") - @GetMapping("/Detail/{post_id}") + @GetMapping("/detail/{post_id}") public RsData getPostDetail(@PathVariable Long post_id) { PostDetailResponse response = postDetailFacade.getDetailWithViewIncrement(post_id); diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostAllResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostAllResponse.java index 67f42276..26f89296 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostAllResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostAllResponse.java @@ -5,20 +5,20 @@ import java.time.LocalDateTime; -@Data -public class PostAllResponse { - private Long id; - private String title; - private String authorName; - private LocalDateTime createdAt; - private int viewCount; - - public PostAllResponse(Post post) { - this.id = post.getId(); - this.title = post.getTitle(); - this.authorName = post.getAuthorName(); - this.createdAt = post.getCreateDate(); - this.viewCount = post.getViewCount(); +public record PostAllResponse( + Long id, + String title, + String authorName, + LocalDateTime createdAt, + int viewCount +) { + public static PostAllResponse from(Post post) { + return new PostAllResponse( + post.getId(), + post.getTitle(), + post.getAuthorName(), + post.getCreateDate(), + post.getViewCount() + ); } - } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostCreateRequest.java b/back/src/main/java/com/back/domain/post/post/dto/PostCreateRequest.java index ce30d3ae..5224d2a8 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostCreateRequest.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostCreateRequest.java @@ -1,13 +1,17 @@ package com.back.domain.post.post.dto; import jakarta.validation.constraints.NotBlank; -import lombok.Data; - -@Data -public class PostCreateRequest { - private String postType; - @NotBlank(message = "제목은 null 혹은 공백일 수 없습니다.") - private String title; - @NotBlank(message = "제목은 null 혹은 공백일 수 없습니다.") - private String content; + + + +public record PostCreateRequest( + String postType, + @NotBlank(message = "제목은 null 혹은 공백일 수 없습니다.") + String title, + @NotBlank(message = "내용은 null 혹은 공백일 수 없습니다.") + String content, + String job +) { + + } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostCreateResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostCreateResponse.java index da16a1d2..5f52c457 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostCreateResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostCreateResponse.java @@ -3,15 +3,13 @@ import com.back.domain.post.post.entity.Post; import lombok.Data; -@Data -public class PostCreateResponse { - private Long postId; - private String title; + +public record PostCreateResponse( + Long postId, + String title) { + public static PostCreateResponse from(Post post) { - PostCreateResponse response = new PostCreateResponse(); - response.setPostId(post.getId()); - response.setTitle(post.getTitle()); - return response; + return new PostCreateResponse(post.getId(), post.getTitle()); } } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostDetailResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostDetailResponse.java index 630e46d4..69bc9875 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostDetailResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostDetailResponse.java @@ -2,47 +2,47 @@ import com.back.domain.post.comment.dto.CommentAllResponse; import com.back.domain.post.post.entity.Post; -import lombok.Data; import java.time.LocalDateTime; import java.util.List; -@Data -public class PostDetailResponse { - private Long id; - private String title; - private String content; - private String authorName; - private LocalDateTime createdAt; - private int viewCount; - - private int likeCount; - private int dislikeCount; - private String userLikeStatus; - - private List comments; - - - - public static PostDetailResponse from(Post post, List comments, int likeCount, int dislikeCount, String userLikeStatus) { - PostDetailResponse postDetailResponse = new PostDetailResponse(); - - postDetailResponse.setId(post.getId()); - postDetailResponse.setTitle(post.getTitle()); - postDetailResponse.setContent(post.getContent()); - postDetailResponse.setAuthorName(post.getAuthorName()); - postDetailResponse.setCreatedAt(post.getCreateDate()); - postDetailResponse.setViewCount(post.getViewCount()); - - postDetailResponse.setLikeCount(likeCount); - postDetailResponse.setDislikeCount(dislikeCount); - postDetailResponse.setUserLikeStatus(userLikeStatus); - - postDetailResponse.setComments(comments); - - - - return postDetailResponse; +public record PostDetailResponse( + Long id, + String title, + String content, + String authorName, + LocalDateTime createdAt, + int viewCount, + + int likeCount, + int dislikeCount, + String userLikeStatus, + + List comments) { + + + + + + public static PostDetailResponse from( + Post post, + List comments, + int likeCount, + int dislikeCount, + String userLikeStatus + ) { + return new PostDetailResponse( + post.getId(), + post.getTitle(), + post.getContent(), + post.getAuthorName(), + post.getCreateDate(), + post.getViewCount(), + likeCount, + dislikeCount, + userLikeStatus, + comments + ); } } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostDto.java b/back/src/main/java/com/back/domain/post/post/dto/PostDto.java index 82947aff..c9d516fa 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostDto.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostDto.java @@ -2,20 +2,23 @@ import com.back.domain.post.post.entity.Post; -import lombok.Data; -@Data -public class PostDto { - private Long postId; - private String title; - private String content; - public static PostDto from(Post post) { - PostDto postDto = new PostDto(); - postDto.setPostId(post.getId()); - postDto.setTitle(post.getTitle()); - postDto.setContent(post.getContent()); - return postDto; +public record PostDto( + Long postId, + String title, + String content, + int viewCount +) { + + + public static PostDto from(Post post) { + return new PostDto( + post.getId(), + post.getTitle(), + post.getContent(), + post.getViewCount() + ); } } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostLikedResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostLikedResponse.java index 037dbb57..ed88e7a3 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostLikedResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostLikedResponse.java @@ -1,12 +1,6 @@ package com.back.domain.post.post.dto; -import lombok.Data; -@Data -public class PostLikedResponse { - private int likeCount; +public record PostLikedResponse(int likeCount) { - public PostLikedResponse(int likeCount) { - this.likeCount = likeCount; - } } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostModifyRequest.java b/back/src/main/java/com/back/domain/post/post/dto/PostModifyRequest.java index 0ff0b8c7..50c79fa1 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostModifyRequest.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostModifyRequest.java @@ -1,9 +1,10 @@ package com.back.domain.post.post.dto; -import lombok.Data; -@Data -public class PostModifyRequest { - String title; - String content; + + +public record PostModifyRequest( + String title, + String content) { + } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostPagingResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostPagingResponse.java index fbdd681d..21962674 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostPagingResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostPagingResponse.java @@ -1,35 +1,33 @@ package com.back.domain.post.post.dto; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; import org.springframework.data.domain.Page; -import org.springframework.security.core.parameters.P; import java.util.List; -@Data -public class PostPagingResponse{ - @Schema(description = "게시글 목록") - List posts; - @Schema(description = "현재 페이지 (0부터 시작)") - int currentPage; - @Schema(description = "총 페이지") - int totalPage; - @Schema(description = "총 개수") - long totalElements; - @Schema(description = "다음 페이지 존재 여부") - boolean hasNext; +public record PostPagingResponse( + @Schema(description = "게시글 목록") + List posts, + @Schema(description = "현재 페이지 (0부터 시작)") + int currentPage, + @Schema(description = "총 페이지") + int totalPage, + @Schema(description = "총 개수") + long totalElements, + @Schema(description = "다음 페이지 존재 여부") + boolean hasNext +) { + public static PostPagingResponse from(Page page) { - PostPagingResponse postPagingResponse = new PostPagingResponse(); - postPagingResponse.setPosts(page.getContent()); - postPagingResponse.setCurrentPage(page.getNumber()); - postPagingResponse.setTotalPage(page.getTotalPages()); - postPagingResponse.setTotalElements(page.getTotalElements()); - postPagingResponse.setHasNext(page.hasNext()); - - return postPagingResponse; + return new PostPagingResponse( + page.getContent(), + page.getNumber(), + page.getTotalPages(), + page.getTotalElements(), + page.hasNext() + ); } } diff --git a/back/src/main/java/com/back/domain/post/post/dto/PostSingleResponse.java b/back/src/main/java/com/back/domain/post/post/dto/PostSingleResponse.java index 6183ad2e..a5532268 100644 --- a/back/src/main/java/com/back/domain/post/post/dto/PostSingleResponse.java +++ b/back/src/main/java/com/back/domain/post/post/dto/PostSingleResponse.java @@ -1,23 +1,24 @@ package com.back.domain.post.post.dto; import com.back.domain.post.post.entity.Post; -import lombok.Data; import java.time.LocalDateTime; -@Data -public class PostSingleResponse { - private Long id; - private String title; - private String authorName; - private LocalDateTime createdAt; - private int viewCount; - public PostSingleResponse(Post post) { - this.id = post.getId(); - this.title = post.getTitle(); - this.authorName = post.getAuthorName(); - this.createdAt = post.getCreateDate(); - this.viewCount = post.getViewCount(); +public record PostSingleResponse( + Long id, + String title, + String authorName, + LocalDateTime createdAt, + int viewCount) { + + public static PostSingleResponse from(Post post) { + return new PostSingleResponse( + post.getId(), + post.getTitle(), + post.getAuthorName(), + post.getCreateDate(), + post.getViewCount() + ); } } diff --git a/back/src/main/java/com/back/domain/post/post/entity/Post.java b/back/src/main/java/com/back/domain/post/post/entity/Post.java index b44c49fc..0056ea51 100644 --- a/back/src/main/java/com/back/domain/post/post/entity/Post.java +++ b/back/src/main/java/com/back/domain/post/post/entity/Post.java @@ -42,7 +42,7 @@ public enum PostType { private int viewCount; private Boolean isMento; - private String career; + private String job; private Boolean isResolve; @@ -74,7 +74,7 @@ public Boolean isAuthor( Member member) { } public String getAuthorName() { - return member.getName(); + return member.getNickname(); } @@ -107,4 +107,11 @@ public void increaseViewCount() { public void updateResolveStatus(Boolean isResolve) { this.isResolve = isResolve; } + + public void updateJob(String job) { + if (job == null || job.isBlank()) { + throw new ServiceException("400", "직업은 null이거나 공백일 수 없습니다."); + } + this.job = job; + } } diff --git a/back/src/main/java/com/back/domain/post/post/service/PostService.java b/back/src/main/java/com/back/domain/post/post/service/PostService.java index 39b00161..b385cfd2 100644 --- a/back/src/main/java/com/back/domain/post/post/service/PostService.java +++ b/back/src/main/java/com/back/domain/post/post/service/PostService.java @@ -1,10 +1,7 @@ package com.back.domain.post.post.service; import com.back.domain.member.member.entity.Member; -import com.back.domain.post.post.dto.PostAllResponse; -import com.back.domain.post.post.dto.PostCreateRequest; -import com.back.domain.post.post.dto.PostDto; -import com.back.domain.post.post.dto.PostSingleResponse; +import com.back.domain.post.post.dto.*; import com.back.domain.post.post.entity.Post; import com.back.domain.post.post.repository.PostRepository; import com.back.global.exception.ServiceException; @@ -34,7 +31,7 @@ public List getAllPosts() { @Transactional public Post createPost(PostCreateRequest postCreateRequest, Member member) { - String postTypeStr = postCreateRequest.getPostType(); + String postTypeStr = postCreateRequest.postType(); Post.validPostType(postTypeStr); Post.PostType postType = Post.PostType.valueOf(postTypeStr); @@ -43,20 +40,18 @@ public Post createPost(PostCreateRequest postCreateRequest, Member member) { throw new ServiceException("400", "실무 경험 공유 게시글은 멘토만 작성할 수 있습니다."); } -// if( postType == Post.PostType.PRACTICEPOST ) { -// if(member.getCareer() == null || member.getCareer().isEmpty()) { -// throw new ServiceException("400", "멘토는 경력을 입력해야 실무 경험 공유 게시글을 작성할 수 있습니다."); -// } -// } - Post post = Post.builder() - .title(postCreateRequest.getTitle()) - .content(postCreateRequest.getContent()) + .title(postCreateRequest.title()) + .content(postCreateRequest.content()) .member(member) .postType(postType) .build(); + if(postType == Post.PostType.PRACTICEPOST) { + post.updateJob(postCreateRequest.job()); + } + // PostType이 QUESTIONPOST인 경우 isResolve를 false로 초기화 if(postType == Post.PostType.QUESTIONPOST) { post.updateResolveStatus(false); @@ -67,6 +62,7 @@ public Post createPost(PostCreateRequest postCreateRequest, Member member) { return post; } + @Transactional public void removePost(Long postId, Member member) { Post post = findById(postId); @@ -80,16 +76,16 @@ public void updatePost(long postId, Member member, @Valid PostCreateRequest post Post post = findById(postId); if (!post.isAuthor(member)) throw new ServiceException("400", "수정 권한이 없습니다."); - if ( postCreateRequest.getTitle() == null || postCreateRequest.getTitle().isBlank()) { + if ( postCreateRequest.title() == null || postCreateRequest.title().isBlank()) { throw new ServiceException("400", "제목을 입력해주세요."); } - if ( postCreateRequest.getContent() == null || postCreateRequest.getContent().isBlank()) { + if ( postCreateRequest.content() == null || postCreateRequest.content().isBlank()) { throw new ServiceException("400", "내용을 입력해주세요."); } - post.updateTitle(postCreateRequest.getTitle()); - post.updateContent(postCreateRequest.getContent()); + post.updateTitle(postCreateRequest.title()); + post.updateContent(postCreateRequest.content()); postRepository.save(post); } @@ -117,13 +113,13 @@ public Post findById(Long postId) { public PostSingleResponse makePostSingleResponse(Long postId) { Post post = postRepository.findById(postId).orElseThrow(() -> new ServiceException("400", "해당 Id의 게시글이 없습니다.")); - PostSingleResponse postSingleResponse = new PostSingleResponse(post); + PostSingleResponse postSingleResponse = PostSingleResponse.from(post); return postSingleResponse; } public List getAllPostResponse() { return postRepository.findAllWithMember().stream() - .map(PostAllResponse::new) + .map(PostAllResponse::from) .toList(); } 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 new file mode 100644 index 00000000..64433b52 --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapController.java @@ -0,0 +1,46 @@ +package com.back.domain.roadmap.roadmap.controller; + +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapListResponse; +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapPagingResponse; +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapResponse; +import com.back.domain.roadmap.roadmap.service.JobRoadmapService; +import com.back.global.rsData.RsData; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/job-roadmaps") +@RequiredArgsConstructor +@Tag(name = "JobRoadmap Controller", description = "직업별 통합 로드맵 관련 API") +public class JobRoadmapController { + private final JobRoadmapService jobRoadmapService; + + @GetMapping + @Operation( + summary = "직업 로드맵 다건 조회", + description = "직업 로드맵 목록을 페이징과 키워드 검색으로 조회합니다." + ) + public RsData getJobRoadmaps( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String keyword + ) { + Page jobRoadmapPage = jobRoadmapService.getJobRoadmaps(keyword, page, size); + JobRoadmapPagingResponse response = JobRoadmapPagingResponse.from(jobRoadmapPage); + + return new RsData<>("200", "직업 로드맵 목록 조회 성공", response); + } + + @GetMapping("/{id}") + @Operation( + summary = "직업 로드맵 상세 조회", + description = "특정 직업 로드맵의 상세 정보(직업 정보 + 모든 노드)를 조회합니다." + ) + public RsData getJobRoadmapById(@PathVariable Long id) { + JobRoadmapResponse roadmap = jobRoadmapService.getJobRoadmapById(id); + return new RsData<>("200", "직업 로드맵 상세 조회 성공", roadmap); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapListResponse.java b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapListResponse.java new file mode 100644 index 00000000..5cafedee --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapListResponse.java @@ -0,0 +1,26 @@ +package com.back.domain.roadmap.roadmap.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "직업 로드맵 다건 조회 응답") +public record JobRoadmapListResponse( + @Schema(description = "로드맵 ID") + Long id, + + @Schema(description = "직업명") + String jobName, + + @Schema(description = "직업 설명 (150자 제한)") + String jobDescription +) { + // 정적 팩토리 메서드 + public static JobRoadmapListResponse of(Long id, String jobName, String jobDescription) { + return new JobRoadmapListResponse(id, jobName, truncateDescription(jobDescription)); + } + + // description 자르기 로직 (150자 초과 시 "..." 추가) + private static String truncateDescription(String description) { + if (description == null) return null; + return description.length() > 150 ? description.substring(0, 150) + "..." : description; + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapNodeResponse.java b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapNodeResponse.java new file mode 100644 index 00000000..1e925d82 --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapNodeResponse.java @@ -0,0 +1,52 @@ +package com.back.domain.roadmap.roadmap.dto.response; + +import com.back.domain.roadmap.roadmap.entity.RoadmapNode; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.List; + +public record JobRoadmapNodeResponse( + Long id, + Long parentId, // 부모 노드 ID (null이면 루트 노드) + List childIds, // 자식 노드 ID 목록 (프론트엔드 렌더링용) + Long taskId, // Task와 연결된 경우의 표준 Task ID + String taskName, // 표시용 Task 이름 + String description, + int stepOrder, + int level, // 트리 깊이 (0: 루트, 1: 1단계 자식...) + boolean isLinkedToTask, + Double weight, // 이 노드의 가중치 (JobRoadmapNodeStat에서) + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + List children +) { + + // 정적 팩토리 메서드 - RoadmapNode로부터 Response DTO 생성 (자식 노드 정보 포함) + public static JobRoadmapNodeResponse from(RoadmapNode node, List children) { + List childIds = children != null ? + children.stream().map(JobRoadmapNodeResponse::id).toList() : + List.of(); + + return new JobRoadmapNodeResponse( + node.getId(), + node.getParent() != null ? node.getParent().getId() : null, + childIds, + node.getTask() != null ? node.getTask().getId() : null, + node.getTask() != null ? node.getTask().getName() : node.getTaskName(), + node.getDescription(), + node.getStepOrder(), + node.getLevel(), + node.getTask() != null, + null, // weight는 서비스에서 별도로 설정 + children != null ? children : List.of() + ); + } + + // 가중치 설정 헬퍼 메서드 (불변 객체이므로 새 인스턴스 반환) + public JobRoadmapNodeResponse withWeight(Double weight) { + return new JobRoadmapNodeResponse( + this.id, this.parentId, this.childIds, this.taskId, this.taskName, this.description, + this.stepOrder, this.level, this.isLinkedToTask, weight, this.children + ); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapPagingResponse.java b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapPagingResponse.java new file mode 100644 index 00000000..1f3edf3d --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapPagingResponse.java @@ -0,0 +1,34 @@ +package com.back.domain.roadmap.roadmap.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Schema(description = "직업 로드맵 페이징 조회 응답") +public record JobRoadmapPagingResponse( + @Schema(description = "직업 로드맵 목록") + List jobRoadmaps, + + @Schema(description = "현재 페이지 (0부터 시작)") + int currentPage, + + @Schema(description = "총 페이지") + int totalPage, + + @Schema(description = "총 개수") + long totalElements, + + @Schema(description = "다음 페이지 존재 여부") + boolean hasNext +) { + public static JobRoadmapPagingResponse from(Page page) { + return new JobRoadmapPagingResponse( + page.getContent(), + page.getNumber(), + page.getTotalPages(), + page.getTotalElements(), + page.hasNext() + ); + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapResponse.java b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapResponse.java new file mode 100644 index 00000000..d33a5849 --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/dto/response/JobRoadmapResponse.java @@ -0,0 +1,82 @@ +package com.back.domain.roadmap.roadmap.dto.response; + +import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import com.back.domain.roadmap.roadmap.entity.RoadmapNode; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public record JobRoadmapResponse( + Long id, + Long jobId, + String jobName, + List nodes, + int totalNodeCount, + LocalDateTime createdDate, + LocalDateTime modifiedDate +) { + + // 정적 팩터리 메서드 - JobRoadmap과 Job 정보로부터 Response DTO 생성 + public static JobRoadmapResponse from(JobRoadmap jobRoadmap, String jobName) { + // 부모-자식 관계 맵 생성 + Map> childrenMap = jobRoadmap.getNodes().stream() + .filter(node -> node.getParent() != null) + .collect(Collectors.groupingBy(node -> node.getParent().getId())); + + // 노드를 재귀적으로 변환하는 함수 + Map nodeResponseMap = new HashMap<>(); + + // 모든 노드를 bottom-up 방식으로 변환 (자식부터 부모 순서) + buildNodeResponses(jobRoadmap.getNodes(), childrenMap, nodeResponseMap); + + // 루트 노드들만 반환 (자식 노드들은 children 필드에 포함되어 전체 트리 구조 제공) + List nodes = jobRoadmap.getNodes().stream() + .filter(node -> node.getParent() == null) + .map(node -> nodeResponseMap.get(node.getId())) + .sorted((a, b) -> { + int levelCompare = Integer.compare(a.level(), b.level()); + return levelCompare != 0 ? levelCompare : Integer.compare(a.stepOrder(), b.stepOrder()); + }) + .toList(); + + return new JobRoadmapResponse( + jobRoadmap.getId(), + jobRoadmap.getJob().getId(), + jobName, + nodes, + jobRoadmap.getNodes().size(), + jobRoadmap.getCreateDate(), + jobRoadmap.getModifyDate() + ); + } + + // 노드 응답 객체들을 재귀적으로 구성하는 헬퍼 메서드 + private static void buildNodeResponses( + List allNodes, + Map> childrenMap, + Map nodeResponseMap) { + + // 노드들을 level 역순으로 정렬 (깊은 노드부터 처리) + List sortedNodes = allNodes.stream() + .sorted((a, b) -> Integer.compare(b.getLevel(), a.getLevel())) + .toList(); + + for (RoadmapNode node : sortedNodes) { + // 자식 노드들의 응답 객체 가져오기 + List childResponses = childrenMap + .getOrDefault(node.getId(), List.of()) + .stream() + .map(child -> nodeResponseMap.get(child.getId())) + .filter(response -> response != null) + .sorted((a, b) -> Integer.compare(a.stepOrder(), b.stepOrder())) + .toList(); + + // 현재 노드의 응답 객체 생성 + JobRoadmapNodeResponse nodeResponse = JobRoadmapNodeResponse.from(node, childResponses); + nodeResponseMap.put(node.getId(), nodeResponse); + } + } +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmap.java b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmap.java index 50d678e9..d62a74a7 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmap.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmap.java @@ -1,10 +1,11 @@ package com.back.domain.roadmap.roadmap.entity; +import com.back.domain.job.job.entity.Job; import com.back.global.jpa.BaseEntity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; import org.hibernate.annotations.SQLRestriction; import java.util.ArrayList; @@ -12,21 +13,23 @@ @Entity @Table(name = "job_roadmap") -@Getter @Setter +@Getter @NoArgsConstructor public class JobRoadmap extends BaseEntity { - @Column(name = "job_id", nullable = false) - private Long jobId; // Job FK + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "job_id", nullable = false) + private Job job; - @OneToMany(fetch = FetchType.LAZY) - @JoinColumn(name = "roadmap_id") // RoadmapNode.roadmapId 참조 + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + @JoinColumn(name = "roadmap_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @SQLRestriction("roadmap_type = 'JOB'") - @OrderBy("stepOrder ASC") + @OrderBy("level ASC, stepOrder ASC") private List nodes; - public JobRoadmap(Long jobId) { - this.jobId = jobId; - this.nodes = new ArrayList<>(); + @Builder + public JobRoadmap(Job job, List nodes) { + this.job = job; + this.nodes = nodes != null ? nodes : new ArrayList<>(); } public RoadmapNode getRootNode() { diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmapNodeStat.java b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmapNodeStat.java index bc75b026..cbdbd117 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmapNodeStat.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/JobRoadmapNodeStat.java @@ -2,22 +2,45 @@ import com.back.global.jpa.BaseEntity; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import lombok.Setter; @Entity @Table(name = "job_roadmap_node_stat") -@Getter @Setter +@Getter @NoArgsConstructor public class JobRoadmapNodeStat extends BaseEntity { @Column(name = "step_order") private Integer stepOrder; @Column(name = "weight", nullable = false) - private Double weight = 0.0; + private Double weight; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "node_id", nullable = false) private RoadmapNode node; + + // ---- 추가 통계 필드 ---- + @Column(name = "average_position") + private Double averagePosition; // 각 노드가 멘토 로드맵에서 평균적으로 위치한 인덱스(1..N) + + @Column(name = "mentor_count") + private Integer mentorCount; // 몇 명의 멘토 로드맵에 등장했는지 (unique mentor count) + + @Column(name = "outgoing_transitions") + private Integer outgoingTransitions; // 이 노드에서 다른 노드로 이동한 총 전이수 + + @Column(name = "incoming_transitions") + private Integer incomingTransitions; // 타 노드에서 이 노드로 들어오는 전이수 + + @Column(name = "transition_counts", columnDefinition = "TEXT") + private String transitionCounts; // (선택) JSON 직렬화: { "T:5":3, "T:7":1 } 형태로 보관 가능 + + @Builder + public JobRoadmapNodeStat(Integer stepOrder, Double weight, RoadmapNode node) { + this.stepOrder = stepOrder; + this.weight = weight != null ? weight : 0.0; + this.node = node; + } } diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/MentorRoadmap.java b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/MentorRoadmap.java index 4fd29adc..4f121492 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/MentorRoadmap.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/MentorRoadmap.java @@ -26,7 +26,7 @@ public class MentorRoadmap extends BaseEntity { private Mentor mentor; @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) - @JoinColumn(name = "roadmap_id") + @JoinColumn(name = "roadmap_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) @SQLRestriction("roadmap_type = 'MENTOR'") @OrderBy("stepOrder ASC") private List nodes; diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/RoadmapNode.java b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/RoadmapNode.java index d7225976..d9ef1b20 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/entity/RoadmapNode.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/entity/RoadmapNode.java @@ -12,8 +12,8 @@ @Entity @Table(name = "roadmap_node", indexes = { - // 노드 순회용 인덱스 - @Index(name = "idx_roadmap_composite", columnList = "roadmap_id, roadmap_type, step_order"), + // 노드 순회용 인덱스 (level, stepOrder 순 정렬) + @Index(name = "idx_roadmap_composite", columnList = "roadmap_id, roadmap_type, level, step_order"), @Index(name = "idx_roadmap_parent", columnList = "roadmap_id, roadmap_type, parent_id") }) @Getter @@ -36,6 +36,9 @@ public class RoadmapNode extends BaseEntity { @Column(name = "step_order", nullable = false) private int stepOrder = 0; + @Column(name = "level", nullable = false) + private int level = 0; // 트리 깊이 (루트: 0, 자식: 부모 + 1) + @Column(name = "raw_task_name") private String taskName; // Task 이름 표시값(DB에 없는 Task 입력시 입력값 그대로 출력) @@ -52,11 +55,12 @@ public enum RoadmapType { // Builder 패턴 적용된 생성자 @Builder - public RoadmapNode(String taskName, String description, Task task, int stepOrder, long roadmapId, RoadmapType roadmapType) { + public RoadmapNode(String taskName, String description, Task task, int stepOrder, int level, long roadmapId, RoadmapType roadmapType) { this.taskName = taskName; this.description = description; this.task = task; this.stepOrder = stepOrder; + this.level = level; this.roadmapId = roadmapId; this.roadmapType = roadmapType; } @@ -71,9 +75,14 @@ public void addChild(RoadmapNode child) { } this.children.add(child); child.setParent(this); + child.setLevel(this.level + 1); // 부모 level + 1로 자동 설정 } private void setParent(RoadmapNode parent) { this.parent = parent; } + + private void setLevel(int level) { + this.level = level; + } } diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/repository/JobRoadmapRepository.java b/back/src/main/java/com/back/domain/roadmap/roadmap/repository/JobRoadmapRepository.java new file mode 100644 index 00000000..36d23518 --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/repository/JobRoadmapRepository.java @@ -0,0 +1,38 @@ +package com.back.domain.roadmap.roadmap.repository; + +import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface JobRoadmapRepository extends JpaRepository { + + @Query("SELECT jr FROM JobRoadmap jr JOIN FETCH jr.job") + List findAllWithJob(); + + @Query(""" + SELECT jr FROM JobRoadmap jr + JOIN FETCH jr.job j + WHERE (:keyword IS NULL OR :keyword = '' OR + LOWER(j.name) LIKE LOWER(CONCAT('%', :keyword, '%')) OR + LOWER(j.description) LIKE LOWER(CONCAT('%', :keyword, '%'))) + """) + Page findAllWithJobAndKeyword(@Param("keyword") String keyword, Pageable pageable); + + @Query(""" + SELECT jr FROM JobRoadmap jr + JOIN FETCH jr.job + LEFT JOIN FETCH jr.nodes n + LEFT JOIN FETCH n.task t + WHERE jr.id = :id + ORDER BY n.level, n.stepOrder""") + Optional findByIdWithJobAndNodes(@Param("id") Long id); + +} \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/repository/MentorRoadmapRepository.java b/back/src/main/java/com/back/domain/roadmap/roadmap/repository/MentorRoadmapRepository.java index 0492ffd0..c81cfd7c 100644 --- a/back/src/main/java/com/back/domain/roadmap/roadmap/repository/MentorRoadmapRepository.java +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/repository/MentorRoadmapRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface MentorRoadmapRepository extends JpaRepository { @@ -38,4 +39,17 @@ public interface MentorRoadmapRepository extends JpaRepository findAllByMentorJobIdWithNodes(@Param("jobId") Long jobId); } \ No newline at end of file diff --git a/back/src/main/java/com/back/domain/roadmap/roadmap/service/JobRoadmapService.java b/back/src/main/java/com/back/domain/roadmap/roadmap/service/JobRoadmapService.java new file mode 100644 index 00000000..6dfe4e08 --- /dev/null +++ b/back/src/main/java/com/back/domain/roadmap/roadmap/service/JobRoadmapService.java @@ -0,0 +1,53 @@ +package com.back.domain.roadmap.roadmap.service; + +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapListResponse; +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapResponse; +import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import com.back.domain.roadmap.roadmap.repository.JobRoadmapRepository; +import com.back.global.exception.ServiceException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Slf4j +@Transactional(readOnly = true) +public class JobRoadmapService { + private final JobRoadmapRepository jobRoadmapRepository; + + public List getAllJobRoadmaps() { + return jobRoadmapRepository.findAllWithJob() + .stream() + .map(this::toListResponse) + .toList(); + } + + public Page getJobRoadmaps(String keyword, int page, int size) { + Pageable pageable = PageRequest.of(page, size); + + return jobRoadmapRepository.findAllWithJobAndKeyword(keyword, pageable) + .map(this::toListResponse); + } + + public JobRoadmapResponse getJobRoadmapById(Long id) { + JobRoadmap jobRoadmap = jobRoadmapRepository.findByIdWithJobAndNodes(id) + .orElseThrow(() -> new ServiceException("404", "직업 로드맵을 찾을 수 없습니다.")); + + return JobRoadmapResponse.from(jobRoadmap, jobRoadmap.getJob().getName()); + } + + private JobRoadmapListResponse toListResponse(JobRoadmap jobRoadmap) { + return JobRoadmapListResponse.of( + jobRoadmap.getId(), + jobRoadmap.getJob().getName(), + jobRoadmap.getJob().getDescription() + ); + } +} \ No newline at end of file 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 7f5af7b6..97bc52af 100644 --- a/back/src/main/java/com/back/global/initData/RoadmapInitData.java +++ b/back/src/main/java/com/back/global/initData/RoadmapInitData.java @@ -9,6 +9,9 @@ import com.back.domain.member.mentor.repository.MentorRepository; 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.RoadmapNode; +import com.back.domain.roadmap.roadmap.repository.JobRoadmapRepository; import com.back.domain.roadmap.roadmap.repository.MentorRoadmapRepository; import com.back.domain.roadmap.roadmap.service.MentorRoadmapService; import com.back.domain.roadmap.task.entity.Task; @@ -22,16 +25,6 @@ import java.util.List; -/** - * RoadmapInitData (풀버전) - * - * - 모든 멘토 로드맵 상단에 '기초' 항목을 추가합니다. - * - Mini Project(권장 실습)는 Task가 아니라 각 노드의 description에 구체적으로 적습니다. - * - Task가 DB에 없으면 자동 생성하도록 createNodeRequest에서 안전장치 적용. - * - * 목적: 청소년(고등학생) 대상의 서비스에서 기초를 우선적으로 확보하고, - * 통합(직업) 로드맵 생성시 의미있는 통계가 나오도록 샘플을 보강합니다. - */ @Configuration @RequiredArgsConstructor @Transactional @@ -44,6 +37,7 @@ public class RoadmapInitData { private final MentorRepository mentorRepository; private final MentorRoadmapService mentorRoadmapService; private final MentorRoadmapRepository mentorRoadmapRepository; + private final JobRoadmapRepository jobRoadmapRepository; @Bean ApplicationRunner baseInitDataApplicationRunner2() { @@ -55,6 +49,7 @@ public void runInitData() { initJobData(); initTaskData(); // 보강된 Task 목록 //initSampleMentorRoadmaps(); // 활성화: 다양한 멘토 로드맵 생성 + //initSampleJobRoadmap(); // 직업 로드맵 조회 API 테스트용 샘플 데이터 } // --- Job 초기화 --- @@ -538,4 +533,140 @@ private RoadmapNodeRequest createNodeRequest(String taskName, int level, int ste stepOrder ); } + + // --- 직업 로드맵 샘플 데이터 생성 (API 테스트용) --- + public void initSampleJobRoadmap() { + if (jobRoadmapRepository.count() > 0) return; + + Job backendJob = jobRepository.findByName("백엔드 개발자") + .orElseThrow(() -> new RuntimeException("백엔드 개발자 직업을 찾을 수 없습니다.")); + + Job frontendJob = jobRepository.findByName("프론트엔드 개발자") + .orElseThrow(() -> new RuntimeException("프론트엔드 개발자 직업을 찾을 수 없습니다.")); + + // 백엔드 개발자 직업 로드맵 생성 (트리 구조로 구성) + JobRoadmap jobRoadmap = JobRoadmap.builder() + .job(backendJob) + .build(); + jobRoadmap = jobRoadmapRepository.save(jobRoadmap); + + // 다건 조회 확인용 프론트엔드 개발자 직업 로드맵 생성 (빈 로드맵) + JobRoadmap frontendRoadmap = JobRoadmap.builder() + .job(frontendJob) + .build(); + + // 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 springBoot = taskRepository.findByNameIgnoreCase("Spring Boot").orElse(null); + Task mysql = taskRepository.findByNameIgnoreCase("MySQL").orElse(null); + Task jpa = taskRepository.findByNameIgnoreCase("JPA").orElse(null); + Task docker = taskRepository.findByNameIgnoreCase("Docker").orElse(null); + Task aws = taskRepository.findByNameIgnoreCase("AWS").orElse(null); + + // 트리 구조로 노드 생성 (루트 노드들과 자식 노드들) + + // 루트 노드 1: Programming Fundamentals (level=0, stepOrder=1) + RoadmapNode fundamentalsNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(programmingFundamentals) + .taskName("Programming Fundamentals") + .description("프로그래밍의 기초 개념: 변수, 조건문, 반복문, 함수 등을 이해하고 활용할 수 있습니다.") + .stepOrder(1) + .level(0) + .build(); + + // 루트 노드 2: Git (level=0, stepOrder=2) + RoadmapNode gitNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(git) + .taskName("Git") + .description("버전 관리 시스템으로 코드 히스토리 관리 및 협업을 위한 필수 도구입니다.") + .stepOrder(2) + .level(0) + .build(); + + // Fundamentals의 자식 노드들 + RoadmapNode javaNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(java) + .taskName("Java") + .description("객체지향 프로그래밍 언어로 백엔드 개발의 기초가 되는 언어입니다.") + .stepOrder(1) + .level(1) + .build(); + + RoadmapNode springBootNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(springBoot) + .taskName("Spring Boot") + .description("Java 기반의 웹 애플리케이션 프레임워크로 REST API 개발에 필수입니다.") + .stepOrder(2) + .level(1) + .build(); + + // Java의 자식 노드들 + RoadmapNode mysqlNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(mysql) + .taskName("MySQL") + .description("관계형 데이터베이스로 데이터 저장 및 관리를 위한 기본 기술입니다.") + .stepOrder(1) + .level(2) + .build(); + + RoadmapNode jpaNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(jpa) + .taskName("JPA") + .description("Java 진영의 ORM 기술로 객체와 관계형 데이터베이스를 매핑합니다.") + .stepOrder(2) + .level(2) + .build(); + + // Spring Boot의 자식 노드들 + RoadmapNode dockerNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(docker) + .taskName("Docker") + .description("컨테이너 기술로 애플리케이션 배포 및 환경 관리를 간소화합니다.") + .stepOrder(1) + .level(2) + .build(); + + RoadmapNode awsNode = RoadmapNode.builder() + .roadmapId(jobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(aws) + .taskName("AWS") + .description("클라우드 서비스로 애플리케이션을 확장 가능하게 배포하고 운영합니다.") + .stepOrder(2) + .level(2) + .build(); + + // 트리 구조 연결 (addChild 메서드 사용) + fundamentalsNode.addChild(javaNode); + fundamentalsNode.addChild(springBootNode); + javaNode.addChild(mysqlNode); + javaNode.addChild(jpaNode); + springBootNode.addChild(dockerNode); + springBootNode.addChild(awsNode); + + // 모든 노드를 JobRoadmap에 추가 + jobRoadmap.getNodes().addAll(List.of( + fundamentalsNode, gitNode, javaNode, springBootNode, + mysqlNode, jpaNode, dockerNode, awsNode + )); + + jobRoadmapRepository.save(jobRoadmap); + jobRoadmapRepository.save(frontendRoadmap); // 빈 로드맵 저장 + } } diff --git a/back/src/test/java/com/back/domain/mentoring/reservation/entity/ReservationTest.java b/back/src/test/java/com/back/domain/mentoring/reservation/entity/ReservationTest.java index e4643496..02e2dd79 100644 --- a/back/src/test/java/com/back/domain/mentoring/reservation/entity/ReservationTest.java +++ b/back/src/test/java/com/back/domain/mentoring/reservation/entity/ReservationTest.java @@ -34,16 +34,16 @@ class ReservationTest { @BeforeEach void setUp() { - Member mentorMember = MemberFixture.create("mentor@test.com", "Mentor", "pass123"); + Member mentorMember = MemberFixture.create(1L, "mentor@test.com", "Mentor", "pass123", Member.Role.MENTOR); mentor = MentorFixture.create(1L, mentorMember); - Member otherMentorMember = MemberFixture.create("other@test.com", "Other", "pass123"); + Member otherMentorMember = MemberFixture.create(2L, "other@test.com", "Other", "pass123", Member.Role.MENTOR); otherMentor = MentorFixture.create(2L, otherMentorMember); - Member menteeMember = MemberFixture.create("mentee@test.com", "Mentee", "pass123"); + Member menteeMember = MemberFixture.create(3L, "mentee@test.com", "Mentee", "pass123", Member.Role.MENTEE); mentee = MenteeFixture.create(1L, menteeMember); - Member otherMenteeMember = MemberFixture.create("other_mentee@test.com", "OtherMentee", "pass123"); + Member otherMenteeMember = MemberFixture.create(4L, "other_mentee@test.com", "OtherMentee", "pass123", Member.Role.MENTEE); otherMentee = MenteeFixture.create(2L, otherMenteeMember); mentoring = MentoringFixture.create(1L, mentor); @@ -145,7 +145,7 @@ class Describe_cancelByMentor { @DisplayName("취소 성공 - PENDING") void cancelPending() { // when - reservation.cancel(mentor); + reservation.cancel(mentor.getMember()); // then assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CANCELED); @@ -160,7 +160,7 @@ void cancelApproved() { reservation.approve(mentor); // when - reservation.cancel(mentor); + reservation.cancel(mentor.getMember()); // then assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CANCELED); @@ -176,7 +176,7 @@ void throwExceptionWhenCannotCancel() { ReflectionTestUtils.setField(reservation, "status", ReservationStatus.COMPLETED); // when & then - assertThatThrownBy(() -> reservation.cancel(mentor)) + assertThatThrownBy(() -> reservation.cancel(mentor.getMember())) .isInstanceOf(ServiceException.class) .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CANNOT_CANCEL.getCode()); } @@ -190,7 +190,7 @@ class Describe_cancelByMentee { @DisplayName("취소 성공 - PENDING") void cancelPending() { // when - reservation.cancel(mentee); + reservation.cancel(mentee.getMember()); // then assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CANCELED); @@ -205,7 +205,7 @@ void cancelApproved() { reservation.approve(mentor); // when - reservation.cancel(mentee); + reservation.cancel(mentee.getMember()); // then assertThat(reservation.getStatus()).isEqualTo(ReservationStatus.CANCELED); @@ -216,9 +216,9 @@ void cancelApproved() { @Test @DisplayName("다른 멘티가 취소하려고 하면 예외") void throwExceptionWhenNotMentee() { - assertThatThrownBy(() -> reservation.cancel(otherMentee)) + assertThatThrownBy(() -> reservation.cancel(otherMentee.getMember())) .isInstanceOf(ServiceException.class) - .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_MENTEE.getCode()); + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_PARTICIPANT.getCode()); } @Test @@ -229,7 +229,7 @@ void throwExceptionWhenCannotCancel() { ReflectionTestUtils.setField(reservation, "status", ReservationStatus.COMPLETED); // when & then - assertThatThrownBy(() -> reservation.cancel(mentee)) + assertThatThrownBy(() -> reservation.cancel(mentee.getMember())) .isInstanceOf(ServiceException.class) .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CANNOT_CANCEL.getCode()); } diff --git a/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java b/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java index e429cad1..fff1a4f3 100644 --- a/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java +++ b/back/src/test/java/com/back/domain/mentoring/reservation/service/ReservationServiceTest.java @@ -6,6 +6,7 @@ import com.back.domain.mentoring.mentoring.entity.Mentoring; import com.back.domain.mentoring.mentoring.service.MentoringStorage; import com.back.domain.mentoring.reservation.constant.ReservationStatus; +import com.back.domain.mentoring.reservation.dto.ReservationDto; import com.back.domain.mentoring.reservation.dto.request.ReservationRequest; import com.back.domain.mentoring.reservation.dto.response.ReservationResponse; import com.back.domain.mentoring.reservation.entity.Reservation; @@ -29,6 +30,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; @@ -61,13 +66,13 @@ class ReservationServiceTest { @BeforeEach void setUp() { - Member mentorMember = MemberFixture.create("mentor@test.com", "Mentor", "pass123"); + Member mentorMember = MemberFixture.create(1L, "mentor@test.com", "Mentor", "pass123", Member.Role.MENTOR); mentor = MentorFixture.create(1L, mentorMember); - Member menteeMember = MemberFixture.create("mentee@test.com", "Mentee", "pass123"); + Member menteeMember = MemberFixture.create(2L, "mentee@test.com", "Mentee", "pass123", Member.Role.MENTEE); mentee = MenteeFixture.create(1L, menteeMember); - Member menteeMember2 = MemberFixture.create("mentee2@test.com", "Mentee2", "pass123"); + Member menteeMember2 = MemberFixture.create(3L, "mentee2@test.com", "Mentee2", "pass123", Member.Role.MENTEE); mentee2 = MenteeFixture.create(2L, menteeMember2); mentoring = MentoringFixture.create(1L, mentor); @@ -76,6 +81,84 @@ void setUp() { reservation = ReservationFixture.create(1L, mentoring, mentee, mentorSlot2); } + @Nested + @DisplayName("멘토링 예약 목록 조회") + class Describe_getReservations { + + @Test + void getReservations() { + // given + int page = 1; + int size = 5; + Pageable pageable = PageRequest.of(page, size); + + Page reservationPage = new PageImpl<>( + List.of(reservation), + pageable, + 10 + ); + + when(reservationRepository.findAllByMentorMember(mentor.getMember(), pageable)) + .thenReturn(reservationPage); + + // when + Page result = reservationService.getReservations( + mentor.getMember(), + page, + size + ); + + // then + assertThat(result.getNumber()).isEqualTo(1); + assertThat(result.getSize()).isEqualTo(5); + assertThat(result.getTotalElements()).isEqualTo(10); + assertThat(result.getTotalPages()).isEqualTo(2); + verify(reservationRepository).findAllByMentorMember(mentor.getMember(), pageable); + } + } + + @Nested + @DisplayName("멘토링 예약 조회") + class Describe_getReservation { + + @Test + void getReservation() { + // given + Long reservationId = reservation.getId(); + + when(reservationRepository.findByIdAndMember(reservationId, mentor.getMember())) + .thenReturn(Optional.of(reservation)); + + // when + ReservationResponse response = reservationService.getReservation( + mentor.getMember(), + reservationId + ); + + // then + assertThat(response).isNotNull(); + assertThat(response.mentoring().mentoringId()).isEqualTo(mentoring.getId()); + assertThat(response.mentee().menteeId()).isEqualTo(mentee.getId()); + assertThat(response.mentor().mentorId()).isEqualTo(mentor.getId()); + assertThat(response.reservation().mentorSlotId()).isEqualTo(mentorSlot2.getId()); + verify(reservationRepository).findByIdAndMember(reservationId, mentor.getMember()); + } + + @Test + @DisplayName("권한이 없을 경우 예외") + void getReservation_notAccessible() { + // given + when(reservationRepository.findByIdAndMember(reservation.getId(), mentee2.getMember())) + .thenReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> reservationService.getReservation(mentee2.getMember(), reservation.getId())) + .isInstanceOf(ServiceException.class) + .hasFieldOrPropertyWithValue("resultCode", + ReservationErrorCode.RESERVATION_NOT_ACCESSIBLE.getCode()); + } + } + @Nested @DisplayName("멘토링 예약 생성") class Describe_createReservation { @@ -339,7 +422,7 @@ void cancelReservationByMentor() { .thenReturn(reservation); // when - ReservationResponse result = reservationService.cancelReservation(mentor, reservation.getId()); + ReservationResponse result = reservationService.cancelReservation(mentor.getMember(), reservation.getId()); // then assertThat(result.reservation().status()).isEqualTo(ReservationStatus.CANCELED); @@ -353,7 +436,7 @@ void cancelReservationByMentee() { .thenReturn(reservation); // when - ReservationResponse result = reservationService.cancelReservation(mentee, reservation.getId()); + ReservationResponse result = reservationService.cancelReservation(mentee.getMember(), reservation.getId()); // then assertThat(result.reservation().status()).isEqualTo(ReservationStatus.CANCELED); @@ -376,7 +459,7 @@ void throwExceptionWhenCompleted() { .thenReturn(completedReservation); // when & then - assertThatThrownBy(() -> reservationService.cancelReservation(mentor, completedReservation.getId())) + assertThatThrownBy(() -> reservationService.cancelReservation(mentor.getMember(), completedReservation.getId())) .isInstanceOf(ServiceException.class) .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.CANNOT_CANCEL.getCode()); } @@ -391,9 +474,9 @@ void throwExceptionWhenNotMentor() { .thenReturn(reservation); // when & then - assertThatThrownBy(() -> reservationService.cancelReservation(anotherMentor, reservation.getId())) + assertThatThrownBy(() -> reservationService.cancelReservation(anotherMentor.getMember(), reservation.getId())) .isInstanceOf(ServiceException.class) - .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_MENTOR.getCode()); + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_PARTICIPANT.getCode()); } @Test @@ -404,9 +487,9 @@ void throwExceptionWhenNotMentee() { .thenReturn(reservation); // when & then - assertThatThrownBy(() -> reservationService.cancelReservation(mentee2, reservation.getId())) + assertThatThrownBy(() -> reservationService.cancelReservation(mentee2.getMember(), reservation.getId())) .isInstanceOf(ServiceException.class) - .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_MENTEE.getCode()); + .hasFieldOrPropertyWithValue("resultCode", ReservationErrorCode.FORBIDDEN_NOT_PARTICIPANT.getCode()); } } } diff --git a/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java b/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java index 67df8bfe..22f1134d 100644 --- a/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java +++ b/back/src/test/java/com/back/domain/post/comment/service/PostCommentServiceTest.java @@ -50,8 +50,7 @@ void createComment_success() { Member member = MemberFixture.create(1L, "user@test.com", "User", "password", Member.Role.MENTEE); Post post = createDefaultPost(member); Long postId = 1L; - CommentCreateRequest request = new CommentCreateRequest(); - request.setComment("테스트 댓글"); + CommentCreateRequest request = new CommentCreateRequest("MENTEE","테스트 댓글"); when(postRepository.findById(postId)).thenReturn(Optional.of(post)); when(postCommentRepository.save(any(PostComment.class))).thenReturn(any(PostComment.class)); @@ -70,8 +69,7 @@ void createComment_postNotExists_failure() { // given Member member = MemberFixture.createDefault(); Long postId = 999L; - CommentCreateRequest request = new CommentCreateRequest(); - request.setComment("테스트 댓글"); + CommentCreateRequest request = new CommentCreateRequest("MENTEE","테스트 댓글"); when(postRepository.findById(postId)).thenReturn(Optional.empty()); @@ -144,8 +142,8 @@ void removePostComment_author_success() { PostComment comment = createComment(author, post, "삭제할 댓글"); Long postId = 1L; Long commentId = 1L; - CommentDeleteRequest request = new CommentDeleteRequest(); - request.setCommentId(commentId); + CommentDeleteRequest request = new CommentDeleteRequest(commentId); + when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); @@ -167,8 +165,8 @@ void removePostComment_notAuthor_failure() { PostComment comment = createComment(author, post, "삭제할 댓글"); Long postId = 1L; Long commentId = 1L; - CommentDeleteRequest request = new CommentDeleteRequest(); - request.setCommentId(commentId); + CommentDeleteRequest request = new CommentDeleteRequest(commentId); + when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); @@ -188,8 +186,7 @@ void removePostComment_commentNotExists_failure() { Member member = MemberFixture.createDefault(); Long postId = 1L; Long commentId = 999L; - CommentDeleteRequest request = new CommentDeleteRequest(); - request.setCommentId(commentId); + CommentDeleteRequest request = new CommentDeleteRequest(commentId); when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.empty()); @@ -216,9 +213,7 @@ void updatePostComment_author_success() { PostComment comment = createComment(author, post, "원본 댓글"); Long postId = 1L; Long commentId = 1L; - CommentModifyRequest request = new CommentModifyRequest(); - request.setCommentId(commentId); - request.setContent("수정된 댓글"); + CommentModifyRequest request = new CommentModifyRequest(commentId,"수정된 댓글"); when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); @@ -241,9 +236,7 @@ void updatePostComment_notAuthor_failure() { Long postId = 1L; Long commentId = 1L; - CommentModifyRequest request = new CommentModifyRequest(); - request.setCommentId(commentId); - request.setContent("400 : 수정된 댓글"); + CommentModifyRequest request = new CommentModifyRequest(commentId, "400 : 수정된 댓글"); when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); @@ -264,9 +257,8 @@ void updatePostComment_emptyContent_failure() { Long postId = 1L; Long commentId = 1L; - CommentModifyRequest request = new CommentModifyRequest(); - request.setCommentId(commentId); - request.setContent(""); + CommentModifyRequest request = new CommentModifyRequest(commentId, ""); + when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); @@ -286,9 +278,7 @@ void updatePostComment_nullContent_failure() { PostComment comment = createComment(author, post, "원본 댓글"); Long postId = 1L; Long commentId = 1L; - CommentModifyRequest request = new CommentModifyRequest(); - request.setCommentId(commentId); - request.setContent(null); + CommentModifyRequest request = new CommentModifyRequest(commentId, null); when(postRepository.existsById(postId)).thenReturn(true); when(postCommentRepository.findById(commentId)).thenReturn(Optional.of(comment)); diff --git a/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java b/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java index f96b3d8c..a4a87c26 100644 --- a/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java +++ b/back/src/test/java/com/back/domain/post/post/controller/PostControllerTest.java @@ -438,22 +438,22 @@ void t12() throws Exception { .andExpect(jsonPath("$.msg").value("게시글 좋아요 성공")); } - @Test - @DisplayName("게시글 좋아요 조회") - void t13() throws Exception { - ResultActions resultActions = mvc - .perform( - get("/post/{post_id}/liked", 1L) - ) - .andDo(print()); - - resultActions - .andExpect(handler().handlerType(PostController.class)) - .andExpect(handler().methodName("getLike")) - .andExpect(jsonPath("$.resultCode").value("200")) - .andExpect(jsonPath("$.msg").value("게시글 좋아요 조회 성공")) - .andExpect(jsonPath("$.data.likeCount").exists()); - } +// @Test +// @DisplayName("게시글 좋아요 조회") +// void t13() throws Exception { +// ResultActions resultActions = mvc +// .perform( +// get("/post/{post_id}/liked", 1L) +// ) +// .andDo(print()); +// +// resultActions +// .andExpect(handler().handlerType(PostController.class)) +// .andExpect(handler().methodName("getLike")) +// .andExpect(jsonPath("$.resultCode").value("200")) +// .andExpect(jsonPath("$.msg").value("게시글 좋아요 조회 성공")) +// .andExpect(jsonPath("$.data.likeCount").exists()); +// } @Test @DisplayName("게시글 싫어요 성공") @@ -471,22 +471,22 @@ void t14() throws Exception { .andExpect(jsonPath("$.msg").value("게시글 싫어요 성공")); } - @Test - @DisplayName("게시글 싫어요 조회") - void t15() throws Exception { - ResultActions resultActions = mvc - .perform( - get("/post/{post_id}/disliked", 1L) - ) - .andDo(print()); - - resultActions - .andExpect(handler().handlerType(PostController.class)) - .andExpect(handler().methodName("getDisLike")) - .andExpect(jsonPath("$.resultCode").value("200")) - .andExpect(jsonPath("$.msg").value("게시글 싫어요 조회 성공")) - .andExpect(jsonPath("$.data.likeCount").exists()); - } +// @Test +// @DisplayName("게시글 싫어요 조회") +// void t15() throws Exception { +// ResultActions resultActions = mvc +// .perform( +// get("/post/{post_id}/disliked", 1L) +// ) +// .andDo(print()); +// +// resultActions +// .andExpect(handler().handlerType(PostController.class)) +// .andExpect(handler().methodName("getDisLike")) +// .andExpect(jsonPath("$.resultCode").value("200")) +// .andExpect(jsonPath("$.msg").value("게시글 싫어요 조회 성공")) +// .andExpect(jsonPath("$.data.likeCount").exists()); +// } @Test @DisplayName("좋아요 -> 싫어요 토글 테스트") @@ -539,7 +539,7 @@ void t18() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/Detail/{post_id}", 1L) + get("/post/detail/{post_id}", 1L) ) .andDo(print()); @@ -566,7 +566,7 @@ void t18() throws Exception { void t19() throws Exception { ResultActions resultActions = mvc .perform( - get("/post/Detail/{post_id}", 999L) + get("/post/detail/{post_id}", 999L) ) .andDo(print()); diff --git a/back/src/test/java/com/back/domain/post/post/entity/PostTest.java b/back/src/test/java/com/back/domain/post/post/entity/PostTest.java index 48c4c258..81f0b1ce 100644 --- a/back/src/test/java/com/back/domain/post/post/entity/PostTest.java +++ b/back/src/test/java/com/back/domain/post/post/entity/PostTest.java @@ -259,7 +259,7 @@ void getAuthorName_success() { String authorName = post.getAuthorName(); // then - assertThat(authorName).isEqualTo("Author Name"); + assertThat(authorName).isEqualTo("Test Nickname"); } } diff --git a/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java b/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java index 1f2c6456..1d3e19a2 100644 --- a/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java +++ b/back/src/test/java/com/back/domain/post/post/service/PostServiceTest.java @@ -45,10 +45,7 @@ class CreatePostTest { void createPost_informationPost_success() { // given Member member = MemberFixture.create(1L, "test@test.com", "Test User", "password", Member.Role.MENTEE); - PostCreateRequest request = new PostCreateRequest(); - request.setContent("내용"); - request.setTitle("제목"); - request.setPostType("INFORMATIONPOST"); + PostCreateRequest request = new PostCreateRequest("INFORMATIONPOST","제목","내용",""); Post expectedPost = createPost("제목", "내용", member, Post.PostType.INFORMATIONPOST); @@ -71,10 +68,7 @@ void createPost_informationPost_success() { void createPost_practicePost_mentor_success() { // given Member mentor = MemberFixture.create(1L, "mentor@test.com", "Mentor", "password", Member.Role.MENTOR); - PostCreateRequest request = new PostCreateRequest(); - request.setContent("실무내용"); - request.setTitle("실무경험"); - request.setPostType("PRACTICEPOST"); + PostCreateRequest request = new PostCreateRequest("PRACTICEPOST","실무경험","실무내용","123"); Post expectedPost = createPost("실무 경험", "실무 내용", mentor, Post.PostType.PRACTICEPOST); when(postRepository.save(any(Post.class))).thenReturn(expectedPost); @@ -92,10 +86,7 @@ void createPost_practicePost_mentor_success() { void createPost_practicePost_mentee_failure() { // given Member mentee = MemberFixture.create(1L, "mentee@test.com", "Mentee", "password", Member.Role.MENTEE); - PostCreateRequest request = new PostCreateRequest(); - request.setContent("실무내용"); - request.setTitle("실무경험"); - request.setPostType("PRACTICEPOST"); + PostCreateRequest request = new PostCreateRequest("PRACTICEPOST","실무경험","실무내용",""); // when & then assertThatThrownBy(() -> postService.createPost(request, mentee)) @@ -110,10 +101,8 @@ void createPost_practicePost_mentee_failure() { void createPost_questionPost_initializeIsResolve() { // given Member member = MemberFixture.create(1L, "test@test.com", "Test User", "password", Member.Role.MENTEE); - PostCreateRequest request = new PostCreateRequest(); - request.setContent("질문내용"); - request.setTitle("질문경험"); - request.setPostType("QUESTIONPOST"); + PostCreateRequest request = new PostCreateRequest("QUESTIONPOST","질문경험","질문내용",""); + Post expectedPost = createPost("질문", "질문 내용", member, Post.PostType.QUESTIONPOST); expectedPost.updateResolveStatus(false); @@ -133,10 +122,7 @@ void createPost_questionPost_initializeIsResolve() { void createPost_invalidPostType_failure() { // given Member member = MemberFixture.createDefault(); - PostCreateRequest request = new PostCreateRequest(); - request.setContent("실무내용"); - request.setTitle("실무경험"); - request.setPostType("INVALIDPOST"); + PostCreateRequest request = new PostCreateRequest("INVALIDPOST","질문경험","질문내용",""); // when & then assertThatThrownBy(() -> postService.createPost(request, member)) @@ -216,10 +202,7 @@ void updatePost_author_success() { Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); Post post = createPost("기존 제목", "기존 내용", author, Post.PostType.INFORMATIONPOST); Long postId = 1L; - PostCreateRequest updateRequest = new PostCreateRequest(); - updateRequest.setTitle("새 제목"); - updateRequest.setContent("새 내용"); - updateRequest.setPostType("INFORMATIONPOST"); + PostCreateRequest updateRequest = new PostCreateRequest("INFORMATIONPOST","새 제목","새 내용",""); when(postRepository.findById(postId)).thenReturn(Optional.of(post)); when(postRepository.save(any(Post.class))).thenReturn(post); @@ -241,10 +224,8 @@ void updatePost_notAuthor_failure() { Member otherUser = MemberFixture.create(2L, "other@test.com", "Other", "password", Member.Role.MENTEE); Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); Long postId = 1L; - PostCreateRequest updateRequest = new PostCreateRequest(); - updateRequest.setTitle("새 제목"); - updateRequest.setContent("새 내용"); - updateRequest.setPostType("INFORMATIONPOST"); + PostCreateRequest updateRequest = new PostCreateRequest("INFORMATIONPOST","새 내용","새 제목",""); + when(postRepository.findById(postId)).thenReturn(Optional.of(post)); @@ -263,10 +244,7 @@ void updatePost_nullOrBlankTitle_failure() { Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); Long postId = 1L; - PostCreateRequest updateRequest = new PostCreateRequest(); - updateRequest.setTitle(""); - updateRequest.setContent("새 내용"); - updateRequest.setPostType("INFORMATIONPOST"); + PostCreateRequest updateRequest = new PostCreateRequest("INFORMATIONPOST","","새 내용",""); when(postRepository.findById(postId)).thenReturn(Optional.of(post)); @@ -285,10 +263,7 @@ void updatePost_nullOrBlankContent_failure() { Member author = MemberFixture.create(1L, "author@test.com", "Author", "password", Member.Role.MENTEE); Post post = createPost("제목", "내용", author, Post.PostType.INFORMATIONPOST); Long postId = 1L; - PostCreateRequest updateRequest = new PostCreateRequest(); - updateRequest.setTitle("새 제목"); - updateRequest.setContent(""); - updateRequest.setPostType("INFORMATIONPOST"); + PostCreateRequest updateRequest = new PostCreateRequest("INFORMATIONPOST","새 제목","",""); when(postRepository.findById(postId)).thenReturn(Optional.of(post)); 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 new file mode 100644 index 00000000..e27e79b6 --- /dev/null +++ b/back/src/test/java/com/back/domain/roadmap/roadmap/controller/JobRoadmapControllerTest.java @@ -0,0 +1,282 @@ +package com.back.domain.roadmap.roadmap.controller; + +import com.back.domain.job.job.entity.Job; +import com.back.domain.job.job.service.JobService; +import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import com.back.domain.roadmap.roadmap.entity.RoadmapNode; +import com.back.domain.roadmap.roadmap.repository.JobRoadmapRepository; +import com.back.domain.roadmap.task.entity.Task; +import com.back.domain.roadmap.task.service.TaskService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@ActiveProfiles("test") +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +@WithMockUser +class JobRoadmapControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private JobRoadmapRepository jobRoadmapRepository; + + @Autowired + private JobService jobService; + + @Autowired + private TaskService taskService; + + private Job testJob1; + private Job testJob2; + private JobRoadmap testJobRoadmap1; + private JobRoadmap testJobRoadmap2; + private Task javaTask; + private Task springTask; + private Task reactTask; + + @BeforeEach + void setUp() { + setupTestData(); + } + + private void setupTestData() { + // 테스트용 Job 생성 + long timestamp = System.currentTimeMillis(); + testJob1 = jobService.create("테스트 백엔드_" + timestamp, "테스트용 서버 사이드 개발자"); + testJob2 = jobService.create("테스트 프론트엔드_" + timestamp, "테스트용 클라이언트 사이드 개발자"); + + // 테스트용 Task 생성 + javaTask = taskService.create("TestJava_" + timestamp); + springTask = taskService.create("TestSpring_" + timestamp); + reactTask = taskService.create("TestReact_" + timestamp); + + // 테스트용 JobRoadmap 1 생성 (백엔드) + testJobRoadmap1 = JobRoadmap.builder() + .job(testJob1) + .build(); + testJobRoadmap1 = jobRoadmapRepository.save(testJobRoadmap1); + + // 백엔드 로드맵 노드 생성 + RoadmapNode javaNode = RoadmapNode.builder() + .roadmapId(testJobRoadmap1.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(javaTask) + .taskName(javaTask.getName()) + .description("Java 프로그래밍 언어") + .stepOrder(1) + .level(0) + .build(); + + RoadmapNode springNode = RoadmapNode.builder() + .roadmapId(testJobRoadmap1.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(springTask) + .taskName(springTask.getName()) + .description("Spring Boot 프레임워크") + .stepOrder(1) + .level(1) + .build(); + javaNode.addChild(springNode); + + testJobRoadmap1.getNodes().add(javaNode); + testJobRoadmap1.getNodes().add(springNode); + jobRoadmapRepository.save(testJobRoadmap1); + + // 테스트용 JobRoadmap 2 생성 (프론트엔드) + testJobRoadmap2 = JobRoadmap.builder() + .job(testJob2) + .build(); + testJobRoadmap2 = jobRoadmapRepository.save(testJobRoadmap2); + + RoadmapNode reactNode = RoadmapNode.builder() + .roadmapId(testJobRoadmap2.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(reactTask) + .taskName(reactTask.getName()) + .description("React 라이브러리") + .stepOrder(1) + .level(0) + .build(); + + testJobRoadmap2.getNodes().add(reactNode); + jobRoadmapRepository.save(testJobRoadmap2); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 기본 페이징") + void getJobRoadmaps_DefaultPaging() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .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.totalPage").value(1)) + .andExpect(jsonPath("$.data.currentPage").value(0)) + .andExpect(jsonPath("$.data.hasNext").value(false)); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 페이지 크기 지정") + void getJobRoadmaps_CustomPageSize() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps") + .param("page", "0") + .param("size", "1") + .contentType(MediaType.APPLICATION_JSON)) + .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.currentPage").value(0)); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 키워드 검색") + void getJobRoadmaps_WithKeyword() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps") + .param("keyword", "백엔드") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(1)) + .andExpect(jsonPath("$.data.jobRoadmaps[0].jobName").value(testJob1.getName())); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 존재하지 않는 키워드") + void getJobRoadmaps_NoResultsKeyword() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps") + .param("keyword", "존재하지않는키워드") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.jobRoadmaps.length()").value(0)) + .andExpect(jsonPath("$.data.totalElements").value(0)); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 응답 필드 검증") + void getJobRoadmaps_ResponseFields() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps") + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].id").exists()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].jobName").exists()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].jobDescription").exists()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].id").isNumber()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].jobName").isString()) + .andExpect(jsonPath("$.data.jobRoadmaps[0].jobDescription").isString()); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 성공") + void getJobRoadmapById_Success() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps/{id}", testJobRoadmap1.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.resultCode").value("200")) + .andExpect(jsonPath("$.msg").value("직업 로드맵 상세 조회 성공")) + .andExpect(jsonPath("$.data.id").value(testJobRoadmap1.getId())) + .andExpect(jsonPath("$.data.jobId").value(testJob1.getId())) + .andExpect(jsonPath("$.data.jobName").value(testJob1.getName())) + .andExpect(jsonPath("$.data.totalNodeCount").value(2)); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 트리 구조 검증") + void getJobRoadmapById_TreeStructure() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps/{id}", testJobRoadmap1.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + // 루트 노드 검증 + .andExpect(jsonPath("$.data.nodes.length()").value(1)) + .andExpect(jsonPath("$.data.nodes[0].taskName").value(javaTask.getName())) + .andExpect(jsonPath("$.data.nodes[0].level").value(0)) + .andExpect(jsonPath("$.data.nodes[0].stepOrder").value(1)) + .andExpect(jsonPath("$.data.nodes[0].parentId").isEmpty()) + // 자식 노드 검증 + .andExpect(jsonPath("$.data.nodes[0].children.length()").value(1)) + .andExpect(jsonPath("$.data.nodes[0].children[0].taskName").value(springTask.getName())) + .andExpect(jsonPath("$.data.nodes[0].children[0].level").value(1)) + .andExpect(jsonPath("$.data.nodes[0].children[0].stepOrder").value(1)) + .andExpect(jsonPath("$.data.nodes[0].children[0].parentId").exists()); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 존재하지 않는 ID") + void getJobRoadmapById_NotFound() throws Exception { + // given + Long nonExistentId = 999L; + + // when & then + mvc.perform(get("/job-roadmaps/{id}", nonExistentId) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 모든 응답 필드 검증") + void getJobRoadmapById_AllResponseFields() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps/{id}", testJobRoadmap1.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.id").isNumber()) + .andExpect(jsonPath("$.data.jobId").isNumber()) + .andExpect(jsonPath("$.data.jobName").isString()) + .andExpect(jsonPath("$.data.nodes").isArray()) + .andExpect(jsonPath("$.data.totalNodeCount").isNumber()) + .andExpect(jsonPath("$.data.createdDate").exists()) + .andExpect(jsonPath("$.data.modifiedDate").exists()); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 노드 필드 검증") + void getJobRoadmapById_NodeFields() throws Exception { + // when & then + mvc.perform(get("/job-roadmaps/{id}", testJobRoadmap1.getId()) + .contentType(MediaType.APPLICATION_JSON)) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.nodes[0].id").isNumber()) + .andExpect(jsonPath("$.data.nodes[0].taskId").isNumber()) + .andExpect(jsonPath("$.data.nodes[0].taskName").isString()) + .andExpect(jsonPath("$.data.nodes[0].description").isString()) + .andExpect(jsonPath("$.data.nodes[0].stepOrder").isNumber()) + .andExpect(jsonPath("$.data.nodes[0].level").isNumber()) + .andExpect(jsonPath("$.data.nodes[0].isLinkedToTask").isBoolean()) + .andExpect(jsonPath("$.data.nodes[0].children").isArray()); + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..a303d69e --- /dev/null +++ b/back/src/test/java/com/back/domain/roadmap/roadmap/service/JobRoadmapServiceTest.java @@ -0,0 +1,266 @@ +package com.back.domain.roadmap.roadmap.service; + +import com.back.domain.job.job.entity.Job; +import com.back.domain.job.job.service.JobService; +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapListResponse; +import com.back.domain.roadmap.roadmap.dto.response.JobRoadmapResponse; +import com.back.domain.roadmap.roadmap.entity.JobRoadmap; +import com.back.domain.roadmap.roadmap.entity.RoadmapNode; +import com.back.domain.roadmap.roadmap.repository.JobRoadmapRepository; +import com.back.domain.roadmap.task.entity.Task; +import com.back.domain.roadmap.task.service.TaskService; +import com.back.global.exception.ServiceException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.*; + +@ActiveProfiles("test") +@SpringBootTest +@Transactional +class JobRoadmapServiceTest { + + @Autowired + private JobRoadmapService jobRoadmapService; + + @Autowired + private JobRoadmapRepository jobRoadmapRepository; + + @Autowired + private JobService jobService; + + @Autowired + private TaskService taskService; + + private Job testJob; + private Task testTask1; + private Task testTask2; + private JobRoadmap testJobRoadmap; + + @BeforeEach + void setUp() { + // 기존 Job 조회 또는 새로 생성 (UNIQUE 제약 조건 회피) + try { + testJob = jobService.create("테스트용 백엔드 개발자", "테스트용 서버 사이드 개발을 담당하는 개발자"); + } catch (Exception e) { + // 이미 존재할 경우 조회로 대체하거나 다른 이름 사용 + testJob = jobService.create("테스트 백엔드 " + System.currentTimeMillis(), "테스트용 서버 사이드 개발자"); + } + + // 테스트용 Task 생성 (UNIQUE 제약 조건 회피) + try { + testTask1 = taskService.create("Test Java"); + testTask2 = taskService.create("Test Spring Boot"); + } catch (Exception e) { + // 이미 존재할 경우 타임스탬프 추가 + long timestamp = System.currentTimeMillis(); + testTask1 = taskService.create("Java_" + timestamp); + testTask2 = taskService.create("SpringBoot_" + timestamp); + } + + // 테스트용 JobRoadmap 생성 + testJobRoadmap = JobRoadmap.builder() + .job(testJob) + .build(); + testJobRoadmap = jobRoadmapRepository.save(testJobRoadmap); + + // 테스트용 RoadmapNode 생성 (트리 구조) + RoadmapNode rootNode = RoadmapNode.builder() + .roadmapId(testJobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(testTask1) + .taskName(testTask1.getName()) + .description("Java 프로그래밍 언어") + .stepOrder(1) + .level(0) + .build(); + + RoadmapNode childNode = RoadmapNode.builder() + .roadmapId(testJobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(testTask2) + .taskName(testTask2.getName()) + .description("Spring Boot 프레임워크") + .stepOrder(1) + .level(1) + .build(); + rootNode.addChild(childNode); + + testJobRoadmap.getNodes().add(rootNode); + testJobRoadmap.getNodes().add(childNode); + jobRoadmapRepository.save(testJobRoadmap); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 전체 조회") + void getAllJobRoadmaps() { + // when + List result = jobRoadmapService.getAllJobRoadmaps(); + + // then + assertThat(result).isNotEmpty(); + assertThat(result).hasSize(1); + + JobRoadmapListResponse response = result.get(0); + assertThat(response.id()).isEqualTo(testJobRoadmap.getId()); + assertThat(response.jobName()).isEqualTo(testJob.getName()); + assertThat(response.jobDescription()).isEqualTo(testJob.getDescription()); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 페이징 및 키워드 검색") + void getJobRoadmaps_WithPagingAndKeyword() { + // given + String keyword = "백엔드"; + int page = 0; + int size = 10; + + // when + Page result = jobRoadmapService.getJobRoadmaps(keyword, page, size); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getTotalElements()).isEqualTo(1); + assertThat(result.getNumber()).isEqualTo(0); + assertThat(result.getSize()).isEqualTo(10); + + JobRoadmapListResponse response = result.getContent().get(0); + assertThat(response.id()).isEqualTo(testJobRoadmap.getId()); + assertThat(response.jobName()).isEqualTo(testJob.getName()); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - 키워드 검색 결과 없음") + void getJobRoadmaps_NoResults() { + // given + String keyword = "존재하지않는키워드"; + int page = 0; + int size = 10; + + // when + Page result = jobRoadmapService.getJobRoadmaps(keyword, page, size); + + // then + assertThat(result.getContent()).isEmpty(); + assertThat(result.getTotalElements()).isEqualTo(0); + } + + @Test + @DisplayName("직업 로드맵 다건 조회 - null 키워드") + void getJobRoadmaps_NullKeyword() { + // given + String keyword = null; + int page = 0; + int size = 10; + + // when + Page result = jobRoadmapService.getJobRoadmaps(keyword, page, size); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getTotalElements()).isEqualTo(1); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 성공") + void getJobRoadmapById_Success() { + // when + JobRoadmapResponse result = jobRoadmapService.getJobRoadmapById(testJobRoadmap.getId()); + + // then + assertThat(result.id()).isEqualTo(testJobRoadmap.getId()); + assertThat(result.jobId()).isEqualTo(testJob.getId()); + assertThat(result.jobName()).isEqualTo(testJob.getName()); + assertThat(result.totalNodeCount()).isEqualTo(2); + + // 트리 구조 검증 + assertThat(result.nodes()).hasSize(1); // 루트 노드 1개 + + // 루트 노드 검증 + var rootNode = result.nodes().get(0); + assertThat(rootNode.taskName()).isEqualTo(testTask1.getName()); + assertThat(rootNode.level()).isEqualTo(0); + assertThat(rootNode.stepOrder()).isEqualTo(1); + assertThat(rootNode.parentId()).isNull(); + + // 자식 노드 검증 + assertThat(rootNode.children()).hasSize(1); + var childNode = rootNode.children().get(0); + assertThat(childNode.taskName()).isEqualTo(testTask2.getName()); + assertThat(childNode.level()).isEqualTo(1); + assertThat(childNode.stepOrder()).isEqualTo(1); + assertThat(childNode.parentId()).isEqualTo(rootNode.id()); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 존재하지 않는 ID") + void getJobRoadmapById_NotFound() { + // given + Long nonExistentId = 999L; + + // when & then + assertThatThrownBy(() -> jobRoadmapService.getJobRoadmapById(nonExistentId)) + .isInstanceOf(ServiceException.class) + .hasMessageContaining("직업 로드맵을 찾을 수 없습니다"); + } + + @Test + @DisplayName("직업 로드맵 단건 조회 - 트리 구조 정렬 검증") + void getJobRoadmapById_TreeStructureSorting() { + // given - 추가 노드들로 복잡한 트리 구조 생성 + long timestamp = System.currentTimeMillis(); + Task testTask3 = taskService.create("MySQL_" + timestamp); + Task testTask4 = taskService.create("Git_" + timestamp); + + // 같은 레벨에 여러 노드 추가 + RoadmapNode rootNode2 = RoadmapNode.builder() + .roadmapId(testJobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(testTask4) + .taskName(testTask4.getName()) + .description("버전 관리 시스템") + .stepOrder(2) + .level(0) + .build(); + + RoadmapNode childNode2 = RoadmapNode.builder() + .roadmapId(testJobRoadmap.getId()) + .roadmapType(RoadmapNode.RoadmapType.JOB) + .task(testTask3) + .taskName(testTask3.getName()) + .description("관계형 데이터베이스") + .stepOrder(2) + .level(1) + .build(); + testJobRoadmap.getNodes().get(0).addChild(childNode2); // Java 노드의 자식 + + testJobRoadmap.getNodes().add(rootNode2); + testJobRoadmap.getNodes().add(childNode2); + jobRoadmapRepository.save(testJobRoadmap); + + // when + JobRoadmapResponse result = jobRoadmapService.getJobRoadmapById(testJobRoadmap.getId()); + + // then + assertThat(result.totalNodeCount()).isEqualTo(4); + assertThat(result.nodes()).hasSize(2); // 루트 노드 2개 + + // 루트 노드 정렬 검증 (level -> stepOrder 순) + assertThat(result.nodes().get(0).taskName()).isEqualTo(testTask1.getName()); // level=0, stepOrder=1 + assertThat(result.nodes().get(1).taskName()).isEqualTo(testTask4.getName()); // level=0, stepOrder=2 + + // Java 노드의 자식 노드 정렬 검증 + var javaNode = result.nodes().get(0); + assertThat(javaNode.children()).hasSize(2); + assertThat(javaNode.children().get(0).taskName()).isEqualTo(testTask2.getName()); // stepOrder=1 + assertThat(javaNode.children().get(1).taskName()).isEqualTo(testTask3.getName()); // stepOrder=2 + } +} \ No newline at end of file