diff --git a/src/main/java/dmu/dasom/api/domain/applicant/dto/ApplicantCreateRequestDto.java b/src/main/java/dmu/dasom/api/domain/applicant/dto/ApplicantCreateRequestDto.java index 02fa342..3f13eed 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/dto/ApplicantCreateRequestDto.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/dto/ApplicantCreateRequestDto.java @@ -1,5 +1,8 @@ package dmu.dasom.api.domain.applicant.dto; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; import dmu.dasom.api.domain.applicant.entity.Applicant; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.*; @@ -9,6 +12,11 @@ @Schema(name = "ApplicantCreateRequestDto", description = "지원자 생성 요청 DTO") public class ApplicantCreateRequestDto { + @NotNull + @Size(max = 16) + @Schema(description = "이름", example = "홍길동") + private String name; + @NotNull @Pattern(regexp = "^[0-9]{8}$") @Size(min = 8, max = 8) @@ -39,13 +47,18 @@ public class ApplicantCreateRequestDto { private String reasonForApply; @Size(max = 200) - @Schema(description = "활동 희망사항", example = "동아리 활동 참여") + @Schema(description = "활동 희망사항", example = "동아리 활동 참여", nullable = true) private String activityWish; @NotNull @Schema(description = "개인정보 처리방침 동의 여부", example = "true") private Boolean isPrivacyPolicyAgreed; + @JsonProperty(defaultValue = "false") + @JsonSetter(nulls = Nulls.SKIP) + @Schema(description = "지원서 덮어쓰기 확인 여부", example = "false", defaultValue = "false", nullable = true) + private Boolean isOverwriteConfirmed = false; + public Applicant toEntity() { return Applicant.builder() .studentNo(this.studentNo) diff --git a/src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java b/src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java index 89e7c6a..1a73590 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/entity/Applicant.java @@ -1,5 +1,6 @@ package dmu.dasom.api.domain.applicant.entity; +import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto; import dmu.dasom.api.domain.applicant.dto.ApplicantDetailsResponseDto; import dmu.dasom.api.domain.applicant.dto.ApplicantResponseDto; import dmu.dasom.api.domain.applicant.enums.ApplicantStatus; @@ -9,6 +10,7 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicUpdate; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @@ -17,6 +19,7 @@ @AllArgsConstructor @Builder +@DynamicUpdate @Entity @EntityListeners(AuditingEntityListener.class) @Getter @@ -32,7 +35,7 @@ public class Applicant { @Size(max = 16) private String name; - @Column(name = "student_no", nullable = false, length = 8) + @Column(name = "student_no", nullable = false, unique = true, length = 8) @Pattern(regexp = "^[0-9]{8}$") @Size(min = 8, max = 8) private String studentNo; @@ -105,4 +108,14 @@ public ApplicantDetailsResponseDto toApplicantDetailsResponse() { .build(); } + public void overwrite(final ApplicantCreateRequestDto request) { + this.name = request.getName(); + this.contact = request.getContact(); + this.email = request.getEmail(); + this.grade = request.getGrade(); + this.reasonForApply = request.getReasonForApply(); + this.activityWish = request.getActivityWish(); + this.isPrivacyPolicyAgreed = request.getIsPrivacyPolicyAgreed(); + } + } diff --git a/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java b/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java index 24aae15..28f316a 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/repository/ApplicantRepository.java @@ -6,9 +6,13 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import java.util.Optional; + public interface ApplicantRepository extends JpaRepository { @Query("SELECT a FROM Applicant a ORDER BY a.id DESC") Page findAllWithPageRequest(final Pageable pageable); + Optional findByStudentNo(final String studentNo); + } diff --git a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java index 8164831..637af13 100644 --- a/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/applicant/service/ApplicantServiceImpl.java @@ -13,9 +13,13 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; @RequiredArgsConstructor @Service +@Transactional public class ApplicantServiceImpl implements ApplicantService { private final static int DEFAULT_PAGE_SIZE = 20; @@ -25,6 +29,20 @@ public class ApplicantServiceImpl implements ApplicantService { // 지원자 저장 @Override public void apply(final ApplicantCreateRequestDto request) { + final Optional applicant = findByStudentNo(request.getStudentNo()); + + // 이미 지원한 학번이 존재할 경우 + if (applicant.isPresent()) { + // 덮어쓰기 확인 여부가 false일 경우 예외 발생 + if (!request.getIsOverwriteConfirmed()) + throw new CustomException(ErrorCode.DUPLICATED_STUDENT_NO); + + // 기존 지원자 정보 갱신 수행 + applicant.get().overwrite(request); + return; + } + + // 새로운 지원자일 경우 저장 applicantRepository.save(request.toEntity()); } @@ -61,4 +79,9 @@ private Applicant findById(final Long id) { .orElseThrow(() -> new CustomException(ErrorCode.EMPTY_RESULT)); } + // 학번으로 지원자 존재 여부 확인 + private Optional findByStudentNo(final String studentNo) { + return applicantRepository.findByStudentNo(studentNo); + } + } diff --git a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java index 1d8a0bf..dcb6c58 100644 --- a/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java +++ b/src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java @@ -18,7 +18,8 @@ public enum ErrorCode { INTERNAL_SERVER_ERROR(500, "C009", "서버에 문제가 발생하였습니다."), NOT_FOUND(404, "C010", "해당 리소스를 찾을 수 없습니다."), WRITE_FAIL(400, "C011", "데이터를 쓰는데 실패하였습니다."), - EMPTY_RESULT(400, "C012", "조회 결과가 없습니다.") + EMPTY_RESULT(400, "C012", "조회 결과가 없습니다."), + DUPLICATED_STUDENT_NO(400, "C013", "이미 등록된 학번입니다."), ; private final int status; diff --git a/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java b/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java index baf12ae..f7761c0 100644 --- a/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java +++ b/src/main/java/dmu/dasom/api/domain/recruit/controller/RecruitController.java @@ -2,7 +2,11 @@ import dmu.dasom.api.domain.applicant.dto.ApplicantCreateRequestDto; import dmu.dasom.api.domain.applicant.service.ApplicantService; +import dmu.dasom.api.domain.common.exception.ErrorResponse; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; @@ -23,8 +27,15 @@ public class RecruitController { // 지원하기 @Operation(summary = "부원 지원하기") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "지원 성공") - }) + @ApiResponse(responseCode = "200", description = "지원 성공"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "학번 중복", + value = "{ \"code\": \"C013\", \"message\": \"이미 등록된 학번입니다.\" }")}))}) @PostMapping("/apply") public ResponseEntity apply(@Valid @RequestBody final ApplicantCreateRequestDto request) { applicantService.apply(request); diff --git a/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java b/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java index 7c77a5c..256a302 100644 --- a/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java +++ b/src/test/java/dmu/dasom/api/domain/applicant/ApplicantServiceTest.java @@ -19,6 +19,7 @@ import org.springframework.data.domain.PageRequest; import java.util.Collections; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -37,6 +38,8 @@ class ApplicantServiceTest { void apply_success() { // given ApplicantCreateRequestDto request = mock(ApplicantCreateRequestDto.class); + when(request.getStudentNo()).thenReturn("20210000"); + when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.empty()); // when applicantService.apply(request); @@ -48,6 +51,38 @@ void apply_success() { @Test @DisplayName("지원자 저장 - 실패") void apply_fail() { + // given + ApplicantCreateRequestDto request = mock(ApplicantCreateRequestDto.class); + when(request.getStudentNo()).thenReturn("20210000"); + when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.of(mock(Applicant.class))); + when(request.getIsOverwriteConfirmed()).thenReturn(false); + + // when + CustomException exception = assertThrows(CustomException.class, () -> { + applicantService.apply(request); + }); + + // then + verify(applicantRepository).findByStudentNo("20210000"); + assertEquals(ErrorCode.DUPLICATED_STUDENT_NO, exception.getErrorCode()); + } + + @Test + @DisplayName("지원자 저장 - 덮어쓰기") + void apply_overwrite() { + // given + ApplicantCreateRequestDto request = mock(ApplicantCreateRequestDto.class); + when(request.getStudentNo()).thenReturn("20210000"); + Applicant applicant = mock(Applicant.class); + when(applicantRepository.findByStudentNo("20210000")).thenReturn(Optional.of(applicant)); + when(request.getIsOverwriteConfirmed()).thenReturn(true); + + // when + applicantService.apply(request); + + // then + verify(applicantRepository).findByStudentNo("20210000"); + verify(applicant).overwrite(request); } @Test