Skip to content

Commit 0a8bf83

Browse files
authored
feat: 지원자 조회 API 구현
* feat: 부원 지원 엔드포인트 추가 * feat: 페이징 응답 전용 DTO 구현 - 기존 Page 인터페이스의 직렬화 문제로 인해 스프링 부트 3.3부터 추가된 WARNING `Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!` - `@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)` 옵션을 적용해 직렬화를 적용할 수 있지만, 응답 데이터가 일부 누락되는 것을 확인 - 직접 페이징 응답 전용 DTO를 구현함으로써 해결 * feat: 지원자 응답 DTO 구현 * feat: 지원자 응답 DTO 변환 메소드 추가 * feat: 지원자 조회 로직 구현 - 페이징 정책에 따라 20건 씩 응답 * feat: 리소스 조회 관련 에러 코드 추가 * test: Applicant 도메인 테스트 케이스 작성 * feat: 어드민; 지원자 조회 엔드포인트 구현 * feat: 리스트 조회, 상세 조회 DTO 분리 - name 필드 추가 * feat: name 필드 추가 - 조회 DTO 변환 메소드 추가 * fix: 에러 코드 수정 - 병합 충돌을 막기 위해 에러 코드 수정 * feat: 지원자 상세 조회 로직 구현 * feat: 지원자 상태 변경 로직 구현 - 요청 DTO 구현 - 상태 변경 후 지원자 정보 리턴
1 parent 6362e2e commit 0a8bf83

File tree

12 files changed

+433
-1
lines changed

12 files changed

