Skip to content

Commit

Permalink
feat: 학생 회원가입 (#271)
Browse files Browse the repository at this point in the history
* feat: 에외 추가

* feat: 이메일 폼 추가

* feat: 골격 추가

* feat: 서비스 로직 구현

* feat: 인증 완료 폼

* feat: 회원 등록 폼 타임리프 설정

* feat: 이메일 인증 실패 폼

* feat: Controller 추가

* feat: 인증 완료시 isAuthed true 변경

* feat: 학번 학부 인증 및 UserIdentity->ordinal로 변경

* feat: 학생 회원가입 dto

* feat: Service 구현

* feat: 토큰으로 부터 찾는 메서드 Repository에 추가

* feat: authToken 암호화 Util

* feat: koreatehc.ac.kr 도메인 검증

* feat: 학번과 학부 검증

* feat: 슬랙 알림

* feat: 학생 이메일 요청, 가입 이벤트

* feat: 토큰 유효기간 설정 DateUtil

* feat: 잘못된 학번 형식 예외

* feat: 이메일 인증 검증 dto

* feat: 회원가입 이메일 인증 폼 데이터

* refactor: contorller 메서드 수정

* feat: 테스트 작성

* refactor: 컨벤션 맞게 수정

* refactor: log().all() 제거

* refactor: 컨벤션에 맞게 수정

* refactor: authToken passwordEncoder를 통해 암호화

* refactor: 학번 검증 수정

* refactor: 학번 검증 수정

* refactor: 메서드 네이밍수정

* refactor: 필드 controller->service 이동

* refactor: 유효하지 않은 데이터 400으로 반환

* refactor: 라인포맷팅

* refactor: 라인포맷팅

* feat: 에러코드반환수정 및 학번검증 테스트 추가

* refactor: 오류 해결

* refactor: 폼 반환 로직 수정

* refactor: 라인포맷팅

* refactor: 날짜 관련 로직 수정

* refactor: 호스트주소 추출 어노테이션으로 변경

* refactor: 학부,학번 검증 로직 변경

* refactor: 비밀번호 example 중복 제거

* refactor: 폼 반환 service 로직 수정

* refactor: 호스트 주소 어노테이션 적용

* refactor: 머지 형식에 맞게 수정

* refactor: 최신 전공 형식에 맞게 수정

* refactor: InvalidDataException -> IllegalException 변경

* refactor: 개행 수정

* refactor: camelCase로 수정

* refactor: 테스트명 수정

* refactor: expiredAt clock인자 받게 수정

* refactor: authToken UUID로 수정

* refactor: LocalTimeStringConverter 이름을 LocalTimeToHHmmStringConverter로 변경

* refactor: User객체 날짜 관련 속성LocalTimeToHHmmStringConverter 이용

* refactor: 학번 검증 수정

* refactor: host어노테이션 이름 변경

* refactor: LocalDate관련 수정

* refactor: 라인포맷팅

* refactor: LocalDate관련 클래스이름 수정

* refactor: URL얻어오는 코드 수정

* refactor: AuthResult orElse -> orElseGet

* refactor: LocalTimeAttributeConverter 원래대로 수정

* refactor: 라인포맷팅

* refactor: orElseGet 수정

(cherry picked from commit 65db27d)
  • Loading branch information
kwoo28 authored and Choi-JJunho committed May 9, 2024
1 parent 93e1129 commit 50b82c7
Show file tree
Hide file tree
Showing 30 changed files with 919 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import in.koreatech.koin.domain.user.dto.AuthResponse;
import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.StudentRegisterRequest;
import in.koreatech.koin.domain.user.dto.StudentResponse;
import in.koreatech.koin.domain.user.dto.StudentUpdateRequest;
import in.koreatech.koin.domain.user.dto.StudentUpdateResponse;
Expand All @@ -23,6 +24,7 @@
import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest;
import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.host.ServerURL;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
Expand Down Expand Up @@ -110,6 +112,21 @@ ResponseEntity<UserTokenRefreshResponse> refresh(
@RequestBody @Valid UserTokenRefreshRequest request
);

@ApiResponses(
value = {
@ApiResponse(responseCode = "201"),
@ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))),
@ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))),
}
)
@Operation(summary = "회원가입")
@PostMapping("/user/student/register")
ResponseEntity<Void> studentRegister(
@RequestBody @Valid StudentRegisterRequest studentRegisterRequest,
@ServerURL String serverURL
);

