diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupNameFragment.kt b/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupNameFragment.kt index 9eb4b96..8b60f1e 100644 --- a/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupNameFragment.kt +++ b/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupNameFragment.kt @@ -51,7 +51,13 @@ class SignupNameFragment : BaseFragment(R.layout.frag private fun initEditName() { // 입력 글자 수 제한 - binding.etSignupNameInput.filters = arrayOf(InputFilter.LengthFilter(5)) + binding.etSignupNameInput.filters = arrayOf( + InputFilter.LengthFilter(5), // 최대 글자 수 제한 + InputFilter { source, _, _, _, _, _ -> + source.toString().replace(" ", "").replace("\n", "") // 띄어쓰기와 엔터 제거 + } + ) + // 입력 필드 포커스 및 텍스트 변화 리스너 설정 binding.etSignupNameInput.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupTermFragment.kt b/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupTermFragment.kt index f7544e5..e627807 100644 --- a/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupTermFragment.kt +++ b/app/src/main/java/com/umc/timeCAlling/presentation/login/SignupTermFragment.kt @@ -25,7 +25,9 @@ class SignupTermFragment : BaseFragment(R.layout.frag binding.clSignupTermBtn2 to binding.ivSignupTermOval2, binding.clSignupTermBtn3 to binding.ivSignupTermOval3, binding.clSignupTermBtn4 to binding.ivSignupTermOval4, - binding.clSignupTermBtn5 to binding.ivSignupTermOval5 + binding.clSignupTermBtn5 to binding.ivSignupTermOval5, + binding.clSignupTermBtn6 to binding.ivSignupTermOval6, + binding.clSignupTermBtn7 to binding.ivSignupTermOval7, ) // '모두 동의' 버튼 @@ -84,7 +86,8 @@ class SignupTermFragment : BaseFragment(R.layout.frag binding.ivSignupTermOval1, binding.ivSignupTermOval2, binding.ivSignupTermOval3, - binding.ivSignupTermOval4 + binding.ivSignupTermOval4, + binding.ivSignupTermOval5 ).all { it.tag == true } binding.tvSignupTermNext.apply { @@ -104,8 +107,11 @@ class SignupTermFragment : BaseFragment(R.layout.frag private fun setupArrowClickListeners() { val arrowConfigs = listOf( Triple(binding.ivSignupTermArrowUp2, binding.ivSignupTermArrowDown2, binding.svSignupTermContent2), - Triple(binding.ivSignupTermArrowUp3, binding.ivSignupTermArrowDown3, binding.tvSignupTermContent3), - Triple(binding.ivSignupTermArrowUp4, binding.ivSignupTermArrowDown4, binding.tvSignupTermContent4) + Triple(binding.ivSignupTermArrowUp3, binding.ivSignupTermArrowDown3, binding.svSignupTermContent3), + Triple(binding.ivSignupTermArrowUp4, binding.ivSignupTermArrowDown4, binding.svSignupTermContent4), + Triple(binding.ivSignupTermArrowUp5, binding.ivSignupTermArrowDown5, binding.svSignupTermContent5), + Triple(binding.ivSignupTermArrowUp6, binding.ivSignupTermArrowDown6, binding.svSignupTermContent6), + Triple(binding.ivSignupTermArrowUp7, binding.ivSignupTermArrowDown7, binding.svSignupTermContent7) ) arrowConfigs.forEach { (arrowUp, arrowDown, contentView) -> @@ -129,7 +135,11 @@ class SignupTermFragment : BaseFragment(R.layout.frag private fun setupTermsContent() { binding.tvSignupTermContent2.text = loadTermsContent(R.raw.term_1) - // 추가 약관 내용을 설정할 수 있습니다. + binding.tvSignupTermContent3.text = loadTermsContent(R.raw.term_2) + binding.tvSignupTermContent4.text = loadTermsContent(R.raw.term_3) + binding.tvSignupTermContent5.text = loadTermsContent(R.raw.term_4) + binding.tvSignupTermContent6.text = loadTermsContent(R.raw.term_5) + binding.tvSignupTermContent7.text = loadTermsContent(R.raw.term_6) } private fun isNextButtonEnabled(): Boolean { diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageFragment.kt b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageFragment.kt index fe1f833..dbeae3e 100644 --- a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageFragment.kt +++ b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageFragment.kt @@ -51,7 +51,7 @@ class MypageFragment: BaseFragment(R.layout.fragment_mypa private fun setClickListener() { binding.apply { clMypageSetting.setOnClickListener { - navigateToMyprofileFragment() // 내 프로필 + navigateToMyprofileFragment() } layoutMypageAlarmlist.setOnClickListener { findNavController().navigate(R.id.action_mypageFragment_to_alarmlistFragment) @@ -63,7 +63,7 @@ class MypageFragment: BaseFragment(R.layout.fragment_mypa navigateToMypageVoiceFragment() } layoutMypageTerms.setOnClickListener { - //이용약관 + navigateToMypageTermFragment() } ivMypageBack.setOnClickListener { findNavController().navigate(R.id.action_global_homeFragment) } } @@ -132,6 +132,10 @@ class MypageFragment: BaseFragment(R.layout.fragment_mypa findNavController().navigate(R.id.action_mypageFragment_to_mypageVoiceFragment) } + private fun navigateToMypageTermFragment() { + findNavController().navigate(R.id.action_mypageFragment_to_mypageTermFragment) + } + private fun initSuccessRate() { binding.viewMypageProgressBackground.post { val maxWidth = binding.viewMypageProgressBackground.width diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageTermFragment.kt b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageTermFragment.kt new file mode 100644 index 0000000..cac97a7 --- /dev/null +++ b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MypageTermFragment.kt @@ -0,0 +1,105 @@ +package com.umc.timeCAlling.presentation.mypage + +import android.view.View +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.umc.timeCAlling.R +import com.umc.timeCAlling.databinding.FragmentMypageTermBinding +import com.umc.timeCAlling.presentation.base.BaseFragment +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.BufferedReader +import java.io.InputStreamReader + +@AndroidEntryPoint +class MypageTermFragment: BaseFragment(R.layout.fragment_mypage_term) { + + override fun initObserver() { + } + + override fun initView() { + setClickListener() + hideViews( + R.id.main_bnv, + R.id.iv_main_add_schedule_btn, + R.id.iv_main_bnv_shadow, + R.id.iv_main_bnv_white_oval + ) + } + + private fun hideViews(vararg viewIds: Int) { + viewIds.forEach { id -> + requireActivity().findViewById(id)?.visibility = View.GONE + } + } + + private fun setClickListener() { + binding.ivMypageBack.setOnClickListener { findNavController().popBackStack() } + binding.ivMypageTermBack.setOnClickListener { showTermList() } + + // 각 이용약관 항목 클릭 시 이용약관 본문 보기 + val termItems = listOf( + binding.tvMypageTerm1 to binding.ivMypageTerm1Arrow, + binding.tvMypageTerm2 to binding.ivMypageTerm2Arrow, + binding.tvMypageTerm3 to binding.ivMypageTerm3Arrow, + binding.tvMypageTerm4 to binding.ivMypageTerm4Arrow, + binding.tvMypageTerm5 to binding.ivMypageTerm5Arrow, + binding.tvMypageTerm6 to binding.ivMypageTerm6Arrow + ) + + val fileResources = listOf( + R.raw.term_1, R.raw.term_2, R.raw.term_3, + R.raw.term_4, R.raw.term_5, R.raw.term_6 + ) + + termItems.forEachIndexed { index, (textView, imageView) -> + val resourceId = fileResources[index] + + textView.setOnClickListener { showTermContent(textView.text.toString(), resourceId) } + imageView.setOnClickListener { showTermContent(textView.text.toString(), resourceId) } + } + } + + private fun showTermContent(title: String, resourceId: Int) { + lifecycleScope.launch(Dispatchers.IO) { + val content = loadTermsContent(resourceId) + launch(Dispatchers.Main) { + binding.tvMypageTitle.text = title + binding.tvMypageTermContent.text = content + binding.ivMypageBack.isVisible = false + binding.ivMypageTermBack.isVisible = true + binding.svMypageTermContent.isVisible = true + toggleTermListVisibility(false) + } + } + } + + private fun showTermList() { + binding.ivMypageBack.isVisible = true + binding.ivMypageTermBack.isVisible = false + binding.svMypageTermContent.isVisible = false + binding.tvMypageTitle.text = "이용약관" + toggleTermListVisibility(true) + } + + private fun toggleTermListVisibility(isVisible: Boolean) { + binding.tvMypageTerm1.isVisible = isVisible + binding.ivMypageTerm1Arrow.isVisible = isVisible + binding.tvMypageTerm2.isVisible = isVisible + binding.ivMypageTerm2Arrow.isVisible = isVisible + binding.tvMypageTerm3.isVisible = isVisible + binding.ivMypageTerm3Arrow.isVisible = isVisible + binding.tvMypageTerm4.isVisible = isVisible + binding.ivMypageTerm4Arrow.isVisible = isVisible + binding.tvMypageTerm5.isVisible = isVisible + binding.ivMypageTerm5Arrow.isVisible = isVisible + binding.tvMypageTerm6.isVisible = isVisible + binding.ivMypageTerm6Arrow.isVisible = isVisible + } + + private fun loadTermsContent(resourceId: Int): String { + return resources.openRawResource(resourceId).bufferedReader().use { it.readText() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MyprofileFragment.kt b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MyprofileFragment.kt index b299bd2..6870a35 100644 --- a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MyprofileFragment.kt +++ b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/MyprofileFragment.kt @@ -376,28 +376,24 @@ class MyprofileFragment : BaseFragment(R.layout.fragme binding.tvMyprofileCurrentName.text.toString() } else null - val updatedAvgPrepTime = if (avgPrepTime != binding.tvMyprofileTimeEdit.text.toString().toInt()) { - binding.tvMyprofileTimeEdit.text.toString().toInt() - } else null + val updatedAvgPrepTime = binding.tvMyprofileTimeEdit.text.toString().toIntOrNull() - val updatedFreeTime = if (freeTime != binding.tvMyprofileSpareEdit.text.toString()) { - when (binding.tvMyprofileSpareEdit.text.toString()) { - "여유" -> "PLENTY" - "넉넉" -> "RELAXED" - "딱딱" -> "TIGHT" - else -> null - } - } else null + val updatedFreeTime = when (binding.tvMyprofileSpareEdit.text.toString()) { + "여유" -> "PLENTY" + "넉넉" -> "RELAXED" + "딱딱" -> "TIGHT" + else -> null + } myprofileViewModel.updateUser( updatedNickname, updatedAvgPrepTime, updatedFreeTime, - imageFile + imageFile, + requireContext() ) } - private fun openGallery() { val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) galleryLauncher.launch(intent) @@ -519,9 +515,19 @@ class MyprofileFragment : BaseFragment(R.layout.fragme dialog.show() } - private fun deleteUser(){ - myprofileViewModel.deleteUser() + private fun clearAppCache(context: Context) { + try { + val cacheDir = context.cacheDir + cacheDir.deleteRecursively() // 앱의 캐시 파일 전체 삭제 + Log.d("MyprofileFragment", "앱 캐시 삭제 완료") + } catch (e: Exception) { + Log.e("MyprofileFragment", "앱 캐시 삭제 실패: ${e.message}") + } + } + + private fun deleteUser() { viewLifecycleOwner.lifecycleScope.launch { + myprofileViewModel.deleteUser() viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { myprofileViewModel.deleteState.collectLatest { state -> when (state) { @@ -529,6 +535,7 @@ class MyprofileFragment : BaseFragment(R.layout.fragme Log.d("MyprofileFragment", "회원탈퇴 성공, 저장된 토큰 삭제 후 로그인 화면으로 이동") signupViewModel.clearAuthToken() + clearAppCache(requireContext()) // ✅ 앱 캐시 삭제 추가 findNavController().navigate(R.id.action_myprofileFragment_to_loginFragment) // 로그인 화면으로 이동 } @@ -543,9 +550,9 @@ class MyprofileFragment : BaseFragment(R.layout.fragme } } - private fun logoutUser(){ - myprofileViewModel.logoutUser() + private fun logoutUser() { viewLifecycleOwner.lifecycleScope.launch { + myprofileViewModel.logoutUser() viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { myprofileViewModel.logoutState.collectLatest { state -> when (state) { @@ -553,6 +560,8 @@ class MyprofileFragment : BaseFragment(R.layout.fragme Log.d("MyprofileFragment", "로그아웃 성공, 로그인 화면으로 이동") signupViewModel.clearAuthToken() + clearAppCache(requireContext()) // ✅ 앱 캐시 삭제 추가 + findNavController().navigate(R.id.action_myprofileFragment_to_loginFragment) // 로그인 화면으로 이동 } is UiState.Error -> { @@ -566,6 +575,7 @@ class MyprofileFragment : BaseFragment(R.layout.fragme } } + private fun showToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/adapter/MyprofileViewModel.kt b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/adapter/MyprofileViewModel.kt index 2c2f277..fad3fd2 100644 --- a/app/src/main/java/com/umc/timeCAlling/presentation/mypage/adapter/MyprofileViewModel.kt +++ b/app/src/main/java/com/umc/timeCAlling/presentation/mypage/adapter/MyprofileViewModel.kt @@ -1,9 +1,10 @@ package com.umc.timeCAlling.presentation.mypage -import android.content.SharedPreferences +import android.content.Context import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.umc.timeCAlling.R import com.umc.timeCAlling.domain.model.request.mypage.UpdateUserRequestModel import com.umc.timeCAlling.domain.model.response.mypage.GetUserResponseModel import com.umc.timeCAlling.domain.repository.MypageRepository @@ -19,13 +20,15 @@ import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONObject +import timber.log.Timber import java.io.File +import java.io.FileOutputStream +import java.io.InputStream import javax.inject.Inject @HiltViewModel class MyprofileViewModel @Inject constructor( - private val mypageRepository: MypageRepository, - private val spf: SharedPreferences + private val mypageRepository: MypageRepository ) : ViewModel() { private val _userInfo = MutableStateFlow>(UiState.Loading) @@ -58,18 +61,39 @@ class MyprofileViewModel @Inject constructor( } } - fun updateUser(nickname: String?, avgPrepTime: Int?, freeTime: String?, imageFile: File?) { - val (imagePart, updateUserRequestModel) = createMultipartRequest(nickname, avgPrepTime, freeTime, imageFile) + fun updateUser(nickname: String?, avgPrepTime: Int?, freeTime: String?, imageFile: File?, context: Context) { + val imagePart = try { + val defaultProfileUrl = "https://timecalling-uploaded-files.s3.ap-northeast-2.amazonaws.com/profile_image.png" + + // 🔍 현재 `profileImage`가 기본 프로필 URL이면 drawable의 기본 이미지를 전송 + if (currentProfileImageUrl == defaultProfileUrl) { + getMultipartBodyFromResource(context, R.drawable.ic_profile_default_default, "profileImage") + } else if (imageFile != null) { + getMultipartBodyFromFile(imageFile, "profileImage") + } else { + null + } + } catch (e: Exception) { + Timber.e("Error creating MultipartBody.Part for profile image: ${e.message}") + null + } + + val userUpdateJson = JSONObject().apply { + put("nickname", nickname ?: JSONObject.NULL) + put("avgPrepTime", avgPrepTime ?: JSONObject.NULL) + put("freeTime", freeTime ?: JSONObject.NULL) + }.toString() + + val userUpdateRequestBody = userUpdateJson.toRequestBody("application/json".toMediaTypeOrNull()) viewModelScope.launch { _updateState.value = UiState.Loading - mypageRepository.updateUser(imagePart, updateUserRequestModel) + mypageRepository.updateUser(imagePart, userUpdateRequestBody) .onSuccess { Log.d("MyprofileViewModel", "updateUser() 성공") _updateState.value = UiState.Success(true) currentProfileImageUrl = imageFile?.absolutePath ?: currentProfileImageUrl - // getUser() // 업데이트 후 최신 정보 다시 가져오기 } .onFailure { exception -> Log.e("MyprofileViewModel", "updateUser() 실패: ${exception.message}\n${exception.stackTraceToString()}") @@ -116,19 +140,9 @@ class MyprofileViewModel @Inject constructor( imageFile: File? ): Pair { val userUpdateJson = JSONObject().apply { - put("nickname", nickname ?: JSONObject.NULL) // ✅ 명시적 null 값 포함 - put("avgPrepTime", avgPrepTime ?: JSONObject.NULL) // ✅ 명시적 null 값 포함 - put("freeTime", freeTime ?: JSONObject.NULL) // ✅ 명시적 null 값 포함 - - if(nickname != null) { - spf.edit().apply { - putString("nickName", nickname) // putString으로 변경 - apply() - } - } - if (imageFile == null) { - put("profileImage", currentProfileImageUrl ?: JSONObject.NULL) - } + put("nickname", nickname ?: JSONObject.NULL) + put("avgPrepTime", avgPrepTime ?: JSONObject.NULL) + put("freeTime", freeTime ?: JSONObject.NULL) }.toString() val userUpdateRequestBody = userUpdateJson.toRequestBody("application/json".toMediaTypeOrNull()) @@ -141,6 +155,27 @@ class MyprofileViewModel @Inject constructor( return Pair(imagePart, userUpdateRequestBody) } + private fun getMultipartBodyFromResource(context: Context, resourceId: Int, paramName: String): MultipartBody.Part { + val file = File(context.cacheDir, "default_profile.png") + + val inputStream: InputStream = context.resources.openRawResource(resourceId) + val outputStream = FileOutputStream(file) + + inputStream.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + + val requestFile = file.asRequestBody("image/png".toMediaTypeOrNull()) + return MultipartBody.Part.createFormData(paramName, file.name, requestFile) + } + + private fun getMultipartBodyFromFile(file: File, paramName: String): MultipartBody.Part { + val requestFile = file.asRequestBody("image/png".toMediaTypeOrNull()) + return MultipartBody.Part.createFormData(paramName, file.name, requestFile) + } + } diff --git a/app/src/main/res/layout/fragment_mypage_term.xml b/app/src/main/res/layout/fragment_mypage_term.xml new file mode 100644 index 0000000..2ac3694 --- /dev/null +++ b/app/src/main/res/layout/fragment_mypage_term.xml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_signup_term.xml b/app/src/main/res/layout/fragment_signup_term.xml index 023fc07..26a8af0 100644 --- a/app/src/main/res/layout/fragment_signup_term.xml +++ b/app/src/main/res/layout/fragment_signup_term.xml @@ -251,7 +251,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 951ba18..570f3bb 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -77,6 +77,9 @@ + +