From 0dd0bf68fa512dcfb881a97b4c70b4210604430d Mon Sep 17 00:00:00 2001 From: bongbak Date: Wed, 18 Feb 2026 14:33:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[chore]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20api=20=EC=97=B0?= =?UTF-8?q?=EB=8F=99=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/stable/scoi/data/api/AuthApi.kt | 15 +- .../scoi/data/local/PreferenceManager.kt | 41 ++-- .../main/java/com/stable/scoi/di/ApiModule.kt | 7 +- .../scoi/di/AuthenticationInterceptor.kt | 8 +- .../domain/repository/auth/AuthRepository.kt | 43 ++++- .../presentation/ui/Auth/JoinAuthFragment.kt | 3 +- .../ui/Auth/JoinCompleteFragment.kt | 9 +- .../scoi/presentation/ui/Auth/JoinFragment.kt | 2 +- .../presentation/ui/Auth/JoinViewModel.kt | 3 +- .../presentation/ui/login/LoginFragment.kt | 39 ++-- .../presentation/ui/login/LoginViewModel.kt | 102 +++++----- .../presentation/ui/splash/SplashFragment.kt | 2 +- .../main/res/layout/dialog_admit_limit.xml | 88 +++++++++ .../main/res/navigation-v26/main_graph.xml | 179 +++++++++++++++++- app/src/main/res/navigation/main_graph.xml | 24 ++- 15 files changed, 440 insertions(+), 125 deletions(-) create mode 100644 app/src/main/res/layout/dialog_admit_limit.xml diff --git a/app/src/main/java/com/stable/scoi/data/api/AuthApi.kt b/app/src/main/java/com/stable/scoi/data/api/AuthApi.kt index 334ec22..605d353 100644 --- a/app/src/main/java/com/stable/scoi/data/api/AuthApi.kt +++ b/app/src/main/java/com/stable/scoi/data/api/AuthApi.kt @@ -31,6 +31,11 @@ interface AuthApi { @POST("/auth/logout") suspend fun logout(): ApiResponse + + @POST("/auth/password/reset") + suspend fun reset( + @Body request: PasswordResetRequest + ): ApiResponse } @@ -53,7 +58,7 @@ data class SmsVerifyRequest( data class PinLoginRequest( val phoneNumber: String = "", val simplePassword: String = "", - val keepLoggedIn: Boolean = false + val verificationToken: String="" ) @Serializable @@ -61,6 +66,7 @@ data class TokenRetryRequest( val refreshToken: String = "" ) + //response @Serializable data class SmsResponse( @@ -74,6 +80,8 @@ data class SmsVerifyResponse( + + @Serializable data class PinLoginResponse( val accessToken:String="", @@ -88,4 +96,9 @@ data class TokenRetryResponse( val accessToken: String = "", val refreshToken: String = "", val accessTokenExpiresIn: Long = -1 +) + +@Serializable +data class PasswordResetResponse( + val password:String="" ) \ No newline at end of file diff --git a/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt b/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt index 24d98c3..efa14f9 100644 --- a/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt +++ b/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt @@ -3,12 +3,15 @@ package com.stable.scoi.data.local import android.content.Context import android.content.SharedPreferences import dagger.hilt.android.qualifiers.ApplicationContext -import javax.inject.Inject // jakarta -> javax (Hilt 환경에 맞게 수정) +import javax.inject.Inject +import javax.inject.Singleton // ★ 싱글톤 추가 -class PreferenceManager @Inject constructor( +@Singleton // ★ 앱 전체에서 하나의 인스턴스만 공유하도록 설정 +class PreferenceManager @Inject constructor( // ★ 클래스명 대문자로 변경 @ApplicationContext context: Context ) { - private val prefs: SharedPreferences = context.getSharedPreferences("scoi_prefs", Context.MODE_PRIVATE) + private val prefs: SharedPreferences = + context.getSharedPreferences("scoi_prefs", Context.MODE_PRIVATE) companion object { private const val KEY_ACCESS_TOKEN = "ACCESS_TOKEN" @@ -19,47 +22,51 @@ class PreferenceManager @Inject constructor( private const val KEY_VERIFY_EXPIRE_TIME = "KEY_VERIFY_EXPIRE_TIME" } + // --- Access Token --- fun saveAccessToken(token: String) { prefs.edit().putString(KEY_ACCESS_TOKEN, token).apply() } - fun getAccessToken(): String = prefs.getString(KEY_ACCESS_TOKEN, "") ?: "" + // --- Refresh Token --- fun saveRefreshToken(token: String) { prefs.edit().putString(KEY_REFRESH_TOKEN, token).apply() } + fun getRefreshToken(): String = prefs.getString(KEY_REFRESH_TOKEN, "") ?: "" - fun getRefreshToken(): String? = prefs.getString(KEY_REFRESH_TOKEN, null) - - fun saveSmsExpiredAt(expiredAt: String) { - prefs.edit().putString(KEY_SMS_EXPIRED_AT, expiredAt).apply() - } - - fun getSmsExpiredAt(): String? = prefs.getString(KEY_SMS_EXPIRED_AT, null) - + // --- Verification Token (로그인/가입용) --- fun saveVerificationToken(token: String) { prefs.edit().putString(KEY_VERIFICATION_TOKEN, token).apply() } + // ViewModel에서 쓰기 편하게 리턴 타입을 String으로 통일했습니다. + fun getVerificationToken(): String = prefs.getString(KEY_VERIFICATION_TOKEN, "") ?: "" - fun getVerificationToken(): String? = prefs.getString(KEY_VERIFICATION_TOKEN, null) - + // --- Phone Number (로그인 시 필수!) --- + // ★ 누락되었던 저장 함수를 추가했습니다. + fun savePhoneNumber(number: String) { + prefs.edit().putString(KEY_PHONE_NUMBER, number).apply() + } fun getPhoneNumber(): String = prefs.getString(KEY_PHONE_NUMBER, "") ?: "" + // --- SMS Expired --- + fun saveSmsExpiredAt(expiredAt: String) { + prefs.edit().putString(KEY_SMS_EXPIRED_AT, expiredAt).apply() + } + fun getSmsExpiredAt(): String = prefs.getString(KEY_SMS_EXPIRED_AT, "") ?: "" fun saveVerificationSuccess() { val currentTime = System.currentTimeMillis() - val tenMinutesInMillis = 10 * 60 * 1000 // 10분 + val tenMinutesInMillis = 10 * 60 * 1000 val expireTime = currentTime + tenMinutesInMillis prefs.edit().putLong(KEY_VERIFY_EXPIRE_TIME, expireTime).apply() } - fun isVerificationValid(): Boolean { val expireTime = prefs.getLong(KEY_VERIFY_EXPIRE_TIME, 0) val currentTime = System.currentTimeMillis() if (expireTime == 0L) return false - return currentTime < expireTime // 현재 시간이 만료 전이면 true + return currentTime < expireTime } // 데이터 초기화 diff --git a/app/src/main/java/com/stable/scoi/di/ApiModule.kt b/app/src/main/java/com/stable/scoi/di/ApiModule.kt index 8087113..d6103e4 100644 --- a/app/src/main/java/com/stable/scoi/di/ApiModule.kt +++ b/app/src/main/java/com/stable/scoi/di/ApiModule.kt @@ -1,6 +1,8 @@ package com.stable.scoi.di import com.stable.scoi.data.api.OkHttpUpbitCandleWsApi +import com.stable.scoi.data.api.AuthApi +import com.stable.scoi.data.api.auth.SignUpApi import com.stable.scoi.data.api.transfer.BalancesAPI import com.stable.scoi.data.api.transfer.CancelOrderAPI import com.stable.scoi.data.api.transfer.DirectoryListAPI @@ -12,8 +14,6 @@ import com.stable.scoi.data.api.transfer.TransactionsRemitAPI import com.stable.scoi.data.api.transfer.TransactionsTopupsAPI import com.stable.scoi.data.api.ChargeApi import com.stable.scoi.data.api.MyPageApi -import com.stable.scoi.data.api.AuthApi -import com.stable.scoi.data.api.auth.SignUpApi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -97,7 +97,7 @@ object ApiModule { fun provideAuthApi( @NormalRetrofit retrofit: Retrofit ): AuthApi { - return retrofit.create(AuthApi::class.java) + return retrofit.create(AuthApi::class.java) } @Provides @@ -113,4 +113,5 @@ object ApiModule { fun provideMyPageApi(@AuthRetrofit retrofit: Retrofit): MyPageApi { return retrofit.create(MyPageApi::class.java) } + } \ No newline at end of file diff --git a/app/src/main/java/com/stable/scoi/di/AuthenticationInterceptor.kt b/app/src/main/java/com/stable/scoi/di/AuthenticationInterceptor.kt index 32c1877..71c80a7 100644 --- a/app/src/main/java/com/stable/scoi/di/AuthenticationInterceptor.kt +++ b/app/src/main/java/com/stable/scoi/di/AuthenticationInterceptor.kt @@ -11,11 +11,13 @@ import javax.inject.Singleton @Singleton class AuthenticationInterceptor @Inject -constructor() : Interceptor { +constructor( + private val preferenceManager: PreferenceManager +) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { // val accessToken = runBlocking { repository.getAccessToken().first() }. - val testToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIwMTAxMjM0MTIzNCIsInR5cGUiOiJBQ0NFU1MiLCJpYXQiOjE3NzA4ODkwODcsImV4cCI6MTk1MDg4OTA4N30.mlpIoIk95c0vvo0rtLpjaObQ5K0rFG6uR1cRTBxTykxmHA0Tr2D8BLhJPwjzGgragvqaol90Q1fblyjO_yjixg" - + val testToken = "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIwMTAxMjM0MTIzNCIsInR5cGUiOiJBQ0NFU1MiLCJpYXQiOjE3NzA4ODkwODcsImV4cCI6MTk1MDg4OTA4N30.mlpIoIk95c0vvo0rtLpjaObQ5K0rFG6uR1cRTBxTykxmHA0Tr2D8BLhJPwjzGgragvqaol90Q1fblyjO_yjixg" +// val verificationToken= preferenceManager.getVerificationToken() val request = chain.request().newBuilder() .addHeader("Authorization", "Bearer ${testToken}").build() diff --git a/app/src/main/java/com/stable/scoi/domain/repository/auth/AuthRepository.kt b/app/src/main/java/com/stable/scoi/domain/repository/auth/AuthRepository.kt index 61400b2..66197c3 100644 --- a/app/src/main/java/com/stable/scoi/domain/repository/auth/AuthRepository.kt +++ b/app/src/main/java/com/stable/scoi/domain/repository/auth/AuthRepository.kt @@ -1,6 +1,9 @@ package com.stable.scoi.domain.repository.auth +import com.auth0.jwt.interfaces.Verification import com.stable.scoi.data.api.AuthApi +import com.stable.scoi.data.api.PasswordResetRequest +import com.stable.scoi.data.api.PasswordResetResponse import com.stable.scoi.data.api.PinLoginRequest import com.stable.scoi.data.api.PinLoginResponse import com.stable.scoi.data.api.SmsRequest @@ -74,24 +77,28 @@ class AuthRepository @Inject constructor( } } - // PIN 로그인 suspend fun pinLogin( phoneNumber: String, - simplePassword: String + simplePassword: String, + verificationToken: String ): Result { return try { val request = PinLoginRequest( phoneNumber = phoneNumber, - simplePassword = simplePassword + simplePassword = simplePassword, + verificationToken = verificationToken ) + val response = authApi.pinLogin(request) if (response.isSuccess && response.result != null) { + preferenceManager.saveAccessToken(response.result.accessToken) preferenceManager.saveRefreshToken(response.result.refreshToken) + Result.success(response.result) } else { - Result.failure(Exception(response.message)) + Result.failure(Exception(response.message ?: "알 수 없는 오류가 발생했습니다.")) } } catch (e: Exception) { Result.failure(e) @@ -120,12 +127,12 @@ class AuthRepository @Inject constructor( Result.failure(e) } } + suspend fun logout(): Result { return try { val response = authApi.logout() if (response.isSuccess) { - preferenceManager.clear() Result.success(Unit) } else { Result.failure(Exception(response.message)) @@ -134,4 +141,28 @@ class AuthRepository @Inject constructor( Result.failure(e) } } -} \ No newline at end of file + +// suspend fun reset( +// verificationToken: String, +// phoneNumber: String, +// newPassword:String +// ): Result { +// return try{ +// val request=PasswordResetRequest( +// verificationToken = verificationToken, +// phoneNumber = phoneNumber, +// simplePassword = newPassword +// ) +// val response=authApi.reset(request) +// if(response.isSuccess&&response.result!=null){ +// Result.success(response.result) +// } +// else{ +// Result.failure(Exception(response.message)) +// } +// } +// catch(e:Exception){ +// Result.failure(e) +// } + +} diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinAuthFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinAuthFragment.kt index 960fbc0..85fa91b 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinAuthFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinAuthFragment.kt @@ -221,8 +221,7 @@ class JoinAuthFragment : binding.phoneAuthCodeActiveTimerTv.visibility = View.GONE binding.phoneAuthCodeActiveCheckIv.visibility = View.VISIBLE binding.phoneAuthCodeCheckIv.visibility = View.GONE - - findNavController().navigate(R.id.action_joinAuthFragment_to_joinFragment) + findNavController().navigate(R.id.action_joinAuthFragment_to_loginFragment) } is JoinEvent.ShowError -> { diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinCompleteFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinCompleteFragment.kt index 3584ab4..539e301 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinCompleteFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinCompleteFragment.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import com.stable.scoi.R import com.stable.scoi.databinding.FragmentRegCompleteBinding import com.stable.scoi.presentation.MainActivity @@ -35,7 +36,7 @@ class JoinCompleteFragment : Fragment() { } binding.guideActiveCv.setOnClickListener { - navigateToMain() + navigateToPin() } } @@ -49,4 +50,8 @@ class JoinCompleteFragment : Fragment() { super.onDestroyView() _binding = null } -} \ No newline at end of file + private fun navigateToPin() { + findNavController().navigate(R.id.action_joinCompleteFragment_to_pinFragment) + } +} + diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinFragment.kt index 5b5b8e1..f07373f 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinFragment.kt @@ -295,7 +295,7 @@ class JoinFragment : BaseFragment stopTimer() - + preferenceManager.savePhoneNumber(phone) + preferenceManager.saveVerificationToken(response.verificationToken) preferenceManager.saveVerificationSuccess() updateState { diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt index c8512de..75ec162 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt @@ -1,6 +1,5 @@ package com.stable.scoi.presentation.ui.login -import android.view.KeyEvent import android.view.View import android.view.WindowManager import android.widget.EditText @@ -12,7 +11,7 @@ import androidx.navigation.fragment.findNavController import com.stable.scoi.R import com.stable.scoi.databinding.FragmentLoginPinBinding import com.stable.scoi.presentation.base.BaseFragment -import com.stable.scoi.presentation.ui.Auth.JoinViewModel +import com.stable.scoi.util.SLOG import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -21,8 +20,6 @@ import kotlinx.coroutines.launch class LoginFragment : BaseFragment( FragmentLoginPinBinding::inflate ) { - - override val viewModel: LoginViewModel by activityViewModels() @@ -50,35 +47,32 @@ class LoginFragment : BaseFragment - if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) { - if (editText.text.isEmpty() && index > 0) { - val prevEt = pinEditTexts[index - 1] - prevEt.requestFocus() - prevEt.text = null - return@setOnKeyListener true - } - } - false } } - binding.loginPinBioTv.setOnClickListener { - viewModel.onBiometricLogin() - } - binding.loginPinInputActiveCv.setOnClickListener { + SLOG.D("버튼이 입력되었습니다.") + viewModel.onCompleteClicked() + } + binding.loginPinInputInactiveCv.setOnClickListener { + SLOG.D("버튼이 입력되었습니다.") viewModel.onCompleteClicked() } } + private fun resetErrorState(pinEditTexts: List) { // 에러 메시지가 떠있다면 안 보이게 처리 if (binding.loginPinErrorTv.visibility == View.VISIBLE) { @@ -136,7 +130,6 @@ class LoginFragment : BaseFragment editText.setBackgroundResource(R.drawable.bg_pin_underline_error) @@ -182,8 +175,6 @@ class LoginFragment : BaseFragment(LoginState()) { init { @@ -19,82 +22,77 @@ class LoginViewModel @Inject constructor( } private fun checkAutoLoginStatus() { - // 토큰이 있는지 확인 (자동 로그인 여부) val token = preferenceManager.getAccessToken() if (token.isEmpty()) { - // 토큰 없으면 만료 처리(또는 로그인 화면 유지) - // emitEvent(LoginEvent.NavigationToExpired) // 기획에 따라 결정 + // 자동 로그인 로직 필요 시 추가 } } - - // 완료 버튼 눌렀을 때 fun onCompleteClicked() { - if (!uiState.value.isLoading && uiState.value.isButtonEnabled) { tryLogin(uiState.value.simplePassword) - } } - // 생체 인증 버튼 클릭 fun onBiometricLogin() { emitEvent(LoginEvent.NavigationToBiometric) } private fun tryLogin(pin: String) { viewModelScope.launch { - // 1. 로딩 시작 - updateState { this.copy(isLoading = true) } + // 로딩 시작 + updateState { copy(isLoading = true) } val savedPhoneNumber = preferenceManager.getPhoneNumber() + val verificationToken = preferenceManager.getVerificationToken() - if (savedPhoneNumber.isEmpty()) { - updateState { this.copy(isLoading = false) } - emitEvent(LoginEvent.ShowError("저장된 사용자 정보가 없습니다. 다시 가입해주세요.")) - return@launch - } + val encryptedPin = EncryptionUtil.encrypt(pin) - // 3. Repository를 통해 서버 API 호출 - val result = authRepository.pinLogin( + authRepository.pinLogin( phoneNumber = savedPhoneNumber, - simplePassword = pin - ) + simplePassword = encryptedPin, + verificationToken = verificationToken + ).onSuccess { response -> - // 4. 결과 처리 - if (result.isSuccess) { - // 성공 시 메인으로 이동 emitEvent(LoginEvent.NavigationToMain) - } else { - // 실패 시 에러 메시지 띄우기 - val errorMsg = result.exceptionOrNull()?.message ?: "로그인에 실패했습니다." - emitEvent(LoginEvent.ShowError(errorMsg)) + }.onFailure { e -> - // (선택) 비밀번호 틀렸으니 입력 초기화 - updateState { this.copy(simplePassword = "", isButtonEnabled = false) } + val errorMessage = parseErrorResponse(e) + emitEvent(LoginEvent.ShowError(errorMessage)) + updateState { copy(simplePassword = "", isButtonEnabled = false) } } - // 5. 로딩 종료 - updateState { this.copy(isLoading = false) } + updateState { copy(isLoading = false) } } } - fun onPhoneAuthNumberChanged(input: String) { - updateState { - this.copy( - phoneNumber = input, - isCodeSendEnabled = true - ) + // --- 에러 응답 파싱 (남은 횟수 추출) --- + private fun parseErrorResponse(e: Throwable): String { + return try { + if (e is HttpException) { + val errorJson = e.response()?.errorBody()?.string() + + if (!errorJson.isNullOrEmpty()) { + val jsonObject = JSONObject(errorJson) + val code = jsonObject.optString("code") + val message = jsonObject.optString("message") + + // 비밀번호 불일치 에러 코드 확인 + if (code == "AUTH401_1") { + val resultObj = jsonObject.optJSONObject("result") + val remain = resultObj?.optInt("remainingAttempts") ?: 0 + return "$message (남은 기회: ${remain}회)" + } else { + return message + } + } + } + e.message ?: "로그인에 실패했습니다." + } catch (ex: Exception) { + "로그인 중 오류가 발생했습니다." } } - fun onAuthChanged(input: String) { - this.updateState { - this.copy( - verificationCode = input, - isCodeEnabled = true - ) - } - } - fun onPinChanged(input:String){ + // --- 입력 핸들러 --- + fun onPinChanged(input: String) { this.updateState { this.copy( simplePassword = input, @@ -103,6 +101,14 @@ class LoginViewModel @Inject constructor( } } + fun onPhoneAuthNumberChanged(input: String) { + updateState { this.copy(phoneNumber = input, isCodeSendEnabled = true) } + } + + fun onAuthChanged(input: String) { + this.updateState { this.copy(verificationCode = input, isCodeEnabled = true) } + } + fun onSendClicked() { verification(uiState.value.verificationCode) } @@ -110,7 +116,7 @@ class LoginViewModel @Inject constructor( private fun verification(code: String) { val currentState = uiState.value viewModelScope.launch { - authRepository.verifySms(currentState.phoneNumber,code) + authRepository.verifySms(currentState.phoneNumber, code) } } } \ No newline at end of file diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt index 20a6fa0..984c5ed 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt @@ -30,7 +30,7 @@ class SplashFragment : BaseFragment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation-v26/main_graph.xml b/app/src/main/res/navigation-v26/main_graph.xml index bd0c11e..ba994ec 100644 --- a/app/src/main/res/navigation-v26/main_graph.xml +++ b/app/src/main/res/navigation-v26/main_graph.xml @@ -13,11 +13,15 @@ + + android:name="com.stable.scoi.presentation.ui.wallet.WalletFragment" + tools:layout="@layout/fragment_wallet"/> + + @@ -70,7 +78,11 @@ + android:name="com.stable.scoi.presentation.ui.transfer.TransferCompleteFragment"> + + + + + + + + + + + + + + @@ -159,6 +197,77 @@ android:id="@+id/action_homeFragment_to_tansfer_fragment" app:destination="@id/tansfer_fragment" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -242,5 +351,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 040fca1..45fd3c0 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -13,6 +13,9 @@ + - @@ -229,6 +229,9 @@ + - - - - + > + + From 6468cfc35b64e33be85d5b352eca411c9f349aa4 Mon Sep 17 00:00:00 2001 From: bongbak Date: Mon, 16 Mar 2026 11:46:08 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[chore]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scoi/data/local/PreferenceManager.kt | 8 +- .../stable/scoi/presentation/MainActivity.kt | 5 +- .../ui/Auth/JoinCompleteFragment.kt | 15 +-- .../presentation/ui/Auth/JoinViewModel.kt | 92 +++++++++---------- .../scoi/presentation/ui/Auth/KeyFragment.kt | 4 +- .../presentation/ui/splash/SplashFragment.kt | 18 +++- .../main/res/navigation-v26/main_graph.xml | 38 +++++--- app/src/main/res/navigation/main_graph.xml | 77 +++------------- 8 files changed, 116 insertions(+), 141 deletions(-) diff --git a/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt b/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt index 89f8f20..15f0815 100644 --- a/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt +++ b/app/src/main/java/com/stable/scoi/data/local/PreferenceManager.kt @@ -72,8 +72,14 @@ class PreferenceManager @Inject constructor( fun getSimplePassword(): String = prefs.getString("SIMPLE_PASSWORD", "") ?: "" + fun setJoinStatus(isJoined:Boolean){ + prefs.edit().putBoolean("JOIN_STATUS", isJoined).apply() + } + + fun getJoinStatus(): Boolean { + return prefs.getBoolean("JOIN_STATUS", false) + } - // 데이터 초기화 fun clear() { prefs.edit().clear().apply() } diff --git a/app/src/main/java/com/stable/scoi/presentation/MainActivity.kt b/app/src/main/java/com/stable/scoi/presentation/MainActivity.kt index 1e5cc5b..6fcfe7a 100644 --- a/app/src/main/java/com/stable/scoi/presentation/MainActivity.kt +++ b/app/src/main/java/com/stable/scoi/presentation/MainActivity.kt @@ -8,12 +8,14 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.stable.scoi.R +import com.stable.scoi.data.local.PreferenceManager import com.stable.scoi.databinding.ActivityMainBinding import com.stable.scoi.extension.gone import com.stable.scoi.extension.visible import com.stable.scoi.presentation.base.BaseActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint class MainActivity : BaseActivity( @@ -22,6 +24,8 @@ class MainActivity : BaseActivity -// android.util.Log.e("JOIN_DEBUG", "회원가입 실패: ${e.message}") -// emitEvent(JoinEvent.ShowError(e.message ?: "회원가입 실패")) -// } -// } catch (e: Exception) { -// android.util.Log.e("JOIN_DEBUG", "암호화 중 오류 발생: ${e.message}") -// emitEvent(JoinEvent.ShowError("보안 처리 중 오류가 발생했습니다.")) -// } finally { -// updateState { copy(isLoading = false) } -// } -// } -// } + fun submitSignUp(exchange: String, apiKey: String, secretKey: String) { + val currentState = uiState.value + updateState { copy(isLoading = true) } + + viewModelScope.launch { + try { + val encryptedSecretKey = EncryptionUtil.encrypt(secretKey) + val encryptedSimplePassword = EncryptionUtil.encrypt(currentState.simplePassword) + + SLOG.D(encryptedSimplePassword.toString()) + + val newApiKey = ApiKeyInfo( + exchangeType = exchange, + publicKey = apiKey, + secretKey = encryptedSecretKey + ) + + val request = SignUpRequest( + koreanName = currentState.koreanName, + englishName = currentState.englishName, + residentNumber = currentState.residentNumber, + phoneNumber = currentState.phoneNumber, + simplePassword = encryptedSimplePassword, + apiKeys = listOf(newApiKey), + memberType = "INDIVIDUAL", + isBioRegistered = currentState.isBioRegistered, + verificationToken = currentState.verificationToken + ) + + authRepository.signUp(request) + .onSuccess { + preferenceManager.saveSimplePassword(currentState.simplePassword) + emitEvent(JoinEvent.NavigateToRegDone) + } + .onFailure { e -> + android.util.Log.e("JOIN_DEBUG", "회원가입 실패: ${e.message}") + emitEvent(JoinEvent.ShowError(e.message ?: "회원가입 실패")) + } + } catch (e: Exception) { + android.util.Log.e("JOIN_DEBUG", "암호화 중 오류 발생: ${e.message}") + emitEvent(JoinEvent.ShowError("보안 처리 중 오류가 발생했습니다.")) + } finally { + updateState { copy(isLoading = false) } + } + } + } } diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/KeyFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/KeyFragment.kt index 76e93d7..7520d59 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/KeyFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/KeyFragment.kt @@ -78,8 +78,8 @@ class KeyFragment : BaseFragment "UPBIT" else -> "" } -// viewModel.submitSignUp(exchangeType, binding.apiKeyInputEt.text.toString(), binding.secretKeyInputEt.text.toString()) - findNavController().navigate(R.id.action_KeyFragment_to_homeFragment) + viewModel.submitSignUp(exchangeType, binding.apiKeyInputEt.text.toString(), binding.secretKeyInputEt.text.toString()) + findNavController().navigate(R.id.action_KeyFragment_to_joinCompleteFragment) } } diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt index 15dcf28..3fceed7 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt @@ -9,6 +9,7 @@ import com.stable.scoi.presentation.base.BaseFragment import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import com.stable.scoi.R +import com.stable.scoi.data.local.PreferenceManager import com.stable.scoi.databinding.FragmentSplashBinding import com.stable.scoi.util.SLOG @@ -16,7 +17,9 @@ import com.stable.scoi.util.SLOG class SplashFragment : BaseFragment( FragmentSplashBinding::inflate, ) { + private val preferenceManager: PreferenceManager by lazy { PreferenceManager(requireContext()) } override val viewModel: SplashViewModel by viewModels() + private var isRegistered=preferenceManager.getJoinStatus() override fun initView() { binding.apply { @@ -30,9 +33,12 @@ class SplashFragment : BaseFragment - + app:destination="@id/joinAuthFragment" /> + android:id="@+id/action_splashFragment_to_loginFragment" + app:destination="@id/login_pin_fragment" /> + tools:layout="@layout/fragment_wallet" /> + app:destination="@id/joinAuthFragment" /> + tools:layout="@layout/fragment_reg_complete"> + + app:destination="@id/login_pin_fragment" /> + tools:layout="@layout/fragment_bio_reg"> + app:destination="@id/homeFragment" /> + + + + + - @@ -20,7 +17,7 @@ android:id="@+id/action_splash_fragment_to_join_auth_fragment" app:destination="@id/joinAuthFragment"/> @@ -64,26 +61,6 @@ - - - - - - - - - - - + " - - - - - - - - - - - @@ -485,6 +431,9 @@ + + tools:layout="@layout/fragment_bio_reg"> + app:destination="@id/login_pin_fragment"/> - - - - \ No newline at end of file + From e8f08a8ff8e088cff8a98dbab142e17a60d50def Mon Sep 17 00:00:00 2001 From: bongbak Date: Tue, 17 Mar 2026 22:11:58 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[chore]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95<=EC=B5=9C?= =?UTF-8?q?=EC=A2=85>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/Auth/JoinViewModel.kt | 2 + .../presentation/ui/login/LoginFragment.kt | 3 + .../presentation/ui/login/LoginViewModel.kt | 2 +- .../presentation/ui/splash/SplashFragment.kt | 4 +- .../main/res/navigation-v26/main_graph.xml | 46 +++++------- app/src/main/res/navigation/main_graph.xml | 75 +++++++++++++++++-- 6 files changed, 93 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinViewModel.kt b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinViewModel.kt index 397de12..1d22b90 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinViewModel.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/Auth/JoinViewModel.kt @@ -46,6 +46,7 @@ class JoinViewModel @Inject constructor( viewModelScope.launch { authRepository.sendSms(phone) .onSuccess { response -> + SLOG.D("전송 성공!") val remainingSeconds = calculateRemainingSeconds(response.expiredAt) startTimer(remainingSeconds) @@ -53,6 +54,7 @@ class JoinViewModel @Inject constructor( emitEvent(JoinEvent.ShowError("인증번호가 발송되었습니다.")) } .onFailure { e -> + SLOG.D("전송 실패함 ㅋ") emitEvent(JoinEvent.ShowError(e.message ?: "전송 실패")) } } diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt index d956ac9..9f676e1 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginFragment.kt @@ -4,6 +4,7 @@ import android.text.method.PasswordTransformationMethod import android.view.View import android.view.WindowManager import android.widget.EditText +import androidx.core.content.ContextCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.core.widget.doOnTextChanged @@ -38,6 +39,8 @@ class LoginFragment : BaseFragment(R.id.main).setBackgroundColor(white) // pinEditTexts.forEach { editText -> // editText.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginViewModel.kt b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginViewModel.kt index b9c73a1..b4b981d 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginViewModel.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/login/LoginViewModel.kt @@ -47,7 +47,7 @@ class LoginViewModel @Inject constructor( updateState { this.copy(isLoading = true) } val savedPhoneNumber = preferenceManager.getPhoneNumber() - val currentToken = preferenceManager.getVerificationToken() + val currentToken = preferenceManager.getVerificationToken() // 원래는 null 값으로 되게끔 한다. if (savedPhoneNumber.isEmpty()) { updateState { this.copy(isLoading = false) } diff --git a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt index 3fceed7..9cc18db 100644 --- a/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt +++ b/app/src/main/java/com/stable/scoi/presentation/ui/splash/SplashFragment.kt @@ -19,7 +19,7 @@ class SplashFragment : BaseFragment + + app:destination="@id/joinAuthFragment"/> + app:destination="@id/login_pin_fragment"/> + tools:layout="@layout/fragment_wallet"/> + app:destination="@id/joinAuthFragment"/> - @@ -316,9 +316,6 @@ - + tools:layout="@layout/fragment_reg_complete" + > + app:destination="@id/homeFragment" + /> + app:destination="@id/login_pin_fragment"/> + tools:layout="@layout/fragment_login_pin_reg" + > + app:destination="@id/homeFragment"/> - - - - - - - - + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index e9c424a..ad0c51a 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -10,6 +10,9 @@ android:name="com.stable.scoi.presentation.ui.splash.SplashFragment" android:label="스플래시" tools:layout="@layout/fragment_splash"> + @@ -61,6 +64,26 @@ + + + + + + + + + + + - " + + + + + + + + + + + @@ -433,7 +487,8 @@ app:destination="@id/login_pin_fragment" /> + app:destination="@id/homeFragment" + /> + tools:layout="@layout/fragment_login_pin_reg" + > + app:destination="@id/login_pin_fragment" /> + + - + + \ No newline at end of file