Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
@@ -1,5 +1,6 @@
package com.alarmy.near.presentation.feature.friendprofile

import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
Expand Down Expand Up @@ -79,7 +80,7 @@ import kotlinx.coroutines.launch
fun FriendProfileRoute(
viewModel: FriendProfileViewModel = hiltViewModel(),
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit = {},
onClickBackButton: (Friend?) -> Unit = {},
onEditFriendInfo: (Friend) -> Unit = {},
onClickCallButton: (phoneNumber: String) -> Unit = {},
onClickMessageButton: (phoneNumber: String) -> Unit = {},
Expand Down Expand Up @@ -107,6 +108,11 @@ fun FriendProfileRoute(
}
}
}
BackHandler {
// 다이얼로그 등이 있으면 해당 백 처리를 우선 수행
onClickBackButton((friendState.value as? FriendState.Success)?.friend)
}

FriendProfileScreen(
friendState = friendState.value,
friendShipRecordState = friendShipRecordState.value,
Expand All @@ -129,7 +135,7 @@ fun FriendProfileScreen(
friendState: FriendState,
friendShipRecordState: FriendShipRecordState,
recordSuccessDialogState: Boolean = false,
onClickBackButton: () -> Unit = {},
onClickBackButton: (Friend) -> Unit = {},

Choose a reason for hiding this comment

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

critical

FriendProfileScreenonClickBackButton 파라미터 타입이 (Friend) -> Unit으로 선언되어 있습니다. 하지만 FriendProfileRoute에서는 (Friend?) -> Unit 타입의 콜백을 전달하고 있어 타입 불일치로 인해 컴파일 오류가 발생할 수 있습니다. BackHandler에서도 nullable한 Friend 객체를 전달할 수 있으므로, FriendProfileScreenonClickBackButton 타입을 (Friend?) -> Unit으로 변경하여 일관성을 맞추고 잠재적인 오류를 해결하는 것이 좋습니다.

Suggested change
onClickBackButton: (Friend) -> Unit = {},
onClickBackButton: (Friend?) -> Unit = {},

onEditFriendInfo: (Friend) -> Unit = {},
onClickCallButton: (phoneNumber: String) -> Unit = {},
onClickMessageButton: (phoneNumber: String) -> Unit = {},
Expand Down Expand Up @@ -190,7 +196,9 @@ fun FriendProfileScreen(
// (1) 상단 AppBar — 고정
NearTopAppbar(
title = stringResource(R.string.friend_profile_title),
onClickBackButton = onClickBackButton,
onClickBackButton = {
onClickBackButton(friend)
},
menuButton = {
Column(modifier = Modifier.padding(end = 20.dp)) {
Image(
Expand Down Expand Up @@ -345,7 +353,7 @@ fun FriendProfileScreen(
NearTheme.colors.GRAY01_888888.copy(
alpha = 0.3f,
),
selected = currentTabPosition.intValue == 0,
selected = currentTabPosition.intValue == 0,
onClick = { currentTabPosition.intValue = 0 },
) {
Text(
Expand Down Expand Up @@ -375,9 +383,10 @@ fun FriendProfileScreen(
.height(50.dp),
selected = currentTabPosition.intValue == 1,
onClick = { currentTabPosition.intValue = 1 },
selectedContentColor = NearTheme.colors.GRAY01_888888.copy(
alpha = 0.3f
),
selectedContentColor =
NearTheme.colors.GRAY01_888888.copy(
alpha = 0.3f,
),
) {
Text(
text = stringResource(R.string.friend_profile_tab_text_record),
Expand Down Expand Up @@ -424,14 +433,21 @@ fun FriendProfileScreen(
// (3) 하단 고정 버튼
Box(
modifier =
Modifier.fillMaxWidth().background(
brush =
Brush.linearGradient(
colors = listOf(Color(0x00FFFFFF), Color(0xFFFFFFFF)), // 파랑 → 밝은 하늘색
start = Offset(0f, 0f), // 위쪽 시작
end = Offset(0f, Float.POSITIVE_INFINITY),
),
),
Modifier
.fillMaxWidth()
.background(
brush =
Brush.linearGradient(
colors =
listOf(
Color(0x00FFFFFF),
Color(0xFFFFFFFF),
),
// 파랑 → 밝은 하늘색
start = Offset(0f, 0f), // 위쪽 시작
end = Offset(0f, Float.POSITIVE_INFINITY),
),
),
) {
NearSolidTypeButton(
modifier =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ class FriendProfileViewModel
}
if (friendFlow.value is FriendState.Success) {
_friendFlow.update {
val updatedLastContactAt = getTodayDashFormat()
(it as FriendState.Success).copy(
friend =
it.friend.copy(
lastContactAt = getTodayDashFormat(),
lastContactFormat = it.friend.lastContactAt?.contactFormat(),
lastContactAt = updatedLastContactAt,
lastContactFormat = updatedLastContactAt.contactFormat(),
isContactToday =
it.friend.lastContactAt?.isToday()
?: false,
updatedLastContactAt.isToday(),
),
)
}
Expand Down Expand Up @@ -163,5 +163,5 @@ class FriendProfileViewModel
val targetDate = LocalDate.parse(this, formatter)
val today = LocalDate.now()
return targetDate == today
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fun NavController.navigateToFriendProfile(

fun NavGraphBuilder.friendProfileNavGraph(
onShowErrorSnackBar: (throwable: Throwable?) -> Unit,
onClickBackButton: () -> Unit,
onClickBackButton: (Friend?) -> Unit,
onEditFriendInfo: (Friend) -> Unit = {},
onClickCallButton: (phoneNumber: String) -> Unit = {},
onClickMessageButton: (phoneNumber: String) -> Unit = {},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.alarmy.near.presentation.feature.home

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.alarmy.near.data.repository.FriendRepository
Expand Down Expand Up @@ -75,13 +76,13 @@ class HomeViewModel
monthlyFriends.filter { it.friendId !in deletedIds }
}.catch {
_errorEvent.send(it)
}.collect {
_uiState.update { state ->
state.copy(
monthlyFriendUIState = MonthlyFriendUIState.Success(it),
)
}
}.collect {
_uiState.update { state ->
state.copy(
monthlyFriendUIState = MonthlyFriendUIState.Success(it),
)
}
}
}
}
}
Expand All @@ -91,4 +92,30 @@ class HomeViewModel
deletedFriendIdsFlow.emit(deletedFriendIdsFlow.value + friendId)
}
}

fun updateFriendReminder(
friendId: String,
friendReminderUpdatedAt: String?,
) {
_uiState.update { state ->
val current = state.myFriendUIState

if (current is MyFriendUIState.Success) {
val updatedList =
current.myFriends.map { friend ->
if (friend.id == friendId) {
friend.copy(lastContactedAt = friendReminderUpdatedAt)
} else {
friend
}
}

return@update state.copy(
myFriendUIState = MyFriendUIState.Success(updatedList),
)
}

state
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package com.alarmy.near.presentation.feature.home.navigation

import android.os.Parcelable
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import com.alarmy.near.presentation.feature.home.HomeRoute
import com.alarmy.near.presentation.feature.home.HomeViewModel
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable

const val HOME_FRIEND_DELETE_COMPLETE_KEY = "HOME_FRIEND_DELETE_COMPLETE_KEY"
const val HOME_RESULT_EVENT = "HOME_RESULT_EVENT"

@Serializable
object RouteHome

sealed interface HomeNavigationEvent : Parcelable {
@Parcelize
@Serializable
data class FriendDeleted(
val friendId: String,
) : HomeNavigationEvent

@Parcelize
@Serializable
data class FriendReminderUpdated(
val friendId: String,
val friendReminderUpdatedAt: String?,
) : HomeNavigationEvent
}

/*
* 추후 홈으로 화면 이동이 필요할 때 이 함수를 사용합니다.
* */
Expand All @@ -31,10 +49,18 @@ fun NavGraphBuilder.homeNavGraph(
) {
composable<RouteHome> { backStackEntry ->
Copy link
Contributor

Choose a reason for hiding this comment

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

제미나이가 remove()로 이벤트를 소비하라는 리뷰를 주었네요!

네비게이션에서는 라우팅만 담당하는 것을 선호하여 ViewModel 사용을 자제하고 있었는데
네비게이션에서 ViewModel을 생성한다면 이점이 있나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

연락 일시 데이터를 이전 화면에서 가지고 와서 viewModel 상태에 반영하기 위함이었습니다!
ViewModel에서 바로 데이터를 받아 쓰고싶은데,saveStateHandle collect로 데이터를 못 받아왔던 것으로 기억합니다🥲
혹시 가능한데, 제가 잘못쓰고 있었던 것일까요?!

Copy link
Contributor

Choose a reason for hiding this comment

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

앗 아닙니다! Navigation 쪽에서 ViewModel을 생성하는 것이 낯설어서 여쭤봤어요 :)
구조적으로 문제가 없다면 이 상태도 좋을 것 같습니다!
saveStateHandle collect로 데이터를 못 받는 현상이 있군요.. 의견으로는 그렇다면
LaunchEffect와 lifecycleOwner를 활용해 RESUME 상태일 때 데이터를 반영하면 어떻까요? 문제가 있을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

답변이 늦었네요😅 lifecycleOwner는 액티비티 생명주기 변경에 대한 중복 이벤트가 막아지는 것으로 알고있습니다!
현재 문제는 리컴포지션에 문제가 되는 것이라 LaunchedEffect에 키를 주어서 대응이 가능해보여요!

저도 안 viewModel에서 못받는 것에 대해서 테스트를 조금 해봤는데, backStackEntry.savedStateHandle 는 이벤트가 넘어 오는데, viewModel saveStateHandle는 이벤트가 안넘어오네요!
정확한 원리는 아직 파악은 못했는데, viewModel에서 쓰이는 saveStateHandle과 composable에서 쓰이는 saveStateHandle이 다른 객체네요! SharedViewModel을 고려해서 그렇게 설계가 된 것일 수도 있겠네요!
참고한 링크 공유드려요!

리컴포지션 문제에 대해서는 LaunchedEffect로 해결하겠습니다!

Copy link
Contributor

@stopstone stopstone Dec 15, 2025

Choose a reason for hiding this comment

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

아 링크 감사합니다! 저도 더 자세히 알게 되었네요 :D
동작에는 문제 없는거 확인해서 머지해도 좋을 것 같습니다!

val viewModel: HomeViewModel = hiltViewModel()
val friendId: String? = backStackEntry.savedStateHandle.get<String>(HOME_FRIEND_DELETE_COMPLETE_KEY)
friendId?.let {
viewModel.deleteFriend(it)
val homeEvent = backStackEntry.savedStateHandle.get<HomeNavigationEvent>(HOME_RESULT_EVENT)
when (homeEvent) {
is HomeNavigationEvent.FriendDeleted -> {
viewModel.deleteFriend(homeEvent.friendId)
}

is HomeNavigationEvent.FriendReminderUpdated -> {
viewModel.updateFriendReminder(homeEvent.friendId, homeEvent.friendReminderUpdatedAt)
}
null -> Unit
}
Comment on lines 55 to 67

Choose a reason for hiding this comment

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

high

SavedStateHandle에서 homeEvent를 가져온 후, 이벤트를 처리하고 나서 remove()를 호출하여 이벤트를 소비하는 로직이 필요합니다. 현재 코드에서는 이벤트를 제거하지 않기 때문에, 화면 회전과 같은 설정 변경이나 다른 이유로 리컴포지션이 발생할 때마다 deleteFriend 또는 updateFriendReminder 함수가 반복적으로 호출될 수 있습니다. 이는 의도치 않은 동작이나 앱의 오작동을 유발할 수 있습니다.

        val homeEvent = backStackEntry.savedStateHandle.get<HomeNavigationEvent>(HOME_RESULT_EVENT)
        if (homeEvent != null) {
            when (homeEvent) {
                is HomeNavigationEvent.FriendDeleted -> {
                    viewModel.deleteFriend(homeEvent.friendId)
                }

                is HomeNavigationEvent.FriendReminderUpdated -> {
                    viewModel.updateFriendReminder(homeEvent.friendId, homeEvent.friendReminderUpdatedAt)
                }
                null -> Unit
            }
            backStackEntry.savedStateHandle.remove<HomeNavigationEvent>(HOME_RESULT_EVENT)
        }


HomeRoute(
onShowErrorSnackBar = onShowErrorSnackBar,
onContactClick = onContactClick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.core.net.toUri
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.navOptions
import com.alarmy.near.model.Friend
import com.alarmy.near.presentation.feature.contact.navigation.CONTACT_SELECTION_COMPLETE_KEY
import com.alarmy.near.presentation.feature.contact.navigation.contactNavGraph
import com.alarmy.near.presentation.feature.contact.navigation.navigateToContact
Expand All @@ -18,7 +19,8 @@ import com.alarmy.near.presentation.feature.friendprofile.navigation.navigateToF
import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.FRIEND_PROFILE_EDIT_COMPLETE_KEY
import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.friendProfileEditorNavGraph
import com.alarmy.near.presentation.feature.friendprofileedittor.navigation.navigateToFriendProfileEditor
import com.alarmy.near.presentation.feature.home.navigation.HOME_FRIEND_DELETE_COMPLETE_KEY
import com.alarmy.near.presentation.feature.home.navigation.HOME_RESULT_EVENT
import com.alarmy.near.presentation.feature.home.navigation.HomeNavigationEvent
import com.alarmy.near.presentation.feature.home.navigation.homeNavGraph
import com.alarmy.near.presentation.feature.home.navigation.navigateToHome
import com.alarmy.near.presentation.feature.login.navigation.loginNavGraph
Expand Down Expand Up @@ -119,40 +121,57 @@ internal fun NearNavHost(
)

// 친구 프로필 화면 NavGraph
friendProfileNavGraph(onShowErrorSnackBar = onShowSnackbar, onClickBackButton = {
navController.popBackStack()
}, onClickCallButton = { phoneNumber ->
val intent =
Intent(Intent.ACTION_DIAL).apply {
data = "tel:$phoneNumber".toUri()
}
context.startActivity(intent)
}, onClickMessageButton = { phoneNumber ->
val intent =
Intent(Intent.ACTION_VIEW).apply {
data = "sms:$phoneNumber".toUri()
friendProfileNavGraph(
onShowErrorSnackBar = onShowSnackbar,
onClickBackButton = { friend: Friend? ->
friend?.let {
navController.previousBackStackEntry?.savedStateHandle?.set(
HOME_RESULT_EVENT,
HomeNavigationEvent.FriendReminderUpdated(
friend.friendId,
friend.lastContactAt,
),
)
}
context.startActivity(intent)
}, onEditFriendInfo = {
navController.navigateToFriendProfileEditor(
friend =
it.copy(
imageUrl =
it.imageUrl?.let { imageUrl ->
URLEncoder.encode(
imageUrl,
StandardCharsets.UTF_8.toString(),
)
},
),
)
}, onDeleteFriendSuccess = {
navController.previousBackStackEntry?.savedStateHandle?.set(
HOME_FRIEND_DELETE_COMPLETE_KEY,
it,
)
navController.popBackStack()
})

navController.popBackStack()
},
onClickCallButton = { phoneNumber ->
val intent =
Intent(Intent.ACTION_DIAL).apply {
data = "tel:$phoneNumber".toUri()
}
context.startActivity(intent)
},
onClickMessageButton = { phoneNumber ->
val intent =
Intent(Intent.ACTION_VIEW).apply {
data = "sms:$phoneNumber".toUri()
}
context.startActivity(intent)
},
onEditFriendInfo = {
navController.navigateToFriendProfileEditor(
friend =
it.copy(
imageUrl =
it.imageUrl?.let { imageUrl ->
URLEncoder.encode(
imageUrl,
StandardCharsets.UTF_8.toString(),
)
},
),
)
},
onDeleteFriendSuccess = {
navController.previousBackStackEntry?.savedStateHandle?.set(
HOME_RESULT_EVENT,
HomeNavigationEvent.FriendDeleted(it),
)
navController.popBackStack()
},
)

// 친구 프로필 편집 화면 NavGraph
friendProfileEditorNavGraph(onShowErrorSnackBar = onShowSnackbar, onClickBackButton = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -92,6 +93,7 @@ class MonthlyReminderAllViewModel
}
}

// 챙김시에 기념일이 나와야 함
fun onRecordFriendShip(friendId: String) {
friendRepository
.recordContact(friendId)
Expand All @@ -103,7 +105,14 @@ class MonthlyReminderAllViewModel
if (recordedFriend != null) {
monthlyReminders.value =
monthlyReminders.value.filter { it.friendId != friendId }
completedReminders.value = listOf(recordedFriend) + completedReminders.value
completedReminders.value = listOf(
recordedFriend.copy(
nextContactAt =
LocalDate
.now()
.format(DateTimeFormatter.ofPattern("yy.MM.dd")),
),
) + completedReminders.value
Comment on lines +108 to +115

Choose a reason for hiding this comment

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

high

챙김(record)이 완료된 친구를 completedReminders 목록으로 옮길 때 nextContactAt 속성을 현재 날짜로 설정하고 있습니다. nextContactAt이라는 속성명은 '다음 연락할 날짜'를 의미하는 것으로 보입니다. 만약 그렇다면, 이 값은 연락 주기에 따라 미래의 날짜로 계산되어야 합니다. 현재 날짜를 설정하는 것은 논리적으로 맞지 않을 수 있습니다. 만약 이 속성이 '마지막으로 연락한 날짜'를 표시하기 위한 것이라면, lastContactAt과 같이 속성명을 변경하여 의도를 명확히 하는 것이 좋겠습니다. 현재 구현은 버그일 가능성이 높아 보입니다.

combineRemindersToUIState()
}
}.handleError(viewModelScope, _uiEvent) { exception ->
Expand Down