Skip to content

Commit 3c342a8

Browse files
authored
feat: 어드민 멘토 승격 지원서 승인/거절 기능, 상태 별 지원서 개수 조회 기능 추가 (#577)
* feat: 어드민 멘토 승격 지원서 승인/거절 기능 추가 * test: 어드민 멘토 지원서 승인/거절 테스트 추가 * feat: 멘토 지원서 상태별 개수 조회 기능 추가 * test: 멘토 지원서 상태별 개수 조회 테스트 추가 * fix: 대학이 선택되지 않은 멘토 지원서 승인 시 예외 발생하도록 수정 * refactor: 리뷰 내용 반영 * refactor: MENTOR_APPLICATION_ALREADY_CONFIRM -> MENTOR_APPLICATION_ALREADY_CONFIRMED 로 수정 * refactor: 멘토 지원서 거절 사유 관련하여 기획에 명시되지 않은 길이 제한 제거 * refactor: 리뷰 적용 * refactor: 변수명, 필드명 일관성 맞추기 * test: assertAll 적용
1 parent d052325 commit 3c342a8

File tree

8 files changed

+280
-0
lines changed

8 files changed

+280
-0
lines changed

src/main/java/com/example/solidconnection/admin/controller/AdminMentorApplicationController.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.example.solidconnection.admin.controller;
22

3+
import com.example.solidconnection.admin.dto.MentorApplicationCountResponse;
4+
import com.example.solidconnection.admin.dto.MentorApplicationRejectRequest;
35
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
46
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
57
import com.example.solidconnection.admin.service.AdminMentorApplicationService;
@@ -12,6 +14,9 @@
1214
import org.springframework.http.ResponseEntity;
1315
import org.springframework.web.bind.annotation.GetMapping;
1416
import org.springframework.web.bind.annotation.ModelAttribute;
17+
import org.springframework.web.bind.annotation.PathVariable;
18+
import org.springframework.web.bind.annotation.PostMapping;
19+
import org.springframework.web.bind.annotation.RequestBody;
1520
import org.springframework.web.bind.annotation.RequestMapping;
1621
import org.springframework.web.bind.annotation.RestController;
1722

@@ -34,4 +39,27 @@ public ResponseEntity<PageResponse<MentorApplicationSearchResponse>> searchMento
3439

3540
return ResponseEntity.ok(PageResponse.of(page));
3641
}
42+
43+
@PostMapping("/{mentorApplicationId}/approve")
44+
public ResponseEntity<Void> approveMentorApplication(
45+
@PathVariable("mentorApplicationId") Long mentorApplicationId
46+
) {
47+
adminMentorApplicationService.approveMentorApplication(mentorApplicationId);
48+
return ResponseEntity.ok().build();
49+
}
50+
51+
@PostMapping("/{mentorApplicationId}/reject")
52+
public ResponseEntity<Void> rejectMentorApplication(
53+
@PathVariable("mentorApplicationId") Long mentorApplicationId,
54+
@Valid @RequestBody MentorApplicationRejectRequest request
55+
) {
56+
adminMentorApplicationService.rejectMentorApplication(mentorApplicationId, request);
57+
return ResponseEntity.ok().build();
58+
}
59+
60+
@GetMapping("/count")
61+
public ResponseEntity<MentorApplicationCountResponse> getMentorApplicationCount() {
62+
MentorApplicationCountResponse response = adminMentorApplicationService.getMentorApplicationCount();
63+
return ResponseEntity.ok(response);
64+
}
3765
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
public record MentorApplicationCountResponse(
4+
long approvedCount,
5+
long pendingCount,
6+
long rejectedCount
7+
) {
8+
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.example.solidconnection.admin.dto;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
5+
public record MentorApplicationRejectRequest(
6+
@NotBlank(message = "거절 사유는 필수입니다")
7+
String rejectedReason
8+
) {
9+
10+
}

src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
package com.example.solidconnection.admin.service;
22

3+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND;
4+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_UNIVERSITY_NOT_SELECTED;
5+
6+
import com.example.solidconnection.admin.dto.MentorApplicationCountResponse;
7+
import com.example.solidconnection.admin.dto.MentorApplicationRejectRequest;
38
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
49
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
10+
import com.example.solidconnection.common.exception.CustomException;
11+
import com.example.solidconnection.mentor.domain.MentorApplication;
12+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
513
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
614
import lombok.RequiredArgsConstructor;
715
import org.springframework.data.domain.Page;
@@ -22,4 +30,37 @@ public Page<MentorApplicationSearchResponse> searchMentorApplications(
2230
) {
2331
return mentorApplicationRepository.searchMentorApplications(mentorApplicationSearchCondition, pageable);
2432
}
33+
34+
@Transactional
35+
public void approveMentorApplication(Long mentorApplicationId) {
36+
MentorApplication mentorApplication = mentorApplicationRepository.findById(mentorApplicationId)
37+
.orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND));
38+
39+
if(mentorApplication.getUniversityId() == null){
40+
throw new CustomException(MENTOR_APPLICATION_UNIVERSITY_NOT_SELECTED);
41+
}
42+
43+
mentorApplication.approve();
44+
}
45+
46+
@Transactional
47+
public void rejectMentorApplication(long mentorApplicationId, MentorApplicationRejectRequest request) {
48+
MentorApplication mentorApplication = mentorApplicationRepository.findById(mentorApplicationId)
49+
.orElseThrow(() -> new CustomException(MENTOR_APPLICATION_NOT_FOUND));
50+
51+
mentorApplication.reject(request.rejectedReason());
52+
}
53+
54+
@Transactional(readOnly = true)
55+
public MentorApplicationCountResponse getMentorApplicationCount() {
56+
long approvedCount = mentorApplicationRepository.countByMentorApplicationStatus(MentorApplicationStatus.APPROVED);
57+
long pendingCount = mentorApplicationRepository.countByMentorApplicationStatus(MentorApplicationStatus.PENDING);
58+
long rejectedCount = mentorApplicationRepository.countByMentorApplicationStatus(MentorApplicationStatus.REJECTED);
59+
60+
return new MentorApplicationCountResponse(
61+
approvedCount,
62+
pendingCount,
63+
rejectedCount
64+
);
65+
}
2566
}

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ public enum ErrorCode {
129129
UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "기타 학교를 선택한 경우 학교 정보를 입력할 수 없습니다."),
130130
INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 학교 선택 방식입니다."),
131131
MENTOR_ALREADY_EXISTS(HttpStatus.BAD_REQUEST.value(), "이미 존재하는 멘토입니다."),
132+
MENTOR_APPLICATION_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토 승격 요청 입니다."),
133+
MENTOR_APPLICATION_UNIVERSITY_NOT_SELECTED(HttpStatus.BAD_REQUEST.value(), "승인하려는 멘토 신청에 대학교가 선택되지 않았습니다."),
132134

