-
Notifications
You must be signed in to change notification settings - Fork 3
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] 사용자 가입승인 기능 구현 #26
Changes from 9 commits
4e3a3ea
5d7326e
82c10bc
99ebf13
97059e4
41cd12e
4715204
a44386f
d1e4d20
2c28a09
a80f8a2
c82cce5
32ec4f7
2d8e04e
7851d34
24fbc92
5c6d9b8
41909aa
2ee34cb
3765657
d888f83
848ce30
979198c
e2ff106
9455bed
e0ac706
e21ab02
382d032
22cfabc
720d726
6791477
a5a25d7
49858f2
c0c760d
59b7b02
01e65fa
e97334b
e330442
fd06fb4
44cf198
1f12dd4
cee2975
3f66cc0
ca15823
c7c8031
fb58e73
b93636e
f8863e6
db0a386
7b88754
c107f78
18a237c
a5649ef
be4f767
cbdc2bc
9e1b67c
0cc447b
ff432ae
4f3e5e6
8996a0b
7f57a62
5e16349
4d40bbb
c78da78
a47f35b
45b8c68
89b64f1
4984314
8abedde
712d6ad
3cf7a40
f2ff33a
a8dbf4d
8dc959d
5d6de40
98de95f
6873fb3
6756816
905d212
361ad30
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 |
---|---|---|
@@ -1,7 +1,33 @@ | ||
package wanted.media.user.config; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
|
||
@EnableWebSecurity | ||
@Configuration | ||
public class SecurityConfig { | ||
// 비밀번호 암호화 기능 | ||
@Bean | ||
public BCryptPasswordEncoder passwordEncoder() { | ||
return new BCryptPasswordEncoder(); | ||
} | ||
|
||
// 임시 설정 | ||
@Bean | ||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
http | ||
.authorizeRequests(authorizeRequests -> | ||
authorizeRequests | ||
.anyRequest().permitAll() // 모든 요청 허용 | ||
) | ||
.csrf().disable() // CSRF 보호 비활성화 (테스트 목적으로만 사용) | ||
.formLogin().disable() // 로그인 폼 비활성화 | ||
.httpBasic().disable(); // HTTP Basic 인증 비활성화 | ||
|
||
return http.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,41 @@ | ||
package wanted.media.user.controller; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.validation.annotation.Validated; | ||
import org.springframework.web.bind.annotation.PostMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
import wanted.media.user.dto.SignUpRequest; | ||
import wanted.media.user.dto.SignUpResponse; | ||
import wanted.media.user.dto.VerifyRequest; | ||
import wanted.media.user.dto.VerifyResponse; | ||
import wanted.media.user.service.UserService; | ||
|
||
@RestController | ||
@RequestMapping("/user") | ||
@RequestMapping("/api/user") | ||
@RequiredArgsConstructor | ||
public class UserController { | ||
|
||
private final UserService userService; | ||
|
||
// 회원가입 API | ||
@PostMapping("/sign-up") | ||
public ResponseEntity<SignUpResponse> signUp(@Validated @RequestBody SignUpRequest request) { | ||
SignUpResponse response = userService.signUp(request); | ||
return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
} | ||
|
||
/* | ||
* 가입승인 API | ||
* 회원등급 (normal -> premium) | ||
* */ | ||
@PostMapping("/approve") | ||
public ResponseEntity<VerifyResponse> approveSignUp(@RequestBody VerifyRequest request) { | ||
VerifyResponse response = userService.approveSignUp(request); | ||
return ResponseEntity.status(HttpStatus.CREATED).body(response); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
import jakarta.persistence.*; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import org.springframework.data.annotation.CreatedDate; | ||
|
@@ -12,6 +13,7 @@ | |
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Getter | ||
@Builder | ||
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. Builder는 어디에 사용되나요? 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.
Code 객체 생성에 사용됩니다. 인증코드 관련 정보를 설정하기 위해서 사용했습니다. |
||
@Entity | ||
@Table(name = "codes") | ||
public class Code { | ||
|
@@ -21,7 +23,7 @@ public class Code { | |
@Column(nullable = false) | ||
private Long codeId; | ||
|
||
@OneToOne | ||
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "user_id") | ||
private User user; | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package wanted.media.user.dto; | ||
|
||
import jakarta.validation.constraints.Email; | ||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class SignUpRequest { | ||
@NotBlank | ||
@Size(max = 50) | ||
private String account; | ||
|
||
@NotBlank | ||
@Size(max = 50) | ||
private String email; | ||
|
||
@NotBlank | ||
@Size(min = 10, max = 200, message = "비밀번호는 최소 10자리 이상으로 설정해주세요.") | ||
private String password; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
package wanted.media.user.dto; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Getter | ||
public class SignUpResponse { | ||
private String message; | ||
private UserInfoDto userInfoDto; // 사용자 정보 DTO | ||
private String authCode; // 사용자 인증코드 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package wanted.media.user.dto; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import wanted.media.user.domain.Grade; | ||
|
||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Getter | ||
public class UserInfoDto { | ||
private String account; | ||
private String email; | ||
private Grade grade; // 현재 회원등급 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package wanted.media.user.dto; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
import jakarta.validation.constraints.Size; | ||
import lombok.Data; | ||
|
||
@Data | ||
public class VerifyRequest { | ||
@NotBlank | ||
@Size(max = 50) | ||
private String account; | ||
|
||
@NotBlank | ||
@Size(min = 10, max = 200) | ||
private String password; | ||
|
||
@NotBlank | ||
@Size(max = 10) | ||
private String inputCode; //사용자 입력 인증코드 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package wanted.media.user.dto; | ||
|
||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor | ||
@AllArgsConstructor | ||
@Getter | ||
public class VerifyResponse { | ||
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. Response, Request, Dto 전부 record 클래스로 변경하면 깔끔해지겠네요 ! |
||
private String message; | ||
private UserInfoDto userInfo; //사용자 정보 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package wanted.media.user.repository; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import wanted.media.user.domain.Code; | ||
import wanted.media.user.domain.User; | ||
|
||
import java.util.Optional; | ||
|
||
public interface CodeRepository extends JpaRepository<Code, Long> { | ||
//인증코드 검증 | ||
Optional<Code> findByUserAndAuthCode(User user, String authCode); | ||
|
||
//사용자가 발급받은 인증코드 삭제 | ||
void deleteByUser(User user); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,24 @@ | ||
package wanted.media.user.repository; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
import org.springframework.data.jpa.repository.Modifying; | ||
import org.springframework.data.jpa.repository.Query; | ||
import org.springframework.data.repository.query.Param; | ||
import wanted.media.user.domain.Grade; | ||
import wanted.media.user.domain.User; | ||
|
||
import java.util.Optional; | ||
import java.util.UUID; | ||
|
||
public interface UserRepository extends JpaRepository<User, UUID> { | ||
// 사용자 계정으로 회원 조회 | ||
Optional<User> findByAccount(String account); | ||
|
||
// 사용자 이메일로 회원 조회 | ||
Optional<User> findByEmail(String email); | ||
|
||
// 가입인증 회원 등급 변경 | ||
@Modifying | ||
@Query("UPDATE User u SET u.grade = :grade WHERE u.account = :account") | ||
void updateUserGrade(@Param("account") String account, @Param("grade") Grade grade); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package wanted.media.user.service; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Random; | ||
|
||
@Component | ||
public class GenerateCode { | ||
private int codeLength = 6; //6자리 코드 | ||
private final char[] characterTable = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', | ||
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', | ||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}; | ||
|
||
public String codeGenerate() { | ||
Random random = new Random(System.currentTimeMillis()); | ||
int tableLength = characterTable.length; | ||
StringBuilder code = new StringBuilder(); | ||
|
||
for (int i = 0; i < codeLength; i++) { | ||
code.append(characterTable[random.nextInt(tableLength)]); | ||
} | ||
return code.toString(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,89 @@ | ||
package wanted.media.user.service; | ||
|
||
import jakarta.transaction.Transactional; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||
import org.springframework.stereotype.Service; | ||
import wanted.media.user.domain.Code; | ||
import wanted.media.user.domain.Grade; | ||
import wanted.media.user.domain.User; | ||
import wanted.media.user.dto.*; | ||
import wanted.media.user.repository.CodeRepository; | ||
import wanted.media.user.repository.UserRepository; | ||
|
||
import java.time.LocalDateTime; | ||
|
||
@Service | ||
@RequiredArgsConstructor | ||
@Transactional | ||
public class UserService { | ||
|
||
private final UserRepository userRepository; | ||
private final CodeRepository codeRepository; | ||
private final BCryptPasswordEncoder passwordEncoder; | ||
private final UserValidator userValidator; | ||
private final GenerateCode generateCode; | ||
|
||
|
||
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. 20,27라인 공백 삭제해주세용 |
||
//회원가입 | ||
public SignUpResponse signUp(SignUpRequest request) { | ||
// 1. 사용자 입력내용 검증 | ||
userValidator.validateRequest(request); | ||
// 2. 비밀번호 암호화 | ||
String encodedPassword = passwordEncoder.encode(request.getPassword()); | ||
// 3. 인증코드 생성 | ||
String verificationCode = generateCode.codeGenerate(); | ||
// 4. User 객체 생성 | ||
User user = User.builder() | ||
.account(request.getAccount()) | ||
.email(request.getEmail()) | ||
.password(encodedPassword) | ||
.grade(Grade.NORMAL_USER) | ||
.build(); | ||
// 5. 사용자 db 저장 | ||
userRepository.save(user); | ||
// 6. Code 객체 생성 | ||
Code code = Code.builder() | ||
.user(user) | ||
.authCode(verificationCode) | ||
.createdTime(LocalDateTime.now()) | ||
.build(); | ||
// 7. 인증코드 db 저장 | ||
codeRepository.save(code); | ||
// 8. UserInfoDto 생성 | ||
UserInfoDto userInfoDto = new UserInfoDto(user.getAccount(), user.getEmail(), user.getGrade()); | ||
// 9. SignUpResponse 생성 | ||
SignUpResponse signUpResponse = new SignUpResponse("회원가입이 성공적으로 완료됐습니다.", userInfoDto, verificationCode); | ||
|
||
return signUpResponse; | ||
} | ||
|
||
//가입승인 | ||
public VerifyResponse approveSignUp(VerifyRequest verifyRequest) { | ||
// 1. account로 사용자 조회 | ||
User user = userRepository.findByAccount(verifyRequest.getAccount()) | ||
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); | ||
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. Exception은 해당 에러와 맞춰서 내야 할 것 같습니다 사용자를 찾을 수 없는 에러라면 NoFoundException이 맞겠죵? |
||
// 2. 비밀번호 검증 | ||
if (!passwordEncoder.matches(verifyRequest.getPassword(), user.getPassword())) { | ||
throw new RuntimeException("비밀번호가 일치하지 않습니다."); | ||
} | ||
// 3. 사용자 인증코드 검증 | ||
Code code = codeRepository.findByUserAndAuthCode(user, verifyRequest.getInputCode()) | ||
.orElseThrow(() -> new RuntimeException("인증코드가 일치하지 않습니다.")); | ||
// 4. 인증코드 유효성 검증 (유효시간 15분) | ||
if (code.getCreatedTime().plusMinutes(15).isBefore(LocalDateTime.now())) { | ||
throw new RuntimeException("만료된 인증코드입니다."); | ||
} | ||
// 5. 인증 완료 -> 회원 등급 변경 (normal -> premium) | ||
userRepository.updateUserGrade(user.getAccount(), Grade.PREMIUM_USER); | ||
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. 여기도 JPA 변경감지 기능을 사용하셔야 할 것 같아요 ~ |
||
// 6. 인증 완료 회원 인증코드 삭제 | ||
codeRepository.deleteByUser(user); | ||
// 7. 변경된 사용자정보 다시 조회 | ||
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. 변경된 사용자 정보는 왜 다시 조회해야 하나요?👀 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.
회원등급을 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. 이미 해당 메서드 내에 user가 있어서 (메서드 시작 부분에 findByAccount로 불러온 User 객체) 레포지토리에서 새로 가져오는 것이 아닌 해당 user를 그대로 반환하시면 될 것 같습니당~ |
||
User updateUserInfo = userRepository.findByAccount(user.getAccount()) | ||
.orElseThrow(() -> new RuntimeException("사용자를 찾을 수 없습니다.")); | ||
|
||
return new VerifyResponse("인증이 성공적으로 완료되었습니다!", | ||
new UserInfoDto(updateUserInfo.getAccount(), updateUserInfo.getEmail(), updateUserInfo.getGrade())); | ||
} | ||
|
||
} |
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.
url은 "/api/users" 복수형으로 수정해야할 것 같습니다.