Skip to content

Commit e4ff858

Browse files
authored
feat: 멘토 승격 api 구현 (#532)
* feat: 멘토 승격 신청 api 추가 * 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 작성 * fix: 멘토 승격 api 를 기획에 맞게 수정합니다 * fix: MentorApplication 엔티티 수정 - UniversitySelectStatus 필드 추가 - region 필트 삭제 - ExchangeStatus 쓰도록 수정 - 도메인 규칙 추가 * feat: 도메인 규칙에 맞게 ErrorCode 추가 * test: 테스트 코드 에러 수정 * test: 도메인 규칙 테스트 추가 * fix: 리뷰 최종 반영 * fix: 코드레빗 리뷰 수정
1 parent 9d639ec commit e4ff858

File tree

13 files changed

+612
-1
lines changed

13 files changed

+612
-1
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,11 @@ public enum ErrorCode {
119119
MENTORING_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "해당 멘토링 신청을 찾을 수 없습니다."),
120120
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
121121
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),
122+
MENTOR_APPLICATION_ALREADY_EXISTED(HttpStatus.CONFLICT.value(),"멘토 승격 요청이 이미 존재합니다."),
123+
INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원 가능한 교환학생 상태가 아닙니다."),
124+
UNIVERSITY_ID_REQUIRED_FOR_CATALOG(HttpStatus.BAD_REQUEST.value(), "목록에서 학교를 선택한 경우 학교 정보가 필요합니다."),
125+
UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER(HttpStatus.BAD_REQUEST.value(), "기타 학교를 선택한 경우 학교 정보를 입력할 수 없습니다."),
126+
INVALID_UNIVERSITY_SELECT_TYPE(HttpStatus.BAD_REQUEST.value(), "지원하지 않는 학교 선택 방식입니다."),
122127