133135
// socket
134136
UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."),

src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.example.solidconnection.mentor.domain;
22

3+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_CONFIRMED;
4+
import static java.time.ZoneOffset.UTC;
5+
import static java.time.temporal.ChronoUnit.MICROS;
6+
37
import com.example.solidconnection.common.BaseEntity;
48
import com.example.solidconnection.common.exception.CustomException;
59
import com.example.solidconnection.common.exception.ErrorCode;
@@ -116,4 +120,20 @@ private void validateExchangeStatus(ExchangeStatus exchangeStatus) {
116120
throw new CustomException(ErrorCode.INVALID_EXCHANGE_STATUS_FOR_MENTOR);
117121
}
118122
}
123+
124+
public void approve(){
125+
if(this.mentorApplicationStatus != MentorApplicationStatus.PENDING) {
126+
throw new CustomException(MENTOR_APPLICATION_ALREADY_CONFIRMED);
127+
}
128+
this.mentorApplicationStatus = MentorApplicationStatus.APPROVED;
129+
this.approvedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS);
130+
}
131+
132+
public void reject(String rejectedReason){
133+
if(this.mentorApplicationStatus != MentorApplicationStatus.PENDING) {
134+
throw new CustomException(MENTOR_APPLICATION_ALREADY_CONFIRMED);
135+
}
136+
this.mentorApplicationStatus = MentorApplicationStatus.REJECTED;
137+
this.rejectedReason = rejectedReason;
138+
}
119139
}

src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ public interface MentorApplicationRepository extends JpaRepository<MentorApplica
1212
boolean existsBySiteUserIdAndMentorApplicationStatusIn(long siteUserId, List<MentorApplicationStatus> mentorApplicationStatuses);
1313

1414
Optional<MentorApplication> findBySiteUserIdAndMentorApplicationStatus(long siteUserId, MentorApplicationStatus mentorApplicationStatus);
15+
16+
long countByMentorApplicationStatus(MentorApplicationStatus mentorApplicationStatus);
1517
}

