Skip to content

Commit dff8235

Browse files
authored
Dev 브랜치 main 병합
Dev 브랜치 main 병합
2 parents e479832 + f413737 commit dff8235

9 files changed

Lines changed: 275 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package hs.kr.backend.devpals.domain.evaluation.controller;
2+
3+
import hs.kr.backend.devpals.domain.evaluation.dto.EvaluationMemberResponse;
4+
import hs.kr.backend.devpals.domain.evaluation.dto.EvaluationRequest;
5+
import hs.kr.backend.devpals.domain.evaluation.service.EvaluationService;
6+
import hs.kr.backend.devpals.global.common.ApiResponse;
7+
import io.swagger.v3.oas.annotations.Operation;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import java.util.List;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.PathVariable;
14+
import org.springframework.web.bind.annotation.PostMapping;
15+
import org.springframework.web.bind.annotation.RequestBody;
16+
import org.springframework.web.bind.annotation.RequestHeader;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
@RestController
21+
@RequestMapping("/evaluations")
22+
@RequiredArgsConstructor
23+
@Tag(name = "Evaluation API", description = "프로젝트 참여자 평가 관련 API")
24+
public class EvaluationController {
25+
26+
private final EvaluationService evaluationService;
27+
28+
@PostMapping
29+
@Operation(
30+
summary = "프로젝트 참여자 평가 제출",
31+
description = "로그인된 사용자가 특정 프로젝트에 대해 참여자들을 평가합니다.",
32+
responses = {
33+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "평가 제출 성공"),
34+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "잘못된 요청")
35+
}
36+
)
37+
public ResponseEntity<ApiResponse<String>> submitEvaluations(
38+
@RequestHeader("Authorization") String token,
39+
@RequestBody EvaluationRequest evaluationRequest) {
40+
return evaluationService.submitEvaluations(token, evaluationRequest);
41+
}
42+
43+
@GetMapping("/{projectId}/members")
44+
@Operation(
45+
summary = "프로젝트 참여자 조회 (평가 여부 포함)",
46+
description = "해당 프로젝트에 참여한 유저 리스트를 조회하며, 각 유저가 평가되었는지 여부도 함께 제공합니다.",
47+
responses = {
48+
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "참여자 조회 성공")
49+
}
50+
)
51+
public ResponseEntity<ApiResponse<List<EvaluationMemberResponse>>> getProjectMembersWithEvaluationStatus(
52+
@RequestHeader("Authorization") String token,
53+
@PathVariable Long projectId) {
54+
return evaluationService.getProjectMembersWithEvaluationStatus(token, projectId);
55+
}
56+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package hs.kr.backend.devpals.domain.evaluation.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.*;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
@Builder
11+
@Schema(description = "프로젝트 참여자 응답 DTO")
12+
public class EvaluationMemberResponse {
13+
14+
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
15+
@Schema(description = "유저 ID", example = "12", accessMode = Schema.AccessMode.READ_ONLY)
16+
private Long userId;
17+
18+
@Schema(description = "닉네임", example = "김철수")
19+
private String nickname;
20+
21+
@Schema(description = "평가 완료 여부", example = "true")
22+
private boolean isEvaluated;
23+
24+
public static EvaluationMemberResponse of(Long userId, String nickname, boolean isEvaluated) {
25+
return EvaluationMemberResponse.builder()
26+
.userId(userId)
27+
.nickname(nickname)
28+
.isEvaluated(isEvaluated)
29+
.build();
30+
}
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package hs.kr.backend.devpals.domain.evaluation.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.util.List;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
@Builder
14+
@Schema(description = "평가 제출 요청 DTO")
15+
public class EvaluationRequest {
16+
17+
@Schema(description = "프로젝트 ID", example = "1234")
18+
private Long projectId;
19+
20+
@Schema(description = "평가 대상 유저 ID", example = "17")
21+
private Long evaluateeId;
22+
23+
@Schema(description = "점수 배열", example = "[5, 4, 3, 3, 2, 5]")
24+
private List<Integer> scores;
25+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package hs.kr.backend.devpals.domain.evaluation.entity;
2+
3+
import hs.kr.backend.devpals.global.exception.CustomException;
4+
import hs.kr.backend.devpals.global.exception.ErrorException;
5+
import jakarta.persistence.*;
6+
import java.time.LocalDateTime;
7+
import java.util.List;
8+
import lombok.*;
9+
10+
@Entity
11+
@Table(name = "evaluation", uniqueConstraints = {
12+
@UniqueConstraint(columnNames = {"projectId", "evaluatorId", "evaluateeId"})
13+
})
14+
@Getter
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
@Builder
18+
public class EvaluationEntity {
19+
20+
@Id
21+
@GeneratedValue(strategy = GenerationType.IDENTITY)
22+
private Long id;
23+
24+
@Column(nullable = false)
25+
private Long projectId;
26+
27+
@Column(nullable = false)
28+
private Long evaluatorId;
29+
30+
@Column(nullable = false)
31+
private Long evaluateeId;
32+
33+
@ElementCollection
34+
@CollectionTable(name = "evaluation_scores", joinColumns = @JoinColumn(name = "evaluation_id"))
35+
@Column(name = "score", nullable = false)
36+
private List<Integer> scores;
37+
38+
@Column(nullable = false, updatable = false)
39+
private LocalDateTime createdAt;
40+
41+
@Builder(builderMethodName = "validatedBuilder")
42+
public static EvaluationEntity create(Long projectId, Long evaluatorId, Long evaluateeId, List<Integer> scores) {
43+
if (scores == null || scores.size() != 6) {
44+
throw new CustomException(ErrorException.INVALID_EVALUATION_SCORES);
45+
}
46+
return EvaluationEntity.builder()
47+
.projectId(projectId)
48+
.evaluatorId(evaluatorId)
49+
.evaluateeId(evaluateeId)
50+
.scores(scores)
51+
.build();
52+
}
53+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package hs.kr.backend.devpals.domain.evaluation.repository;
2+
3+
import hs.kr.backend.devpals.domain.evaluation.entity.EvaluationEntity;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.stereotype.Repository;
6+
7+
@Repository
8+
public interface EvaluationRepository extends JpaRepository<EvaluationEntity, Long> {
9+
10+
boolean existsByProjectIdAndEvaluatorIdAndEvaluateeId(Long projectId, Long evaluatorId, Long evaluateeId);
11+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package hs.kr.backend.devpals.domain.evaluation.service;
2+
3+
import hs.kr.backend.devpals.domain.evaluation.dto.EvaluationMemberResponse;
4+
import hs.kr.backend.devpals.domain.evaluation.dto.EvaluationRequest;
5+
import hs.kr.backend.devpals.domain.evaluation.entity.EvaluationEntity;
6+
import hs.kr.backend.devpals.domain.evaluation.repository.EvaluationRepository;
7+
import hs.kr.backend.devpals.domain.project.entity.ApplicantEntity;
8+
import hs.kr.backend.devpals.domain.project.repository.ApplicantRepository;
9+
import hs.kr.backend.devpals.domain.user.entity.UserEntity;
10+
import hs.kr.backend.devpals.domain.user.repository.UserRepository;
11+
import hs.kr.backend.devpals.global.common.ApiResponse;
12+
import hs.kr.backend.devpals.global.common.enums.ApplicantStatus;
13+
import hs.kr.backend.devpals.global.exception.CustomException;
14+
import hs.kr.backend.devpals.global.exception.ErrorException;
15+
import hs.kr.backend.devpals.global.jwt.JwtTokenValidator;
16+
import java.util.List;
17+
import lombok.RequiredArgsConstructor;
18+
import org.springframework.http.ResponseEntity;
19+
import org.springframework.stereotype.Service;
20+
import org.springframework.transaction.annotation.Transactional;
21+
22+
@Service
23+
@RequiredArgsConstructor
24+
public class EvaluationService {
25+
26+
private final JwtTokenValidator jwtTokenValidator;
27+
private final UserRepository userRepository;
28+
private final EvaluationRepository evaluationRepository;
29+
private final ApplicantRepository applicantRepository;
30+
31+
@Transactional
32+
public ResponseEntity<ApiResponse<String>> submitEvaluations(String token, EvaluationRequest request) {
33+
Long evaluatorId = jwtTokenValidator.getUserId(token);
34+
35+
userRepository.findById(evaluatorId)
36+
.orElseThrow(() -> new CustomException(ErrorException.USER_NOT_FOUND));
37+
38+
Long projectId = request.getProjectId();
39+
Long evaluateeId = request.getEvaluateeId();
40+
41+
//중복 평가 방지
42+
if (evaluationRepository.existsByProjectIdAndEvaluatorIdAndEvaluateeId(projectId, evaluatorId, evaluateeId)) {
43+
throw new CustomException(ErrorException.EVALUATION_ALREADY_EXISTS);
44+
}
45+
46+
ApplicantEntity applicant = applicantRepository.findByProjectIdAndUserId(projectId, evaluateeId)
47+
.orElseThrow(() -> new CustomException(ErrorException.APPLICANT_NOT_FOUND));
48+
49+
if (!applicant.isAccepted()) {
50+
throw new CustomException(ErrorException.INVALID_EVALUATION_TARGET);
51+
}
52+
53+
EvaluationEntity evaluation = EvaluationEntity.create(
54+
projectId, evaluatorId, evaluateeId, request.getScores()
55+
);
56+
57+
evaluationRepository.save(evaluation);
58+
59+
return ResponseEntity.ok(new ApiResponse<>(true, "평가가 성공적으로 제출되었습니다.", null));
60+
}
61+
62+
@Transactional(readOnly = true)
63+
public ResponseEntity<ApiResponse<List<EvaluationMemberResponse>>> getProjectMembersWithEvaluationStatus(
64+
String token, Long projectId) {
65+
66+
Long evaluatorId = jwtTokenValidator.getUserId(token);
67+
68+
//프로젝트에 참여 중인 유저 리스트 조회
69+
List<ApplicantEntity> acceptedApplicants = applicantRepository.findAllByProjectIdAndStatus(projectId, ApplicantStatus.ACCEPTED);
70+
71+
//평가 여부 판별
72+
List<EvaluationMemberResponse> responseList = acceptedApplicants.stream()
73+
.map(applicant -> {
74+
UserEntity user = applicant.getUser();
75+
boolean isEvaluated = evaluationRepository.existsByProjectIdAndEvaluatorIdAndEvaluateeId(
76+
projectId, evaluatorId, user.getId());
77+
return EvaluationMemberResponse.of(user.getId(), user.getNickname(), isEvaluated);
78+
})
79+
.toList();
80+
81+
return ResponseEntity.ok(new ApiResponse<>(true, "참여자 조회 성공", responseList));
82+
}
83+
84+
85+
}

src/main/java/hs/kr/backend/devpals/domain/project/entity/ApplicantEntity.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,7 @@ public static ApplicantEntity createApplicant(UserEntity user, ProjectEntity pro
8080
.build();
8181
}
8282

83+
public boolean isAccepted() {
84+
return status == ApplicantStatus.ACCEPTED;
85+
}
8386
}

src/main/java/hs/kr/backend/devpals/domain/project/repository/ApplicantRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hs.kr.backend.devpals.domain.project.entity.ApplicantEntity;
44
import hs.kr.backend.devpals.domain.project.entity.ProjectEntity;
55
import hs.kr.backend.devpals.domain.user.entity.UserEntity;
6+
import hs.kr.backend.devpals.global.common.enums.ApplicantStatus;
67
import org.springframework.data.jpa.repository.JpaRepository;
78
import org.springframework.data.jpa.repository.Query;
89
import org.springframework.data.repository.query.Param;
@@ -18,4 +19,8 @@ public interface ApplicantRepository extends JpaRepository<ApplicantEntity, Long
1819
"JOIN FETCH a.project " +
1920
"WHERE a.project = :project")
2021
List<ApplicantEntity> findByProject(@Param("project") ProjectEntity project);
22+
23+
Optional<ApplicantEntity> findByProjectIdAndUserId(Long projectId, Long evaluateeId);
24+
25+
List<ApplicantEntity> findAllByProjectIdAndStatus(Long projectId, ApplicantStatus applicantStatus);
2126
}

src/main/java/hs/kr/backend/devpals/global/exception/ErrorException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ public enum ErrorException {
4848
NOT_INQUIRY_DELETE("문의글을 삭제할 권한이 없습니다."),
4949
FAQ_NOT_FOUND("FAQ글을 찾을 수 없습니다."),
5050
NO_PERMISSION("관리자 기능입니다."),
51+
52+
INVALID_EVALUATION_SCORES("점수는 6개 항목으로 구성되어야 합니다."),
53+
EVALUATION_ALREADY_EXISTS("이미 평가를 완료한 사용자입니다."),
54+
APPLICANT_NOT_FOUND("해당 프로젝트에 참여자로 등록된 유저가 아닙니다."),
55+
INVALID_EVALUATION_TARGET("평가 대상은 프로젝트에 참여 중인 유저여야 합니다.");
56+
5157
NOT_FOUND_NOTICE("공지사항을 찾을 수 없습니다.");
5258

5359
private final String message;

0 commit comments

Comments
 (0)