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
Expand Up @@ -3,7 +3,6 @@ package com.eatssu.android.data.repository
import com.eatssu.android.data.dto.request.ChangeNicknameRequest
import com.eatssu.android.data.dto.request.UserDepartmentRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.MyNickNameResponse
import com.eatssu.android.data.dto.response.MyReviewResponse
import com.eatssu.android.data.dto.response.toDomain
import com.eatssu.android.data.service.UserService
Expand All @@ -17,10 +16,10 @@ import javax.inject.Inject
class UserRepositoryImpl @Inject constructor(private val userService: UserService) :
UserRepository {

override suspend fun updateUserName(body: ChangeNicknameRequest): Flow<BaseResponse<Void>> =
flow {
emit(userService.changeNickname(body))
}
override suspend fun updateUserName(body: ChangeNicknameRequest) {
userService.changeNickname(body)
}



override suspend fun checkUserNameValidation(nickname: String): Flow<BaseResponse<Boolean>> =
Expand All @@ -33,10 +32,7 @@ class UserRepositoryImpl @Inject constructor(private val userService: UserServic
emit(userService.getMyReviews())
}

override suspend fun getUserNickName(): Flow<BaseResponse<MyNickNameResponse>> =
flow {
emit(userService.getMyInfo())
}
override suspend fun getUserNickName() = userService.getMyInfo().result?.nickname ?: ""

override suspend fun signOut(): Boolean {
return userService.signOut().result ?: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.eatssu.android.domain.repository

import com.eatssu.android.data.dto.request.ChangeNicknameRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.MyNickNameResponse
import com.eatssu.android.data.dto.response.MyReviewResponse
import com.eatssu.android.domain.model.College
import com.eatssu.android.domain.model.Department
Expand All @@ -12,14 +11,14 @@ interface UserRepository {

suspend fun updateUserName(
body: ChangeNicknameRequest,
): Flow<BaseResponse<Void>>
)

suspend fun checkUserNameValidation(
nickname: String,
): Flow<BaseResponse<Boolean>>

suspend fun getUserReviews(): Flow<BaseResponse<MyReviewResponse>>
suspend fun getUserNickName(): Flow<BaseResponse<MyNickNameResponse>>
suspend fun getUserNickName(): String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오홋 앞으로 단순 api 호출 작업에서는 flow를 덜어내는 방향으로 진행하실 예정일까요?
저번에 제휴지도 부분에서 리뷰 코멘트로 단순 api호출 결과를 flow로 계속 방출하는 게 잘 이해가 안된다고 답변을 남겼던거같은데
그 후로 팀내에서 정확히 정한 부분은 없는 것 같아서요~!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유리님 말씀이 맞는 것 같아 viewmodel -> ui 말고는 suspend로 수정하려고 합니다! 이부분을 이야기하지 않은 것을 까먹었네요 😅 좋은 인사이트 감사합니다

suspend fun signOut(): Boolean

// 모든 단과대 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@ package com.eatssu.android.domain.usecase.user

import android.content.Context
import com.eatssu.android.data.MySharedPreferences
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.data.dto.response.MyNickNameResponse
import com.eatssu.android.domain.repository.UserRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import javax.inject.Inject

class GetUserNickNameUseCase @Inject constructor(
private val userRepository: UserRepository,
private val context: Context // SharedPreferences 접근용
) {
suspend operator fun invoke(): Flow<BaseResponse<MyNickNameResponse>> =
userRepository.getUserNickName().onEach { response ->
response.result?.let { nicknameResponse ->
MySharedPreferences.setUserName(context, nicknameResponse.nickname ?: "")
}
suspend operator fun invoke(): String {
return MySharedPreferences.getUserName(context).ifEmpty {
val remoteNickname = userRepository.getUserNickName()
MySharedPreferences.setUserName(context, remoteNickname)
remoteNickname
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ package com.eatssu.android.domain.usecase.user
import android.content.Context
import com.eatssu.android.data.MySharedPreferences
import com.eatssu.android.data.dto.request.ChangeNicknameRequest
import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.domain.model.UserInfo
import com.eatssu.android.domain.repository.UserRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

//class SetUserNameUseCase @Inject constructor(
Expand All @@ -26,10 +23,9 @@ class SetUserNicknameUseCase @Inject constructor(
private val userRepository: UserRepository,
@ApplicationContext private val context: Context
) {
suspend operator fun invoke(nickname: String): Flow<BaseResponse<Void>> {
suspend operator fun invoke(nickname: String) {
// 로컬 저장
MySharedPreferences.setUserName(context, nickname)

return userRepository.updateUserName(ChangeNicknameRequest(nickname))
userRepository.updateUserName(ChangeNicknameRequest(nickname))
}
}
59 changes: 30 additions & 29 deletions app/src/main/java/com/eatssu/android/presentation/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import com.eatssu.android.domain.usecase.user.GetUserNickNameUseCase
import com.eatssu.android.domain.usecase.user.SetUserCollegeDepartmentUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
Expand Down Expand Up @@ -56,35 +55,37 @@ class MainViewModel @Inject constructor(
)
}

fun fetchAndCheckNickname() {
private fun fetchAndCheckNickname() {
viewModelScope.launch {
getUserNickNameUseCase().onStart {
_uiState.value = UiState.Loading
}.catch { e ->
_uiState.value = UiState.Error
_uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.not_found)))
Timber.e(e.toString())
}.collectLatest { result ->
Timber.d(result.toString())
result.result?.let { userInfo ->
val nickname = userInfo.nickname

if (nickname.isNullOrBlank() || nickname.startsWith("user-")) {
_uiState.value = UiState.Success(MainState.NicknameNull)
_uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname)))
return@let
}

_uiState.value = UiState.Success(MainState.NicknameExists(nickname))
_uiEvent.emit(
UiEvent.ShowToast(
String.format(
context.getString(R.string.hello_user),
nickname
)
_uiState.value = UiState.Loading
runCatching {
withContext(Dispatchers.IO) { getUserNickNameUseCase() }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바뀐 코드가 더 좋네요!!

}.onSuccess { nickname ->
// 1) 닉네임 없음/기본 프리셋
if (nickname.isNullOrBlank() || nickname.startsWith("user-")) {
_uiState.value = UiState.Success(MainState.NicknameNull)
_uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.set_nickname)))
return@launch // ← 아래 분기 실행 막기
}

// 2) 정상 닉네임
_uiState.value = UiState.Success(MainState.NicknameExists(nickname))
_uiEvent.emit(
UiEvent.ShowToast(
String.format(
context.getString(R.string.hello_user),
nickname
)
)
}
)
}.onFailure { e ->
_uiState.value = UiState.Error
_uiEvent.emit(
UiEvent.ShowToast(
context.getString(R.string.not_found)
)
)
Timber.e(e)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Paint
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
Expand All @@ -20,15 +20,18 @@ import androidx.lifecycle.repeatOnLifecycle
import com.eatssu.android.R
import com.eatssu.android.databinding.FragmentMyPageBinding
import com.eatssu.android.presentation.MainViewModel
import com.eatssu.android.presentation.UiEvent
import com.eatssu.android.presentation.UiState
import com.eatssu.android.presentation.base.BaseFragment
import com.eatssu.android.presentation.login.LoginActivity
import com.eatssu.android.presentation.mypage.myreview.MyReviewListActivity
import com.eatssu.android.presentation.mypage.terms.WebViewActivity
import com.eatssu.android.presentation.mypage.userinfo.UserInfoActivity
import com.eatssu.android.presentation.util.showToast
import com.eatssu.common.enums.ScreenId
import com.google.android.gms.oss.licenses.OssLicensesMenuActivity
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDateTime
Expand All @@ -46,46 +49,85 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.tvSignout.paintFlags = Paint.UNDERLINE_TEXT_FLAG
setupObservers()
setOnClickListener()
}

override fun onResume() {
super.onResume()
myPageViewModel.fetchMyInfo() // 닉네임 변경 등으로부터 복귀 시 정보 갱신
}

Comment on lines +57 to +61
Copy link
Member

@kangyuri1114 kangyuri1114 Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

결과적으로 닉네임 수정 후 화면에 바로 반영 안되는 오류는 onresume에서 호출하는 방식으로 해결하신것 같아요

onResume에서 api로 닉네임을 계속 호출하고 있잖아요
사실 이 이슈를 제가 가져가려고 전부터 머리로만 계속 생각했었는데
저는 myInfoActivity에서 성공적으로 닉네임을 반영한 경우 sharedPreference를 업데이트하고,
mypage에서 닉네임은 sharedPreference값을 보여주는 방식으로 수정할 생각이었거든요

혹시 이 부분에 대해서는 어떻게 생각하시나요??

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 사실 이슈를 수정하려고 한건 아닌데 어쩌다보니 함께 수정이 되었습니다.. 😮

유리님께서 말씀하신 방법으로 동작합니다!

닉네임 수정하면 setNicknameUsecase에서 api호출이랑 sharedPreference set 함수 둘다 호출되고
getNicknameUsecase 호출하면 sharedPreference 먼저 검사해서 있으면 로컬 값 주고 비어있으면 api호출하고 sharedPreference set합니다!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하 그렇군요!! 수고하셨씁니당~!

private fun setupObservers() {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
myPageViewModel.uiState.collect {
binding.tvAppVersion.text = it.appVersion
launch {
myPageViewModel.uiState.collectLatest { ui ->
when (ui) {
is UiState.Init, UiState.Loading -> Unit // 닉네임만 불러옴으로 로딩 인디케이터 없음
is UiState.Success -> {
ui.data?.let { render(it) }
}

if (it.nickname.isNotEmpty()) {
binding.tvNickname.text = it.nickname
is UiState.Error -> {
showToast(getString(R.string.not_found))
}
}
}

binding.alarmSwitch.setOnCheckedChangeListener(null)
binding.alarmSwitch.isChecked = it.isAlarmOn
binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked ->
handleAlarmSwitchChange(isChecked)
}
launch {
myPageViewModel.uiEvent.collectLatest { event ->
when (event) {
is UiEvent.ShowToast -> showToast(event.message)
else -> Unit
}
}
}
}
}
}

private fun render(state: MyPageState) {
// 앱 버전
binding.tvAppVersion.text = state.appVersion

// 닉네임
if (state.hasNickname) {
binding.tvNickname.text = state.nickname
} else {
// 필요 시 미설정 안내 문구
binding.tvNickname.text = "닉네임을 설정해주세요"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

"닉네임을 설정해주세요" 문자열이 하드코딩되어 있습니다. 다국어 지원 및 유지보수 편의성을 위해 strings.xml 리소스 파일로 추출하는 것을 권장합니다.

Suggested change
binding.tvNickname.text = "닉네임을 설정해주세요"
binding.tvNickname.text = getString(R.string.mypage_set_nickname_prompt)

}

// 알람 스위치 (리스너 잠시 해제 후 값 반영)
binding.alarmSwitch.setOnCheckedChangeListener(null)
binding.alarmSwitch.isChecked = state.isAlarmOn
binding.alarmSwitch.setOnCheckedChangeListener { _, isChecked ->
handleAlarmSwitchChange(isChecked)
}
}

private fun handleAlarmSwitchChange(isChecked: Boolean) {
val nowDatetime = LocalDateTime.now()
val formattedDate = nowDatetime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))

if (isChecked) {
if (checkNotificationPermission(requireContext())) {
myPageViewModel.setNotificationOn()
showSnackbar("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate")
showToast("EAT-SSU 알림 수신을 동의하였습니다.\n$formattedDate")
} else {
showNotificationPermissionDialog()
// 권한 미허용이면 스위치 원복
binding.alarmSwitch.setOnCheckedChangeListener(null)
binding.alarmSwitch.isChecked = false
binding.alarmSwitch.setOnCheckedChangeListener { _, checked ->
handleAlarmSwitchChange(checked)
}
Comment on lines +121 to +126
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

알림 스위치의 OnCheckedChangeListener를 설정하는 로직이 render 함수(104-108 라인)와 handleAlarmSwitchChange 함수에 중복되어 있습니다. 이 로직을 별도의 헬퍼 함수로 추출하여 코드 중복을 줄이고 가독성을 높이는 것을 고려해 보세요.

}
} else {
myPageViewModel.setNotificationOff()
showSnackbar("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate")
showToast("EAT-SSU 알림 수신을 거부하였습니다.\n$formattedDate")
}
}

Expand All @@ -111,8 +153,10 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN)
}

