diff --git a/.gitignore b/.gitignore index 2dbb335..525bd2f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ out/ ### VS Code ### .vscode/ -db/mysql/data/* \ No newline at end of file +db/mysql/data/* +/src/main/generated/ \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java index 1dceb1a..c9db4a7 100644 --- a/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java +++ b/src/main/java/com/_119/wepro/project/domain/repository/ProjectMemberCustomRepository.java @@ -1,15 +1,12 @@ package com._119.wepro.project.domain.repository; -import static com._119.wepro.project.domain.QProjectMember.projectMember; - import com._119.wepro.member.domain.Member; import com._119.wepro.project.domain.Project; import com._119.wepro.alarm.domain.QAlarm; import com._119.wepro.global.enums.AlarmType; import com._119.wepro.member.domain.QMember; -import com._119.wepro.project.domain.ProjectMember; import com._119.wepro.project.domain.QProjectMember; -import com.querydsl.jpa.JPAExpressions; +import com._119.wepro.project.dto.response.MemberRequestStatusResponse; import com.querydsl.jpa.impl.JPAQueryFactory; import java.util.List; import lombok.RequiredArgsConstructor; @@ -22,6 +19,8 @@ public class ProjectMemberCustomRepository { private final JPAQueryFactory queryFactory; public Boolean existsByProjectAndMember(Project project, Member member) { + QProjectMember projectMember = QProjectMember.projectMember; + Integer fetchOne = queryFactory .selectOne() .from(projectMember) @@ -30,22 +29,28 @@ public Boolean existsByProjectAndMember(Project project, Member member) { return fetchOne != null; } - public List getProjectMembersWithoutReviewRequest(Long reviewFormId) { + public List getProjectMembersWithReviewRequestStatus(Long reviewFormId) { QProjectMember projectMember = QProjectMember.projectMember; QAlarm alarm = QAlarm.alarm; - QMember member = QMember.member; // Member 엔티티를 가져오기 위해 추가 + QMember member = QMember.member; + + List requestedMemberIds = queryFactory + .select(alarm.receiver.id) + .from(alarm) + .where(alarm.targetId.eq(reviewFormId) + .and(alarm.alarmType.eq(AlarmType.REVIEW_REQUEST))) + .fetch(); return queryFactory .selectFrom(projectMember) .join(projectMember.member, member).fetchJoin() - .where(projectMember.id.notIn( - JPAExpressions - .select(alarm.receiver.id) - .from(alarm) - .where(alarm.targetId.eq(reviewFormId) - .and(alarm.alarmType.eq(AlarmType.REVIEW_REQUEST))) + .fetch() + .stream() + .map(pm -> MemberRequestStatusResponse.of( + pm.getMember(), + requestedMemberIds.contains(pm.getMember().getId()) )) - .fetch(); + .toList(); } } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/dto/response/MemberRequestStatusResponse.java b/src/main/java/com/_119/wepro/project/dto/response/MemberRequestStatusResponse.java new file mode 100644 index 0000000..f3e40fb --- /dev/null +++ b/src/main/java/com/_119/wepro/project/dto/response/MemberRequestStatusResponse.java @@ -0,0 +1,26 @@ +package com._119.wepro.project.dto.response; + +import com._119.wepro.member.domain.Member; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class MemberRequestStatusResponse { + + private Long id; + private String profileUrl; + private String name; + private String tag; + private boolean isAlreadyRequest; + + public static MemberRequestStatusResponse of(Member member, boolean isAlreadyRequest) { + return MemberRequestStatusResponse.builder() + .id(member.getId()) + .profileUrl(member.getProfile().getProfileImageUrl()) + .name(member.getProfile().getName()) + .tag(member.getTag()) + .isAlreadyRequest(isAlreadyRequest) + .build(); + } +} diff --git a/src/main/java/com/_119/wepro/project/presentation/ProjectController.java b/src/main/java/com/_119/wepro/project/presentation/ProjectController.java index 592ad65..0d64ba3 100644 --- a/src/main/java/com/_119/wepro/project/presentation/ProjectController.java +++ b/src/main/java/com/_119/wepro/project/presentation/ProjectController.java @@ -7,10 +7,12 @@ import com._119.wepro.project.dto.response.MyProjectResponse; import com._119.wepro.project.dto.response.ProjectDetailResponse; import com._119.wepro.project.dto.response.ProjectListResponse; +import com._119.wepro.project.dto.response.MemberRequestStatusResponse; import com._119.wepro.project.service.ProjectService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; @@ -201,4 +203,37 @@ public ResponseEntity addMember( projectService.addProjectMember(projectMemberCreateRequest.getMemberId(), id); return ResponseEntity.ok(null); } + + @GetMapping("/members/request/review/{reviewFormId}") + @Operation(summary = "리뷰 요청할 때 프로젝트 멤버 조회", description = "리뷰 요청 가능 여부를 포함하여 프로젝트 멤버 정보를 조회합니다.") + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "[\n" + + " {\n" + + " \"id\": 1,\n" + + " \"profileUrl\": \"https://img1.kakaocdn.net/thumb/R110x110.q70/?fname=https://t1.kakaocdn.net/account_images/default_profile.jpeg\",\n" + + " \"name\": \"김희진\",\n" + + " \"tag\": \"1\",\n" + + " \"alreadyRequest\": true\n" + + " },\n" + + " {\n" + + " \"id\": 2,\n" + + " \"profileUrl\": null,\n" + + " \"name\": \"테스트1\",\n" + + " \"tag\": \"2\",\n" + + " \"alreadyRequest\": false\n" + + " }\n" + + "]" + ) + ) + ) + public ResponseEntity> getProjectMembersWithRequestStatus( + @PathVariable("reviewFormId") Long reviewFormId) { + securityUtil.getCurrentMemberId(); + return ResponseEntity.ok(projectService.getProjectMembersWithRequestStatus(reviewFormId)); + } } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/project/service/ProjectService.java b/src/main/java/com/_119/wepro/project/service/ProjectService.java index c406a5b..66308c6 100644 --- a/src/main/java/com/_119/wepro/project/service/ProjectService.java +++ b/src/main/java/com/_119/wepro/project/service/ProjectService.java @@ -17,9 +17,11 @@ import com._119.wepro.project.domain.repository.ProjectRepository; import com._119.wepro.project.dto.request.ProjectRequest.ProjectCreateRequest; import com._119.wepro.project.dto.request.ProjectRequest.ProjectUpdateRequest; +import com._119.wepro.project.dto.response.MemberRequestStatusResponse; import com._119.wepro.project.dto.response.MyProjectResponse; import com._119.wepro.project.dto.response.ProjectDetailResponse; import com._119.wepro.project.dto.response.ProjectListResponse; +import com._119.wepro.review.domain.repository.ReviewFormRepository; import jakarta.transaction.Transactional; import java.util.List; import java.util.stream.Collectors; @@ -35,6 +37,7 @@ public class ProjectService { private final ProjectMemberCustomRepository projectMemberCustomRepository; private final MemberRepository memberRepository; private final ProjectCustomRepository projectCustomRepository; + private final ReviewFormRepository reviewFormRepository; public List searchProjects(String keyword) { List result = projectCustomRepository.searchProjects(keyword); @@ -78,7 +81,8 @@ public Long createProject(ProjectCreateRequest projectCreateRequest, Long projec @Transactional public Long updateProject(Long projectId, ProjectUpdateRequest projectUpdateRequest) { Project project = projectRepository.findById(projectId) - .orElseThrow(() -> new IllegalArgumentException("Project not found with id: " + projectId)); + .orElseThrow( + () -> new IllegalArgumentException("Project not found with id: " + projectId)); Project updatedProject = Project.of(projectUpdateRequest); @@ -118,8 +122,9 @@ private void registerProjectMember(Project project, Long memberId, String role) public Long deleteProject(Long projectId) { - Project project = projectRepository.findById(projectId).orElseThrow(() -> new RestApiException( - RESOURCE_NOT_FOUND)); + Project project = projectRepository.findById(projectId) + .orElseThrow(() -> new RestApiException( + RESOURCE_NOT_FOUND)); projectRepository.delete(project); return project.getId(); @@ -151,4 +156,10 @@ public void addProjectMember(Long projectId, Long userId) { project.setMemberNum(project.getMemberNum() + 1); projectRepository.save(project); } + + public List getProjectMembersWithRequestStatus(Long reviewFormId) { + + reviewFormRepository.findByIdOrThrow(reviewFormId); + return projectMemberCustomRepository.getProjectMembersWithReviewRequestStatus(reviewFormId); + } } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/review/dto/request/ReviewRequest.java b/src/main/java/com/_119/wepro/review/dto/request/ReviewRequest.java index 7924157..bd176c5 100644 --- a/src/main/java/com/_119/wepro/review/dto/request/ReviewRequest.java +++ b/src/main/java/com/_119/wepro/review/dto/request/ReviewRequest.java @@ -35,7 +35,7 @@ public static class ReviewAskRequest { private Long reviewFormId; @NotNull - private List memberIdList; + private Long reviewerId; } @Getter diff --git a/src/main/java/com/_119/wepro/review/dto/response/ReviewResponse.java b/src/main/java/com/_119/wepro/review/dto/response/ReviewResponse.java index d6c1e33..016d6d3 100644 --- a/src/main/java/com/_119/wepro/review/dto/response/ReviewResponse.java +++ b/src/main/java/com/_119/wepro/review/dto/response/ReviewResponse.java @@ -20,38 +20,4 @@ public static ReviewFormCreateResponse of(ReviewForm reviewForm) { .build(); } } - - @Getter - @Builder - public static class ProjectMemberGetResponse { - - private List memberList; - - public static ProjectMemberGetResponse of(List projectMembers) { - return ProjectMemberGetResponse.builder() - .memberList(projectMembers.stream() - .map(MemberDto::of) - .toList()) - .build(); - } - - @Getter - @Builder - public static class MemberDto { - - private Long id; - private String profileUrl; - private String name; - private String tag; - - public static MemberDto of(ProjectMember projectMember) { - return MemberDto.builder() - .id(projectMember.getId()) - .profileUrl(projectMember.getMember().getProfile().getProfileImageUrl()) - .name(projectMember.getMember().getProfile().getName()) - .tag(projectMember.getMember().getTag()) - .build(); - } - } - } } diff --git a/src/main/java/com/_119/wepro/review/presentation/QuestionController.java b/src/main/java/com/_119/wepro/review/presentation/QuestionController.java index 99495ce..f29969c 100644 --- a/src/main/java/com/_119/wepro/review/presentation/QuestionController.java +++ b/src/main/java/com/_119/wepro/review/presentation/QuestionController.java @@ -5,6 +5,9 @@ import com._119.wepro.review.dto.response.QuestionResponse.QuestionInReviewFormGetResponse; import com._119.wepro.review.service.QuestionService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -21,15 +24,142 @@ public class QuestionController { private final QuestionService questionService; - @Operation(summary = "카테고리에 해당하는 질문들 반환 API") @GetMapping("/categories") + @Operation(summary = "카테고리에 해당하는 질문들 반환", description = "리뷰 폼을 생성할 때, 선택된 카테고리에 해당하는 질문들을 반환합니다") + @ApiResponse( + responseCode = "200", + description = "성공", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{\n" + + " \"allQuestions\": [\n" + + " {\n" + + " \"categoryType\": \"COMMUNICATION\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"questionId\": 1,\n" + + " \"question\": \"${username}님은 팀 회의에서 의견을 제시할 때, 어떻게 소통하셨나요?\",\n" + + + " \"options\": [\n" + + " {\n" + + " \"content\": \"자신의 의견을 명확히 표현하고 다른 팀원들의 반응을 잘 경청하였다.\"\n" + + + " },\n" + + " {\n" + + " \"content\": \"의견을 제시했으나, 다른 팀원들의 의견을 충분히 반영하지 못하였다.\"\n" + + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"questionId\": 2,\n" + + " \"question\": \"${username}님이 팀 내 갈등 상황에서 대화를 중재할 때, 어떤 방식으로 접근하였나요?\",\n" + + + " \"options\": [\n" + + " {\n" + + " \"content\": \"양측의 의견을 경청하고 공정하게 갈등을 중재하였다.\"\n" + + " },\n" + + " {\n" + + " \"content\": \"중재를 시도했으나 대화가 제대로 이루어지지 않았다.\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"categoryType\": \"FOLLOWERSHIP\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"questionId\": 32,\n" + + " \"question\": \"${username}님은 업무를 맡았을 때 끝까지 책임감 있게 수행하셨나요?\",\n" + + + " \"options\": [\n" + + " {\n" + + " \"content\": \"항상 맡은 업무를 끝까지 책임감 있게 수행하며, 완성도를 높였다.\"\n" + + + " },\n" + + " {\n" + + " \"content\": \"대부분의 경우 책임감 있게 업무를 수행하며, 결과를 잘 마무리했다.\"\n" + + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}" + ) + ) + ) public ResponseEntity getQuestionsInCategories( @RequestParam List categoryTypes) { return ResponseEntity.ok(questionService.getQuestionsInCategories(categoryTypes)); } - @Operation(summary = "리뷰폼에 해당하는 질문들 반환 API") @GetMapping("/reviewform/{id}") + @Operation(summary = "리뷰폼에 해당하는 질문들 반환", description = "리뷰를 시작할 때, 리뷰 폼에 해당하는 질문들을 반환합니다") + @ApiResponse( + responseCode = "200", + description = "username: 리뷰를 받는 사람의 이름, 답변 데이터가 존재할 경우 answerOptionId(객관식)과 answer(주관식)을 함께 반환합니다.", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{\n" + + " \"username\": \"김희진\",\n" + + " \"choiceQuestions\": [\n" + + " {\n" + + " \"categoryType\": \"COMMUNICATION\",\n" + + " \"questions\": [\n" + + " {\n" + + " \"questionId\": 1,\n" + + " \"question\": \"${username}님은 팀 회의에서 의견을 제시할 때, 어떻게 소통하셨나요?\",\n" + + + " \"options\": [\n" + + " {\n" + + " \"optionId\": 1,\n" + + " \"content\": \"자신의 의견을 명확히 표현하고 다른 팀원들의 반응을 잘 경청하였다.\"\n" + + + " },\n" + + " {\n" + + " \"optionId\": 2,\n" + + " \"content\": \"의견을 제시했으나, 다른 팀원들의 의견을 충분히 반영하지 못하였다.\"\n" + + + " }\n" + + " ],\n" + + " \"answerOptionId\": 2\n" + + " },\n" + + " {\n" + + " \"questionId\": 3,\n" + + " \"question\": \"${username}님이 팀 회의에서 다른 팀원의 의견을 어떻게 대하였나요?\",\n" + + + " \"options\": [\n" + + " {\n" + + " \"optionId\": 9,\n" + + " \"content\": \"다른 팀원의 의견을 경청하고 존중하며, 적극적으로 피드백을 제공하였다.\"\n" + + + " },\n" + + " {\n" + + " \"optionId\": 10,\n" + + " \"content\": \"다른 팀원의 의견을 경청하였으나, 소극적으로 피드백을 제공하였다.\"\n" + + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"subQuestions\": [\n" + + " {\n" + + " \"questionId\": 1,\n" + + " \"content\": \"그만(Stop)했으면 하는 점은 무엇인가요?\",\n" + + " \"answer\": \"없어요\"\n" + + " }\n" + + " ]\n" + + "}" + ) + ) + ) public ResponseEntity getQuestionsInReviewForm( @PathVariable("id") Long reviewFormId) { return ResponseEntity.ok(questionService.getQuestionsInReviewForm(reviewFormId)); diff --git a/src/main/java/com/_119/wepro/review/presentation/ReviewController.java b/src/main/java/com/_119/wepro/review/presentation/ReviewController.java index b2f0e29..e2ff19c 100644 --- a/src/main/java/com/_119/wepro/review/presentation/ReviewController.java +++ b/src/main/java/com/_119/wepro/review/presentation/ReviewController.java @@ -1,23 +1,22 @@ package com._119.wepro.review.presentation; import com._119.wepro.global.util.SecurityUtil; -import com._119.wepro.review.dto.request.ReviewRequest; import com._119.wepro.review.dto.request.ReviewRequest.ReviewAskRequest; import com._119.wepro.review.dto.request.ReviewRequest.ReviewFormCreateRequest; import com._119.wepro.review.dto.request.ReviewRequest.ReviewSaveRequest; -import com._119.wepro.review.dto.response.ReviewResponse.ProjectMemberGetResponse; import com._119.wepro.review.dto.response.ReviewResponse.ReviewFormCreateResponse; import com._119.wepro.review.service.ReviewService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -28,24 +27,44 @@ public class ReviewController { private final ReviewService reviewService; private final SecurityUtil securityUtil; - @Operation(summary = "리뷰 폼 생성 API") @PostMapping("/form") + @Operation(summary = "리뷰 폼 생성하기", description = "해당 프로젝트에 대한 리뷰 폼을 생성합니다") + @ApiResponse( + responseCode = "200", + description = "생성된 리뷰 폼 아이디를 반환합니다", + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = "{\n" + + " \"reviewFormId\": 1\n" + + "}" + ) + ) + ) public ResponseEntity createReviewForm( @RequestBody @Valid ReviewFormCreateRequest request) { Long memberId = securityUtil.getCurrentMemberId(); return ResponseEntity.ok(reviewService.createReviewForm(request, memberId)); } - @Operation(summary = "리뷰 요청하기 API") @PostMapping("/request") + @Operation(summary = "리뷰 요청하기", description = "특정 멤버에게 리뷰 요청 알림을 전송합니다") + @ApiResponse( + responseCode = "200", + description = "성공" + ) public ResponseEntity requestReview(@RequestBody @Valid ReviewAskRequest request) { Long memberId = securityUtil.getCurrentMemberId(); reviewService.requestReview(request, memberId); return ResponseEntity.ok().build(); } - @Operation(summary = "리뷰 임시저장 API") @PostMapping("/draft/{reviewFormId}") + @Operation(summary = "리뷰 임시저장", description = "진행 중인 리뷰의 응답 데이터를 임시저장합니다") + @ApiResponse( + responseCode = "200", + description = "성공" + ) public ResponseEntity draftReview(@PathVariable(name = "reviewFormId") Long reviewFormId, @RequestBody @Valid ReviewSaveRequest request) { Long memberId = securityUtil.getCurrentMemberId(); @@ -53,16 +72,12 @@ public ResponseEntity draftReview(@PathVariable(name = "reviewFormId") Lon return ResponseEntity.ok().build(); } - @Operation(summary = "프로젝트 멤버 조회(리뷰 요청 받은 멤버 제외) API") - @GetMapping("/project/members") - public ResponseEntity getProjectMembers( - @RequestParam Long reviewFormId) { - securityUtil.getCurrentMemberId(); - return ResponseEntity.ok(reviewService.getProjectMembers(reviewFormId)); - } - - @Operation(summary = "리뷰 제출하기 API") @PostMapping("/{reviewFormId}") + @Operation(summary = "리뷰 제출하기", description = "진행 중인 리뷰가 완료되어 최종 제출합니다.") + @ApiResponse( + responseCode = "200", + description = "성공" + ) public ResponseEntity submitReview(@PathVariable(name = "reviewFormId") Long reviewFormId, @RequestBody @Valid ReviewSaveRequest request) { Long memberId = securityUtil.getCurrentMemberId(); diff --git a/src/main/java/com/_119/wepro/review/service/ReviewService.java b/src/main/java/com/_119/wepro/review/service/ReviewService.java index 6cc414c..e9abfbe 100644 --- a/src/main/java/com/_119/wepro/review/service/ReviewService.java +++ b/src/main/java/com/_119/wepro/review/service/ReviewService.java @@ -7,7 +7,6 @@ import com._119.wepro.member.domain.Member; import com._119.wepro.member.domain.repository.MemberRepository; import com._119.wepro.project.domain.Project; -import com._119.wepro.project.domain.ProjectMember; import com._119.wepro.project.domain.repository.ProjectMemberCustomRepository; import com._119.wepro.project.domain.repository.ProjectRepository; import com._119.wepro.review.domain.ReviewForm; @@ -21,7 +20,6 @@ import com._119.wepro.review.dto.request.ReviewRequest.ReviewAskRequest; import com._119.wepro.review.dto.request.ReviewRequest.ReviewFormCreateRequest; import com._119.wepro.review.dto.request.ReviewRequest.ReviewSaveRequest; -import com._119.wepro.review.dto.response.ReviewResponse.ProjectMemberGetResponse; import com._119.wepro.review.dto.response.ReviewResponse.ReviewFormCreateResponse; import jakarta.transaction.Transactional; import java.util.List; @@ -59,18 +57,9 @@ public ReviewFormCreateResponse createReviewForm(ReviewFormCreateRequest request public void requestReview(ReviewAskRequest request, Long memberId) { Member member = memberRepository.findByIdOrThrow(memberId); - request.getMemberIdList().forEach(reviewerId -> { - alarmService.createAlarm(member, reviewerId, AlarmType.REVIEW_REQUEST, - request.getReviewFormId()); - }); - } - - public ProjectMemberGetResponse getProjectMembers(Long reviewFormId) { - reviewFormRepository.findByIdOrThrow(reviewFormId); - List filteredMembers = projectMemberCustomRepository.getProjectMembersWithoutReviewRequest( - reviewFormId); - - return ProjectMemberGetResponse.of(filteredMembers); + Long reviewerId = request.getReviewerId(); + Long reviewFormId = request.getReviewFormId(); + alarmService.createAlarm(member, reviewerId, AlarmType.REVIEW_REQUEST, reviewFormId); } @Transactional