From 3dd6f6fa4e498424a7ff18dd95dcf2e70e37d32b Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Sat, 11 Oct 2025 16:39:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EB=A9=98=ED=86=A0=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20=EC=8B=A0=EC=B2=AD=20api=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 멘티 승격 요청 관련 entity, controller, service, repository 기본셋팅 * fix: MentorApplication 를 prefix 로 클래스명 수정 * feat: MentorApplication 엔티티 필드 확정 - universityId 는 null 을 허용합니다.(서비스에서 제공하지 않는 학교는 universityId가 null) * feat: 멘토 승격 신청 로직 작성 - 요청자의 증명서를 S3에 저장하기 위해 ImgType 클래스에 MENTOR_PROOF 추가 - 멘토 승격 요청자의 수학 상태를 나타내는 ExchangePhase 클래스 추가 * feat: 멘토 승격 중복 요청을 막는 조건 추가 - ErrorCode클래스에 MENTOR_APPLICATION_ALREADY_EXISTED 예외 추가 * style: code 컨벤션 적용 * test: 멘토 승격 요청 api 테스트 코드 추가 * feat: 멘토 지원 테이블 DDL 작성 --- .../common/exception/ErrorCode.java | 1 + .../MentorApplicationController.java | 28 ++++ .../mentor/domain/ExchangePhase.java | 8 ++ .../mentor/domain/MentorApplication.java | 59 ++++++++ .../domain/MentorApplicationStatus.java | 8 ++ .../mentor/dto/MentorApplicationRequest.java | 13 ++ .../MentorApplicationRepository.java | 9 ++ .../service/MentorApplicationService.java | 54 +++++++ .../solidconnection/s3/domain/ImgType.java | 9 +- .../V35__add_mentor_application_table.sql | 15 ++ .../service/MentorApplicationServiceTest.java | 133 ++++++++++++++++++ 11 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java create mode 100644 src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java create mode 100644 src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java create mode 100644 src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java create mode 100644 src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java create mode 100644 src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java create mode 100644 src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java create mode 100644 src/main/resources/db/migration/V35__add_mentor_application_table.sql create mode 100644 src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index fbd110f9d..8839d7efc 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -119,6 +119,7 @@ public enum ErrorCode { MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."), UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), + MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.BAD_REQUEST.value(),"이미 멘토 승격 신청이 존재해 중복 신청 할 수 없습니다."), // socket UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java new file mode 100644 index 000000000..fc6da2b8c --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java @@ -0,0 +1,28 @@ +package com.example.solidconnection.mentor.controller; + +import com.example.solidconnection.common.resolver.AuthorizedUser; +import com.example.solidconnection.mentor.dto.MentorApplicationRequest; +import com.example.solidconnection.mentor.service.MentorApplicationService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RequiredArgsConstructor +@RequestMapping("/mentees") +@RestController +public class MentorApplicationController { + + private final MentorApplicationService mentorApplicationService; + + @PostMapping("/mentor-applications") + public ResponseEntity requestMentorApplication( + @AuthorizedUser long siteUserId, + @Valid @RequestPart("mentorApplicationRequest") MentorApplicationRequest mentorApplicationRequest, + @RequestParam("file") MultipartFile file + ) { + mentorApplicationService.submitMentorApplication(siteUserId, mentorApplicationRequest, file); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java b/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java new file mode 100644 index 000000000..9fe362cb2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.mentor.domain; + +public enum ExchangePhase { + + STUDYING_ABROAD, + AFTER_EXCHANGE, + ; +} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java new file mode 100644 index 000000000..d21a75d17 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java @@ -0,0 +1,59 @@ +package com.example.solidconnection.mentor.domain; + +import com.example.solidconnection.common.BaseEntity; +import com.example.solidconnection.siteuser.domain.SiteUser; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MentorApplication extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column + private long siteUserId; + + @Column(name = "country_code") + private String countryCode; + + @Column(name = "region_code") + private String regionCode; + + @Column + private Long universityId; + + @Column(nullable = false, name = "mentor_proof_url", length = 500) + private String mentorProofUrl; + + @Column + private String rejectedReason; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ExchangePhase exchangePhase; + + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private MentorApplicationStatus mentorApplicationStatus = MentorApplicationStatus.PENDING; + + public MentorApplication( + SiteUser siteUser, + String countryCode, + String regionCode, + Long universityId, + String mentorProofUrl, + ExchangePhase exchangePhase + ) { + this.siteUserId = siteUser.getId(); + this.countryCode = countryCode; + this.regionCode = regionCode; + this.universityId = universityId; + this.mentorProofUrl = mentorProofUrl; + this.exchangePhase = exchangePhase; + } +} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java new file mode 100644 index 000000000..d63c3da61 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java @@ -0,0 +1,8 @@ +package com.example.solidconnection.mentor.domain; + +public enum MentorApplicationStatus { + PENDING, + APPROVED, + REJECTED, + ; +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java new file mode 100644 index 000000000..fe0fdd4aa --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.mentor.dto; + +import com.example.solidconnection.mentor.domain.ExchangePhase; +import com.fasterxml.jackson.annotation.JsonProperty; + +public record MentorApplicationRequest( + @JsonProperty("preparationStatus") + ExchangePhase exchangePhase, + String country, + String region, + Long universityId +) { +} diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java new file mode 100644 index 000000000..3c475881f --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.mentor.repository; + +import com.example.solidconnection.mentor.domain.MentorApplication; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MentorApplicationRepository extends JpaRepository { + + boolean existsBySiteUserId(long siteUserId); +} diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java new file mode 100644 index 000000000..e1dd2768f --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.mentor.service; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.dto.MentorApplicationRequest; +import com.example.solidconnection.mentor.repository.MentorApplicationRepository; +import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; +import com.example.solidconnection.s3.service.S3Service; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + +@Service +@RequiredArgsConstructor +@Slf4j +public class MentorApplicationService { + + private final MentorApplicationRepository mentorApplicationRepository; + private final SiteUserRepository siteUserRepository; + private final S3Service s3Service; + + @Transactional + public void submitMentorApplication( + long siteUserId, + MentorApplicationRequest mentorApplicationRequest, + MultipartFile file + ) { + if (mentorApplicationRepository.existsBySiteUserId(siteUserId)) { + throw new CustomException(MENTOR_APPLICATION_ALREADY_EXISTED); + } + + SiteUser siteUser = siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); + UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF); + MentorApplication mentorApplication = new MentorApplication( + siteUser, + mentorApplicationRequest.country(), + mentorApplicationRequest.region(), + mentorApplicationRequest.universityId(), + uploadedFile.fileUrl(), + mentorApplicationRequest.exchangePhase() + ); + mentorApplicationRepository.save(mentorApplication); + } + +} diff --git a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java b/src/main/java/com/example/solidconnection/s3/domain/ImgType.java index 50ac78b30..b26d5fc10 100644 --- a/src/main/java/com/example/solidconnection/s3/domain/ImgType.java +++ b/src/main/java/com/example/solidconnection/s3/domain/ImgType.java @@ -4,7 +4,14 @@ @Getter public enum ImgType { - PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"), CHAT("chat"); + PROFILE("profile"), + GPA("gpa"), + LANGUAGE_TEST("language"), + COMMUNITY("community"), + NEWS("news"), + CHAT("chat"), + MENTOR_PROOF("mentor-proof"), + ; private final String type; diff --git a/src/main/resources/db/migration/V35__add_mentor_application_table.sql b/src/main/resources/db/migration/V35__add_mentor_application_table.sql new file mode 100644 index 000000000..a24b1cc67 --- /dev/null +++ b/src/main/resources/db/migration/V35__add_mentor_application_table.sql @@ -0,0 +1,15 @@ +create table mentor_application +( + created_at datetime(6), + id bigint not null auto_increment, + site_user_id bigint, + university_id bigint, + updated_at datetime(6), + mentor_proof_url varchar(500) not null, + country_code varchar(255), + exchange_phase enum ('AFTER_EXCHANGE','STUDYING_ABROAD') not null, + mentor_application_status enum ('APPROVED','PENDING','REJECTED') not null, + region_code varchar(255), + rejected_reason varchar(255), + primary key (id) +) engine=InnoDB \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java new file mode 100644 index 000000000..3abafd6c0 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -0,0 +1,133 @@ +package com.example.solidconnection.mentor.service; + + +import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.BDDMockito.given; + +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.mentor.domain.ExchangePhase; +import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.dto.MentorApplicationRequest; +import com.example.solidconnection.mentor.repository.MentorApplicationRepository; +import com.example.solidconnection.s3.domain.ImgType; +import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; +import com.example.solidconnection.s3.service.S3Service; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; + +@TestContainerSpringBootTest +@DisplayName("멘토 승격 신청 서비스 테스트") +public class MentorApplicationServiceTest { + + @Autowired + private MentorApplicationService mentorApplicationService; + + @Autowired + private MentorApplicationRepository mentorApplicationRepository; + + @Autowired + private SiteUserFixture siteUserFixture; + + @MockBean + private S3Service s3Service; + + private SiteUser user; + + @BeforeEach + void setUp() { + user = siteUserFixture.사용자(); + } + + @Test + void 멘토_승격_신청을_등록한다() { + // given + MentorApplicationRequest request = createMentorApplicationRequest(); + MockMultipartFile file = createMentorProofFile(); + String fileUrl = "/mentor-proof.pdf"; + given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + .willReturn(new UploadedFileUrlResponse(fileUrl)); + + // when + mentorApplicationService.submitMentorApplication(user.getId(), request, file); + + // then + assertThat(mentorApplicationRepository.existsBySiteUserId(user.getId())).isEqualTo(true); + } + + @Test + void universityId_가_null_로_들어와도_멘토_승격_신청을_등록한다() { + // given + MentorApplicationRequest request = createMentorApplicationRequestWithoutUniversityId(); + MockMultipartFile file = createMentorProofFile(); + String fileUrl = "/mentor-proof.pdf"; + given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + .willReturn(new UploadedFileUrlResponse(fileUrl)); + + // when + mentorApplicationService.submitMentorApplication(user.getId(), request, file); + + // then + assertThat(mentorApplicationRepository.existsBySiteUserId(user.getId())).isEqualTo(true); + } + + @Test + void 한_유저가_1회_이상_멘토_신청_요청을_보내면_예외() { + // given + MentorApplication existsMentorApplication = new MentorApplication( + user, + "미주권", + "헝가리2", + 1L, + "/mentor-proof.pdf", + ExchangePhase.AFTER_EXCHANGE + ); + mentorApplicationRepository.save(existsMentorApplication); + + MentorApplicationRequest request = createMentorApplicationRequest(); + MockMultipartFile file = createMentorProofFile(); + + // when + // then + assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTOR_APPLICATION_ALREADY_EXISTED.getMessage()); + } + + private MockMultipartFile createMentorProofFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private MentorApplicationRequest createMentorApplicationRequest() { + return new MentorApplicationRequest( + ExchangePhase.AFTER_EXCHANGE, + "미주권", + "헝가리2", + 1L + ); + } + + private MentorApplicationRequest createMentorApplicationRequestWithoutUniversityId() { + return new MentorApplicationRequest( + ExchangePhase.AFTER_EXCHANGE, + "미주권", + "헝가리2", + null + ); + } + + +} From ee17a0e3a141cf7cb14b35305669a088c2edd7cd Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 14 Oct 2025 22:30:18 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=A9=98=ED=86=A0=20=EC=8A=B9?= =?UTF-8?q?=EA=B2=A9=20api=20=EB=A5=BC=20=EA=B8=B0=ED=9A=8D=EC=97=90=20?= =?UTF-8?q?=EB=A7=9E=EA=B2=8C=20=EC=88=98=EC=A0=95=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: MentorApplication 엔티티 수정 - UniversitySelectStatus 필드 추가 - region 필트 삭제 - ExchangeStatus 쓰도록 수정 - 도메인 규칙 추가 * feat: 도메인 규칙에 맞게 ErrorCode 추가 * test: 테스트 코드 에러 수정 * test: 도메인 규칙 테스트 추가 --- .../common/exception/ErrorCode.java | 6 +- .../mentor/domain/ExchangePhase.java | 8 -- .../mentor/domain/MentorApplication.java | 56 ++++++++++--- .../mentor/domain/UniversitySelectType.java | 7 ++ .../mentor/dto/MentorApplicationRequest.java | 7 +- .../service/MentorApplicationService.java | 4 +- .../service/MentorApplicationServiceTest.java | 80 +++++++++++++------ 7 files changed, 120 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java create mode 100644 src/main/java/com/example/solidconnection/mentor/domain/UniversitySelectType.java diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 8839d7efc..f286c6034 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -119,7 +119,11 @@ public enum ErrorCode { MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."), UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), - MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.BAD_REQUEST.value(),"이미 멘토 승격 신청이 존재해 중복 신청 할 수 없습니다."), + MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(),"이미 멘토 승격 신청이 존재해 중복 신청 할 수 없습니다."), + INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원은 STUDYING_ABROAD 또는 AFTER_EXCHANGE 상태만 가능합니다."), + UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 CATALOG이면 universityId가 필요합니다."), + UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 OTHER이면 universityId가 null 이어야 합니다."), + INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 UniversitySelectType 입니다."), // socket UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java b/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java deleted file mode 100644 index 9fe362cb2..000000000 --- a/src/main/java/com/example/solidconnection/mentor/domain/ExchangePhase.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.solidconnection.mentor.domain; - -public enum ExchangePhase { - - STUDYING_ABROAD, - AFTER_EXCHANGE, - ; -} diff --git a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java index d21a75d17..7b95fd09a 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java @@ -1,8 +1,14 @@ package com.example.solidconnection.mentor.domain; import com.example.solidconnection.common.BaseEntity; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.*; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Set; import lombok.*; @Entity @@ -15,45 +21,73 @@ public class MentorApplication extends BaseEntity { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column + @Column(nullable = false) private long siteUserId; - @Column(name = "country_code") + @Column(nullable = false, name = "country_code") private String countryCode; - @Column(name = "region_code") - private String regionCode; - @Column private Long universityId; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private UniversitySelectType universitySelectType; + @Column(nullable = false, name = "mentor_proof_url", length = 500) private String mentorProofUrl; - @Column private String rejectedReason; @Column(nullable = false) @Enumerated(EnumType.STRING) - private ExchangePhase exchangePhase; + private ExchangeStatus exchangeStatus; @Column(nullable = false) @Enumerated(EnumType.STRING) private MentorApplicationStatus mentorApplicationStatus = MentorApplicationStatus.PENDING; + private static final Set ALLOWED = + Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE)); + public MentorApplication( SiteUser siteUser, String countryCode, - String regionCode, Long universityId, + UniversitySelectType universitySelectType, String mentorProofUrl, - ExchangePhase exchangePhase + ExchangeStatus exchangeStatus ) { + validateExchangeStatus(exchangeStatus); + validateUniversitySelection(universitySelectType, universityId); + this.siteUserId = siteUser.getId(); this.countryCode = countryCode; - this.regionCode = regionCode; this.universityId = universityId; + this.universitySelectType = universitySelectType; this.mentorProofUrl = mentorProofUrl; - this.exchangePhase = exchangePhase; + this.exchangeStatus = exchangeStatus; + } + + private void validateUniversitySelection(UniversitySelectType universitySelectType, Long universityId) { + switch (universitySelectType) { + case CATALOG -> { + if(universityId == null) { + throw new CustomException(ErrorCode.UNIVERSITY_ID_REQUIRED_FOR_CATALOG); + } + } + case OTHER -> { + if(universityId != null) { + throw new CustomException(ErrorCode.UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER); + } + } + default -> throw new CustomException(ErrorCode.INVALID_UNIVERSITY_SELECT_TYPE); + } + } + + private void validateExchangeStatus(ExchangeStatus exchangeStatus) { + if(!ALLOWED.contains(exchangeStatus)) { + throw new CustomException(ErrorCode.INVALID_EXCHANGE_STATUS_FOR_MENTOR); + } } } diff --git a/src/main/java/com/example/solidconnection/mentor/domain/UniversitySelectType.java b/src/main/java/com/example/solidconnection/mentor/domain/UniversitySelectType.java new file mode 100644 index 000000000..92c3622a1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/mentor/domain/UniversitySelectType.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.mentor.domain; + +public enum UniversitySelectType { + + CATALOG, + OTHER +} diff --git a/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java b/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java index fe0fdd4aa..da6d206ab 100644 --- a/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java +++ b/src/main/java/com/example/solidconnection/mentor/dto/MentorApplicationRequest.java @@ -1,13 +1,14 @@ package com.example.solidconnection.mentor.dto; -import com.example.solidconnection.mentor.domain.ExchangePhase; +import com.example.solidconnection.mentor.domain.UniversitySelectType; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; import com.fasterxml.jackson.annotation.JsonProperty; public record MentorApplicationRequest( @JsonProperty("preparationStatus") - ExchangePhase exchangePhase, + ExchangeStatus exchangeStatus, + UniversitySelectType universitySelectType, String country, - String region, Long universityId ) { } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index e1dd2768f..fc48e8581 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -43,10 +43,10 @@ public void submitMentorApplication( MentorApplication mentorApplication = new MentorApplication( siteUser, mentorApplicationRequest.country(), - mentorApplicationRequest.region(), mentorApplicationRequest.universityId(), + mentorApplicationRequest.universitySelectType(), uploadedFile.fileUrl(), - mentorApplicationRequest.exchangePhase() + mentorApplicationRequest.exchangeStatus() ); mentorApplicationRepository.save(mentorApplication); } diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java index 3abafd6c0..bd3ce5cff 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -2,18 +2,21 @@ import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; +import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER; +import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_REQUIRED_FOR_CATALOG; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.mockito.BDDMockito.given; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.mentor.domain.ExchangePhase; import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.UniversitySelectType; import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; import com.example.solidconnection.s3.domain.ImgType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; import com.example.solidconnection.s3.service.S3Service; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; @@ -50,7 +53,9 @@ void setUp() { @Test void 멘토_승격_신청을_등록한다() { // given - MentorApplicationRequest request = createMentorApplicationRequest(); + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) @@ -66,7 +71,9 @@ void setUp() { @Test void universityId_가_null_로_들어와도_멘토_승격_신청을_등록한다() { // given - MentorApplicationRequest request = createMentorApplicationRequestWithoutUniversityId(); + UniversitySelectType universitySelectType = UniversitySelectType.OTHER; + Long universityId = null; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); String fileUrl = "/mentor-proof.pdf"; given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) @@ -79,20 +86,58 @@ void setUp() { assertThat(mentorApplicationRepository.existsBySiteUserId(user.getId())).isEqualTo(true); } + @Test + void 대학_선택_타입이_CATALOG_인데_universityId가_null_이면_예외(){ + // given + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = null; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); + MockMultipartFile file = createMentorProofFile(); + String fileUrl = "/mentor-proof.pdf"; + given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + .willReturn(new UploadedFileUrlResponse(fileUrl)); + + // when + // then + assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_ID_REQUIRED_FOR_CATALOG.getMessage()); + } + + @Test + void 대학_선택_타입이_OTHER_인데_universityId가_존재하면_예외(){ + // given + UniversitySelectType universitySelectType = UniversitySelectType.OTHER; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); + MockMultipartFile file = createMentorProofFile(); + String fileUrl = "/mentor-proof.pdf"; + given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + .willReturn(new UploadedFileUrlResponse(fileUrl)); + + // when + // then + assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER.getMessage()); + } + @Test void 한_유저가_1회_이상_멘토_신청_요청을_보내면_예외() { // given MentorApplication existsMentorApplication = new MentorApplication( user, - "미주권", - "헝가리2", + "US", 1L, + UniversitySelectType.CATALOG, "/mentor-proof.pdf", - ExchangePhase.AFTER_EXCHANGE + ExchangeStatus.AFTER_EXCHANGE ); mentorApplicationRepository.save(existsMentorApplication); - MentorApplicationRequest request = createMentorApplicationRequest(); + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); // when @@ -111,23 +156,12 @@ private MockMultipartFile createMentorProofFile() { ); } - private MentorApplicationRequest createMentorApplicationRequest() { - return new MentorApplicationRequest( - ExchangePhase.AFTER_EXCHANGE, - "미주권", - "헝가리2", - 1L - ); - } - - private MentorApplicationRequest createMentorApplicationRequestWithoutUniversityId() { + private MentorApplicationRequest createMentorApplicationRequest(UniversitySelectType universitySelectType, Long universityId) { return new MentorApplicationRequest( - ExchangePhase.AFTER_EXCHANGE, - "미주권", - "헝가리2", - null + ExchangeStatus.AFTER_EXCHANGE, + universitySelectType, + "US", + universityId ); } - - } From b7c1ca6bd100b69227c2659f7377d06a13e01557 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 21 Oct 2025 19:15:27 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=A6=AC=EB=B7=B0=20=EC=B5=9C?= =?UTF-8?q?=EC=A2=85=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/ErrorCode.java | 4 +- .../MentorApplicationController.java | 6 +- .../mentor/domain/MentorApplication.java | 26 +++++-- .../domain/MentorApplicationStatus.java | 1 + .../MentorApplicationRepository.java | 4 +- .../service/MentorApplicationService.java | 10 ++- .../V35__add_mentor_application_table.sql | 33 ++++---- .../fixture/MentorApplicationFixture.java | 66 ++++++++++++++++ .../MentorApplicationFixtureBuilder.java | 77 +++++++++++++++++++ .../service/MentorApplicationServiceTest.java | 73 ++++++++++++------ 10 files changed, 252 insertions(+), 48 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixture.java create mode 100644 src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixtureBuilder.java diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index f286c6034..059fb3458 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -119,8 +119,8 @@ public enum ErrorCode { MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."), UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."), MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), - MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(),"이미 멘토 승격 신청이 존재해 중복 신청 할 수 없습니다."), - INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원은 STUDYING_ABROAD 또는 AFTER_EXCHANGE 상태만 가능합니다."), + MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(),"멘토 승격 요청이 이미 존재합니다."), + INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원 가능한 교환학생 상태가 아닙니다."), UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 CATALOG이면 universityId가 필요합니다."), UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 OTHER이면 universityId가 null 이어야 합니다."), INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 UniversitySelectType 입니다."), diff --git a/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java b/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java index fc6da2b8c..4f85e5621 100644 --- a/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java +++ b/src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java @@ -6,7 +6,11 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; @RequiredArgsConstructor diff --git a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java index 7b95fd09a..8f800dcff 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplication.java @@ -4,17 +4,33 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.siteuser.domain.ExchangeStatus; -import com.example.solidconnection.siteuser.domain.SiteUser; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; import java.util.Collections; import java.util.EnumSet; import java.util.Set; -import lombok.*; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Check; @Entity @Getter @AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) +@Check( + name = "chk_ma_university_select_rule", + constraints = """ + (university_select_type = 'CATALOG' AND university_id IS NOT NULL) OR + (university_select_type = 'OTHER' AND university_id IS NULL) + """ +) public class MentorApplication extends BaseEntity { @Id @@ -51,7 +67,7 @@ public class MentorApplication extends BaseEntity { Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE)); public MentorApplication( - SiteUser siteUser, + long siteUserId, String countryCode, Long universityId, UniversitySelectType universitySelectType, @@ -61,7 +77,7 @@ public MentorApplication( validateExchangeStatus(exchangeStatus); validateUniversitySelection(universitySelectType, universityId); - this.siteUserId = siteUser.getId(); + this.siteUserId = siteUserId; this.countryCode = countryCode; this.universityId = universityId; this.universitySelectType = universitySelectType; diff --git a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java index d63c3da61..e562e02cf 100644 --- a/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java +++ b/src/main/java/com/example/solidconnection/mentor/domain/MentorApplicationStatus.java @@ -1,6 +1,7 @@ package com.example.solidconnection.mentor.domain; public enum MentorApplicationStatus { + PENDING, APPROVED, REJECTED, diff --git a/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java b/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java index 3c475881f..d03b53892 100644 --- a/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java +++ b/src/main/java/com/example/solidconnection/mentor/repository/MentorApplicationRepository.java @@ -1,9 +1,11 @@ package com.example.solidconnection.mentor.repository; import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface MentorApplicationRepository extends JpaRepository { - boolean existsBySiteUserId(long siteUserId); + boolean existsBySiteUserIdAndMentorApplicationStatusIn(long siteUserId, List mentorApplicationStatuses); } diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index fc48e8581..d0716a156 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -2,6 +2,7 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.dto.MentorApplicationRequest; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; import com.example.solidconnection.s3.domain.ImgType; @@ -9,6 +10,7 @@ import com.example.solidconnection.s3.service.S3Service; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -33,7 +35,10 @@ public void submitMentorApplication( MentorApplicationRequest mentorApplicationRequest, MultipartFile file ) { - if (mentorApplicationRepository.existsBySiteUserId(siteUserId)) { + if (mentorApplicationRepository.existsBySiteUserIdAndMentorApplicationStatusIn( + siteUserId, + List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED)) + ) { throw new CustomException(MENTOR_APPLICATION_ALREADY_EXISTED); } @@ -41,7 +46,7 @@ public void submitMentorApplication( .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF); MentorApplication mentorApplication = new MentorApplication( - siteUser, + siteUser.getId(), mentorApplicationRequest.country(), mentorApplicationRequest.universityId(), mentorApplicationRequest.universitySelectType(), @@ -50,5 +55,4 @@ public void submitMentorApplication( ); mentorApplicationRepository.save(mentorApplication); } - } diff --git a/src/main/resources/db/migration/V35__add_mentor_application_table.sql b/src/main/resources/db/migration/V35__add_mentor_application_table.sql index a24b1cc67..be07fe901 100644 --- a/src/main/resources/db/migration/V35__add_mentor_application_table.sql +++ b/src/main/resources/db/migration/V35__add_mentor_application_table.sql @@ -1,15 +1,20 @@ -create table mentor_application +CREATE TABLE mentor_application ( - created_at datetime(6), - id bigint not null auto_increment, - site_user_id bigint, - university_id bigint, - updated_at datetime(6), - mentor_proof_url varchar(500) not null, - country_code varchar(255), - exchange_phase enum ('AFTER_EXCHANGE','STUDYING_ABROAD') not null, - mentor_application_status enum ('APPROVED','PENDING','REJECTED') not null, - region_code varchar(255), - rejected_reason varchar(255), - primary key (id) -) engine=InnoDB \ No newline at end of file + id BIGINT NOT NULL AUTO_INCREMENT, + site_user_id BIGINT, + country_code VARCHAR(255), + university_id BIGINT, + university_select_type enum ('CATALOG','OTHER') not null, + mentor_proof_url VARCHAR(500) NOT NULL, + rejected_reason VARCHAR(255), + exchange_status enum('AFTER_EXCHANGE','STUDYING_ABROAD') NOT NULL, + mentor_application_status enum('APPROVED','PENDING','REJECTED') NOT NULL, + created_at DATETIME(6), + updated_at DATETIME(6), + PRIMARY KEY (id), + CONSTRAINT fk_mentor_application_site_user FOREIGN KEY (site_user_id) REFERENCES site_user (id), + CONSTRAINT chk_ma_university_select_rule CHECK ( + (university_select_type = 'CATALOG' AND university_id IS NOT NULL) OR + (university_select_type = 'OTHER' AND university_id IS NULL) + ) +) ENGINE=InnoDB diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixture.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixture.java new file mode 100644 index 000000000..918db7ef4 --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixture.java @@ -0,0 +1,66 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; +import com.example.solidconnection.mentor.domain.UniversitySelectType; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; + +@TestComponent +@RequiredArgsConstructor +public class MentorApplicationFixture { + + private final MentorApplicationFixtureBuilder mentorApplicationFixtureBuilder; + + private static final String DEFAULT_COUNTRY_CODE = "US"; + private static final String DEFAULT_PROOF_URL = "/mentor-proof.pdf"; + private static final ExchangeStatus DEFAULT_EXCHANGE_STATUS = ExchangeStatus.AFTER_EXCHANGE; + + public MentorApplication 대기중_멘토신청( + long siteUserId, + UniversitySelectType selectType, + Long universityId + ) { + return mentorApplicationFixtureBuilder.mentorApplication() + .siteUserId(siteUserId) + .countryCode(DEFAULT_COUNTRY_CODE) + .universityId(universityId) + .universitySelectType(selectType) + .mentorProofUrl(DEFAULT_PROOF_URL) + .exchangeStatus(DEFAULT_EXCHANGE_STATUS) + .create(); + } + + public MentorApplication 승인된_멘토신청( + long siteUserId, + UniversitySelectType selectType, + Long universityId + ){ + return mentorApplicationFixtureBuilder.mentorApplication() + .siteUserId(siteUserId) + .countryCode(DEFAULT_COUNTRY_CODE) + .universityId(universityId) + .universitySelectType(selectType) + .mentorProofUrl(DEFAULT_PROOF_URL) + .exchangeStatus(DEFAULT_EXCHANGE_STATUS) + .mentorApplicationStatus(MentorApplicationStatus.APPROVED) + .create(); + } + + public MentorApplication 거절된_멘토신청( + long siteUserId, + UniversitySelectType selectType, + Long universityId + ){ + return mentorApplicationFixtureBuilder.mentorApplication() + .siteUserId(siteUserId) + .countryCode(DEFAULT_COUNTRY_CODE) + .universityId(universityId) + .universitySelectType(selectType) + .mentorProofUrl(DEFAULT_PROOF_URL) + .exchangeStatus(DEFAULT_EXCHANGE_STATUS) + .mentorApplicationStatus(MentorApplicationStatus.REJECTED) + .create(); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixtureBuilder.java b/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixtureBuilder.java new file mode 100644 index 000000000..fc6fe547d --- /dev/null +++ b/src/test/java/com/example/solidconnection/mentor/fixture/MentorApplicationFixtureBuilder.java @@ -0,0 +1,77 @@ +package com.example.solidconnection.mentor.fixture; + +import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; +import com.example.solidconnection.mentor.domain.UniversitySelectType; +import com.example.solidconnection.mentor.repository.MentorApplicationRepository; +import com.example.solidconnection.siteuser.domain.ExchangeStatus; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.test.util.ReflectionTestUtils; + +@TestComponent +@RequiredArgsConstructor +public class MentorApplicationFixtureBuilder { + + private final MentorApplicationRepository mentorApplicationRepository; + + private long siteUserId; + private String countryCode = "US"; + private Long universityId = null; + private UniversitySelectType universitySelectType = UniversitySelectType.OTHER; + private String mentorProofUrl = "/mentor-proof.pdf"; + private ExchangeStatus exchangeStatus = ExchangeStatus.AFTER_EXCHANGE; + private MentorApplicationStatus mentorApplicationStatus = MentorApplicationStatus.PENDING; + + public MentorApplicationFixtureBuilder mentorApplication() { + return new MentorApplicationFixtureBuilder(mentorApplicationRepository); + } + + public MentorApplicationFixtureBuilder siteUserId(long siteUserId) { + this.siteUserId = siteUserId; + return this; + } + + public MentorApplicationFixtureBuilder countryCode(String countryCode) { + this.countryCode = countryCode; + return this; + } + + public MentorApplicationFixtureBuilder universityId(Long universityId) { + this.universityId = universityId; + return this; + } + + public MentorApplicationFixtureBuilder universitySelectType(UniversitySelectType universitySelectType) { + this.universitySelectType = universitySelectType; + return this; + } + + public MentorApplicationFixtureBuilder mentorProofUrl(String mentorProofUrl) { + this.mentorProofUrl = mentorProofUrl; + return this; + } + + public MentorApplicationFixtureBuilder exchangeStatus(ExchangeStatus exchangeStatus) { + this.exchangeStatus = exchangeStatus; + return this; + } + + public MentorApplicationFixtureBuilder mentorApplicationStatus(MentorApplicationStatus mentorApplicationStatus) { + this.mentorApplicationStatus = mentorApplicationStatus; + return this; + } + + public MentorApplication create() { + MentorApplication mentorApplication = new MentorApplication( + siteUserId, + countryCode, + universityId, + universitySelectType, + mentorProofUrl, + exchangeStatus + ); + ReflectionTestUtils.setField(mentorApplication, "mentorApplicationStatus", mentorApplicationStatus); + return mentorApplicationRepository.save(mentorApplication); + } +} diff --git a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java index bd3ce5cff..aaf63b600 100644 --- a/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java +++ b/src/test/java/com/example/solidconnection/mentor/service/MentorApplicationServiceTest.java @@ -1,6 +1,5 @@ package com.example.solidconnection.mentor.service; - import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED; import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER; import static com.example.solidconnection.common.exception.ErrorCode.UNIVERSITY_ID_REQUIRED_FOR_CATALOG; @@ -9,9 +8,10 @@ import static org.mockito.BDDMockito.given; import com.example.solidconnection.common.exception.CustomException; -import com.example.solidconnection.mentor.domain.MentorApplication; +import com.example.solidconnection.mentor.domain.MentorApplicationStatus; import com.example.solidconnection.mentor.domain.UniversitySelectType; import com.example.solidconnection.mentor.dto.MentorApplicationRequest; +import com.example.solidconnection.mentor.fixture.MentorApplicationFixture; import com.example.solidconnection.mentor.repository.MentorApplicationRepository; import com.example.solidconnection.s3.domain.ImgType; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; @@ -20,6 +20,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -40,6 +41,9 @@ public class MentorApplicationServiceTest { @Autowired private SiteUserFixture siteUserFixture; + @Autowired + private MentorApplicationFixture mentorApplicationFixture; + @MockBean private S3Service s3Service; @@ -65,7 +69,7 @@ void setUp() { mentorApplicationService.submitMentorApplication(user.getId(), request, file); // then - assertThat(mentorApplicationRepository.existsBySiteUserId(user.getId())).isEqualTo(true); + assertThat(mentorApplicationRepository.existsBySiteUserIdAndMentorApplicationStatusIn(user.getId(), List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED))).isEqualTo(true); } @Test @@ -83,11 +87,11 @@ void setUp() { mentorApplicationService.submitMentorApplication(user.getId(), request, file); // then - assertThat(mentorApplicationRepository.existsBySiteUserId(user.getId())).isEqualTo(true); + assertThat(mentorApplicationRepository.existsBySiteUserIdAndMentorApplicationStatusIn(user.getId(),List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED))).isEqualTo(true); } @Test - void 대학_선택_타입이_CATALOG_인데_universityId가_null_이면_예외(){ + void 대학_선택_타입이_CATALOG_인데_universityId가_null_이면_예외가_발생한다(){ // given UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; Long universityId = null; @@ -97,15 +101,14 @@ void setUp() { given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); - // when - // then + // when & then assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_ID_REQUIRED_FOR_CATALOG.getMessage()); } @Test - void 대학_선택_타입이_OTHER_인데_universityId가_존재하면_예외(){ + void 대학_선택_타입이_OTHER_인데_universityId가_존재하면_예외가_발생한다(){ // given UniversitySelectType universitySelectType = UniversitySelectType.OTHER; Long universityId = 1L; @@ -115,38 +118,64 @@ void setUp() { given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) .willReturn(new UploadedFileUrlResponse(fileUrl)); - // when - // then + // when & then assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER.getMessage()); } @Test - void 한_유저가_1회_이상_멘토_신청_요청을_보내면_예외() { + void 이미_PENDING_상태인_멘토_승격_요청이_존재할_때_중복으로_멘토_승격_신청_시_예외가_발생한다() { // given - MentorApplication existsMentorApplication = new MentorApplication( - user, - "US", - 1L, - UniversitySelectType.CATALOG, - "/mentor-proof.pdf", - ExchangeStatus.AFTER_EXCHANGE - ); - mentorApplicationRepository.save(existsMentorApplication); + mentorApplicationFixture.대기중_멘토신청(user.getId(), UniversitySelectType.CATALOG, 1L); UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; Long universityId = 1L; MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); MockMultipartFile file = createMentorProofFile(); - // when - // then + // when & then assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) .isInstanceOf(CustomException.class) .hasMessage(MENTOR_APPLICATION_ALREADY_EXISTED.getMessage()); } + @Test + void 이미_APPROVE_상태인_멘토_승격_요청이_존재할_때_중복으로_멘토_승격_신청_시_예외가_발생한다() { + // given + mentorApplicationFixture.승인된_멘토신청(user.getId(), UniversitySelectType.CATALOG, 1L); + + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); + MockMultipartFile file = createMentorProofFile(); + + // when & then + assertThatCode(() -> mentorApplicationService.submitMentorApplication(user.getId(), request, file)) + .isInstanceOf(CustomException.class) + .hasMessage(MENTOR_APPLICATION_ALREADY_EXISTED.getMessage()); + } + + @Test + void 이미_REJECTED_상태인_멘토_승격_요청이_존재할_때_멘토_신청이_등록된다() { + // given + mentorApplicationFixture.거절된_멘토신청(user.getId(), UniversitySelectType.CATALOG, 1L); + + UniversitySelectType universitySelectType = UniversitySelectType.CATALOG; + Long universityId = 1L; + MentorApplicationRequest request = createMentorApplicationRequest(universitySelectType, universityId); + MockMultipartFile file = createMentorProofFile(); + String fileUrl = "/mentor-proof.pdf"; + given(s3Service.uploadFile(file, ImgType.MENTOR_PROOF)) + .willReturn(new UploadedFileUrlResponse(fileUrl)); + + // when + mentorApplicationService.submitMentorApplication(user.getId(), request, file); + + // then + assertThat(mentorApplicationRepository.existsBySiteUserIdAndMentorApplicationStatusIn(user.getId(),List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED))).isEqualTo(true); + } + private MockMultipartFile createMentorProofFile() { return new MockMultipartFile( "image", From 661e91b17a3ae25969ee1e687489e1594f48f2b9 Mon Sep 17 00:00:00 2001 From: sukangpunch Date: Tue, 21 Oct 2025 19:27:56 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EC=BD=94=EB=93=9C=EB=A0=88=EB=B9=97?= =?UTF-8?q?=20=EB=A6=AC=EB=B7=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/common/exception/ErrorCode.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index 059fb3458..f3de315b1 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -121,9 +121,9 @@ public enum ErrorCode { MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."), MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(),"멘토 승격 요청이 이미 존재합니다."), INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원 가능한 교환학생 상태가 아닙니다."), - UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 CATALOG이면 universityId가 필요합니다."), - UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "UniversitySelectType이 OTHER이면 universityId가 null 이어야 합니다."), - INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 UniversitySelectType 입니다."), + UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "목록에서 학교를 선택한 경우 학교 정보가 필요합니다."), + UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "기타 학교를 선택한 경우 학교 정보를 입력할 수 없습니다."), + INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 학교 선택 방식입니다."), // socket UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."),