diff --git a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt index 79b16339..6731edf9 100644 --- a/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/account/view/AccountFragment.kt @@ -86,17 +86,22 @@ class AccountFragment : Fragment() { viewModel.userIdState.collect { state -> when (state) { - is UiState.Idle -> Unit + is UiState.Idle -> { + binding.tvRealAccountId.visibility = View.INVISIBLE + } is UiState.Loading -> { + binding.tvRealAccountId.visibility = View.INVISIBLE //추후 로딩뷰를 삽입하자 } is UiState.Success -> { + binding.tvRealAccountId.visibility = View.VISIBLE binding.tvRealAccountId.text = state.data } is UiState.Failure -> { + binding.tvRealAccountId.visibility = View.INVISIBLE Toast.makeText(requireContext(), "유저 id를 가져올 수 없습니다.", Toast.LENGTH_SHORT).show() } } @@ -188,7 +193,7 @@ class AccountFragment : Fragment() { accountDeleteDialog1Fragment.show(childFragmentManager, "AccountDeleteDialog1Fragment") } - // ✅ 고객지원 클릭 + // 고객지원 클릭 tvSupport.setOnClickListener { copySupportEmailToClipboard() } diff --git a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt index 7ad51c7c..e7922694 100644 --- a/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/account/viewmodel/AccountViewModel.kt @@ -17,8 +17,8 @@ import com.egobook.app.domain.model.auth.AuthError class AccountViewModel @Inject constructor( private val accountRepository: AccountRepository ) : ViewModel() { - private val _userIdState = MutableStateFlow>(UiState.Idle) - val userIdState = _userIdState.asStateFlow() + private val _userIdState = MutableStateFlow>(UiState.Idle) + val userIdState = _userIdState.asStateFlow() private val _linkState = MutableStateFlow>(UiState.Idle) val linkState = _linkState.asStateFlow() diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryListFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryListFragment.kt index cf57af6a..940b5f12 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryListFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryListFragment.kt @@ -18,6 +18,7 @@ import com.egobook.app.databinding.FragmentDiaryListBinding import com.egobook.app.domain.model.diary.entity.DiarySummary import com.egobook.app.ui.diary.adapter.DiaryRVAdapter import com.egobook.app.ui.diary.viewmodel.DiariesViewModel +import com.egobook.app.util.UiState import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -64,20 +65,48 @@ class DiaryListFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collectLatest { state -> - state.diaries.collectLatest { pagingData -> - diaryRVAdapter.submitData(pagingData) + when (val diariesState = state.diaries) { + is UiState.Loading -> { + // Paging3의 LoadState로 로딩 관리 + } + is UiState.Success -> { + // 데이터 Flow 수집 + launch { + diariesState.data.collectLatest { pagingData -> + diaryRVAdapter.submitData(pagingData) + } + } + } + is UiState.Failure -> { + // TODO: 에러 처리 + } + is UiState.Idle -> { } } } } } - - // LoadState를 관찰하여 빈 상태 처리 + + // Paging3 LoadState로 프로그레스바 + 빈 상태 관리 viewLifecycleOwner.lifecycleScope.launch { diaryRVAdapter.loadStateFlow.collectLatest { loadStates -> - val isEmpty = loadStates.refresh is LoadState.NotLoading && diaryRVAdapter.itemCount == 0 - - binding.layoutEmpty.isVisible = isEmpty - binding.rvDiary.isVisible = !isEmpty + when (val refreshState = loadStates.refresh) { + is LoadState.Loading -> { + // 초기 로딩 중 + binding.progressBar.isVisible = true + binding.rvDiary.isVisible = false + binding.layoutEmpty.isVisible = false + } + is LoadState.NotLoading -> { + binding.progressBar.isVisible = false + val isEmpty = diaryRVAdapter.itemCount == 0 + binding.layoutEmpty.isVisible = isEmpty + binding.rvDiary.isVisible = !isEmpty + } + is LoadState.Error -> { + binding.progressBar.isVisible = false + // TODO: 에러 UI 처리 + } + } } } } diff --git a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryWriteFragment.kt b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryWriteFragment.kt index b87a5fca..e6cd0c69 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/view/DiaryWriteFragment.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/view/DiaryWriteFragment.kt @@ -1,6 +1,5 @@ package com.egobook.app.ui.diary.view -import android.graphics.Color import android.os.Bundle import android.text.Editable import android.text.InputFilter @@ -8,11 +7,11 @@ import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import android.widget.Toast import androidx.annotation.DrawableRes import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -27,6 +26,7 @@ import com.egobook.app.ui.util.toDateTimeString import com.egobook.app.ui.util.toDayOfMonthString import com.egobook.app.ui.util.toMonthString import com.egobook.app.ui.util.toYearString +import com.egobook.app.util.UiState import com.google.android.material.imageview.ShapeableImageView import com.google.gson.Gson import dagger.hilt.android.AndroidEntryPoint @@ -90,7 +90,8 @@ class DiaryWriteFragment : Fragment() { setupDiaryContentEditText() // 일기 내용 입력 필드 설정 (글자수 제한, TextWatcher) observeSelectedDate() // 선택된 날짜 관찰 및 UI 업데이트 observeContentState() // 컨텐츠 상태 관찰 (글자수, 감정 섹션, 저장 버튼 활성화) - observeSaveResult() // 저장 성공/실패 관찰 + observeSaveResult() // 저장 성공/실패 관찰 + observeDiaryLoadState() // 수정 모드 데이터 로드 상태 관찰 } private fun setupDiaryTypeCards() { @@ -176,6 +177,11 @@ class DiaryWriteFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.contentState.collectLatest { state -> + // 로딩 중이면 UI 업데이트 스킵 (프로그레스바 표시 중) + if (state.diaryLoadState is UiState.Loading) { + return@collectLatest + } + // 글자수 표시 업데이트 (예: 0/400, 1/400, ...) binding.tvCharCount.text = "${state.charCount}/${state.maxCharCount}" @@ -208,6 +214,42 @@ class DiaryWriteFragment : Fragment() { } } } + + private fun observeDiaryLoadState() { + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.contentState.collectLatest { state -> + when (val loadState = state.diaryLoadState) { + is UiState.Loading -> { + binding.progressBar.isVisible = true + // 입력 UI 숨기기 + binding.guideLayout.isVisible = false + binding.typeLayout.isVisible = false + binding.tvHowIsYourFeeling.isVisible = false + binding.stateLayout.isVisible = false + binding.emotionLayout.isVisible = false + binding.inputBoxLayout.isVisible = false + binding.btnSave.isVisible = false + } + is UiState.Success, is UiState.Idle -> { + binding.progressBar.isVisible = false + // 입력 UI 표시 + binding.guideLayout.isVisible = true + binding.typeLayout.isVisible = true + // 감정 섹션은 선택 상태에 따라 표시 (observeContentState에서 처리) + binding.inputBoxLayout.isVisible = true + binding.btnSave.isVisible = true + } + is UiState.Failure -> { + binding.progressBar.isVisible = false + // 에러 처리 (필요시 토스트 또는 에러 UI 표시) + Toast.makeText(requireContext(), loadState.message ?: "데이터 로드에 실패했습니다.", Toast.LENGTH_SHORT).show() + } + } + } + } + } + } private fun updateEmotionImages(selectedLevel: Int) { // 모든 감정 이미지 업데이트 @@ -257,6 +299,11 @@ class DiaryWriteFragment : Fragment() { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.saveResult.collectLatest { result -> when (result) { + is DiaryWriteViewModel.SaveResult.Loading -> { + // 저장 중 + binding.progressBar.visibility = View.VISIBLE + binding.btnSave.isEnabled = false + } is DiaryWriteViewModel.SaveResult.Success -> { // 저장 성공 -> 결과를 이전 화면(DiaryFragment)에 전달하고 이동 val messages = result.toastMessages diff --git a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiariesViewModel.kt b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiariesViewModel.kt index 603ce800..c1de6041 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiariesViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiariesViewModel.kt @@ -9,12 +9,12 @@ import com.egobook.app.domain.model.diary.entity.DiarySummary import com.egobook.app.domain.model.diary.entity.DiaryType import com.egobook.app.domain.usecase.diaryusecase.DiaryUseCases import com.egobook.app.ui.diary.mapper.DiaryEntityMapper +import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.launch import java.time.LocalDate import javax.inject.Inject @@ -61,11 +61,15 @@ class DiariesViewModel @Inject constructor( //상태를 보지 말고 뷰모델 내부 state 기반으로만 동작 private fun loadDiaries(selectedDate: LocalDate, types: Set?) { val filter = DiaryFilter(selectedDate, types) + + // 로딩 상태 설정 + _state.value = state.value.copy(diaries = UiState.Loading) + val diariesFlow = diaryUseCases .getDiaries(filter) .cachedIn(viewModelScope) - _state.value = state.value.copy(diaries = diariesFlow) + _state.value = state.value.copy(diaries = UiState.Success(diariesFlow)) // dailyCount도 함께 로드 //loadDailyCount(selectedDate) @@ -99,7 +103,7 @@ sealed class DiariesEvent { } data class DiariesState( - val diaries: Flow> = emptyFlow(), + val diaries: UiState>> = UiState.Idle, val selectedTabType: Set? = null, // 내부 로직용 diff --git a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiaryWriteViewModel.kt b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiaryWriteViewModel.kt index 09d7b92c..c6f770f3 100644 --- a/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiaryWriteViewModel.kt +++ b/app/src/main/java/com/egobook/app/ui/diary/viewmodel/DiaryWriteViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.egobook.app.domain.usecase.diaryusecase.DiaryUseCases import com.egobook.app.ui.diary.mapper.DiaryEntityMapper import com.egobook.app.ui.diary.model.ToastMessage +import com.egobook.app.util.UiState import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -16,7 +17,6 @@ import kotlinx.coroutines.launch import java.time.LocalDate import java.time.LocalDateTime import javax.inject.Inject -import com.egobook.app.domain.model.diary.entity.DiaryRewards @HiltViewModel @@ -64,6 +64,9 @@ class DiaryWriteViewModel @Inject constructor( */ private fun loadDiaryForEdit() { viewModelScope.launch { + // 로딩 상태 설정 + _contentState.value = _contentState.value.copy(diaryLoadState = UiState.Loading) + diaryUseCases.getDiary(diaryId) .onSuccess { diary -> if (diary != null) { @@ -78,12 +81,20 @@ class DiaryWriteViewModel @Inject constructor( selectedTypes = displayTypes, selectedEmotionLevel = diary.emotionLevel ?: 3, charCount = diary.content.length, - isSaveButtonEnabled = true + isSaveButtonEnabled = true, + diaryLoadState = UiState.Success(Unit) + ) + } else { + _contentState.value = _contentState.value.copy( + diaryLoadState = UiState.Failure("일기를 찾을 수 없습니다.") ) } } - .onFailure { - // 로드 실패 시 에러 처리 (필요시 Toast 등으로 알림) + .onFailure { error -> + // 로드 실패 시 에러 상태 설정 + _contentState.value = _contentState.value.copy( + diaryLoadState = UiState.Failure(error.message) + ) } } } @@ -160,6 +171,8 @@ class DiaryWriteViewModel @Inject constructor( * 수정 모드 저장 처리 */ private suspend fun saveEditMode(state: ContentState) { + _saveResult.emit(SaveResult.Loading) + val emotionLevel = if (state.selectedTypes.contains("감정")) { state.selectedEmotionLevel } else { @@ -191,6 +204,8 @@ class DiaryWriteViewModel @Inject constructor( private suspend fun saveCreateMode(state: ContentState) { val now = LocalDateTime.now() + _saveResult.emit(SaveResult.Loading) + val newDiary = DiaryEntityMapper.createNewDiary( selectedTypes = state.selectedTypes, content = state.content, @@ -217,6 +232,7 @@ class DiaryWriteViewModel @Inject constructor( } sealed class SaveResult { + object Loading: SaveResult() data class Success(val toastMessages: List) : SaveResult() data class Error(val message: String?) : SaveResult() } @@ -237,7 +253,8 @@ class DiaryWriteViewModel @Inject constructor( val isHintVisible: Boolean = false, val charCount: Int = 0, val maxCharCount: Int = 400, - val isSaveButtonEnabled: Boolean = false // 저장 버튼 활성화 유무 + val isSaveButtonEnabled: Boolean = false, // 저장 버튼 활성화 유무 + val diaryLoadState: UiState = UiState.Idle // 수정 모드 데이터 로드 상태 ) } \ No newline at end of file diff --git a/app/src/main/java/com/egobook/app/ui/login/view/LoginActivity.kt b/app/src/main/java/com/egobook/app/ui/login/view/LoginActivity.kt index 2bd7c9b0..7a3dcaca 100644 --- a/app/src/main/java/com/egobook/app/ui/login/view/LoginActivity.kt +++ b/app/src/main/java/com/egobook/app/ui/login/view/LoginActivity.kt @@ -182,30 +182,40 @@ import javax.inject.Inject } } private fun observeLoginState() { - lifecycleScope.launch { - viewModel.loginState.collect { state -> - when (state) { - is LoginState.Success -> { - Toast.makeText( - this@LoginActivity, - "로그인 성공!", - Toast.LENGTH_SHORT - ).show() - navigateToMain() - } - is LoginState.Error -> { - val message = state.error.message ?: "알 수 없는 오류가 발생했습니다" - Toast.makeText( - this@LoginActivity, - message, - Toast.LENGTH_SHORT - ).show() - } - else -> {} + lifecycleScope.launch { + viewModel.loginState.collect { state -> + when (state) { + is LoginState.Loading -> { + binding.progressBar.visibility = View.VISIBLE + binding.btnLogin.isEnabled = false + binding.btnGoogleLogin.isEnabled = false + binding.btnGuestLogin.isEnabled = false + } + is LoginState.Success -> { + binding.progressBar.visibility = View.GONE + Toast.makeText( + this@LoginActivity, + "로그인 성공!", + Toast.LENGTH_SHORT + ).show() + navigateToMain() + } + is LoginState.Error -> { + binding.progressBar.visibility = View.GONE + val message = state.error.message ?: "알 수 없는 오류가 발생했습니다" + Toast.makeText( + this@LoginActivity, + message, + Toast.LENGTH_SHORT + ).show() + } + else -> { + binding.progressBar.visibility = View.GONE } } } } + } private fun observeFirstSignUp() { lifecycleScope.launch { diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 9d184c41..77b7ace7 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -19,6 +19,18 @@ android:layout_height="match_parent" android:background="@drawable/img_login_background_ver2"> + + + android:fontFamily="@font/arita_semibold" + android:gravity="center"/> - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_diary_list.xml b/app/src/main/res/layout/fragment_diary_list.xml index 0fd1a09e..14f17a35 100644 --- a/app/src/main/res/layout/fragment_diary_list.xml +++ b/app/src/main/res/layout/fragment_diary_list.xml @@ -13,7 +13,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"/> - + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_diary_write.xml b/app/src/main/res/layout/fragment_diary_write.xml index fab86c15..b0db0837 100644 --- a/app/src/main/res/layout/fragment_diary_write.xml +++ b/app/src/main/res/layout/fragment_diary_write.xml @@ -391,5 +391,17 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" /> + +