Skip to content

Commit 2e4d946

Browse files
authored
[FEAT] 제출 완료된 지원서 수정 (#313)
* feat: 제출된 지원서 수정 DTO 추 * feat: 제출된 지원서 수정 기능 비즈니스 로직 구현 * test: 제출된 지원서 수정 기능 테스트 * feat: 제출된 지원서 수정 API 스펙 추가 * feat: 제출된 지원서 수정 API 엔드포인트 추가 * style: private method을 하단으로 배치 * feat: 제출된 지원서 다수 삭제 기능에서 삭제한 수 반환 추가 * test: 제출된 지원서 수정 테스트에서 전화번호 형식 변경 * feat: 제출된 지원서 수정 API에 관리자 권한 인증 추가 * feat: 제출된 지원서 수정 API의 경로 수정 * test: 제출된 지원서 수정 테스트에서 전화번호 형식 통일
1 parent 5ceb5ce commit 2e4d946

5 files changed

Lines changed: 235 additions & 5 deletions

File tree

src/main/java/org/ject/support/domain/admin/controller/SubmittedApplyApiSpec.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.ject.support.domain.admin.dto.SubmittedApplyBulkDeleteRequest;
77
import org.ject.support.domain.admin.dto.SubmittedApplyCountResponse;
88
import org.ject.support.domain.admin.dto.SubmittedApplyDetailResponse;
9+
import org.ject.support.domain.admin.dto.SubmittedApplyEditRequest;
910
import org.ject.support.domain.admin.dto.SubmittedApplyResponse;
1011
import org.ject.support.domain.member.JobFamily;
1112
import org.springframework.data.domain.Page;
@@ -34,13 +35,19 @@ Page<SubmittedApplyResponse> findSubmittedApplies(@RequestParam(required = false
3435
description = "전달한 ID에 해당하는 제출된 지원서의 상세 정보를 조회합니다.")
3536
SubmittedApplyDetailResponse findSubmittedApplyDetail(@PathVariable("applyId") final Long applyId);
3637

38+
@Operation(
39+
summary = "제출된 지원서 수정",
40+
description = "전달한 ID에 해당하는 제출된 지원서의 정보를 수정 합니다.")
41+
void editSubmittedApply(@PathVariable("applyId") final Long applyId,
42+
@RequestBody @Valid final SubmittedApplyEditRequest request);
43+
3744
@Operation(
3845
summary = "제출된 지원서 삭제",
3946
description = "선택한 제출된 지원서를 삭제합니다.")
4047
void deleteSubmittedApply(@PathVariable("applyId") final Long applyId);
4148

4249
@Operation(
4350
summary = "제출된 지원서 다수 삭제",
44-
description = "선택한 다수의 제출된 지원서들을 삭제합니다.")
45-
void deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request);
51+
description = "선택한 다수의 제출된 지원서들을 삭제합니다. 삭제한 수를 반환합니다.")
52+
int deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request);
4653
}