@ApiResponses(
value = {
@ApiResponse(responseCode = "204"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,25 @@
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import in.koreatech.koin.domain.user.dto.AuthTokenRequest;
import in.koreatech.koin.domain.user.dto.AuthResponse;
import in.koreatech.koin.domain.user.dto.EmailCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.NicknameCheckExistsRequest;
import in.koreatech.koin.domain.user.dto.StudentRegisterRequest;
import in.koreatech.koin.domain.user.dto.StudentResponse;
import in.koreatech.koin.domain.user.dto.StudentUpdateRequest;
import in.koreatech.koin.domain.user.dto.StudentUpdateResponse;
import in.koreatech.koin.domain.user.dto.UserLoginRequest;
import in.koreatech.koin.domain.user.dto.UserLoginResponse;
import in.koreatech.koin.domain.user.dto.UserTokenRefreshRequest;
import in.koreatech.koin.domain.user.dto.UserTokenRefreshResponse;

import in.koreatech.koin.domain.user.service.StudentService;
import in.koreatech.koin.domain.user.service.UserService;
import in.koreatech.koin.global.auth.Auth;
import in.koreatech.koin.global.host.ServerURL;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

Expand Down Expand Up @@ -97,6 +102,22 @@ public ResponseEntity<Void> checkUserEmailExist(
return ResponseEntity.ok().build();
}

@PostMapping("/user/student/register")
public ResponseEntity<Void> studentRegister(
@Valid @RequestBody StudentRegisterRequest request,
@ServerURL String serverURL) {
studentService.studentRegister(request, serverURL);
return ResponseEntity.ok().build();
}

@GetMapping(value = "/user/authenticate")
public ModelAndView authenticate(
@ModelAttribute("auth_token")
@Valid AuthTokenRequest request
) {
return studentService.authenticate(request);
}

@GetMapping("/user/check/nickname")
public ResponseEntity<Void> checkDuplicationOfNickname(
@ModelAttribute("nickname")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package in.koreatech.koin.domain.user.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*;

import com.fasterxml.jackson.databind.annotation.JsonNaming;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

@JsonNaming(value = SnakeCaseStrategy.class)
public record AuthTokenRequest(
@Schema(description = "인증토큰")
@NotBlank(message = "토큰은 필수입니다.")
String authToken
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package in.koreatech.koin.domain.user.dto;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.*;

import java.time.Clock;
import java.time.LocalDateTime;
import java.util.UUID;

import org.springframework.security.crypto.password.PasswordEncoder;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

import in.koreatech.koin.domain.user.model.Student;
import in.koreatech.koin.domain.user.model.User;
import in.koreatech.koin.domain.user.model.UserGender;
import in.koreatech.koin.domain.user.model.UserIdentity;
import in.koreatech.koin.domain.user.model.UserType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

@JsonNaming(value = SnakeCaseStrategy.class)
public record StudentRegisterRequest(
@Schema(description = "이메일", example = "koin123@koreatech.ac.kr")
@Email(message = "이메일 형식을 지켜주세요.")
@NotBlank(message = "이메일을 입력해주세요.")
String email,

@Schema(description = "이름", example = "최준호")
@Size(max = 50, message = "이름은 50자 이내여야 합니다.")
String name,

@Schema(description = " SHA 256 해시 알고리즘으로 암호화된 비밀번호", example = "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268")
@NotBlank(message = "비밀번호를 입력해주세요.")
String password,

@Schema(description = "닉네임", example = "bbo")
@Size(max = 10, message = "닉네임은 최대 10자입니다.")
String nickname,

@Schema(description = "성별(남:0, 여:1)", example = "0")
UserGender gender,

@Schema(description = "졸업 여부", example = "false")
Boolean isGraduated,

@Schema(description = "전공{기계공학부, 컴퓨터공학부, 메카트로닉스공학부, 전기전자통신공학부, 디자인공학부, 건축공학부, 화학생명공학부, 에너지신소재공학부, 산업경영학부, 고용서비스정책학과}", example = "컴퓨터공학부")
@JsonProperty("major")
String department,

@Schema(description = "학번", example = "2021136012")
@Size(min = 10, max = 10, message = "학번은 10자여야합니다.")
String studentNumber,

@Schema(description = "휴대폰 번호", example = "010-0000-0000")
@Pattern(regexp = "^[0-9]{3}-[0-9]{3,4}-[0-9]{4}", message = "전화번호 형식이 올바르지 않습니다.")
String phoneNumber
) {
public Student toStudent(PasswordEncoder passwordEncoder, Clock clock) {
User user = User.builder()
.password(passwordEncoder.encode(password))
.email(email)
.name(name)
.nickname(nickname)
.gender(gender)
.phoneNumber(phoneNumber)
.isAuthed(false)
.isDeleted(false)
.userType(UserType.STUDENT)
.authToken(UUID.randomUUID().toString())
.authExpiredAt(LocalDateTime.now(clock).plusHours(1))
.build();

return Student.builder()
.user(user)
.anonymousNickname("익명_" + (System.currentTimeMillis()))
.isGraduated(isGraduated)
.userIdentity(UserIdentity.UNDERGRADUATE)
.department(department)
.studentNumber(studentNumber)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package in.koreatech.koin.domain.user.exception;

public class StudentNumberNotValidException extends IllegalArgumentException {

private static final String DEFAULT_MESSAGE = "학생의 학번 형식이 아닙니다.";

public StudentNumberNotValidException(String message) {
super(message);
}

public static StudentNumberNotValidException withDetail(String detail) {
String message = String.format("%s %s", DEFAULT_MESSAGE, detail);
return new StudentNumberNotValidException(message);
}
}
45 changes: 45 additions & 0 deletions src/main/java/in/koreatech/koin/domain/user/model/AuthResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package in.koreatech.koin.domain.user.model;

import java.time.Clock;
import java.time.LocalDateTime;
import java.util.Optional;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.servlet.ModelAndView;

public class AuthResult {

private final Optional<User> user;
private final ApplicationEventPublisher eventPublisher;
private final Clock clock;

public AuthResult(Optional<User> user, ApplicationEventPublisher eventPublisher, Clock clock) {
this.user = user;
this.eventPublisher = eventPublisher;
this.clock = clock;
}

public ModelAndView toModelAndViewForStudent() {
return user.map(user -> {
if (user.getAuthExpiredAt().isBefore(LocalDateTime.now(clock))) {
return createErrorModelAndView("이미 만료된 토큰입니다.");
}
if (!user.getIsAuthed()) {
user.auth();
eventPublisher.publishEvent(new StudentRegisterEvent(user.getEmail()));
return createSuccessModelAndView();
}
return createErrorModelAndView("이미 인증된 사용자입니다.");
}).orElseGet(() -> createErrorModelAndView("토큰에 해당하는 사용자를 찾을 수 없습니다."));
}

private ModelAndView createErrorModelAndView(String errorMessage) {
ModelAndView modelAndView = new ModelAndView("error_config");
modelAndView.addObject("errorMessage", errorMessage);
return modelAndView;
}

private ModelAndView createSuccessModelAndView() {
return new ModelAndView("success_register_config");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,8 @@ public void update(String studentNumber, String department) {
this.studentNumber = studentNumber;
this.department = department;
}

public static Integer parseStudentNumberYear(String studentNumber) {
return Integer.parseInt(studentNumber.substring(0, 4));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package in.koreatech.koin.domain.user.model;

public record StudentEmailRequestEvent(
String email
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package in.koreatech.koin.domain.user.model;

import static org.springframework.transaction.event.TransactionPhase.AFTER_COMMIT;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;

import in.koreatech.koin.global.domain.slack.SlackClient;
import in.koreatech.koin.global.domain.slack.model.SlackNotificationFactory;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class StudentEventListener {

private final SlackClient slackClient;
private final SlackNotificationFactory slackNotificationFactory;

@TransactionalEventListener(phase = AFTER_COMMIT)
public void onStudentEmailRequest(StudentEmailRequestEvent event) {
var notification = slackNotificationFactory.generateStudentEmailVerificationRequestNotification(event.email());
slackClient.sendMessage(notification);
}

@TransactionalEventListener(phase = AFTER_COMMIT)
public void onStudentRegister(StudentRegisterEvent event) {
var notification = slackNotificationFactory.generateStudentRegisterCompleteNotification(event.email());
slackClient.sendMessage(notification);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package in.koreatech.koin.domain.user.model;

public record StudentRegisterEvent(
String email
) {

}
16 changes: 11 additions & 5 deletions src/main/java/in/koreatech/koin/domain/user/model/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
import org.hibernate.annotations.Where;
import org.springframework.security.crypto.password.PasswordEncoder;

import in.koreatech.koin.global.config.LocalDateTimeAttributeConverter;
import in.koreatech.koin.global.domain.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
Expand Down Expand Up @@ -85,25 +87,25 @@ public class User extends BaseEntity {
@Column(name = "auth_token")
private String authToken;

@Size(max = 255)
@Column(name = "auth_expired_at")
private String authExpiredAt;
@Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime authExpiredAt;

@Size(max = 255)
@Column(name = "reset_token")
private String resetToken;

@Size(max = 255)
@Column(name = "reset_expired_at")
private String resetExpiredAt;
@Convert(converter = LocalDateTimeAttributeConverter.class)
private LocalDateTime resetExpiredAt;

@Column(name = "device_token", nullable = true)
private String deviceToken;

@Builder
private User(String password, String nickname, String name, String phoneNumber, UserType userType,
String email, UserGender gender, Boolean isAuthed, LocalDateTime lastLoggedAt, String profileImageUrl,
Boolean isDeleted, String authToken, String authExpiredAt, String resetToken, String resetExpiredAt,
Boolean isDeleted, String authToken, LocalDateTime authExpiredAt, String resetToken, LocalDateTime resetExpiredAt,
String deviceToken) {
this.password = password;
this.nickname = nickname;
Expand Down Expand Up @@ -149,4 +151,8 @@ public void update(String nickname, String name, String phoneNumber, UserGender
this.phoneNumber = phoneNumber;
this.gender = gender;
}

public void auth() {
this.isAuthed = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ public interface UserRepository extends Repository<User, Long> {

Optional<User> findByNickname(String nickname);

Optional<User> findByAuthToken(String authToken);

default User getByEmail(String email) {
return findByEmail(email)
.orElseThrow(() -> UserNotFoundException.withDetail("email: " + email));
Expand Down
Loading

0 comments on commit 50b82c7

Please sign in to comment.