+433
-1
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package dmu.dasom.api.domain.applicant.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import lombok.*;
5+
import lombok.experimental.SuperBuilder;
6+
7+
import java.time.LocalDateTime;
8+
9+
@AllArgsConstructor
10+
@SuperBuilder
11+
@Getter
12+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
13+
@Schema(name = "ApplicantDetailsResponseDto", description = "지원자 상세 응답 DTO")
14+
public class ApplicantDetailsResponseDto extends ApplicantResponseDto {
15+
16+
@Schema(description = "연락처", example = "010-1234-5678")
17+
private String contact;
18+
19+
@Schema(description = "이메일", example = "[email protected]")
20+
private String email;
21+
22+
@Schema(description = "학년", example = "3")
23+
private int grade;
24+
25+
@Schema(description = "지원 동기", example = "동아리 활동을 통해 새로운 경험을 쌓고 싶어서 지원합니다.")
26+
private String reasonForApply;
27+
28+
@Schema(description = "희망 활동", example = "프로젝트")
29+
private String activityWish;
30+
31+
@Schema(description = "개인정보 처리방침 동의 여부", example = "true")
32+
private Boolean isPrivacyPolicyAgreed;
33+
34+
@Schema(description = "지원일시", example = "2021-10-01T00:00:00")
35+
private LocalDateTime createdAt;
36+
37+
@Schema(description = "수정일시", example = "2021-10-01T00:00:00")
38+
private LocalDateTime updatedAt;
39+
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package dmu.dasom.api.domain.applicant.dto;
2+
3+
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.*;
6+
import lombok.experimental.SuperBuilder;
7+
8+
@AllArgsConstructor
9+
@SuperBuilder
10+
@Getter
11+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12+
@Schema(name = "ApplicantResponseDto", description = "지원자 응답 DTO")
13+
public class ApplicantResponseDto {
14+
15+
@Schema(description = "id", example = "1")
16+
private Long id;
17+
18+
@Schema(description = "이름", example = "홍길동")
19+
private String name;
20+
21+
@Schema(description = "학번", example = "20210000")
22+
private String studentNo;
23+
24+
@Schema(description = "상태", example = "PENDING")
25+
private ApplicantStatus status;
26+
27+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package dmu.dasom.api.domain.applicant.dto;
2+
3+
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import lombok.Getter;
6+
7+
@Getter
8+
@Schema(name = "ApplicantStatusUpdateRequestDto", description = "지원자 상태 변경 요청 DTO")
9+
public class ApplicantStatusUpdateRequestDto {
10+
11+
@Schema(description = "상태", example = "DOCUMENT_PASSED")
12+
private ApplicantStatus status;
13+
14+
}

src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package dmu.dasom.api.domain.applicant.entity;
22

3+
import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto;
4+
import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto;
35
import dmu.dasom.api.domain.applicant.enums.ApplicantStatus;
46
import jakarta.persistence.*;
57
import jakarta.validation.constraints.*;
@@ -26,6 +28,10 @@ public class Applicant {
2628
@Id
2729
private Long id;
2830

31+
@Column(name = "name", nullable = false, length = 16)
32+
@Size(max = 16)
33+
private String name;
34+
2935
@Column(name = "student_no", nullable = false, length = 8)
3036
@Pattern(regexp = "^[0-9]{8}$")
3137
@Size(min = 8, max = 8)
@@ -73,4 +79,30 @@ public void updateStatus(final ApplicantStatus status) {
7379
this.status = status;
7480
}
7581

82+
public ApplicantResponseDto toApplicantResponse() {
83+
return ApplicantResponseDto.builder()
84+
.id(this.id)
85+
.name(this.name)
86+
.studentNo(this.studentNo)
87+
.status(this.status)
88+
.build();
89+
}
90+
91+
public ApplicantDetailsResponseDto toApplicantDetailsResponse() {
92+
return ApplicantDetailsResponseDto.builder()
93+
.id(this.id)
94+
.name(this.name)
95+
.studentNo(this.studentNo)
96+
.contact(this.contact)
97+
.email(this.email)
98+
.grade(this.grade)
99+
.reasonForApply(this.reasonForApply)
100+
.activityWish(this.activityWish)
101+
.isPrivacyPolicyAgreed(this.isPrivacyPolicyAgreed)
102+
.status(this.status)
103+
.createdAt(this.createdAt)
104+
.updatedAt(this.updatedAt)
105+
.build();
106+
}
107+
76108
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
package dmu.dasom.api.domain.applicant.repository;
22

33
import dmu.dasom.api.domain.applicant.entity.Applicant;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
46
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Query;
58

69
public interface ApplicantRepository extends JpaRepository<Applicant, Long> {
10+
11+
@Query("SELECT a FROM Applicant a ORDER BY a.id DESC")
12+
Page<Applicant> findAllWithPageRequest(final Pageable pageable);
13+
714
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package dmu.dasom.api.domain.applicant.service;
22

33
import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto;
4+
import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto;
5+
import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto;
6+
import dmu.dasom.api.domain.applicant.dto.ApplicantStatusUpdateRequestDto;
7+
import dmu.dasom.api.global.dto.PageResponse;
48

59
public interface ApplicantService {
610

711
void apply(final ApplicantCreateRequestDto request);
812

13+
PageResponse<ApplicantResponseDto> getApplicants(final int page);
14+
15+
ApplicantDetailsResponseDto getApplicant(final Long id);
16+
17+
ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final ApplicantStatusUpdateRequestDto request);
18+
919
}

src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
package dmu.dasom.api.domain.applicant.service;
22

33
import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto;
4+
import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto;
5+
import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto;
6+
import dmu.dasom.api.domain.applicant.dto.ApplicantStatusUpdateRequestDto;
7+
import dmu.dasom.api.domain.applicant.entity.Applicant;
48
import dmu.dasom.api.domain.applicant.repository.ApplicantRepository;
9+
import dmu.dasom.api.domain.common.exception.CustomException;
10+
import dmu.dasom.api.domain.common.exception.ErrorCode;
11+
import dmu.dasom.api.global.dto.PageResponse;
512
import lombok.RequiredArgsConstructor;
13+
import org.springframework.data.domain.Page;
14+
import org.springframework.data.domain.PageRequest;
615
import org.springframework.stereotype.Service;
716

817
@RequiredArgsConstructor
918
@Service
1019
public class ApplicantServiceImpl implements ApplicantService {
1120

21+
private final static int DEFAULT_PAGE_SIZE = 20;
22+
1223
private final ApplicantRepository applicantRepository;
1324

1425
// 지원자 저장
@@ -17,4 +28,37 @@ public void apply(final ApplicantCreateRequestDto request) {
1728
applicantRepository.save(request.toEntity());
1829
}
1930

31+
// 지원자 조회
32+
@Override
33+
public PageResponse<ApplicantResponseDto> getApplicants(final int page) {
34+
final Page<Applicant> applicants = applicantRepository.findAllWithPageRequest(PageRequest.of(page, DEFAULT_PAGE_SIZE));
35+
36+
// 조회 조건에 따라 결과가 없을 수 있음
37+
if (applicants.isEmpty())
38+
throw new CustomException(ErrorCode.EMPTY_RESULT);
39+
40+
return PageResponse.from(applicants.map(Applicant::toApplicantResponse));
41+
}
42+
43+
// 지원자 상세 조회
44+
@Override
45+
public ApplicantDetailsResponseDto getApplicant(final Long id) {
46+
return findById(id).toApplicantDetailsResponse();
47+
}
48+
49+
// 지원자 상태 변경
50+
@Override
51+
public ApplicantDetailsResponseDto updateApplicantStatus(final Long id, final ApplicantStatusUpdateRequestDto request) {
52+
final Applicant applicant = findById(id);
53+
applicant.updateStatus(request.getStatus());
54+
55+
return applicant.toApplicantDetailsResponse();
56+
}
57+
58+
// Repository에서 ID로 지원자 조회
59+
private Applicant findById(final Long id) {
60+
return applicantRepository.findById(id)
61+
.orElseThrow(() -> new CustomException(ErrorCode.EMPTY_RESULT));
62+
}
63+
2064
}

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ public enum ErrorCode {
1717
TOKEN_NOT_VALID(400, "C008", "토큰이 올바르지 않습니다."),
1818
INTERNAL_SERVER_ERROR(500, "C009", "서버에 문제가 발생하였습니다."),
1919
NOT_FOUND(404, "C010", "해당 리소스를 찾을 수 없습니다."),
20-
WRITE_FAIL(400, "C011", "데이터를 쓰는데 실패하였습니다.")
20+
WRITE_FAIL(400, "C011", "데이터를 쓰는데 실패하였습니다."),
21+
EMPTY_RESULT(400, "C012", "조회 결과가 없습니다.")
2122
;
2223

2324
private final int status;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package dmu.dasom.api.domain.recruit.controller;
2+
3+
import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto;
4+
import dmu.dasom.api.domain.applicant.service.ApplicantService;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
7+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
8+
import jakarta.validation.Valid;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.PostMapping;
12+
import org.springframework.web.bind.annotation.RequestBody;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequestMapping("/api/recruit")
18+
@RequiredArgsConstructor
19+
public class RecruitController {
20+
21+
private final ApplicantService applicantService;
22+
23+
// 지원하기
24+
@Operation(summary = "부원 지원하기")
25+
@ApiResponses(value = {
26+
@ApiResponse(responseCode = "200", description = "지원 성공")
27+
})
28+
@PostMapping("/apply")
29+
public ResponseEntity<Void> apply(@Valid @RequestBody final ApplicantCreateRequestDto request) {
30+
applicantService.apply(request);
31+
return ResponseEntity.ok().build();
32+
}
33+
34+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package dmu.dasom.api.global.admin.controller;
2+
3+
import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto;
4+
import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto;
5+
import dmu.dasom.api.domain.applicant.dto.ApplicantStatusUpdateRequestDto;
6+
import dmu.dasom.api.domain.applicant.service.ApplicantService;
7+
import dmu.dasom.api.global.dto.PageResponse;
8+
import io.swagger.v3.oas.annotations.Operation;
9+
import io.swagger.v3.oas.annotations.media.Content;
10+
import io.swagger.v3.oas.annotations.media.ExampleObject;
11+
import io.swagger.v3.oas.annotations.media.Schema;
12+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
13+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
14+
import jakarta.validation.Valid;
15+
import jakarta.validation.constraints.Min;
16+
import lombok.RequiredArgsConstructor;
17+
import org.springframework.http.ResponseEntity;
18+
import org.springframework.web.ErrorResponse;
19+
import org.springframework.web.bind.annotation.*;
20+
21+
@RestController
22+
@RequestMapping("/api/admin")
23+
@RequiredArgsConstructor
24+
public class AdminController {
25+
26+
private final ApplicantService applicantService;
27+
28+
// 지원자 조회
29+
@Operation(summary = "지원자 전체 조회")
30+
@ApiResponses(value = {
31+
@ApiResponse(responseCode = "200", description = "지원자 조회 성공"),
32+
@ApiResponse(responseCode = "400", description = "잘못된 요청",
33+
content = @Content(
34+
mediaType = "application/json",
35+
schema = @Schema(implementation = ErrorResponse.class),
36+
examples = {
37+
@ExampleObject(
38+
name = "조회 결과 없음",
39+
value = "{ \"code\": \"C012\", \"message\": \"조회 결과가 없습니다.\" }"
40+
)
41+
}
42+
)
43+
)
44+
})
45+
@GetMapping("/applicants")
46+
public ResponseEntity<PageResponse<ApplicantResponseDto>> getApplicants(
47+
@RequestParam(value = "page", defaultValue = "0") @Min(0) final int page
48+
) {
49+
return ResponseEntity.ok(applicantService.getApplicants(page));
50+
}
51+
52+
// 지원자 상세 조회
53+
@Operation(summary = "지원자 상세 조회")
54+
@ApiResponses(value = {
55+
@ApiResponse(responseCode = "200", description = "지원자 상세 조회 성공"),
56+
@ApiResponse(responseCode = "400", description = "잘못된 요청",
57+
content = @Content(
58+
mediaType = "application/json",
59+
schema = @Schema(implementation = ErrorResponse.class),
60+
examples = {
61+
@ExampleObject(
62+
name = "조회 결과 없음",
63+
value = "{ \"code\": \"C012\", \"message\": \"조회 결과가 없습니다.\" }"
64+
)
65+
}
66+
)
67+
)
68+
})
69+
@GetMapping("/applicants/{id}")
70+
public ResponseEntity<ApplicantDetailsResponseDto> getApplicant(@PathVariable("id") @Min(0) final Long id) {
71+
return ResponseEntity.ok(applicantService.getApplicant(id));
72+
}
73+
74+
// 지원자 상태 변경
75+
@Operation(summary = "지원자 상태 변경")
76+
@PatchMapping("/applicants/{id}/status")
77+
public ResponseEntity<ApplicantDetailsResponseDto> updateApplicantStatus(@PathVariable("id") @Min(0) final Long id,
78+
@Valid @RequestBody final ApplicantStatusUpdateRequestDto request) {
79+
return ResponseEntity.ok(applicantService.updateApplicantStatus(id, request));
80+
}
81+
82+
}

0 commit comments

Comments
 (0)