-
Notifications
You must be signed in to change notification settings - Fork 8
feat: 멘토 승격 api 구현 #532
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 멘토 승격 api 구현 #532
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| 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.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 | ||
| @RequestMapping("/mentees") | ||
| @RestController | ||
| public class MentorApplicationController { | ||
|
|
||
| private final MentorApplicationService mentorApplicationService; | ||
|
|
||
| @PostMapping("/mentor-applications") | ||
| public ResponseEntity<Void> requestMentorApplication( | ||
| @AuthorizedUser long siteUserId, | ||
| @Valid @RequestPart("mentorApplicationRequest") MentorApplicationRequest mentorApplicationRequest, | ||
| @RequestParam("file") MultipartFile file | ||
| ) { | ||
| mentorApplicationService.submitMentorApplication(siteUserId, mentorApplicationRequest, file); | ||
| return ResponseEntity.ok().build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,109 @@ | ||
| 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 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.AccessLevel; | ||
| import lombok.AllArgsConstructor; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
| import org.hibernate.annotations.Check; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @AllArgsConstructor | ||
| @NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 가능할지모르겠는데 이런식으로 flyway 스크립트와 동기화가 가능한지 확인해봐주실 수 있나요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위와 같이 셋팅 했을 시 문제가 발생하지 않았고, |
||
| @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 | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Long id; | ||
|
|
||
| @Column(nullable = false) | ||
| private long siteUserId; | ||
|
|
||
| @Column(nullable = false, name = "country_code") | ||
| private String countryCode; | ||
|
|
||
| @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; | ||
|
|
||
| private String rejectedReason; | ||
|
|
||
| @Column(nullable = false) | ||
| @Enumerated(EnumType.STRING) | ||
| private ExchangeStatus exchangeStatus; | ||
|
|
||
| @Column(nullable = false) | ||
| @Enumerated(EnumType.STRING) | ||
| private MentorApplicationStatus mentorApplicationStatus = MentorApplicationStatus.PENDING; | ||
|
|
||
| private static final Set<ExchangeStatus> ALLOWED = | ||
| Collections.unmodifiableSet(EnumSet.of(ExchangeStatus.STUDYING_ABROAD, ExchangeStatus.AFTER_EXCHANGE)); | ||
|
|
||
| public MentorApplication( | ||
| long siteUserId, | ||
| String countryCode, | ||
| Long universityId, | ||
| UniversitySelectType universitySelectType, | ||
| String mentorProofUrl, | ||
| ExchangeStatus exchangeStatus | ||
| ) { | ||
| validateExchangeStatus(exchangeStatus); | ||
| validateUniversitySelection(universitySelectType, universityId); | ||
|
|
||
| this.siteUserId = siteUserId; | ||
| this.countryCode = countryCode; | ||
| this.universityId = universityId; | ||
| this.universitySelectType = universitySelectType; | ||
| this.mentorProofUrl = mentorProofUrl; | ||
| 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); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.example.solidconnection.mentor.domain; | ||
|
|
||
| public enum MentorApplicationStatus { | ||
|
|
||
| PENDING, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 개행 하나 해주시죠! |
||
| APPROVED, | ||
| REJECTED, | ||
| ; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.example.solidconnection.mentor.domain; | ||
|
|
||
| public enum UniversitySelectType { | ||
|
|
||
| CATALOG, | ||
| OTHER | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package com.example.solidconnection.mentor.dto; | ||
|
|
||
| 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") | ||
| ExchangeStatus exchangeStatus, | ||
| UniversitySelectType universitySelectType, | ||
| String country, | ||
| Long universityId | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +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<MentorApplication, Long> { | ||
|
|
||
| boolean existsBySiteUserIdAndMentorApplicationStatusIn(long siteUserId, List<MentorApplicationStatus> mentorApplicationStatuses); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| 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.domain.MentorApplicationStatus; | ||
| 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 java.util.List; | ||
| 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.existsBySiteUserIdAndMentorApplicationStatusIn( | ||
| siteUserId, | ||
| List.of(MentorApplicationStatus.PENDING, MentorApplicationStatus.APPROVED)) | ||
| ) { | ||
| 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.getId(), | ||
| mentorApplicationRequest.country(), | ||
| mentorApplicationRequest.universityId(), | ||
| mentorApplicationRequest.universitySelectType(), | ||
| uploadedFile.fileUrl(), | ||
| mentorApplicationRequest.exchangeStatus() | ||
| ); | ||
| mentorApplicationRepository.save(mentorApplication); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| CREATE TABLE mentor_application | ||
| ( | ||
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Controller 구조는 명확하지만, 파라미터 어노테이션을 통일해주세요.
Controller는 적절히 service로 위임하고 있으나 다음을 개선하면 좋겠습니다:
@Valid 동작 안 함 (Line 22):
MentorApplicationRequest에 validation 어노테이션이 없어@Valid가 작동하지 않습니다.@RequestParam vs @RequestPart 불일치 (Line 23):
mentorApplicationRequest는@RequestPart,file은@RequestParam을 사용하고 있습니다.@RequestPart를 사용하는 것이 좋습니다.파일 검증 부재:
다음과 같이 개선해보세요:
@PostMapping("/mentor-applications") public ResponseEntity<Void> requestMentorApplication( @AuthorizedUser long siteUserId, @Valid @RequestPart("mentorApplicationRequest") MentorApplicationRequest mentorApplicationRequest, - @RequestParam("file") MultipartFile file + @RequestPart("file") MultipartFile file ) { + // Optional: Add file validation + if (file.isEmpty()) { + throw new CustomException(ErrorCode.FILE_REQUIRED); + } + if (file.getSize() > 10 * 1024 * 1024) { // 10MB limit + throw new CustomException(ErrorCode.FILE_TOO_LARGE); + } + mentorApplicationService.submitMentorApplication(siteUserId, mentorApplicationRequest, file); return ResponseEntity.ok().build(); }📝 Committable suggestion
🤖 Prompt for AI Agents
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
파일검증은 serivce 내에서 해줘도 좋을 거 같습니다~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.