이번 포스팅에서는 저번 포스팅에 이어서 회원가입 및 로그인 기능을 완성시켜보도록 하겠습니다.
① user 패키지 하위로, profile 패키지를 생성하고 이 패키지 하위로, Profile, ProfileRepository, ProfileService과 dto > GetS3Res를 추가한다.
② Profile에 아래의 내용을 입력한다.
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Profile extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long profileId;
private String profileUrl; // 프로필 사진 URL
private String profileFileName; // 프로필 사진명
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
public void setUser(User user){
this.user = user;
}
}
③ ProfileRepository에는 아래의 내용을 입력한다.
public interface ProfileRepository extends JpaRepository<Profile, Long> {
@Query("select p from Profile p where p.user.id = :userId")
Optional<Profile> findProfileById(@Param("userId") Long userId);
@Modifying
@Query("delete from Profile p where p.user.id = :userId")
void deleteProfileById(@Param("userId") Long userId);
}
④ ProfileService에는 아래의 내용을 입력한다.
@Service
@RequiredArgsConstructor
public class ProfileService {
private final ProfileRepository profileRepository;
private final S3Service s3Service;
@Transactional
public void saveProfile(GetS3Res getS3Res, User user){
Profile profile;
if(getS3Res.getImgUrl() != null) {
profile = Profile.builder()
.profileUrl(getS3Res.getImgUrl())
.profileFileName(getS3Res.getFileName())
.user(user)
.build();
profileRepository.save(profile);
}
}
@Transactional
public void deleteProfile(Profile profile) {
s3Service.deleteFile(profile.getProfileFileName());
}
@Transactional
public void deleteProfileById(Long memberId) {
profileRepository.deleteProfileById(memberId);
}
}
① user 패키지 하위로 dto 패키지를 추가하고, 그 안에 PostUserReq, PostUserRes, PostLoginReq, PostLoginRes를 추가한다.
② PostLoginReq에 아래의 내용을 입력한다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class PostLoginReq {
private String uid;
private String email;
private String password;
}
③ PostLoginRes에 아래의 내용을 입력한다.
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PostLoginRes {
private Long userId;
private String accessToken;
private String refreshToken;
public PostLoginRes(User user, Token token) {
this.userId = user.getId();
this.accessToken = token.getAccessToken();
this.refreshToken = token.getRefreshToken();
}
}
④ PostUserReq에 아래의 내용을 입력한다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PostUserReq {
private String nickName;
private String email;
private String password;
private String passwordChk; // 비밀번호 확인
}
⑤ PostUserRes에는 아래의 내용을 입력한다.
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class PostUserRes {
private Long userId;
private String nickName;
public PostUserRes(User user){
this.userId = user.getId();
this.nickName = user.getNickName();
}
}
UserController에 아래의 내용을 입력한다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
private final JwtService jwtService;
/**
* 회원 가입
*/
@PostMapping("")
public BaseResponse<PostUserRes> createUser(@RequestBody PostUserReq postUserReq) {
try {
return new BaseResponse<>(userService.createUser(postUserReq));
} catch (BaseException exception) {
return new BaseResponse<>((exception.getStatus()));
}
}
/**
* 로그인
*/
@PostMapping("/log-in")
public BaseResponse<PostLoginRes> loginUser(@RequestBody PostLoginReq postLoginReq) {
try {
return new BaseResponse<>(userService.login(postLoginReq));
} catch (BaseException exception) {
return new BaseResponse<>(exception.getStatus());
}
}
}
@EnableTransactionManagement
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final UtilService utilService;
private final TokenRepository tokenRepository;
private final JwtProvider jwtProvider;
private final JwtService jwtService;
private final RedisTemplate redisTemplate;
/**
* 유저 생성 후 DB에 저장(회원 가입) with JWT
*/
@Transactional
public PostUserRes createUser(PostUserReq postUserReq) throws BaseException {
if(userRepository.findByEmailCount(postUserReq.getEmail()) >= 1) {
throw new BaseException(BaseResponseStatus.POST_USERS_EXISTS_EMAIL);
}
if(postUserReq.getPassword().isEmpty()){
throw new BaseException(BaseResponseStatus.PASSWORD_CANNOT_BE_NULL);
}
if(!postUserReq.getPassword().equals(postUserReq.getPasswordChk())) {
throw new BaseException(BaseResponseStatus.PASSWORD_MISSMATCH);
}
if(postUserReq.getNickName() == null || postUserReq.getNickName().isEmpty()) {
throw new BaseException(BaseResponseStatus.NICKNAME_CANNOT_BE_NULL);
}
String pwd;
try{
pwd = new AES128(Secret.USER_INFO_PASSWORD_KEY).encrypt(postUserReq.getPassword()); // 암호화 코드
}
catch (Exception ignored) { // 암호화가 실패하였을 경우 에러 발생
throw new BaseException(BaseResponseStatus.PASSWORD_ENCRYPTION_ERROR);
}
User user = new User();
user.createUser(postUserReq.getNickName(),postUserReq.getEmail(), pwd, null);
userRepository.save(user);
return new PostUserRes(user);
}
/**
* 유저 로그인 with JWT
*/
public PostLoginRes login(PostLoginReq postLoginReq) throws BaseException {
User user = utilService.findByEmailWithValidation(postLoginReq.getEmail());
user.setUid(postLoginReq.getUid());
String password;
try {
password = new AES128(Secret.USER_INFO_PASSWORD_KEY).decrypt(user.getPassword());
} catch (Exception ignored) {
throw new BaseException(BaseResponseStatus.PASSWORD_DECRYPTION_ERROR);
}
if (postLoginReq.getPassword().equals(password)) {
Token token = tokenRepository.findTokenByUserId(user.getId()).orElse(null);
if (token == null) {
JwtResponseDto.TokenInfo tokenInfo = jwtProvider.generateToken(user.getId());
token = Token.builder()
.accessToken(tokenInfo.getAccessToken())
.refreshToken(tokenInfo.getRefreshToken())
.user(user)
.build();
tokenRepository.save(token);
}
return new PostLoginRes(user, token);
} else {
throw new BaseException(BaseResponseStatus.PASSWORD_NOT_MATCH);
}
}
}
① Module 수준의 build.gradle 파일의 dependencies에 Retrofit을 사용하기 위한 의존성을 추가한다.
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation ("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2")
② Manifest 파일에 아래의 내용을 추가한다.
<uses-permission android:name="android.permission.INTERNET"/>
③ default 패키지 하위로 api 패키지를 생성한 후, 그 안에 UserApi 인터페이스, ApiRepository, RetrofitInstance, BaseResponse, dto > PostLoginReq, PostLoginRes, PostUserReq, PostUserRes를 추가한다.
④ PostLoginReq에 아래의 내용을 입력한다.
data class PostLoginReq (
@SerializedName("uid")
val uid : String,
@SerializedName("email")
val email : String,
@SerializedName("password")
val password : String
)
⑤ PostLoginRes에는 아래의 내용을 입력한다.
data class PostLoginRes (
@SerializedName("userId")
val userId : Long,
@SerializedName("accessToken")
val accessToken : String,
@SerializedName("refreshToken")
val refreshToken : String
)
⑥ PostUserReq에는 아래의 내용을 입력한다.
data class PostUserReq (
@SerializedName("nickName")
val nickName : String,
@SerializedName("email")
val email : String,
@SerializedName("password")
val password : String,
@SerializedName("passwordChk")
val passwordChk : String
)
⑦ PostUserRes에는 아래의 내용을 입력한다.
data class PostUserRes (
@SerializedName("userId")
val userId : Long,
@SerializedName("nickName")
val nickName : String
)
⑧ ApiRepository에는 아래의 내용을 입력한다.
class ApiRepository {
companion object {
const val BASE_URL = "http://192.168.XX.XXX:8080"
const val CONTENT_TYPE = "application/json"
}
}
⑨ BaseResponse에는 아래의 내용을 입력한다.
data class BaseResponse<T> (
val isSuccess: Boolean,
val code: Int,
val message: String,
val result: T?
)
⑩ RetrofitInstance에는 아래의 내용을 입력한다.
class RetrofitInstance {
companion object {
private val retrofit by lazy {
Retrofit.Builder()
.baseUrl(ApiRepository.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val userApi = retrofit.create(UserApi::class.java)
}
}
⑪ UserApi에는 아래의 내용을 입력한다.
interface UserApi {
@POST("/users")
suspend fun createUser(@Body postUserReq: PostUserReq): BaseResponse<PostUserRes>
@POST("/users/log-in")
suspend fun loginUser(@Body postLoginReq: PostLoginReq): BaseResponse<PostLoginRes>
}
⑫ JoinActivity를 아래와 같이 수정한다.
class JoinActivity : AppCompatActivity() {
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_join)
auth = Firebase.auth
val joinBtn = findViewById<Button>(R.id.joinBtn)
joinBtn.setOnClickListener {
val nickname = findViewById<TextInputEditText>(R.id.nickname)
val email = findViewById<TextInputEditText>(R.id.joinEmail)
val password = findViewById<TextInputEditText>(R.id.joinPassword)
val passwordChk = findViewById<TextInputEditText>(R.id.passwordChk)
val postUserReq = PostUserReq(nickname.text.toString(), email.text.toString(),
password.text.toString(), passwordChk.text.toString())
auth.createUserWithEmailAndPassword(email.text.toString(), password.text.toString())
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
createUser(postUserReq)
Toast.makeText(this, "가입을 환영합니다!", Toast.LENGTH_SHORT).show()
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
} else {
Log.d("JoinActivity", "회원가입 실패")
}
}
}
}
private fun createUser(postUserReq: PostUserReq) = CoroutineScope(Dispatchers.IO).launch {
RetrofitInstance.userApi.createUser(postUserReq)
}
}
⑬ LoginActivity를 아래와 같이 수정한다.
class LoginActivity : AppCompatActivity() {
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
auth = Firebase.auth
val loginBtn = findViewById<Button>(R.id.loginBtn)
loginBtn.setOnClickListener {
val email = findViewById<TextInputEditText>(R.id.email)
val password = findViewById<TextInputEditText>(R.id.password)
val uid = FirebaseAuthUtils.getUid()
val postLoginReq = PostLoginReq(uid, email.text.toString(), password.text.toString())
auth.signInWithEmailAndPassword(email.text.toString(), password.text.toString())
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
CoroutineScope(Dispatchers.IO).launch {
val response = loginUser(postLoginReq)
Log.d("LoginActivity", response.toString())
if (response.isSuccess) {
Log.d("LoginActivity", "로그인 완료")
val intent = Intent(this@LoginActivity, MainActivity::class.java)
startActivity(intent)
} else {
// 로그인 실패 처리
Log.d("LoginActivity", "로그인 실패")
}
}
} else {
Log.d("LoginActivity", "로그인 실패")
}
}
}
}
private suspend fun loginUser(postLoginReq: PostLoginReq): BaseResponse<PostLoginRes> {
return RetrofitInstance.userApi.loginUser(postLoginReq)
}
}
⑭ 안드로이드 스튜디오에서 API를 호출할 때 HTTP로 호출하면 에러가 발생한다. 그러므로 Manifest 파일의 application 컨테이너의 속성에 아래의 내용을 추가한다.
<manifest
<application
...
android:usesCleartextTraffic="true"
코드를 실행시켜보면 회원가입 및 로그인한 유저의 정보가 파이어베이스의 Authentication > Users와 AWS RDS에 잘 저장될 것이다.