123128
// socket
124129
UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."),
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.example.solidconnection.mentor.controller;
2+
3+
import com.example.solidconnection.common.resolver.AuthorizedUser;
4+
import com.example.solidconnection.mentor.dto.MentorApplicationRequest;
5+
import com.example.solidconnection.mentor.service.MentorApplicationService;
6+
import jakarta.validation.Valid;
7+
import lombok.RequiredArgsConstructor;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.PostMapping;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
import org.springframework.web.bind.annotation.RequestPart;
13+
import org.springframework.web.bind.annotation.RestController;
14+
import org.springframework.web.multipart.MultipartFile;
15+
16+
@RequiredArgsConstructor
17+
@RequestMapping("/mentees")
18+
@RestController
19+
public class MentorApplicationController {
20+
21+
private final MentorApplicationService mentorApplicationService;
22+
23+
@PostMapping("/mentor-applications")
24+
public ResponseEntity<Void> requestMentorApplication(
25+
@AuthorizedUser long siteUserId,
26+
@Valid @RequestPart("mentorApplicationRequest") MentorApplicationRequest mentorApplicationRequest,
27+
@RequestParam("file") MultipartFile file
28+
) {
29+
mentorApplicationService.submitMentorApplication(siteUserId, mentorApplicationRequest, file);
30+
return ResponseEntity.ok().build();
31+
}
32+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package com.example.solidconnection.mentor.domain;
2+
3+
import com.example.solidconnection.common.BaseEntity;
4+
import com.example.solidconnection.common.exception.CustomException;
5+
import com.example.solidconnection.common.exception.ErrorCode;
6+
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
7+
import jakarta.persistence.Column;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.EnumType;
10+
import jakarta.persistence.Enumerated;
11+
import jakarta.persistence.GeneratedValue;
12+
import jakarta.persistence.GenerationType;
13+
import jakarta.persistence.Id;
14+
import java.util.Collections;
15+
import java.util.EnumSet;
16+
import java.util.Set;
17+
import lombok.AccessLevel;
18+
import lombok.AllArgsConstructor;
19+
import lombok.Getter;
20+
import lombok.NoArgsConstructor;
21+
import org.hibernate.annotations.Check;
22+
23+
@Entity
24+
@Getter
25+
@AllArgsConstructor
26+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
27+
@Check(
28+
name = "chk_ma_university_select_rule",
29+
constraints = """
30+
(university_select_type = 'CATALOG' AND university_id IS NOT NULL) OR
31+
(university_select_type = 'OTHER' AND university_id IS NULL)
32+
"""
33+
)
34+
public class MentorApplication extends BaseEntity {
35+
36+
@Id
37+
@GeneratedValue(strategy = GenerationType.IDENTITY)
38+
private Long id;
39+
40+
@Column(nullable = false)
41+
private long siteUserId;
42+
43+
@Column(nullable = false, name = "country_code")
44+
private String countryCode;
45+
46+
@Column
47+
private Long universityId;
48+
49+
@Column(nullable = false)
50+
@Enumerated(EnumType.STRING)
51+
private UniversitySelectType universitySelectType;
52+
53+
@Column(nullable = false, name = "mentor_proof_url", length = 500)
54+
private String mentorProofUrl;
55+
56+
private String rejectedReason;
57+
58+
@Column(nullable = false)
59+
@Enumerated(EnumType.STRING)
60+
private ExchangeStatus exchangeStatus;
61+
62+
@Column(nullable = false)
63+
@Enumerated(EnumType.STRING)
64+
private MentorApplicationStatus mentorApplicationStatus = MentorApplicationStatus.PENDING;
65+
66+
private static final Set<ExchangeStatus> ALLOWED =
67+
Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE));
68+
69+
public MentorApplication(
70+
long siteUserId,
71+
String countryCode,
72+
Long universityId,
73+
UniversitySelectType universitySelectType,
74+
String mentorProofUrl,
75+
ExchangeStatus exchangeStatus
76+
) {
77+
validateExchangeStatus(exchangeStatus);
78+
validateUniversitySelection(universitySelectType, universityId);
79+
80+
this.siteUserId = siteUserId;
81+
this.countryCode = countryCode;
82+
this.universityId = universityId;
83+
this.universitySelectType = universitySelectType;
84+
this.mentorProofUrl = mentorProofUrl;
85+
this.exchangeStatus = exchangeStatus;
86+
}
87+
88+
private void validateUniversitySelection(UniversitySelectType universitySelectType, Long universityId) {
89+
switch (universitySelectType) {
90+
case CATALOG -> {
91+
if(universityId == null) {
92+
throw new CustomException(ErrorCode.UNIVERSITY_ID_REQUIRED_FOR_CATALOG);
93+
}
94+
}
95+
case OTHER -> {
96+
if(universityId != null) {
97+
throw new CustomException(ErrorCode.UNIVERSITY_ID_MUST_BE_NULL_FOR_OTHER);
98+
}
99+
}
100+
default -> throw new CustomException(ErrorCode.INVALID_UNIVERSITY_SELECT_TYPE);
101+
}
102+
}
103+
104+
private void validateExchangeStatus(ExchangeStatus exchangeStatus) {
105+
if(!ALLOWED.contains(exchangeStatus)) {
106+
throw new CustomException(ErrorCode.INVALID_EXCHANGE_STATUS_FOR_MENTOR);
107+
}
108+
}
109+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.solidconnection.mentor.domain;
2+
3+
public enum MentorApplicationStatus {
4+
5+
PENDING,
6+
APPROVED,
7+
REJECTED,
8+
;
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.example.solidconnection.mentor.domain;
2+
3+
public enum UniversitySelectType {
4+
5+
CATALOG,
6+
OTHER
7+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.example.solidconnection.mentor.dto;
2+
3+
import com.example.solidconnection.mentor.domain.UniversitySelectType;
4+
import com.example.solidconnection.siteuser.domain.ExchangeStatus;
5+
import com.fasterxml.jackson.annotation.JsonProperty;
6+
7+
public record MentorApplicationRequest(
8+
@JsonProperty("preparationStatus")
9+
ExchangeStatus exchangeStatus,
10+
UniversitySelectType universitySelectType,
11+
String country,
12+
Long universityId
13+
) {
14+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.solidconnection.mentor.repository;
2+
3+
import com.example.solidconnection.mentor.domain.MentorApplication;
4+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
5+
import java.util.List;
6+
import org.springframework.data.jpa.repository.JpaRepository;
7+
8+
public interface MentorApplicationRepository extends JpaRepository<MentorApplication, Long> {
9+
10+
boolean existsBySiteUserIdAndMentorApplicationStatusIn(long siteUserId, List<MentorApplicationStatus> mentorApplicationStatuses);
11+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.example.solidconnection.mentor.service;
2+
3+
import com.example.solidconnection.common.exception.CustomException;
4+
import com.example.solidconnection.mentor.domain.MentorApplication;
5+
import com.example.solidconnection.mentor.domain.MentorApplicationStatus;
6+
import com.example.solidconnection.mentor.dto.MentorApplicationRequest;
7+
import com.example.solidconnection.mentor.repository.MentorApplicationRepository;
8+
import com.example.solidconnection.s3.domain.ImgType;
9+
import com.example.solidconnection.s3.dto.UploadedFileUrlResponse;
10+
import com.example.solidconnection.s3.service.S3Service;
11+
import com.example.solidconnection.siteuser.domain.SiteUser;
12+
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
13+
import java.util.List;
14+
import lombok.RequiredArgsConstructor;
15+
import lombok.extern.slf4j.Slf4j;
16+
import org.springframework.stereotype.Service;
17+
import org.springframework.transaction.annotation.Transactional;
18+
import org.springframework.web.multipart.MultipartFile;
19+
20+
import static com.example.solidconnection.common.exception.ErrorCode.MENTOR_APPLICATION_ALREADY_EXISTED;
21+
import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND;
22+
23+
@Service
24+
@RequiredArgsConstructor
25+
@Slf4j
26+
public class MentorApplicationService {
27+
28+
private final MentorApplicationRepository mentorApplicationRepository;
29+
private final SiteUserRepository siteUserRepository;
30+
private final S3Service s3Service;
31+
32+
@Transactional
33+
public void submitMentorApplication(
34+
long siteUserId,
35+
MentorApplicationRequest mentorApplicationRequest,
36+
MultipartFile file
37+
) {
38+
if (mentorApplicationRepository.existsBySiteUserIdAndMentorApplicationStatusIn(
39+
siteUserId,
40+
List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED))
41+
) {
42+
throw new CustomException(MENTOR_APPLICATION_ALREADY_EXISTED);
43+
}
44+
45+
SiteUser siteUser = siteUserRepository.findById(siteUserId)
46+
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
47+
UploadedFileUrlResponse uploadedFile = s3Service.uploadFile(file, ImgType.MENTOR_PROOF);
48+
MentorApplication mentorApplication = new MentorApplication(
49+
siteUser.getId(),
50+
mentorApplicationRequest.country(),
51+
mentorApplicationRequest.universityId(),
52+
mentorApplicationRequest.universitySelectType(),
53+
uploadedFile.fileUrl(),
54+
mentorApplicationRequest.exchangeStatus()
55+
);
56+
mentorApplicationRepository.save(mentorApplication);
57+
}
58+
}

src/main/java/com/example/solidconnection/s3/domain/ImgType.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44

55
@Getter
66
public enum ImgType {
7-
PROFILE("profile"), GPA("gpa"), LANGUAGE_TEST("language"), COMMUNITY("community"), NEWS("news"), CHAT("chat");
7+
PROFILE("profile"),
8+
GPA("gpa"),
9+
LANGUAGE_TEST("language"),
10+
COMMUNITY("community"),
11+
NEWS("news"),
12+
CHAT("chat"),
13+
MENTOR_PROOF("mentor-proof"),
14+
;
815

916
private final String type;
1017

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CREATE TABLE mentor_application
2+
(
3+
id BIGINT NOT NULL AUTO_INCREMENT,
4+
site_user_id BIGINT,
5+
country_code VARCHAR(255),
6+
university_id BIGINT,
7+
university_select_type enum ('CATALOG','OTHER') not null,
8+
mentor_proof_url VARCHAR(500) NOT NULL,
9+
rejected_reason VARCHAR(255),
10+
exchange_status enum('AFTER_EXCHANGE','STUDYING_ABROAD') NOT NULL,
11+
mentor_application_status enum('APPROVED','PENDING','REJECTED') NOT NULL,
12+
created_at DATETIME(6),
13+
updated_at DATETIME(6),
14+
PRIMARY KEY (id),
15+
CONSTRAINT fk_mentor_application_site_user FOREIGN KEY (site_user_id) REFERENCES site_user (id),
16+
CONSTRAINT chk_ma_university_select_rule CHECK (
17+
(university_select_type = 'CATALOG' AND university_id IS NOT NULL) OR
18+
(university_select_type = 'OTHER' AND university_id IS NULL)
19+
)
20+
) ENGINE=InnoDB

0 commit comments

Comments
 (0)