src/main/java/org/ject/support/domain/admin/controller/SubmittedApplyController.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.ject.support.domain.admin.dto.SubmittedApplyBulkDeleteRequest;
66
import org.ject.support.domain.admin.dto.SubmittedApplyCountResponse;
77
import org.ject.support.domain.admin.dto.SubmittedApplyDetailResponse;
8+
import org.ject.support.domain.admin.dto.SubmittedApplyEditRequest;
89
import org.ject.support.domain.admin.dto.SubmittedApplyResponse;
910
import org.ject.support.domain.admin.service.SubmittedApplyService;
1011
import org.ject.support.domain.member.JobFamily;
@@ -15,6 +16,7 @@
1516
import org.springframework.web.bind.annotation.DeleteMapping;
1617
import org.springframework.web.bind.annotation.GetMapping;
1718
import org.springframework.web.bind.annotation.PathVariable;
19+
import org.springframework.web.bind.annotation.PutMapping;
1820
import org.springframework.web.bind.annotation.RequestBody;
1921
import org.springframework.web.bind.annotation.RequestMapping;
2022
import org.springframework.web.bind.annotation.RequestParam;
@@ -49,6 +51,14 @@ public SubmittedApplyDetailResponse findSubmittedApplyDetail(@PathVariable("appl
4951
return submittedApplyService.findSubmittedApplyDetail(applyId);
5052
}
5153

54+
@Override
55+
@PutMapping("/{applyId}")
56+
@PreAuthorize("hasRole('ROLE_ADMIN')")
57+
public void editSubmittedApply(@PathVariable("applyId") final Long applyId,
58+
@RequestBody @Valid final SubmittedApplyEditRequest request) {
59+
submittedApplyService.updateSubmittedApply(applyId, request);
60+
}
61+
5262
@Override
5363
@DeleteMapping("/{applyId}")
5464
@PreAuthorize("hasRole('ROLE_ADMIN')")
@@ -59,7 +69,7 @@ public void deleteSubmittedApply(@PathVariable("applyId") final Long applyId) {
5969
@Override
6070
@DeleteMapping
6171
@PreAuthorize("hasRole('ROLE_ADMIN')")
62-
public void deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request) {
63-
submittedApplyService.deleteSubmittedApplies(request.applyIds());
72+
public int deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request) {
73+
return submittedApplyService.deleteSubmittedApplies(request.applyIds());
6474
}
6575
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package org.ject.support.domain.admin.dto;
2+
3+
import jakarta.validation.constraints.Email;
4+
import jakarta.validation.constraints.NotBlank;
5+
import jakarta.validation.constraints.NotNull;
6+
import jakarta.validation.constraints.Pattern;
7+
import java.util.List;
8+
import java.util.Map;
9+
import org.ject.support.domain.apply.dto.ApplyPortfolioDto;
10+
import org.ject.support.domain.member.JobFamily;
11+
12+
public record SubmittedApplyEditRequest(
13+
@NotBlank @Pattern(regexp = "^[가-힣]{1,5}$", message = "한글 1~5글자만 입력 가능합니다.")
14+
String name,
15+
@NotBlank @Pattern(regexp = "^010\\d{8}$", message = "010으로 시작하는 11자리 숫자를 입력하세요.")
16+
String phoneNumber,
17+
@NotBlank @Email
18+
String email,
19+
@NotNull(message = "포지션은 필수입니다.")
20+
JobFamily jobFamily,
21+
@NotNull(message = "답변은 필수입니다.")
22+
Map<String, String> answers,
23+
List<ApplyPortfolioDto> portfolios
24+
) {
25+
}

src/main/java/org/ject/support/domain/admin/service/SubmittedApplyService.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,26 @@
55
import java.util.Optional;
66
import lombok.RequiredArgsConstructor;
77
import org.ject.support.common.data.PageResponse;
8+
import org.ject.support.common.util.Map2JsonSerializer;
89
import org.ject.support.common.util.String2MapSerializer;
910
import org.ject.support.domain.admin.dto.SubmittedApplyCountResponse;
1011
import org.ject.support.domain.admin.dto.SubmittedApplyDetailResponse;
12+
import org.ject.support.domain.admin.dto.SubmittedApplyEditRequest;
1113
import org.ject.support.domain.admin.dto.SubmittedApplyResponse;
1214
import org.ject.support.domain.apply.domain.ApplicationForm;
1315
import org.ject.support.domain.apply.domain.Apply;
1416
import org.ject.support.domain.apply.domain.Apply.Status;
17+
import org.ject.support.domain.apply.domain.Portfolio;
1518
import org.ject.support.domain.apply.dto.ApplyPortfolioDto;
1619
import org.ject.support.domain.apply.exception.ApplyErrorCode;
1720
import org.ject.support.domain.apply.exception.ApplyException;
1821
import org.ject.support.domain.apply.repository.ApplyRepository;
1922
import org.ject.support.domain.member.JobFamily;
2023
import org.ject.support.domain.member.entity.Member;
24+
import org.ject.support.domain.member.entity.MemberEditor;
25+
import org.ject.support.domain.recruit.domain.Recruit;
26+
import org.ject.support.domain.recruit.exception.QuestionErrorCode;
27+
import org.ject.support.domain.recruit.exception.QuestionException;
2128
import org.springframework.data.domain.Page;
2229
import org.springframework.data.domain.Pageable;
2330
import org.springframework.stereotype.Service;
@@ -29,6 +36,7 @@ public class SubmittedApplyService {
2936

3037
private final ApplyRepository applyRepository;
3138
private final String2MapSerializer string2MapSerializer;
39+
private final Map2JsonSerializer map2JsonSerializer;
3240

3341
@Transactional(readOnly = true)
3442
public Page<SubmittedApplyResponse> findSubmittedApplies(final JobFamily jobFamily,
@@ -55,6 +63,35 @@ public SubmittedApplyCountResponse countSubmittedApply() {
5563
return new SubmittedApplyCountResponse(count);
5664
}
5765

66+
@Transactional
67+
public void updateSubmittedApply(final Long applyId,
68+
final SubmittedApplyEditRequest request) {
69+
Apply apply = applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED)
70+
.orElseThrow(() -> new ApplyException(ApplyErrorCode.NOT_FOUND_APPLY));
71+
ensureSubmitted(apply);
72+
73+
Member member = apply.getMember();
74+
MemberEditor memberEditor = member.toEditor()
75+
.name(request.name())
76+
.phoneNumber(request.phoneNumber())
77+
.email(request.email())
78+
.jobFamily(request.jobFamily())
79+
.build();
80+
member.edit(memberEditor);
81+
82+
Map<String, String> answers = request.answers();
83+
validateQuestions(answers, apply.getRecruit());
84+
String newContent = map2JsonSerializer.serializeAsString(answers);
85+
86+
List<Portfolio> newPortfolios = request.portfolios()
87+
.stream()
88+
.map(ApplyPortfolioDto::toEntity)
89+
.toList();
90+
91+
apply.getApplicationForm()
92+
.updateContentAndPortfolios(newContent, newPortfolios);
93+
}
94+
5895
@Transactional
5996
public void deleteSubmittedApply(final Long applyId) {
6097
Apply apply = applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED)
@@ -64,7 +101,7 @@ public void deleteSubmittedApply(final Long applyId) {
64101
}
65102

66103
@Transactional
67-
public void deleteSubmittedApplies(final List<Long> applyIds) {
104+
public int deleteSubmittedApplies(final List<Long> applyIds) {
68105
final List<Long> distinctIds = applyIds.stream().distinct().toList();
69106
final List<Apply> applies = applyRepository.findAllByIdAndStatusWithMember(distinctIds, Status.SUBMITTED);
70107

@@ -73,6 +110,7 @@ public void deleteSubmittedApplies(final List<Long> applyIds) {
73110
}
74111

75112
applies.forEach(this::deleteProfileAndApplicationForm);
113+
return applies.size();
76114
}
77115

78116
private SubmittedApplyDetailResponse toSubmittedApplyDetailResponse(final Apply apply) {
@@ -111,6 +149,15 @@ private void ensureSubmitted(final Apply apply) {
111149
}
112150
}
113151

152+
private void validateQuestions(final Map<String, String> answers, final Recruit recruit) {
153+
answers.keySet().stream()
154+
.map(Long::parseLong)
155+
.filter(recruit::isInvalidQuestionId)
156+
.forEach(key -> {
157+
throw new QuestionException(QuestionErrorCode.NOT_FOUND_QUESTION);
158+
});
159+
}
160+
114161
private void deleteProfileAndApplicationForm(final Apply apply) {
115162
// 제출된 지원서인지 검증
116163
ensureSubmitted(apply);

src/test/java/org/ject/support/domain/admin/service/SubmittedApplyServiceTest.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,17 @@
88
import static org.mockito.Mockito.verify;
99

1010
import java.util.List;
11+
import java.util.Map;
1112
import java.util.Optional;
13+
import org.ject.support.common.util.Map2JsonSerializer;
1214
import org.ject.support.common.util.String2MapSerializer;
15+
import org.ject.support.domain.admin.dto.SubmittedApplyEditRequest;
1316
import org.ject.support.domain.admin.dto.SubmittedApplyResponse;
17+
import org.ject.support.domain.apply.dto.ApplyPortfolioDto;
1418
import org.ject.support.domain.member.JobFamily;
19+
import org.ject.support.domain.recruit.domain.Question;
20+
import org.ject.support.domain.recruit.exception.QuestionErrorCode;
21+
import org.ject.support.domain.recruit.exception.QuestionException;
1522
import org.springframework.data.domain.Page;
1623
import org.springframework.data.domain.PageImpl;
1724
import org.springframework.data.domain.PageRequest;
@@ -43,6 +50,9 @@ class SubmittedApplyServiceTest extends UnitTestSupport {
4350
@Mock
4451
private String2MapSerializer string2MapSerializer;
4552

53+
@Mock
54+
private Map2JsonSerializer map2JsonSerializer;
55+
4656
private static Apply submittedApply;
4757

4858
@BeforeEach
@@ -54,9 +64,19 @@ void setUp() {
5464
var semester = Semester.builder()
5565
.name("1")
5666
.build();
67+
68+
var question1 = Question.builder()
69+
.id(1L)
70+
.build();
71+
var question2 = Question.builder()
72+
.id(2L)
73+
.build();
74+
5775
var recruit = Recruit.builder()
5876
.semester(semester)
77+
.questions(List.of(question1, question2))
5978
.build();
79+
6080
submittedApply = Apply.builder()
6181
.id(applyId)
6282
.member(member)
@@ -342,4 +362,125 @@ void setUp() {
342362
assertThat(actual.applyId()).isEqualTo(submittedApply.getId());
343363
}
344364

365+
@Test
366+
void 제출된_지원서_수정_성공() {
367+
// given
368+
var applyId = submittedApply.getId();
369+
var newName = "수정된이름";
370+
var newPhoneNumber = "010-1234-5678";
371+
var newEmail = "updated@example.com";
372+
var newJobFamily = JobFamily.FE;
373+
374+
var newAnswers = Map.of(
375+
"1", "수정된 답변1",
376+
"2", "수정된 답변2"
377+
);
378+
379+
var newPortfolios = List.of(
380+
new ApplyPortfolioDto("url1", "name1", "1", "1"),
381+
new ApplyPortfolioDto("url2", "name2", "2", "2")
382+
);
383+
384+
var request = new SubmittedApplyEditRequest(
385+
newName,
386+
newPhoneNumber,
387+
newEmail,
388+
newJobFamily,
389+
newAnswers,
390+
newPortfolios
391+
);
392+
393+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
394+
.willReturn(Optional.of(submittedApply));
395+
given(map2JsonSerializer.serializeAsString(newAnswers))
396+
.willReturn("{\"1\":\"수정된 답변1\",\"2\":\"수정된 답변2\"}");
397+
398+
// when
399+
submittedApplyService.updateSubmittedApply(applyId, request);
400+
401+
// then
402+
verify(applyRepository).findByIdAndStatusWithMember(applyId, Status.SUBMITTED);
403+
verify(map2JsonSerializer).serializeAsString(newAnswers);
404+
405+
assertThat(submittedApply.getMember().getName()).isEqualTo(newName);
406+
assertThat(submittedApply.getMember().getPhoneNumber()).isEqualTo(newPhoneNumber);
407+
assertThat(submittedApply.getMember().getEmail()).isEqualTo(newEmail);
408+
assertThat(submittedApply.getMember().getJobFamily()).isEqualTo(newJobFamily);
409+
}
410+
411+
@Test
412+
void 제출된_지원서_수정시_존재하지_않으면_예외_발생() {
413+
// given
414+
var applyId = 999L;
415+
var request = new SubmittedApplyEditRequest(
416+
"이름",
417+
"01012345678",
418+
"test@example.com",
419+
JobFamily.BE,
420+
Map.of("1", "답변"),
421+
List.of()
422+
);
423+
424+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
425+
.willReturn(Optional.empty());
426+
427+
// expected
428+
assertThatThrownBy(() -> submittedApplyService.updateSubmittedApply(applyId, request))
429+
.isInstanceOf(ApplyException.class)
430+
.hasFieldOrPropertyWithValue("errorCode", ApplyErrorCode.NOT_FOUND_APPLY);
431+
}
432+
433+
@Test
434+
void 제출된_지원서_수정시_유효하지_않은_질문ID면_예외_발생() {
435+
// given
436+
var applyId = submittedApply.getId();
437+
var invalidQuestionId = "999";
438+
439+
var request = new SubmittedApplyEditRequest(
440+
"이름",
441+
"01012345678",
442+
"test@example.com",
443+
JobFamily.BE,
444+
Map.of(invalidQuestionId, "답변"),
445+
List.of()
446+
);
447+
448+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
449+
.willReturn(Optional.of(submittedApply));
450+
451+
// expected
452+
assertThatThrownBy(() -> submittedApplyService.updateSubmittedApply(applyId, request))
453+
.isInstanceOf(QuestionException.class)
454+
.hasFieldOrPropertyWithValue("errorCode", QuestionErrorCode.NOT_FOUND_QUESTION);
455+
}
456+
457+
@Test
458+
void 제출되지_않은_지원서_수정시_예외_발생() {
459+
// given
460+
var applyId = submittedApply.getId();
461+
var tempSavedApply = Apply.builder()
462+
.id(applyId)
463+
.member(submittedApply.getMember())
464+
.recruit(submittedApply.getRecruit())
465+
.status(Status.TEMP_SAVED)
466+
.applicationForm(ApplicationForm.builder().build())
467+
.build();
468+
469+
var request = new SubmittedApplyEditRequest(
470+
"이름",
471+
"01012345678",
472+
"test@example.com",
473+
JobFamily.BE,
474+
Map.of("1", "답변"),
475+
List.of()
476+
);
477+
478+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
479+
.willReturn(Optional.of(tempSavedApply));
480+
481+
// expected
482+
assertThatThrownBy(() -> submittedApplyService.updateSubmittedApply(applyId, request))
483+
.isInstanceOf(ApplyException.class)
484+
.hasFieldOrPropertyWithValue("errorCode", ApplyErrorCode.NOT_FOUND_SUBMITTED_APPLICATION_FORM);
485+
}
345486
}

0 commit comments

Comments
 (0)