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
@@ -1,16 +1,19 @@
package busanVibe.busan.domain.user.controller

import busanVibe.busan.domain.user.data.dto.login.TokenResponseDto
import busanVibe.busan.domain.user.data.dto.login.UserLoginRequestDTO
import busanVibe.busan.domain.user.data.dto.login.UserLoginResponseDTO
import busanVibe.busan.domain.user.service.UserCommandService
import busanVibe.busan.domain.user.util.LoginRedirectUtil
import busanVibe.busan.global.apiPayload.exception.ApiResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
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.RequestParam
import org.springframework.web.bind.annotation.RestController
Expand All @@ -30,12 +33,34 @@ class UserAuthController (
return ResponseEntity.status(HttpStatus.FOUND).headers(redirectHeader).build()
}

@PostMapping("/guest/login")
@PostMapping("/login/guest")
@Operation(summary = "게스트 로그인 API", description = "1회용 계정 로그인입니다. 재로그인이 불가능합니다.")
fun guestLogin(): ApiResponse<TokenResponseDto>{
val userResponse: UserLoginResponseDTO.LoginDto = userCommandService.guestLogin()
return ApiResponse.onSuccess(userResponse.tokenResponseDTO)
}

@PostMapping("/login/local")
@Operation(summary = "로컬 로그인 API")
fun localLogin(@Valid @RequestBody requestDTO: UserLoginRequestDTO.LocalLoginDto): ApiResponse<TokenResponseDto>{
val userResponse = userCommandService.localLogin(requestDTO)
return ApiResponse.onSuccess(userResponse.tokenResponseDTO)
}

@PostMapping("/signup/local")
@Operation(summary = "로컬 회원가입 API",
description =
"""
email: 이메일 형식 안 지킬 시 오류 발생
password: 4~20글자. 형식 자유.
"""
)
fun localSignUp(@Valid @RequestBody requestDTO: UserLoginRequestDTO.LocalSignUpDto): ResponseEntity<Void>{
userCommandService.localSignUp(requestDTO)
return ResponseEntity.ok().build()
}




}
14 changes: 12 additions & 2 deletions src/main/kotlin/busanVibe/busan/domain/user/data/User.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package busanVibe.busan.domain.user.data

