diff --git a/backend/src/main/java/com/festago/application/StudentService.java b/backend/src/main/java/com/festago/application/StudentService.java index 77e320730..b380c1523 100644 --- a/backend/src/main/java/com/festago/application/StudentService.java +++ b/backend/src/main/java/com/festago/application/StudentService.java @@ -5,6 +5,7 @@ import com.festago.domain.MemberRepository; import com.festago.domain.School; import com.festago.domain.SchoolRepository; +import com.festago.domain.Student; import com.festago.domain.StudentCode; import com.festago.domain.StudentCodeRepository; import com.festago.domain.StudentRepository; @@ -12,6 +13,7 @@ import com.festago.domain.VerificationCodeProvider; import com.festago.domain.VerificationMailPayload; import com.festago.dto.StudentSendMailRequest; +import com.festago.dto.StudentVerificateRequest; import com.festago.exception.BadRequestException; import com.festago.exception.ErrorCode; import com.festago.exception.NotFoundException; @@ -47,7 +49,7 @@ public void sendVerificationMail(Long memberId, StudentSendMailRequest request) School school = findSchool(request.schoolId()); VerificationCode code = codeProvider.provide(); studentCodeRepository.deleteByMember(member); - studentCodeRepository.save(new StudentCode(code, member, school)); + studentCodeRepository.save(new StudentCode(code, school, member, request.username())); mailClient.send(new VerificationMailPayload(code, request.username(), school.getDomain())); } @@ -72,4 +74,13 @@ private School findSchool(Long schoolId) { return schoolRepository.findById(schoolId) .orElseThrow(() -> new NotFoundException(ErrorCode.SCHOOL_NOT_FOUND)); } + + public void verificate(Long memberId, StudentVerificateRequest request) { + validateStudent(memberId); + Member member = findMember(memberId); + StudentCode studentCode = studentCodeRepository.findByCode(new VerificationCode(request.code())) + .orElseThrow(() -> new BadRequestException(ErrorCode.INVALID_STUDENT_VERIFICATION_CODE)); + studentRepository.save(new Student(member, studentCode.getSchool(), studentCode.getUsername())); + studentCodeRepository.deleteByMember(member); + } } diff --git a/backend/src/main/java/com/festago/domain/School.java b/backend/src/main/java/com/festago/domain/School.java index 6e701a961..236b740f0 100644 --- a/backend/src/main/java/com/festago/domain/School.java +++ b/backend/src/main/java/com/festago/domain/School.java @@ -27,6 +27,10 @@ public class School { protected School() { } + public School(String domain, String name) { + this(null, domain, name); + } + public School(Long id, String domain, String name) { validate(domain, name); this.id = id; diff --git a/backend/src/main/java/com/festago/domain/Student.java b/backend/src/main/java/com/festago/domain/Student.java index 768b0e462..497832bef 100644 --- a/backend/src/main/java/com/festago/domain/Student.java +++ b/backend/src/main/java/com/festago/domain/Student.java @@ -34,6 +34,10 @@ public class Student { protected Student() { } + public Student(Member member, School school, String username) { + this(null, member, school, username); + } + public Student(Long id, Member member, School school, String username) { validate(member, school, username); this.id = id; diff --git a/backend/src/main/java/com/festago/domain/StudentCode.java b/backend/src/main/java/com/festago/domain/StudentCode.java index 2d00b2647..935f500a2 100644 --- a/backend/src/main/java/com/festago/domain/StudentCode.java +++ b/backend/src/main/java/com/festago/domain/StudentCode.java @@ -1,5 +1,7 @@ package com.festago.domain; +import com.festago.exception.ErrorCode; +import com.festago.exception.InternalServerException; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -8,6 +10,7 @@ import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToOne; +import org.springframework.util.StringUtils; @Entity public class StudentCode extends BaseTimeEntity { @@ -25,18 +28,32 @@ public class StudentCode extends BaseTimeEntity { @OneToOne(fetch = FetchType.LAZY) private Member member; + private String username; + protected StudentCode() { } - public StudentCode(VerificationCode code, Member member, School school) { - this(null, code, member, school); + public StudentCode(VerificationCode code, School school, Member member, String username) { + this(null, code, school, member, username); } - public StudentCode(Long id, VerificationCode code, Member member, School school) { + public StudentCode(Long id, VerificationCode code, School school, Member member, String username) { + validate(username); this.id = id; this.code = code; - this.member = member; this.school = school; + this.member = member; + this.username = username; + } + + private void validate(String username) { + if (!StringUtils.hasText(username)) { + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } + + if (username.length() > 255) { + throw new InternalServerException(ErrorCode.INTERNAL_SERVER_ERROR); + } } public Long getId() { @@ -54,4 +71,8 @@ public School getSchool() { public Member getMember() { return member; } + + public String getUsername() { + return username; + } } diff --git a/backend/src/main/java/com/festago/domain/StudentCodeRepository.java b/backend/src/main/java/com/festago/domain/StudentCodeRepository.java index 312b93f6d..ba38fae79 100644 --- a/backend/src/main/java/com/festago/domain/StudentCodeRepository.java +++ b/backend/src/main/java/com/festago/domain/StudentCodeRepository.java @@ -1,8 +1,11 @@ package com.festago.domain; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface StudentCodeRepository extends JpaRepository { void deleteByMember(Member member); + + Optional findByCode(VerificationCode code); } diff --git a/backend/src/main/java/com/festago/dto/StudentVerificateRequest.java b/backend/src/main/java/com/festago/dto/StudentVerificateRequest.java new file mode 100644 index 000000000..2c22c8ecb --- /dev/null +++ b/backend/src/main/java/com/festago/dto/StudentVerificateRequest.java @@ -0,0 +1,5 @@ +package com.festago.dto; + +public record StudentVerificateRequest(String code) { + +} diff --git a/backend/src/main/java/com/festago/exception/ErrorCode.java b/backend/src/main/java/com/festago/exception/ErrorCode.java index 0de87ad2a..c9bf91f7b 100644 --- a/backend/src/main/java/com/festago/exception/ErrorCode.java +++ b/backend/src/main/java/com/festago/exception/ErrorCode.java @@ -22,6 +22,7 @@ public enum ErrorCode { ALREADY_STUDENT_VERIFIED("이미 학교 인증이 완료된 사용자입니다."), DUPLICATE_STUDENT_EMAIL("이미 인증된 이메일입니다."), TICKET_CANNOT_RESERVE_STAGE_START("공연의 시작 시간 이후로 예매할 수 없습니다."), + INVALID_STUDENT_VERIFICATION_CODE("존재하지 않는 학생 인증 코드입니다."), // 401 EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."), diff --git a/backend/src/main/java/com/festago/presentation/StudentController.java b/backend/src/main/java/com/festago/presentation/StudentController.java index ff04ea373..7f0c1270a 100644 --- a/backend/src/main/java/com/festago/presentation/StudentController.java +++ b/backend/src/main/java/com/festago/presentation/StudentController.java @@ -3,6 +3,7 @@ import com.festago.application.StudentService; import com.festago.auth.annotation.Member; import com.festago.dto.StudentSendMailRequest; +import com.festago.dto.StudentVerificateRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.ResponseEntity; @@ -30,4 +31,13 @@ public ResponseEntity sendEmail(@Member Long memberId, return ResponseEntity.ok() .build(); } + + @PostMapping("/verification") + @Operation(description = "학교 인증을 수행한다.", summary = "학생 인증 수행") + public ResponseEntity verificate(@Member Long memberId, + @RequestBody StudentVerificateRequest request) { + studentService.verificate(memberId, request); + return ResponseEntity.ok() + .build(); + } } diff --git a/backend/src/test/java/com/festago/application/StudentServiceTest.java b/backend/src/test/java/com/festago/application/StudentServiceTest.java index b120fe6be..8a5b57042 100644 --- a/backend/src/test/java/com/festago/application/StudentServiceTest.java +++ b/backend/src/test/java/com/festago/application/StudentServiceTest.java @@ -2,6 +2,8 @@ import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import com.festago.domain.MailClient; @@ -9,11 +11,13 @@ import com.festago.domain.MemberRepository; import com.festago.domain.School; import com.festago.domain.SchoolRepository; +import com.festago.domain.StudentCode; import com.festago.domain.StudentCodeRepository; import com.festago.domain.StudentRepository; import com.festago.domain.VerificationCode; import com.festago.domain.VerificationCodeProvider; import com.festago.dto.StudentSendMailRequest; +import com.festago.dto.StudentVerificateRequest; import com.festago.exception.BadRequestException; import com.festago.exception.NotFoundException; import com.festago.support.MemberFixture; @@ -168,4 +172,59 @@ class 인증_메일_전송 { .isThrownBy(() -> studentService.sendVerificationMail(memberId, request)); } } + + @Nested + class 학생_인증 { + + @Test + void 이미_학생인증정보가_존재하면_예외() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + given(studentRepository.existsByMemberId(memberId)) + .willReturn(true); + + // when & then + assertThatThrownBy(() -> studentService.verificate(memberId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("이미 학교 인증이 완료된 사용자입니다."); + } + + @Test + void 인증_코드가_존재하지_않으면_예외() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(MemberFixture.member().build())); + given(studentCodeRepository.findByCode(any())) + .willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> studentService.verificate(memberId, request)) + .isInstanceOf(BadRequestException.class) + .hasMessage("존재하지 않는 학생 인증 코드입니다."); + } + + @Test + void 성공() { + // given + Long memberId = 1L; + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + Member member = MemberFixture.member().build(); + given(memberRepository.findById(anyLong())) + .willReturn(Optional.of(member)); + given(studentCodeRepository.findByCode(any())) + .willReturn(Optional.of(new StudentCode( + new VerificationCode("123456"), + new School("snu.ac.kr", "서울대학교"), + member, + "ohs" + ))); + + // when & then + assertThatNoException() + .isThrownBy(() -> studentService.verificate(memberId, request)); + } + } } diff --git a/backend/src/test/java/com/festago/presentation/StudentControllerTest.java b/backend/src/test/java/com/festago/presentation/StudentControllerTest.java index 525b3c3cd..19322565d 100644 --- a/backend/src/test/java/com/festago/presentation/StudentControllerTest.java +++ b/backend/src/test/java/com/festago/presentation/StudentControllerTest.java @@ -6,10 +6,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.festago.application.StudentService; import com.festago.dto.StudentSendMailRequest; +import com.festago.dto.StudentVerificateRequest; import com.festago.support.CustomWebMvcTest; import com.festago.support.WithMockAuth; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; @@ -31,30 +33,64 @@ class StudentControllerTest { @MockBean StudentService studentService; - @Test - void 인증이_되지_않으면_401() throws Exception { - // given - StudentSendMailRequest request = new StudentSendMailRequest("user", 1L); + @Nested + class 학생_인증_메일_전송 { - // when & then - mockMvc.perform(post("/students/send-verification") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isUnauthorized()); + @Test + void 인증이_되지_않으면_401() throws Exception { + // given + StudentSendMailRequest request = new StudentSendMailRequest("user", 1L); + + // when & then + mockMvc.perform(post("/students/send-verification") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth + void 학교_인증_요청() throws Exception { + // given + StudentSendMailRequest request = new StudentSendMailRequest("user", 1L); + + // when & then + mockMvc.perform(post("/students/send-verification") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + + } } - @Test - @WithMockAuth - void 학교_인증_요청() throws Exception { - // given - StudentSendMailRequest request = new StudentSendMailRequest("user", 1L); + @Nested + class 학생_인증 { + + @Test + void 인증이_되지_않으면_401() throws Exception { + // given + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + + // when & then + mockMvc.perform(post("/students/verification") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isUnauthorized()); + } - // when & then - mockMvc.perform(post("/students/send-verification") - .contentType(MediaType.APPLICATION_JSON) - .header(HttpHeaders.AUTHORIZATION, "Bearer token") - .content(objectMapper.writeValueAsString(request))) - .andExpect(status().isOk()); + @Test + @WithMockAuth + void 학교_인증_요청() throws Exception { + // given + StudentVerificateRequest request = new StudentVerificateRequest("123456"); + // when & then + mockMvc.perform(post("/students/verification") + .contentType(MediaType.APPLICATION_JSON) + .header(HttpHeaders.AUTHORIZATION, "Bearer token") + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()); + } } }