Skip to content

Commit 54344f4

Browse files
authored
[FEAT] #197: 신규 멤버 모집 관련 관리자 페이지 api 구현
[FEAT] #197: 신규 멤버 모집 관련 관리자 페이지 api 구현
2 parents a6be106 + 3da088d commit 54344f4

File tree

12 files changed

+275
-6
lines changed

12 files changed

+275
-6
lines changed

src/main/java/inha/gdgoc/domain/recruit/controller/RecruitMemberController.java

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,48 @@
11
package inha.gdgoc.domain.recruit.controller;
22

3+
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.MEMBER_LIST_RETRIEVED_SUCCESS;
34
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.MEMBER_RETRIEVED_SUCCESS;
45
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.MEMBER_SAVE_SUCCESS;
6+
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.PAYMENT_MARKED_COMPLETE_SUCCESS;
7+
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.PAYMENT_MARKED_INCOMPLETE_SUCCESS;
58
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS;
69
import static inha.gdgoc.domain.recruit.controller.message.RecruitMemberMessage.STUDENT_ID_DUPLICATION_CHECK_SUCCESS;
710

811
import inha.gdgoc.domain.recruit.dto.request.ApplicationRequest;
12+
import inha.gdgoc.domain.recruit.dto.request.PaymentUpdateRequest;
913
import inha.gdgoc.domain.recruit.dto.response.CheckPhoneNumberResponse;
1014
import inha.gdgoc.domain.recruit.dto.response.CheckStudentIdResponse;
15+
import inha.gdgoc.domain.recruit.dto.response.RecruitMemberSummaryResponse;
1116
import inha.gdgoc.domain.recruit.dto.response.SpecifiedMemberResponse;
17+
import inha.gdgoc.domain.recruit.entity.RecruitMember;
1218
import inha.gdgoc.domain.recruit.service.RecruitMemberService;
1319
import inha.gdgoc.global.dto.response.ApiResponse;
20+
import inha.gdgoc.global.dto.response.PageMeta;
1421
import io.swagger.v3.oas.annotations.Operation;
22+
import io.swagger.v3.oas.annotations.Parameter;
1523
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
24+
import io.swagger.v3.oas.annotations.tags.Tag;
1625
import jakarta.validation.constraints.NotBlank;
1726
import jakarta.validation.constraints.Pattern;
27+
import java.util.List;
1828
import lombok.RequiredArgsConstructor;
29+
import org.springframework.data.domain.Page;
30+
import org.springframework.data.domain.PageRequest;
31+
import org.springframework.data.domain.Pageable;
32+
import org.springframework.data.domain.Sort;
33+
import org.springframework.data.domain.Sort.Direction;
1934
import org.springframework.http.ResponseEntity;
2035
import org.springframework.security.access.prepost.PreAuthorize;
2136
import org.springframework.web.bind.annotation.GetMapping;
37+
import org.springframework.web.bind.annotation.PatchMapping;
2238
import org.springframework.web.bind.annotation.PathVariable;
2339
import org.springframework.web.bind.annotation.PostMapping;
2440
import org.springframework.web.bind.annotation.RequestBody;
2541
import org.springframework.web.bind.annotation.RequestMapping;
2642
import org.springframework.web.bind.annotation.RequestParam;
2743
import org.springframework.web.bind.annotation.RestController;
2844

45+
@Tag(name = "Recruit - Members", description = "리크루팅 지원자 관리 API")
2946
@RequestMapping("/api/v1")
3047
@RequiredArgsConstructor
3148
@RestController
@@ -67,7 +84,7 @@ public ResponseEntity<ApiResponse<CheckPhoneNumberResponse, Void>> duplicatedPho
6784
return ResponseEntity.ok(ApiResponse.ok(PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS, response));
6885
}
6986