binding.llSignout.setOnClickListener {
// 현재 Success 상태에서 안전하게 닉네임 추출
val nickname = (myPageViewModel.uiState.value as? UiState.Success)?.data?.nickname
Intent(requireContext(), SignOutActivity::class.java).apply {
putExtra("nickname", myPageViewModel.uiState.value.nickname)
putExtra("nickname", nickname)
startActivity(this)
}
}
Expand All @@ -121,13 +165,9 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN)
startActivity(Intent(requireContext(), DeveloperActivity::class.java))
}

binding.llOss.setOnClickListener {
moveToOss()
}
binding.llOss.setOnClickListener { moveToOss() }

binding.llAppVersion.setOnClickListener {
moveToPlayStore()
}
binding.llAppVersion.setOnClickListener { moveToPlayStore() }

binding.llServiceRule.setOnClickListener {
startWebView(
Expand Down Expand Up @@ -182,16 +222,15 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN)
try {
startActivity(Intent(requireContext(), OssLicensesMenuActivity::class.java))
} catch (e: Exception) {
showSnackbar("오픈소스 라이브러리를 불러올 수 없습니다.")
showToast("오픈소스 라이브러리를 불러올 수 없습니다.")
Timber.e("Error opening OSS Licenses: ${e.message}")
}
}

private fun moveToPlayStore() {
val appPackageName = requireContext().packageName
val uri = Uri.parse("market://details?id=$appPackageName")
val fallbackUri = Uri.parse("https://play.google.com/store/apps/details?id=$appPackageName")

val uri = "market://details?id=$appPackageName".toUri()
val fallbackUri = "https://play.google.com/store/apps/details?id=$appPackageName".toUri()
try {
startActivity(Intent(Intent.ACTION_VIEW, uri))
} catch (e: Exception) {
Expand All @@ -206,10 +245,6 @@ class MyPageFragment : BaseFragment<FragmentMyPageBinding>(ScreenId.MYPAGE_MAIN)
context.startActivity(intent)
}

private fun showSnackbar(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show()
}

private fun startWebView(url: String, title: String, screenId: ScreenId) {
val intent = Intent(requireContext(), WebViewActivity::class.java).apply {
putExtra("URL", url)
Expand Down
Loading