import busanVibe.busan.domain.common.BaseEntity
import busanVibe.busan.domain.user.enums.LoginType
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
Expand Down Expand Up @@ -35,15 +38,22 @@ class User(
@Column(nullable = true, length = 255)
var profileImageUrl: String?,

@Column(nullable = true, length = 255)
var passwordHash: String? = null,

@Column(nullable = false)
@Enumerated(EnumType.STRING)
val loginType: LoginType


) : BaseEntity(), UserDetails {
) : BaseEntity(), UserDetails {

override fun getAuthorities(): Collection<GrantedAuthority?>? {
return arrayListOf(SimpleGrantedAuthority("ROLE_USER"))
}

override fun getPassword(): String? {
return null;
return passwordHash.toString();
}

override fun getUsername(): String? {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package busanVibe.busan.domain.user.data.dto.login

import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size

class UserLoginRequestDTO {

data class LocalSignUpDto(
@field:NotBlank(message = "이메일은 필수 입력값입니다.")
@field:Email(message = "올바르지 않은 이메일 형식입니다.")
val email: String,

@field:NotBlank(message = "비밀번호는 필수입니다.")
@field:Size(min = 4, max = 20, message = "비밀번호는 4자 이상 20자 이하로 입력해야 합니다.")
val password: String
)

data class LocalLoginDto(
@field:NotBlank(message = "이메일은 필수 입력값입니다.")
@field:Email(message = "올바르지 않은 이메일 형식입니다.")
val email: String,

@field:NotBlank(message = "비밀번호는 필수입니다.")
val password: String

)

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package busanVibe.busan.domain.user.data.dto.login

class UserLoginResponseDTO {

companion object class LoginDto(
data class LoginDto(
val id: Long?,
val tokenResponseDTO: TokenResponseDto,
val email: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package busanVibe.busan.domain.user.enums

enum class LoginType {

GUEST,
LOCAL,
KAKAO

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ interface UserRepository : JpaRepository<User, Long> {
fun findByEmail(email: String?): Optional<User>

fun findUsersByIdIn(ids: List<Long>): List<User>

fun existsByEmail(email: String): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@ import busanVibe.busan.domain.user.converter.UserConverter
import busanVibe.busan.domain.user.data.User
import busanVibe.busan.domain.user.data.dto.login.KaKaoUserInfoResponseDTO
import busanVibe.busan.domain.user.data.dto.login.KakaoTokenResponseDTO
import busanVibe.busan.domain.user.data.dto.login.UserLoginRequestDTO
import busanVibe.busan.domain.user.data.dto.login.UserLoginResponseDTO
import busanVibe.busan.domain.user.enums.LoginType
import busanVibe.busan.domain.user.repository.UserRepository
import busanVibe.busan.global.apiPayload.code.status.ErrorStatus
import busanVibe.busan.global.apiPayload.exception.GeneralException
import busanVibe.busan.global.apiPayload.exception.handler.ExceptionHandler
import busanVibe.busan.global.config.security.JwtTokenProvider
import io.lettuce.core.KillArgs.Builder.user
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatusCode
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
Expand All @@ -29,7 +34,8 @@ class UserCommandService(
private val kakaoUserInfoWebClient: WebClient,
private val userRepository: UserRepository,
private val userConverter: UserConverter,
private val jwtTokenProvider: JwtTokenProvider
private val jwtTokenProvider: JwtTokenProvider,
private val passwordEncoder: PasswordEncoder
) {

@Value("\${spring.kakao.client-id}")
Expand All @@ -46,10 +52,73 @@ class UserCommandService(
val nickname = UUID.randomUUID().toString().substring(0, 7)
val profileImageUrl: String? = null

return isNewUser(email, nickname, profileImageUrl)
return isNewUser(email, nickname, profileImageUrl, LoginType.GUEST)
}

/**
* 로컬 로그인
*/
fun localLogin(requestDTO: UserLoginRequestDTO.LocalLoginDto): UserLoginResponseDTO.LoginDto{

// request body 로부터 정보 가져옴
val email = requestDTO.email
val password = requestDTO.password

// 유저 조회
val user = userRepository.findByEmail(email)
.orElseThrow { ExceptionHandler(ErrorStatus.USER_NOT_FOUND) }

// 비밀번호 검증 - 틀리면 예외 발생
if(!passwordEncoder.matches(password, user.passwordHash)){
throw GeneralException(ErrorStatus.LOGIN_INVALID_PASSWORD)
}

// 토큰 생성 및 반환
log.info("[LOCAL LOGIN] email : {}", email)
val token = jwtTokenProvider.createToken(user)
return userConverter.toLoginDto(user, false, token)
}

/**
* 로컬 회원가입
*/
fun localSignUp(requestDTO: UserLoginRequestDTO.LocalSignUpDto) {

// 요청받은 값들
val email = requestDTO.email
val password = requestDTO.password

// 이메일 중복 검사
if(userRepository.existsByEmail(email)) // 이메일 이미 존재하면 예외 발생
throw GeneralException(ErrorStatus.SIGNUP_EMAIL_EXISTS)

// 이메일 앞부분 추출하여 nickname 생성
lateinit var nickname: String
try{
nickname = email.substring(0, email.indexOf("@"))
}catch (e: Exception) {
throw GeneralException(ErrorStatus.INVALID_EMAIL_STYLE)
}
val profileImageUrl: String? = null

// password encode
val encodedPassword = passwordEncoder.encode(password)

// 유저 저장
userRepository.save(User(
email = email,
nickname = nickname,
profileImageUrl = profileImageUrl,
loginType = LoginType.LOCAL,
passwordHash = encodedPassword
))


}

/**
* 카카오 로그인
*/
fun loginOrRegisterByKakao(code: String): UserLoginResponseDTO.LoginDto {
val token: KakaoTokenResponseDTO = getKakaoToken(code)
val userInfo = getUserInfo(token.accessToken)
Expand All @@ -58,7 +127,7 @@ class UserCommandService(
val nickname = userInfo.kakaoAccount.profile?.nickName ?: "\uCE74\uCE74\uC624 \uC0AC\uC6A9\uC790"
val profileImageUrl = userInfo.kakaoAccount.profile?.profileImageUrl ?: ""

return isNewUser(email, nickname, profileImageUrl)
return isNewUser(email, nickname, profileImageUrl, LoginType.KAKAO)
}

private fun getKakaoToken(code: String): KakaoTokenResponseDTO {
Expand Down Expand Up @@ -102,20 +171,22 @@ class UserCommandService(
email: String,
nickname: String,
profileImageUrl: String?,
loginType: LoginType
): UserLoginResponseDTO.LoginDto {
val user = userRepository.findByEmail(email)

return user.map {
log.info("기존 유저 로그인: {}", email)
log.info("[{} LOGIN] 기존 유저 로그인: {}", loginType.name, email)
val token = jwtTokenProvider.createToken(it)
userConverter.toLoginDto(it, false, token)
}.orElseGet {
log.info("신규 유저 회원가입: {}", email)
log.info("[{} LOGIN] 신규 유저 회원가입: {}", loginType.name, email)

val newUser = User(
email = email,
nickname = nickname,
profileImageUrl = profileImageUrl
profileImageUrl = profileImageUrl,
loginType = loginType
)

userRepository.save(newUser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ enum class ErrorStatus(
// 사용자 관련 에러
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER4004", "유저를 찾을 수 없습니다."),

// 로그인 관련
SIGNUP_EMAIL_EXISTS(HttpStatus.BAD_REQUEST, "LOGIN4001", "이미 존재하는 이메일입니다."),
INVALID_EMAIL_STYLE(HttpStatus.BAD_REQUEST, "LOGIN4002", "이메일 형식이 올바르지 않습니다."),
LOGIN_INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "LOGIN4003", "비밀번호가 올바르지 않습니다."),

// 명소 관련 에러
PLACE_NOT_FOUND(HttpStatus.NOT_FOUND, "PLACE4004", "명소를 찾을 수 없습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ class SecurityConfig {
"/v3/api-docs/**",
"/users/oauth/kakao",
"/ws-chat/**",
"/users/guest/login"
"/users/guest/login",
"/users/login/**",
"/users/signup/**"
).permitAll()
.anyRequest().authenticated()
}
Expand Down