70-
@Operation(summary = "특정 멤버 가입 신청서 조회", security = { @SecurityRequirement(name = "BearerAuth") })
87+
@Operation(summary = "특정 멤버 가입 신청서 조회", security = {@SecurityRequirement(name = "BearerAuth")})
7188
@PreAuthorize("hasRole('ADMIN')")
7289
@GetMapping("/recruit/members/{memberId}")
7390
public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMember(
@@ -78,9 +95,64 @@ public ResponseEntity<ApiResponse<SpecifiedMemberResponse, Void>> getSpecifiedMe
7895
return ResponseEntity.ok(ApiResponse.ok(MEMBER_RETRIEVED_SUCCESS, response));
7996
}
8097

81-
// TODO 전체 응답 조회 및 검색
98+
@Operation(
99+
summary = "입금 상태 변경",
100+
description = "설정하려는 상태(NOT 현재 상태)를 body에 보내주세요. true=입금 완료, false=입금 미완료",
101+
security = { @SecurityRequirement(name = "BearerAuth") }
102+
)
103+
@PreAuthorize("hasRole('ADMIN')")
104+
@PatchMapping("/recruit/members/{memberId}/payment")
105+
public ResponseEntity<ApiResponse<Void, Void>> updatePayment(
106+
@PathVariable Long memberId,
107+
@RequestBody PaymentUpdateRequest paymentUpdateRequest
108+
) {
109+
recruitMemberService.updatePayment(memberId, paymentUpdateRequest.isPayed());
110+
111+
return ResponseEntity.ok(
112+
ApiResponse.ok(
113+
paymentUpdateRequest.isPayed()
114+
? PAYMENT_MARKED_COMPLETE_SUCCESS
115+
: PAYMENT_MARKED_INCOMPLETE_SUCCESS
116+
)
117+
);
118+
}
119+
120+
@Operation(
121+
summary = "지원자 목록 조회",
122+
description = "전체 목록 또는 이름 검색 결과를 반환합니다. 검색어(question)를 주면 이름 포함 검색, 없으면 전체 조회. sort랑 dir은 example 값 그대로 코딩하는 것 추천...",
123+
security = { @SecurityRequirement(name = "BearerAuth") }
124+
)
125+
@PreAuthorize("hasRole('ADMIN')")
126+
@GetMapping("/recruit/members")
127+
public ResponseEntity<ApiResponse<List<RecruitMemberSummaryResponse>, PageMeta>> getMembers(
128+
@Parameter(description = "검색어(이름 부분 일치). 없으면 전체 조회", example = "소연")
129+
@RequestParam(required = false) String question,
130+
131+
@Parameter(description = "페이지(0부터 시작)", example = "0")
132+
@RequestParam(defaultValue = "0") int page,
82133

83-
// TODO 입금 완료
134+
@Parameter(description = "페이지 크기", example = "20")
135+
@RequestParam(defaultValue = "20") int size,
136+
137+
@Parameter(description = "정렬 필드", example = "createdAt")
138+
@RequestParam(defaultValue = "createdAt") String sort,
139+
140+
@Parameter(description = "정렬 방향 ASC/DESC", example = "DESC")
141+
@RequestParam(defaultValue = "DESC") String dir
142+
) {
143+
Direction direction = "ASC".equalsIgnoreCase(dir) ? Direction.ASC : Direction.DESC;
144+
Pageable pageable = PageRequest.of(page, size, Sort.by(direction, sort));
145+
146+
Page<RecruitMember> memberPage = (question == null || question.isBlank())
147+
? recruitMemberService.findAllMembersPage(pageable)
148+
: recruitMemberService.searchMembersByNamePage(question, pageable);
149+
150+
List<RecruitMemberSummaryResponse> list = memberPage
151+
.map(RecruitMemberSummaryResponse::from)
152+
.getContent();
153+
PageMeta meta = PageMeta.of(memberPage);
154+
155+
return ResponseEntity.ok(ApiResponse.ok(MEMBER_LIST_RETRIEVED_SUCCESS, list, meta));
156+
}
84157

85-
// TODO 입금 미완료
86158
}

