Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +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.CONFLICT.value(),"멘토 승격 요청이 이미 존재합니다."),
INVALID_EXCHANGE_STATUS_FOR_MENTOR(HttpStatus.BAD_REQUEST.value(), "멘토 승격 지원 가능한 교환학생 상태가 아닙니다."),
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(), "구독 권한이 없습니다."),
Expand Down
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();
}
Comment on lines +23 to +31
Copy link

@coderabbitai coderabbitai bot Oct 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Controller 구조는 명확하지만, 파라미터 어노테이션을 통일해주세요.

Controller는 적절히 service로 위임하고 있으나 다음을 개선하면 좋겠습니다:

  1. @Valid 동작 안 함 (Line 22):

    • MentorApplicationRequest에 validation 어노테이션이 없어 @Valid가 작동하지 않습니다.
    • DTO 리뷰 코멘트를 참고해주세요.
  2. @RequestParam vs @RequestPart 불일치 (Line 23):

    • mentorApplicationRequest@RequestPart, file@RequestParam을 사용하고 있습니다.
    • Multipart 요청에서는 일관성을 위해 둘 다 @RequestPart를 사용하는 것이 좋습니다.
  3. 파일 검증 부재:

    • 파일 크기, 타입 등에 대한 검증이 없습니다.
    • 서비스나 컨트롤러 레벨에서 파일 검증을 추가하면 더 안전합니다.

다음과 같이 개선해보세요:

 @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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@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();
}
@PostMapping("/mentor-applications")
public ResponseEntity<Void> requestMentorApplication(
@AuthorizedUser long siteUserId,
@Valid @RequestPart("mentorApplicationRequest") MentorApplicationRequest mentorApplicationRequest,
@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();
}
🤖 Prompt for AI Agents
In
src/main/java/com/example/solidconnection/mentor/controller/MentorApplicationController.java
around lines 19-27, the endpoint uses @Valid on MentorApplicationRequest which
has no validation annotations, mixes @RequestPart and @RequestParam for a
multipart request, and lacks file validation; fix by adding appropriate
javax/jakarta validation annotations to MentorApplicationRequest fields (so
@Valid takes effect), change the controller to accept both parts as @RequestPart
and set consumes = MediaType.MULTIPART_FORM_DATA_VALUE on the @PostMapping, and
add server-side file checks (size limit, allowed content types and null/empty
checks) either in the controller before delegating or in the service, returning
a 400 response for invalid files.

Copy link
Contributor

@Gyuhyeok99 Gyuhyeok99 Oct 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일검증은 serivce 내에서 해줘도 좋을 거 같습니다~

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

}
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)
Copy link
Contributor

Choose a reason for hiding this comment

The 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)
        """
)

이런식으로 flyway 스크립트와 동기화가 가능한지 확인해봐주실 수 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위와 같이 셋팅 했을 시 문제가 발생하지 않았고,
테스트에서 ddl로 스키마 생성 했을 때도, 잘 적용 되었습니다!

@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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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
Expand Up @@ -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;

Expand Down
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();
}
}
Loading
Loading