Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [Feature] 지원자 목록 확인 API #804

Merged
merged 18 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import java.util.List;
import java.util.Optional;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand All @@ -12,6 +15,50 @@
public interface ApplicationAdminRepository extends JpaRepository<Application, Long> {
Optional<Application> findByIdAndRecruitId(Long applicationId, Long recruitId);

/**
* id 조건에 일치하는 지원서 목록 반환
* @param recruitId
* @param pageable
* @return
*/
@EntityGraph(attributePaths = {"user", "applicationAnswers", "applicationAnswers.question",
"applicationAnswers.question.checkLists"})
Page<Application> findByRecruitIdAndIsDeletedFalseOrderByIdDesc(Long recruitId, Pageable pageable);

/**
* id 조건 및 체크리스트 조건에 일치하는 지원서 목록 반환
* @param recruitId
* @param questionId
* @param checkListIds
* @param pageable
*/
@EntityGraph(attributePaths = {"user", "applicationAnswers", "applicationAnswers.question",
"applicationAnswers.question.checkLists"})
@Query("SELECT a FROM Application a WHERE a.isDeleted = false AND a.id IN "
+ "(SELECT aa.application.id FROM ApplicationAnswerCheckList aa "
+ "JOIN aa.checkList cl "
+ "JOIN aa.application.recruit r "
+ "WHERE r.id =:recruitId AND aa.question.id = :questionId AND cl.id IN :checkListIds) "
+ "ORDER BY a.id DESC")
Page<Application> findAllByCheckList(
@Param("recruitId") Long recruitId,
@Param("questionId") Long questionId,
@Param("checkListIds") List<Long> checkListIds,
Pageable pageable);

@EntityGraph(attributePaths = {"user", "applicationAnswers", "applicationAnswers.question",
"applicationAnswers.question.checkLists"})
@Query("SELECT a FROM Application a WHERE a.isDeleted = false AND a.id IN "
+ "(SELECT aa.application.id FROM ApplicationAnswerText aa "
+ "JOIN aa.application.recruit r "
+ "WHERE r.id =:recruitId AND aa.question.id = :questionId AND aa.answer LIKE CONCAT('%', :search, '%')) "
+ "ORDER BY a.id DESC")
Page<Application> findAllByContainSearch(
@Param("recruitId") Long recruitId,
@Param("questionId") Long questionId,
@Param("search") String search,
Pageable pageable);

@Query("SELECT a FROM Application a "
+ "JOIN FETCH a.user "
+ "LEFT JOIN FETCH a.recruitStatus "
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
package gg.admin.repo.recruit;

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

import javax.persistence.EntityManager;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.transaction.annotation.Transactional;

import gg.data.recruit.application.Application;
import gg.data.recruit.application.ApplicationAnswerCheckList;
import gg.data.recruit.application.ApplicationAnswerText;
import gg.data.recruit.application.RecruitStatus;
import gg.data.recruit.recruitment.CheckList;
import gg.data.recruit.recruitment.Question;
import gg.data.recruit.recruitment.Recruitment;
import gg.data.recruit.recruitment.enums.InputType;
import gg.data.user.User;
import gg.utils.TestDataUtils;
import gg.utils.annotation.IntegrationTest;
Expand Down Expand Up @@ -93,4 +103,129 @@ void findSuccess() {
Assertions.assertThat(res.get(1).getRecruitStatus()).isNull();
}
}

@Nested
@DisplayName("application by getRecruitmentApplications condition success test")
class ApplicationByGetRecruitmentApplicationsCondition {
Long wrongApplicationId;
Long userId;
Long recruitmentId;
Long applicationId;
Long questionId;
Long checkListId1;
Long checkListId2;
Long applicationAnswerCheckListId;
String search = "hello world";

@BeforeEach
public void init() {
Recruitment recruitment = testDataUtils.createNewRecruitment();
User user = testDataUtils.createNewUser();

//target 1
Application application = testDataUtils.createApplication(user, recruitment);
Question question = testDataUtils.createNewQuestion(recruitment, InputType.SINGLE_CHECK, "dubby", 2);
CheckList checkList = testDataUtils.createNewCheckList(question, "dd");
ApplicationAnswerCheckList applicationAnswerCheckList = testDataUtils.createNewApplicationAnswerCheckList(
application, question, checkList);

//target 2
Application application2 = testDataUtils.createApplication(user, recruitment);
CheckList checkList2 = testDataUtils.createNewCheckList(question, "dd");
ApplicationAnswerCheckList applicationAnswerCheckList2 = testDataUtils.createNewApplicationAnswerCheckList(
application2, question, checkList2);

//must not contain
Application application3 = testDataUtils.createApplication(user, recruitment);
CheckList checkList3 = testDataUtils.createNewCheckList(question, "dd");
ApplicationAnswerCheckList applicationAnswerCheckList3 = testDataUtils.createNewApplicationAnswerCheckList(
application, question, checkList3);

// search target
ApplicationAnswerText answerText = testDataUtils.createNewApplicationAnswerText(application, question,
search);
ApplicationAnswerText answerText2 = testDataUtils.createNewApplicationAnswerText(application2, question,
"pp" + search + "pp");
ApplicationAnswerText answerText3 = testDataUtils.createNewApplicationAnswerText(application3, question,
"pp" + search);

wrongApplicationId = application3.getId();
userId = user.getId();
recruitmentId = recruitment.getId();
applicationId = application.getId();
questionId = question.getId();
checkListId1 = checkList.getId();
checkListId2 = checkList2.getId();
applicationAnswerCheckListId = applicationAnswerCheckList.getId();

entityManager.flush();
entityManager.clear();
}

@Nested
@DisplayName("findByRecruitIdAndIsDeletedFalse")
class FindByRecruitIdAndIsDeletedFalse {

@Test
@DisplayName("조회 성공")
void success() {
//Arrange
Pageable pageable = PageRequest.of(0, 10);

// Act
Page<Application> result = applicationAdminRepository.findByRecruitIdAndIsDeletedFalseOrderByIdDesc(
recruitmentId,
pageable);

// Assert
Assertions.assertThat(result.getContent().size()).isEqualTo(3);
}
}

@Nested
@DisplayName("FindAllByCheckList")
class FindAllByCheckList {

@Test
@DisplayName("여러개의 조건 중 하나만 만족해도 조회가 성공")
void success() {
//Arrange
List<Long> checkListTargetId = new ArrayList<>();
checkListTargetId.add(checkListId1);
checkListTargetId.add(checkListId2);
Pageable pageable = PageRequest.of(0, 10);

// Act
Page<Application> result = applicationAdminRepository.findAllByCheckList(recruitmentId,
questionId, checkListTargetId, pageable);

// Assert
Assertions.assertThat(result.getContent().size()).isEqualTo(2);
for (Application entity : result.getContent()) {
Assertions.assertThat(entity.getId()).isNotEqualTo(wrongApplicationId);
}
}
}

@Nested
@DisplayName("findAllByContainSearch")
class FindAllByContainSearch {

@Test
@DisplayName("search를 포함하면 조회 성공")
void success() {
//Arrange
Pageable pageable = PageRequest.of(0, 10);

// Act
Page<Application> result = applicationAdminRepository.findAllByContainSearch(recruitmentId,
questionId, search, pageable);

// Assert
Assertions.assertThat(result.getContent().size()).isEqualTo(3);
}
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package gg.data.recruit.application;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
Expand All @@ -10,6 +13,7 @@
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;

import gg.data.BaseTimeEntity;
Expand Down Expand Up @@ -48,6 +52,9 @@ public class Application extends BaseTimeEntity {
@Column(length = 15, nullable = false)
private ApplicationStatus status;

@OneToMany(mappedBy = "application", fetch = FetchType.LAZY)
private Set<ApplicationAnswer> applicationAnswers = new HashSet<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 set으로 하신 이유가 따로 있나요?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

n+1 때문에 엔티티 그래프로 "applicationAnswers", "applicationAnswers.question.checkLists" 데이터를 조인하는 과정에서 여러 리스트 자료를 명시하는게 허용이 안되더라구요 checkLists는 이미 구현된 내용이고 applicationAnswers는 이번에 양방향을 추가한 내용이기 때문에 해당 부분을 set으로 대체했습니다.


public Application(User user, Recruitment recruit) {
this.user = user;
this.recruit = recruit;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "answer_type")
@Getter
public abstract class ApplicationAnswer extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;

import gg.data.recruit.recruitment.CheckList;
import gg.data.recruit.recruitment.Question;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
@DiscriminatorValue("CHECK_LIST")
@Getter
public class ApplicationAnswerCheckList extends ApplicationAnswer {

@Id
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "check_list_id", nullable = false)
private CheckList checkList;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import javax.persistence.Column;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Id;

import gg.data.recruit.recruitment.Question;
import lombok.NoArgsConstructor;
Expand All @@ -12,10 +11,6 @@
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
@DiscriminatorValue("TEXT")
public class ApplicationAnswerText extends ApplicationAnswer {

@Id
private Long id;
Kimhan-nah marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 ID빠지면 flyway도 수정해줘야 하지 않나요?? 여기 ID값이 있으면 applicationAnswer삭제시에 연결된 applicationAnswerText나 applicationAnswerCheckList에 삭제쿼리가 같이 안나간다는거죠?

Copy link
Member Author

@middlefitting middlefitting Apr 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 그렇습니다 우선 id 컬럼 자체는 부모 엔티티에 정의되어 있으면 자동상속이 이루어지는것 같습니다. 문제가 되는 부분이라면 원래대로 롤백할게요. flyway는 동일하게 작동합니다


@Column(length = 1000)
private String answer;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package gg.recruit.api.admin.controller;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import javax.validation.Valid;
import javax.validation.constraints.Positive;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
Expand All @@ -28,13 +32,17 @@
import gg.recruit.api.admin.controller.request.SetFinalApplicationStatusResultReqDto;
import gg.recruit.api.admin.controller.request.UpdateStatusRequestDto;
import gg.recruit.api.admin.controller.response.CreatedRecruitmentResponse;
import gg.recruit.api.admin.controller.response.GetRecruitmentApplicationResponseDto;
import gg.recruit.api.admin.controller.response.RecruitmentApplicantResultResponseDto;
import gg.recruit.api.admin.controller.response.RecruitmentApplicantResultsResponseDto;
import gg.recruit.api.admin.controller.response.RecruitmentsResponse;
import gg.recruit.api.admin.service.RecruitmentAdminService;
import gg.recruit.api.admin.service.dto.GetRecruitmentApplicationsDto;
import gg.recruit.api.admin.service.dto.UpdateApplicationStatusDto;
import gg.recruit.api.admin.service.dto.UpdateRecruitStatusParam;
import gg.utils.dto.PageRequestDto;
import gg.utils.exception.ErrorCode;
import gg.utils.exception.custom.BusinessException;
import lombok.RequiredArgsConstructor;

@RestController
Expand Down Expand Up @@ -83,6 +91,40 @@ public ResponseEntity<Void> setFinalApplicationStatusResult(
return new ResponseEntity<>(HttpStatus.CREATED);
}

@GetMapping("/{recruitId}/applications")
public ResponseEntity<GetRecruitmentApplicationResponseDto> getRecruitmentApplications(
@PathVariable @Positive Long recruitId,
@RequestParam(value = "question", required = false) Long questionId,
@RequestParam(value = "checks", required = false) String checks,
Kimhan-nah marked this conversation as resolved.
Show resolved Hide resolved
@RequestParam(value = "search", required = false) String search,
@PageableDefault(sort = "id", page = 1) Pageable page
) {
GetRecruitmentApplicationsDto dto;

Pageable parsedPage = PageRequest.of(page.getPageNumber() - 1, Math.min(page.getPageSize(), 20));
List<Long> checkListIds = parseChecks(checks);
dto = new GetRecruitmentApplicationsDto(recruitId, questionId, checkListIds, search, parsedPage);
Page<Application> applicationsPage = recruitmentAdminService.findApplicationsWithAnswersAndUserWithFilter(dto);
return ResponseEntity.ok(GetRecruitmentApplicationResponseDto.applicationsPageToDto(applicationsPage));
}

/**
* 전달된 checkListId 목록 파싱
* @param checks
* @return List<Long>
*/
private List<Long> parseChecks(String checks) {
try {
if (checks != null) {
return Arrays.stream(checks.split(",")).map(Long::parseLong).collect(Collectors.toList());
} else {
return new ArrayList<>();
}
} catch (Exception e) {
throw new BusinessException(ErrorCode.BAD_ARGU);
}
}

@DeleteMapping("/{recruitId}")
public ResponseEntity<Void> deleteRecruitment(@PathVariable @Positive Long recruitId) {
recruitmentAdminService.deleteRecruitment(recruitId);
Expand Down
Loading
Loading