src/main/java/inha/gdgoc/domain/recruit/controller/message/RecruitMemberMessage.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ public class RecruitMemberMessage {
55
public static final String STUDENT_ID_DUPLICATION_CHECK_SUCCESS = "성공적으로 학번 중복 조회를 완료했습니다.";
66
public static final String PHONE_NUMBER_DUPLICATION_CHECK_SUCCESS = "성공적으로 전화번호 중복 조회를 완료했습니다.";
77
public static final String MEMBER_RETRIEVED_SUCCESS = "성공적으로 특정 멤버의 지원서를 조회했습니다.";
8+
public static final String PAYMENT_MARKED_COMPLETE_SUCCESS = "성공적으로 입금 완료로 변경했습니다.";
9+
public static final String PAYMENT_MARKED_INCOMPLETE_SUCCESS = "성공적으로 입금 미완료로 변경했습니다.";
10+
public static final String MEMBER_LIST_RETRIEVED_SUCCESS = "성공적으로 멤버 목록을 조회했습니다.";
811
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package inha.gdgoc.domain.recruit.dto.request;
2+
3+
public record PaymentUpdateRequest(
4+
boolean isPayed
5+
) {
6+
7+
}

src/main/java/inha/gdgoc/domain/recruit/dto/request/RecruitMemberRequest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import inha.gdgoc.domain.recruit.entity.RecruitMember;
44
import inha.gdgoc.domain.recruit.enums.EnrolledClassification;
55
import inha.gdgoc.domain.recruit.enums.Gender;
6+
import inha.gdgoc.global.util.SemesterCalculator;
67
import java.time.LocalDate;
78
import lombok.AllArgsConstructor;
89
import lombok.Builder;
@@ -41,6 +42,7 @@ public RecruitMember toEntity() {
4142
.major(major)
4243
.doubleMajor(doubleMajor)
4344
.isPayed(false)
45+
.admissionSemester(SemesterCalculator.currentSemester())
4446
.build();
4547
}
4648
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package inha.gdgoc.domain.recruit.dto.response;
2+
3+
import inha.gdgoc.domain.recruit.entity.RecruitMember;
4+
5+
public record RecruitMemberSummaryResponse(
6+
Long id,
7+
String name,
8+
String phoneNumber,
9+
String major,
10+
String studentId,
11+
String admissionSemester,
12+
Boolean isPayed
13+
) {
14+
15+
public static RecruitMemberSummaryResponse from(RecruitMember recruitMember) {
16+
String semester = null;
17+
if (recruitMember.getAdmissionSemester() != null) {
18+
String enumName = recruitMember.getAdmissionSemester().name();
19+
semester = enumName.substring(1).replace('_', '-');
20+
}
21+
22+
return new RecruitMemberSummaryResponse(
23+
recruitMember.getId(),
24+
recruitMember.getName(),
25+
recruitMember.getPhoneNumber(),
26+
recruitMember.getMajor(),
27+
recruitMember.getStudentId(),
28+
semester,
29+
recruitMember.getIsPayed()
30+
);
31+
}
32+
}

src/main/java/inha/gdgoc/domain/recruit/entity/RecruitMember.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package inha.gdgoc.domain.recruit.entity;
22

33
import com.fasterxml.jackson.annotation.JsonFormat;
4+
import inha.gdgoc.domain.recruit.enums.AdmissionSemester;
45
import inha.gdgoc.domain.recruit.enums.EnrolledClassification;
56
import inha.gdgoc.domain.recruit.enums.Gender;
67
import inha.gdgoc.global.entity.BaseEntity;
@@ -67,13 +68,25 @@ public class RecruitMember extends BaseEntity {
6768
@Column(name = "major", nullable = false)
6869
private String major;
6970

70-
@Column(name = "double_major", nullable = true)
71+
@Column(name = "double_major")
7172
private String doubleMajor;
7273

7374
@Column(name = "is_payed", nullable = false)
7475
private Boolean isPayed;
7576

77+
@Enumerated(EnumType.STRING)
78+
@Column(name = "admission_semester", nullable = false, length = 10)
79+
private AdmissionSemester admissionSemester;
80+
7681
@Builder.Default
7782
@OneToMany(mappedBy = "recruitMember", cascade = CascadeType.ALL, orphanRemoval = true)
7883
private List<Answer> answers = new ArrayList<>();
84+
85+
public void markPaid() {
86+
this.isPayed = Boolean.TRUE;
87+
}
88+
89+
public void markUnpaid() {
90+
this.isPayed = Boolean.FALSE;
91+
}
7992
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package inha.gdgoc.domain.recruit.enums;
2+
3+
public enum AdmissionSemester {
4+
Y25_1, Y25_2, Y26_1, Y26_2
5+
}

src/main/java/inha/gdgoc/domain/recruit/repository/RecruitMemberRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22

33
import inha.gdgoc.domain.recruit.entity.RecruitMember;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.domain.Page;
6+
import org.springframework.data.domain.Pageable;
57

68
public interface RecruitMemberRepository extends JpaRepository<RecruitMember, Long> {
79
boolean existsByStudentId(String studentId);
810
boolean existsByPhoneNumber(String phoneNumber);
11+
Page<RecruitMember> findByNameContainingIgnoreCase(String name, Pageable pageable);
912
}

src/main/java/inha/gdgoc/domain/recruit/service/RecruitMemberService.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
import inha.gdgoc.domain.recruit.exception.RecruitMemberException;
1515
import inha.gdgoc.domain.recruit.repository.AnswerRepository;
1616
import inha.gdgoc.domain.recruit.repository.RecruitMemberRepository;
17-
import jakarta.transaction.Transactional;
1817
import java.util.List;
1918
import lombok.RequiredArgsConstructor;
19+
import org.springframework.data.domain.Page;
20+
import org.springframework.data.domain.Pageable;
2021
import org.springframework.stereotype.Service;
22+
import org.springframework.transaction.annotation.Transactional;
2123

2224
@RequiredArgsConstructor
2325
@Service
@@ -67,4 +69,26 @@ public SpecifiedMemberResponse findSpecifiedMember(Long id) {
6769

6870
return SpecifiedMemberResponse.from(member, answers, objectMapper);
6971
}
72+
73+
@Transactional
74+
public void updatePayment(Long memberId, boolean isPayed) {
75+
RecruitMember m = recruitMemberRepository.findById(memberId)
76+
.orElseThrow(() -> new RecruitMemberException(RECRUIT_MEMBER_NOT_FOUND));
77+
78+
if (Boolean.TRUE.equals(m.getIsPayed()) == isPayed) return;
79+
80+
if (isPayed) m.markPaid();
81+
else m.markUnpaid();
82+
}
83+
84+
@Transactional(readOnly = true)
85+
public Page<RecruitMember> findAllMembersPage(Pageable pageable) {
86+
return recruitMemberRepository.findAll(pageable);
87+
}
88+
89+
@Transactional(readOnly = true)
90+
public Page<RecruitMember> searchMembersByNamePage(String name, Pageable pageable) {
91+
return recruitMemberRepository.findByNameContainingIgnoreCase(name, pageable);
92+
}
93+
7094
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package inha.gdgoc.global.dto.response;
2+
3+
import org.springframework.data.domain.Page;
4+
import org.springframework.data.domain.Sort;
5+
6+
public record PageMeta(
7+
int page,
8+
int size,
9+
long totalElements,
10+
int totalPages,
11+
boolean hasNext,
12+
boolean hasPrevious,
13+
String sort,
14+
String direction
15+
) {
16+
17+
public static PageMeta of(Page<?> page) {
18+
String sortProps = page.getSort().stream()
19+
.map(Sort.Order::getProperty)
20+
.reduce((a, b) -> a + "," + b)
21+
.orElse("createdAt");
22+
23+
String dir = page.getSort().stream()
24+
.findFirst()
25+
.map(o -> o.getDirection().name())
26+
.orElse("DESC");
27+
28+
return new PageMeta(
29+
page.getNumber(),
30+
page.getSize(),
31+
page.getTotalElements(),
32+
page.getTotalPages(),
33+
page.hasNext(),
34+
page.hasPrevious(),
35+
sortProps,
36+
dir
37+
);
38+
}
39+
}

0 commit comments

Comments
 (0)