Skip to content

Commit 9cb662f

Browse files
authored
Merge pull request #12 from hackathon-soa/feat/#2
feat : 회원가입 API 구현
2 parents 3b30a24 + d195265 commit 9cb662f

17 files changed

Lines changed: 272 additions & 21 deletions

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ repositories {
2525

2626
dependencies {
2727
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
28-
// implementation 'org.springframework.boot:spring-boot-starter-security'
28+
implementation 'org.springframework.boot:spring-boot-starter-security'
2929
implementation 'org.springframework.boot:spring-boot-starter-validation'
3030
implementation 'org.springframework.boot:spring-boot-starter-web'
3131
compileOnly 'org.projectlombok:lombok'

src/main/java/hackathon/soa/common/apiPayload/code/status/ErrorStatus.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ public enum ErrorStatus implements BaseErrorCode {
1717
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
1818
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),
1919

20+
// 사용자 관련
21+
ID_DUPLICATED(HttpStatus.CONFLICT, "AUTH4002", "중복된 아이디입니다."),
2022
;
2123

2224

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package hackathon.soa.common.apiPayload.exception;
2+
3+
import hackathon.soa.common.apiPayload.code.BaseErrorCode;
4+
5+
6+
public class AuthHandler extends GeneralException {
7+
public AuthHandler(BaseErrorCode errorCode) {
8+
super(errorCode);
9+
}
10+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package hackathon.soa.config;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
7+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
8+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
9+
import org.springframework.security.crypto.password.PasswordEncoder;
10+
import org.springframework.security.web.SecurityFilterChain;
11+
12+
@Configuration
13+
@EnableWebSecurity
14+
@RequiredArgsConstructor
15+
public class SecurityConfig {
16+
17+
@Bean
18+
public PasswordEncoder passwordEncoder() {
19+
return new BCryptPasswordEncoder();
20+
}
21+
22+
23+
@Bean
24+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
25+
http
26+
.authorizeHttpRequests(authz -> authz
27+
// TODO 임시적으로 모든 경로 허용
28+
.requestMatchers("/**").permitAll()
29+
// .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // Swagger 인증 없이 허용
30+
// .requestMatchers("/auth/**").permitAll() // 회원가입 & 로그인 인증 없이 허용
31+
// .anyRequest().authenticated() // 그 외의 경로는 인증 요구
32+
)
33+
.csrf(csrf -> csrf.disable()); // CSRF 보호 비활성화
34+
35+
return http.build();
36+
}
37+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package hackathon.soa.domain.auth;
2+
3+
import hackathon.soa.common.apiPayload.ApiResponse;
4+
import hackathon.soa.domain.auth.dto.AuthRequestDTO;
5+
import hackathon.soa.domain.auth.dto.AuthResponseDTO;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import jakarta.validation.Valid;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.web.bind.annotation.*;
10+
11+
@RestController
12+
@RequestMapping("/api/auth")
13+
@RequiredArgsConstructor
14+
public class AuthController {
15+
16+
private final AuthService authService;
17+
18+
@PostMapping("/signup")
19+
@Operation(
20+
summary = "회원가입",
21+
description = "사용자 정보를 받아 새로운 계정을 생성합니다."
22+
)
23+
public ApiResponse<AuthResponseDTO.SignupResponseDTO> register(
24+
@RequestBody @Valid AuthRequestDTO.SignupRequestDTO request
25+
) {
26+
AuthResponseDTO.SignupResponseDTO result = authService.register(request);
27+
return ApiResponse.onSuccess(result);
28+
}
29+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package hackathon.soa.domain.auth;
2+
3+
import hackathon.soa.domain.auth.dto.AuthResponseDTO;
4+
import hackathon.soa.entity.Member;
5+
6+
public class AuthConverter {
7+
public static AuthResponseDTO.SignupResponseDTO toSignUpResponseDTO(Member savedMember) {
8+
return AuthResponseDTO.SignupResponseDTO.builder()
9+
.userId(savedMember.getId())
10+
.appId(savedMember.getAppId())
11+
.build()
12+
;
13+
}
14+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package hackathon.soa.domain.auth;
2+
3+
import hackathon.soa.common.apiPayload.code.status.ErrorStatus;
4+
import hackathon.soa.common.apiPayload.exception.AuthHandler;
5+
import hackathon.soa.domain.auth.dto.AuthRequestDTO;
6+
import hackathon.soa.domain.auth.dto.AuthResponseDTO;
7+
import hackathon.soa.domain.member.MemberConverter;
8+
import hackathon.soa.domain.member.MemberRepository;
9+
import hackathon.soa.entity.Member;
10+
import jakarta.validation.Valid;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.security.crypto.password.PasswordEncoder;
13+
import org.springframework.stereotype.Service;
14+
import org.springframework.transaction.annotation.Transactional;
15+
16+
@Service
17+
@Transactional
18+
@RequiredArgsConstructor
19+
public class AuthService {
20+
21+
private final MemberRepository memberRepository;
22+
private final PasswordEncoder passwordEncoder;
23+
24+
public AuthResponseDTO.SignupResponseDTO register(AuthRequestDTO.SignupRequestDTO request) {
25+
// 1) 아이디 중복 체크
26+
if (memberRepository.existsByAppId(request.getAppId())) {
27+
throw new AuthHandler(ErrorStatus.ID_DUPLICATED);
28+
}
29+
30+
31+
// 2) Entity 변환 Converter 작업 (비밀번호 암호화)
32+
Member member = MemberConverter.toMember(request, passwordEncoder);
33+
34+
35+
// 3) Rdpository 저장
36+
Member savedMember = memberRepository.save(member);
37+
38+
39+
// 4) ResponseDTO 변환 Converter 작업
40+
return AuthConverter.toSignUpResponseDTO(savedMember);
41+
}
42+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package hackathon.soa.domain.auth.dto;
2+
3+
import jakarta.validation.constraints.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
import lombok.NoArgsConstructor;
8+
9+
public class AuthRequestDTO {
10+
@Builder
11+
@Getter
12+
@NoArgsConstructor
13+
@AllArgsConstructor
14+
public static class SignupRequestDTO {
15+
16+
@NotBlank(message = "아이디는 필수입니다")
17+
@Size(min = 4, max = 20, message = "아이디는 4~20자 사이여야 합니다")
18+
@Pattern(regexp = "^[a-zA-Z0-9]+$", message = "아이디는 영문자와 숫자만 가능합니다")
19+
private String appId;
20+
21+
@NotBlank(message = "비밀번호는 필수입니다")
22+
@Size(min = 8, max = 20, message = "비밀번호는 8~20자 사이여야 합니다")
23+
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
24+
message = "비밀번호는 영문자, 숫자, 특수문자를 포함해야 합니다")
25+
private String password;
26+
27+
@NotBlank(message = "이름은 필수입니다")
28+
@Size(min = 2, max = 10, message = "이름은 2~10자 사이여야 합니다")
29+
private String name;
30+
31+
@NotBlank(message = "닉네임은 필수입니다")
32+
@Size(min = 2, max = 15, message = "닉네임은 2~15자 사이여야 합니다")
33+
private String nickname;
34+
35+
@NotBlank(message = "전화번호는 필수입니다")
36+
@Pattern(regexp = "^01[0-9]-\\d{4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다 (ex. 010-1234-5678)")
37+
private String phoneNumber;
38+
39+
@NotBlank(message = "생년월일은 필수입니다")
40+
@Pattern(regexp = "^\\d{8}$", message = "생년월일 형식이 올바르지 않습니다 (ex. 20010528)")
41+
private String birth;
42+
43+
@NotBlank(message = "성별은 필수입니다")
44+
@Pattern(regexp = "^(남성|여성)$", message = "성별은 '남성' 또는 '여성'이어야 합니다")
45+
private String gender;
46+
47+
@NotBlank(message = "장애 유형은 필수입니다")
48+
@Pattern(regexp = "^\\[.*\\]$", message = "장애 유형은 JSON 배열 형식이어야 합니다")
49+
private String disabilityType;
50+
51+
@NotNull(message = "프로필 이미지는 필수입니다")
52+
private Integer profileImage;
53+
}
54+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package hackathon.soa.domain.auth.dto;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
8+
public class AuthResponseDTO {
9+
@Builder
10+
@Getter
11+
@NoArgsConstructor
12+
@AllArgsConstructor
13+
public static class SignupResponseDTO {
14+
private Long userId;
15+
private String appId;
16+
}
17+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package hackathon.soa.domain.member;
2+
3+
import hackathon.soa.domain.auth.dto.AuthRequestDTO;
4+
import hackathon.soa.entity.Gender;
5+
import hackathon.soa.entity.Member;
6+
import org.springframework.security.crypto.password.PasswordEncoder;
7+
8+
public class MemberConverter {
9+
public static Member toMember(AuthRequestDTO.SignupRequestDTO request, PasswordEncoder passwordEncoder) {
10+
// 성별 변환
11+
Gender gender = "남성".equals(request.getGender()) ? Gender.MALE : Gender.FEMALE;
12+
13+
// 프로필 이미지 변환
14+
String profileImageUrl;
15+
switch (request.getProfileImage()) {
16+
case 1:
17+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit1.png";
18+
break;
19+
case 2:
20+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit2.png";
21+
break;
22+
case 3:
23+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit3.png";
24+
break;
25+
case 4:
26+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit4.png";
27+
break;
28+
case 5:
29+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit5.png";
30+
break;
31+
default:
32+
profileImageUrl = "https://umc-hack-demo-bucket.s3.ap-northeast-2.amazonaws.com/rabbit1.png";
33+
}
34+
35+
return Member.builder()
36+
.appId(request.getAppId())
37+
.password(passwordEncoder.encode(request.getPassword()))
38+
.name(request.getName())
39+
.nickname(request.getNickname())
40+
.phoneNumber(request.getPhoneNumber())
41+
.birth(request.getBirth())
42+
.gender(gender)
43+
.disabilityType(request.getDisabilityType())
44+
.profileImageUrl(profileImageUrl)
45+
.mileage(0)
46+
.build();
47+
}
48+
49+
}

0 commit comments

Comments
 (0)