Skip to content

Commit 451d030

Browse files
authored
[FEAT] 제출된 지원서 삭제 (#306)
* feat: 제출된 지원서 삭제 API 엔드포인트 추가 * feat: 다수의 제출된 지원서 삭제를 위한 dto 구현 * feat: 제출된 지원서 삭제 비즈니스 로직 구현 * feat: 제출된 지원서 조회를 위한 레포지토리 메서드 추가 * feat: Apply 엔티티에 지원서 삭제 및 상태 검증 메서드 추가 * feat: 제출된 지원서 검증 관련 에러 코드 추가 * test: 제출된 지원서 삭제 기능 테스트 추가 * refactor: 사용하지 않는 Mock 제거 * refactor: 사용하지 않는 import 제거 * fix: 파일의 끝에 개행 추가 * refactor: 지원서 삭제 시 상태 업데이트 코드 제거 * refactor: 필드명을 클래스명과 일치 * refactor: ApplicationForm 검증 로직 최소화 * feat: 제출된 지원서 다건 조회 메소드 수정 * refactor: 불필요한 주석 수정 * refactor: 중복되지 않는 apply id들로 처리 되게끔 수정 * refactor: 바인딩 오류를 방지하지 하기 위한 @Param 어노테이션 추가
1 parent 33cf23a commit 451d030

8 files changed

Lines changed: 292 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package org.ject.support.domain.admin.controller;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.tags.Tag;
5+
import jakarta.validation.Valid;
6+
import org.ject.support.domain.admin.dto.SubmittedApplyBulkDeleteRequest;
7+
import org.springframework.web.bind.annotation.PathVariable;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
10+
@Tag(name = "Submitted Apply", description = "제출된 지원서 관리 API")
11+
public interface SubmittedApplyApiSpec {
12+
13+
@Operation(
14+
summary = "제출된 지원서 삭제",
15+
description = "선택한 제출된 지원서를 삭제합니다.")
16+
void deleteSubmittedApply(@PathVariable final Long applyId);
17+
18+
@Operation(
19+
summary = "제출된 지원서 다수 삭제",
20+
description = "선택한 다수의 제출된 지원서들을 삭제합니다.")
21+
void deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request);
22+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.ject.support.domain.admin.controller;
2+
3+
import jakarta.validation.Valid;
4+
import lombok.RequiredArgsConstructor;
5+
import org.ject.support.domain.admin.dto.SubmittedApplyBulkDeleteRequest;
6+
import org.ject.support.domain.admin.service.SubmittedApplyService;
7+
import org.springframework.security.access.prepost.PreAuthorize;
8+
import org.springframework.web.bind.annotation.DeleteMapping;
9+
import org.springframework.web.bind.annotation.PathVariable;
10+
import org.springframework.web.bind.annotation.RequestBody;
11+
import org.springframework.web.bind.annotation.RequestMapping;
12+
import org.springframework.web.bind.annotation.RestController;
13+
14+
@RestController
15+
@RequestMapping("/admin/submitted-applies")
16+
@RequiredArgsConstructor
17+
public class SubmittedApplyController implements SubmittedApplyApiSpec {
18+
19+
private final SubmittedApplyService submittedApplyService;
20+
21+
@Override
22+
@DeleteMapping("/{applyId}")
23+
@PreAuthorize("hasRole('ROLE_ADMIN')")
24+
public void deleteSubmittedApply(@PathVariable final Long applyId) {
25+
submittedApplyService.deleteSubmittedApply(applyId);
26+
}
27+
28+
@Override
29+
@DeleteMapping
30+
@PreAuthorize("hasRole('ROLE_ADMIN')")
31+
public void deleteSubmittedApplies(@RequestBody @Valid final SubmittedApplyBulkDeleteRequest request) {
32+
submittedApplyService.deleteSubmittedApplies(request.applyIds());
33+
}
34+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.ject.support.domain.admin.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import jakarta.validation.constraints.NotEmpty;
5+
import java.util.List;
6+
7+
@Schema(description = "지원서 일괄 삭제 요청")
8+
public record SubmittedApplyBulkDeleteRequest(
9+
@Schema(description = "삭제할 지원서 ID 목록", example = "[1, 2, 3]")
10+
@NotEmpty List<Long> applyIds
11+
) {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package org.ject.support.domain.admin.service;
2+
3+
import java.util.List;
4+
import lombok.RequiredArgsConstructor;
5+
import org.ject.support.domain.apply.domain.Apply;
6+
import org.ject.support.domain.apply.domain.Apply.Status;
7+
import org.ject.support.domain.apply.exception.ApplyErrorCode;
8+
import org.ject.support.domain.apply.exception.ApplyException;
9+
import org.ject.support.domain.apply.repository.ApplyRepository;
10+
import org.ject.support.domain.member.entity.Member;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class SubmittedApplyService {
17+
18+
private final ApplyRepository applyRepository;
19+
20+
@Transactional
21+
public void deleteSubmittedApply(final Long applyId) {
22+
Apply apply = applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED)
23+
.orElseThrow(() -> new ApplyException(ApplyErrorCode.NOT_FOUND_APPLY));
24+
25+
deleteProfileAndApplicationForm(apply);
26+
}
27+
28+
@Transactional
29+
public void deleteSubmittedApplies(final List<Long> applyIds) {
30+
final List<Long> distinctIds = applyIds.stream().distinct().toList();
31+
final List<Apply> applies = applyRepository.findAllByIdAndStatusWithMember(distinctIds, Status.SUBMITTED);
32+
33+
if (applies.size() != distinctIds.size()) {
34+
throw new ApplyException(ApplyErrorCode.NOT_FOUND_APPLY);
35+
}
36+
37+
applies.forEach(this::deleteProfileAndApplicationForm);
38+
}
39+
40+
private void ensureSubmitted(final Apply apply) {
41+
if (apply.isNotSubmitted()) {
42+
throw new ApplyException(ApplyErrorCode.NOT_FOUND_SUBMITTED_APPLICATION_FORM);
43+
}
44+
}
45+
46+
private void deleteProfileAndApplicationForm(final Apply apply) {
47+
// 제출된 지원서인지 검증
48+
ensureSubmitted(apply);
49+
50+
// 제출된 지원서 제거
51+
apply.deleteApplicationForm();
52+
53+
// 프로필 제거
54+
Member applicant = apply.getMember();
55+
applicant.deleteProfile();
56+
}
57+
}

src/main/java/org/ject/support/domain/apply/domain/Apply.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public boolean isNotTempSaved() {
6969
|| (status.equals(Status.TEMP_SAVED) && applicationForm == null);
7070
}
7171

72+
public boolean isNotSubmitted() {
73+
return status.equals(Status.JOINED)
74+
|| status.equals(Status.TEMP_SAVED)
75+
|| (status.equals(Status.SUBMITTED) && applicationForm == null);
76+
}
77+
7278
public void deleteApplicationForm() {
7379
if (applicationForm != null) {
7480
applicationForm = null;

src/main/java/org/ject/support/domain/apply/exception/ApplyErrorCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
public enum ApplyErrorCode implements ErrorCode {
1414
NOT_FOUND_APPLY(NOT_FOUND, 1, "지원 정보를 찾을 수 없습니다."),
1515
ALREADY_SUBMITTED(CONFLICT, 2, "이미 지원서를 제출한 상태입니다."),
16-
NOT_FOUND_TEMP_APPLICATION_FORM(NOT_FOUND, 3, "임시 저장한 지원서가 존재하지 않습니다.");
16+
NOT_FOUND_TEMP_APPLICATION_FORM(NOT_FOUND, 3, "임시 저장한 지원서가 존재하지 않습니다."),
17+
NOT_FOUND_SUBMITTED_APPLICATION_FORM(NOT_FOUND, 4, "제출된 지원서가 존재하지 않습니다."),
18+
;
1719

1820
private final HttpStatus httpStatus;
1921
private final String code;

src/main/java/org/ject/support/domain/apply/repository/ApplyRepository.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@
77

88
import java.util.List;
99
import java.util.Optional;
10+
import org.springframework.data.repository.query.Param;
1011

1112
public interface ApplyRepository extends JpaRepository<Apply, Long> {
1213

1314
@Query("select a from Apply a where a.member.id = :memberId")
1415
Optional<Apply> findByMemberId(Long memberId);
1516

1617
List<Apply> findByRecruitAndStatus(Recruit recruit, Apply.Status status);
18+
19+
@Query("select a from Apply a join fetch a.member m where a.id = :applyId and a.status = :status")
20+
Optional<Apply> findByIdAndStatusWithMember(@Param("applyId") Long applyId, @Param("status") Apply.Status status);
21+
22+
@Query("select a from Apply a join fetch a.member m where a.id in :applyIds and a.status = :status")
23+
List<Apply> findAllByIdAndStatusWithMember(@Param("applyIds") List<Long> applyIds, @Param("status") Apply.Status status);
1724
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package org.ject.support.domain.admin.service;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
import static org.mockito.BDDMockito.given;
6+
import static org.mockito.Mockito.verify;
7+
8+
import java.util.List;
9+
import java.util.Optional;
10+
import org.ject.support.base.UnitTestSupport;
11+
import org.ject.support.domain.apply.domain.ApplicationForm;
12+
import org.ject.support.domain.apply.domain.Apply;
13+
import org.ject.support.domain.apply.domain.Apply.Status;
14+
import org.ject.support.domain.apply.exception.ApplyErrorCode;
15+
import org.ject.support.domain.apply.exception.ApplyException;
16+
import org.ject.support.domain.apply.repository.ApplyRepository;
17+
import org.ject.support.domain.member.entity.Member;
18+
import org.ject.support.domain.recruit.domain.Recruit;
19+
import org.ject.support.domain.recruit.domain.Semester;
20+
import org.junit.jupiter.api.BeforeEach;
21+
import org.junit.jupiter.api.Test;
22+
import org.mockito.InjectMocks;
23+
import org.mockito.Mock;
24+
25+
class SubmittedApplyServiceTest extends UnitTestSupport {
26+
27+
@InjectMocks
28+
private SubmittedApplyService submittedApplyService;
29+
30+
@Mock
31+
private ApplyRepository applyRepository;
32+
33+
private static Apply submittedApply;
34+
35+
@BeforeEach
36+
void setUp() {
37+
var applyId = 1L;
38+
var member = Member.builder()
39+
.name("김젝트")
40+
.build();
41+
var semester = Semester.builder()
42+
.name("1")
43+
.build();
44+
var recruit = Recruit.builder()
45+
.semester(semester)
46+
.build();
47+
submittedApply = Apply.builder()
48+
.id(applyId)
49+
.member(member)
50+
.recruit(recruit)
51+
.status(Apply.Status.SUBMITTED)
52+
.applicationForm(ApplicationForm.builder().build())
53+
.build();
54+
}
55+
56+
@Test
57+
void 제출된_지원서_단건_삭제_성공() {
58+
// given
59+
var applyId = submittedApply.getId();
60+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
61+
.willReturn(Optional.of(submittedApply));
62+
63+
// when
64+
submittedApplyService.deleteSubmittedApply(applyId);
65+
66+
// then
67+
verify(applyRepository).findByIdAndStatusWithMember(applyId, Status.SUBMITTED);
68+
assertThat(submittedApply.getApplicationForm()).isNull();
69+
assertThat(submittedApply.getMember().getName()).isNull();
70+
}
71+
72+
@Test
73+
void 제출된_지원서_단건_삭제시_존재하지_않으면_예외_발생() {
74+
// given
75+
var applyId = submittedApply.getId();
76+
given(applyRepository.findByIdAndStatusWithMember(applyId, Status.SUBMITTED))
77+
.willReturn(Optional.empty());
78+
79+
// expected
80+
assertThatThrownBy(() -> submittedApplyService.deleteSubmittedApply(applyId))
81+
.isInstanceOf(ApplyException.class)
82+
.hasFieldOrPropertyWithValue("errorCode", ApplyErrorCode.NOT_FOUND_APPLY);
83+
}
84+
85+
@Test
86+
void 제출된_지원서_여러건_삭제_성공() {
87+
// given
88+
var applyIds = List.of(1L, 2L, 3L);
89+
var member2 = Member.builder().name("김젝트2").build();
90+
var member3 = Member.builder().name("김젝트3").build();
91+
92+
var apply2 = Apply.builder()
93+
.id(2L)
94+
.member(member2)
95+
.recruit(submittedApply.getRecruit())
96+
.status(Status.SUBMITTED)
97+
.applicationForm(ApplicationForm.builder().build())
98+
.build();
99+
100+
var apply3 = Apply.builder()
101+
.id(3L)
102+
.member(member3)
103+
.recruit(submittedApply.getRecruit())
104+
.status(Status.SUBMITTED)
105+
.applicationForm(ApplicationForm.builder().build())
106+
.build();
107+
108+
var applies = List.of(submittedApply, apply2, apply3);
109+
110+
given(applyRepository.findAllByIdAndStatusWithMember(applyIds, Status.SUBMITTED))
111+
.willReturn(applies);
112+
113+
// when
114+
submittedApplyService.deleteSubmittedApplies(applyIds);
115+
116+
// then
117+
verify(applyRepository).findAllByIdAndStatusWithMember(applyIds, Status.SUBMITTED);
118+
assertThat(submittedApply.getApplicationForm()).isNull();
119+
assertThat(submittedApply.getMember().getName()).isNull();
120+
assertThat(apply2.getApplicationForm()).isNull();
121+
assertThat(apply3.getApplicationForm()).isNull();
122+
}
123+
124+
@Test
125+
void 제출된_지원서_여러건_삭제시_일부가_존재하지_않으면_예외_발생() {
126+
// given
127+
var applyIds = List.of(1L, 2L, 3L);
128+
var applies = List.of(submittedApply); // 1개만 반환
129+
130+
given(applyRepository.findAllByIdAndStatusWithMember(applyIds, Status.SUBMITTED))
131+
.willReturn(applies);
132+
133+
// expected
134+
assertThatThrownBy(() -> submittedApplyService.deleteSubmittedApplies(applyIds))
135+
.isInstanceOf(ApplyException.class)
136+
.hasFieldOrPropertyWithValue("errorCode", ApplyErrorCode.NOT_FOUND_APPLY);
137+
}
138+
139+
@Test
140+
void 제출된_지원서_여러건_삭제시_빈_리스트면_예외_발생() {
141+
// given
142+
var applyIds = List.of(1L, 2L, 3L);
143+
144+
given(applyRepository.findAllByIdAndStatusWithMember(applyIds, Status.SUBMITTED))
145+
.willReturn(List.of());
146+
147+
// expected
148+
assertThatThrownBy(() -> submittedApplyService.deleteSubmittedApplies(applyIds))
149+
.isInstanceOf(ApplyException.class)
150+
.hasFieldOrPropertyWithValue("errorCode", ApplyErrorCode.NOT_FOUND_APPLY);
151+
}
152+
}

0 commit comments

Comments
 (0)