src/test/java/com/example/solidconnection/admin/service/AdminMentorApplicationServiceTest.java

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package com.example.solidconnection.admin.service;
22

3+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_CONFIRMED;
4+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_NOT_FOUND;
5+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_UNIVERSITY_NOT_SELECTED;
36
import static org.assertj.core.api.Assertions.assertThat;
7+
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
8+
import static org.junit.jupiter.api.Assertions.assertAll;
49

10+
import com.example.solidconnection.admin.dto.MentorApplicationCountResponse;
11+
import com.example.solidconnection.admin.dto.MentorApplicationRejectRequest;
512
import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition;
613
import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse;
14+
import com.example.solidconnection.common.exception.CustomException;
715
import com.example.solidconnection.mentor.domain.MentorApplication;
816
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
917
import com.example.solidconnection.mentor.domain.UniversitySelectType;
1018
import com.example.solidconnection.mentor.fixture.MentorApplicationFixture;
19+
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
1120
import com.example.solidconnection.siteuser.domain.SiteUser;
1221
import com.example.solidconnection.siteuser.fixture.SiteUserFixture;
1322
import com.example.solidconnection.support.TestContainerSpringBootTest;
@@ -40,6 +49,9 @@ class AdminMentorApplicationServiceTest {
4049
@Autowired
4150
private UniversityFixture universityFixture;
4251

52+
@Autowired
53+
private MentorApplicationRepository mentorApplicationRepository;
54+
4355
private MentorApplication mentorApplication1;
4456
private MentorApplication mentorApplication2;
4557
private MentorApplication mentorApplication3;
@@ -209,4 +221,160 @@ class 멘토_승격_지원서_목록_조회 {
209221
.containsOnly(regionKoreanName);
210222
}
211223
}
224+
225+
@Nested
226+
class 멘토_승격_지원서_승인{
227+
228+
@Test
229+
void 대기중인_멘토_지원서를_승인한다() {
230+
// given
231+
long pendingMentorApplicationId = mentorApplication2.getId();
232+
233+
// when
234+
adminMentorApplicationService.approveMentorApplication(pendingMentorApplicationId);
235+
236+
// then
237+
MentorApplication result = mentorApplicationRepository.findById(mentorApplication2.getId()).get();
238+
assertThat(result.getMentorApplicationStatus()).isEqualTo(MentorApplicationStatus.APPROVED);
239+
assertThat(result.getApprovedAt()).isNotNull();
240+
}
241+
242+
@Test
243+
void 대학이_선택되지_않은_멘토_지원서를_승인하면_예외가_발생한다(){
244+
// given
245+
SiteUser user = siteUserFixture.사용자();
246+
MentorApplication noUniversityIdMentorApplication = mentorApplicationFixture.대기중_멘토신청(user.getId(), UniversitySelectType.OTHER, null);
247+
248+
// when & then
249+
assertThatCode(() -> adminMentorApplicationService.approveMentorApplication(noUniversityIdMentorApplication.getId()))
250+
.isInstanceOf(CustomException.class)
251+
.hasMessage(MENTOR_APPLICATION_UNIVERSITY_NOT_SELECTED.getMessage());
252+
}
253+
254+
@Test
255+
void 이미_승인된_멘토_지원서를_승인하면_예외가_발생한다() {
256+
// given
257+
long approvedMentorApplicationId = mentorApplication1.getId();
258+
259+
// when & then
260+
assertThatCode(() -> adminMentorApplicationService.approveMentorApplication(approvedMentorApplicationId))
261+
.isInstanceOf(CustomException.class)
262+
.hasMessage(MENTOR_APPLICATION_ALREADY_CONFIRMED.getMessage());
263+
}
264+
265+
@Test
266+
void 이미_거절된_멘토_지원서를_승인하면_예외가_발생한다() {
267+
// given
268+
long rejectedMentorApplicationId = mentorApplication3.getId();
269+
270+
// when & then
271+
assertThatCode(() -> adminMentorApplicationService.approveMentorApplication(rejectedMentorApplicationId))
272+
.isInstanceOf(CustomException.class)
273+
.hasMessage(MENTOR_APPLICATION_ALREADY_CONFIRMED.getMessage());
274+
}
275+
276+
@Test
277+
void 존재하지_않는_멘토_지원서를_승인하면_예외가_발생한다() {
278+
// given
279+
long nonExistentId = 99999L;
280+
281+
// when & then
282+
assertThatCode(() -> adminMentorApplicationService.approveMentorApplication(nonExistentId))
283+
.isInstanceOf(CustomException.class)
284+
.hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage());
285+
}
286+
}
287+
288+
@Nested
289+
class 멘토_승격_지원서_거절{
290+
291+
@Test
292+
void 대기중인_멘토_지원서를_거절한다() {
293+
// given
294+
long pendingMentorApplicationId = mentorApplication2.getId();
295+
MentorApplicationRejectRequest request = new MentorApplicationRejectRequest("파견학교 인증 자료 누락");
296+
297+
// when
298+
adminMentorApplicationService.rejectMentorApplication(pendingMentorApplicationId, request);
299+
300+
// then
301+
MentorApplication result = mentorApplicationRepository.findById(mentorApplication2.getId()).get();
302+
assertThat(result.getMentorApplicationStatus()).isEqualTo(MentorApplicationStatus.REJECTED);
303+
assertThat(result.getRejectedReason()).isEqualTo(request.rejectedReason());
304+
}
305+
306+
@Test
307+
void 이미_승인된_멘토_지원서를_거절하면_예외가_발생한다() {
308+
// given
309+
long approvedMentorApplicationId = mentorApplication1.getId();
310+
MentorApplicationRejectRequest request = new MentorApplicationRejectRequest("파견학교 인증 자료 누락");
311+
312+
// when & then
313+
assertThatCode(() -> adminMentorApplicationService.rejectMentorApplication(approvedMentorApplicationId, request))
314+
.isInstanceOf(CustomException.class)
315+
.hasMessage(MENTOR_APPLICATION_ALREADY_CONFIRMED.getMessage());
316+
}
317+
318+
@Test
319+
void 이미_거절된_멘토_지원서를_거절하면_예외가_발생한다() {
320+
// given
321+
long rejectedMentorApplicationId = mentorApplication3.getId();
322+
MentorApplicationRejectRequest request = new MentorApplicationRejectRequest("파견학교 인증 자료 누락");
323+
324+
// when & then
325+
assertThatCode(() -> adminMentorApplicationService.rejectMentorApplication(rejectedMentorApplicationId, request))
326+
.isInstanceOf(CustomException.class)
327+
.hasMessage(MENTOR_APPLICATION_ALREADY_CONFIRMED.getMessage());
328+
}
329+
330+
@Test
331+
void 존재하지_않는_멘토_지원서를_거절하면_예외가_발생한다() {
332+
// given
333+
long nonExistentId = 99999L;
334+
MentorApplicationRejectRequest request = new MentorApplicationRejectRequest("파견학교 인증 자료 누락");
335+
336+
// when & then
337+
assertThatCode(() -> adminMentorApplicationService.rejectMentorApplication(nonExistentId, request))
338+
.isInstanceOf(CustomException.class)
339+
.hasMessage(MENTOR_APPLICATION_NOT_FOUND.getMessage());
340+
}
341+
}
342+
343+
@Nested
344+
class 멘토_지원서_상태별_개수_조회 {
345+
346+
@Test
347+
void 상태별_멘토_지원서_개수를_조회한다() {
348+
// given
349+
List<MentorApplication> expectedApprovedCount = List.of(mentorApplication1, mentorApplication4);
350+
List<MentorApplication> expectedPendingCount = List.of(mentorApplication2, mentorApplication5);
351+
List<MentorApplication> expectedRejectedCount = List.of(mentorApplication3, mentorApplication6);
352+
353+
// when
354+
MentorApplicationCountResponse response = adminMentorApplicationService.getMentorApplicationCount();
355+
356+
// then
357+
assertAll(
358+
() -> assertThat(response.approvedCount()).isEqualTo(expectedApprovedCount.size()),
359+
() -> assertThat(response.pendingCount()).isEqualTo(expectedPendingCount.size()),
360+
() -> assertThat(response.rejectedCount()).isEqualTo(expectedRejectedCount.size())
361+
);
362+
}
363+
364+
@Test
365+
void 멘토_지원서가_없으면_모든_개수가_0이다() {
366+
// given
367+
mentorApplicationRepository.deleteAll();
368+
369+
// when
370+
MentorApplicationCountResponse response = adminMentorApplicationService.getMentorApplicationCount();
371+
372+
// then
373+
assertAll(
374+
() -> assertThat(response.approvedCount()).isEqualTo(0L),
375+
() -> assertThat(response.pendingCount()).isEqualTo(0L),
376+
() -> assertThat(response.rejectedCount()).isEqualTo(0L)
377+
);
378+
}
379+
}
212380
}

0 commit comments

Comments
 (0)