Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.lyrics.feelin.presentation.view.mypage.blockedusers

import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.lyrics.feelin.presentation.view.mypage.blockedusers.component.BlockedUserListItem
import com.lyrics.feelin.presentation.view.mypage.blockedusers.BlockedUserListItemData
import com.lyrics.feelin.presentation.designsystem.theme.FeelinTheme
import org.junit.Rule
import org.junit.Test

class BlockedUsersScreenTest {
@get:Rule
val composeRule = createComposeRule()

@Test
fun listItem_showsNicknameAndUnblockButton() {
composeRule.setContent {
FeelinTheme {
BlockedUserListItem(
data = BlockedUserListItemData(
userId = 1L,
nickname = "username01",
profileImageUrl = null,
),
onUnblockClick = {},
)
}
}

composeRule.onNodeWithText("username01").assertExists()
composeRule.onNodeWithText("차단 해제").assertExists()
}

@Test
fun blockedUsersScreen_unblockClick_showsSnackbar() {
composeRule.setContent {
FeelinTheme {
BlockedUsersScreen(
blockedUsers = listOf(
BlockedUserListItemData(
userId = 1L,
nickname = "username01",
profileImageUrl = null,
),
),
onBackClick = {},
)
}
}

composeRule.onNodeWithText("차단 해제").performClick()
composeRule.waitForIdle()

composeRule.onNodeWithText("차단 해제되었습니다").assertExists()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.lyrics.feelin.core.designsystem.component

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.lyrics.feelin.presentation.designsystem.theme.FeelinTheme
import com.lyrics.feelin.presentation.designsystem.theme.FeelinTypography
import com.lyrics.feelin.presentation.designsystem.theme.LocalDarkTheme
import com.lyrics.feelin.presentation.designsystem.theme.LocalFeelinColors

@Composable
fun FeelinSnackbarHost(
hostState: SnackbarHostState,
modifier: Modifier = Modifier
) {
SnackbarHost(
hostState = hostState,
modifier = modifier.padding(horizontal = 20.dp),
snackbar = { snackbarData ->
FeelinSnackbar(
message = snackbarData.visuals.message
)
}
)
}

@Composable
fun FeelinSnackbar(
message: String,
modifier: Modifier = Modifier
) {
val isDark = LocalDarkTheme.current
val backgroundColor = if (isDark) {
LocalFeelinColors.current.backgroundSecondary
} else {
LocalFeelinColors.current.gray07
}

val textColor = if (isDark) {
LocalFeelinColors.current.gray09
} else {
Color.White
}

Surface(
modifier = modifier
.fillMaxWidth()
.height(56.dp)
.shadow(
elevation = 10.dp,
shape = RoundedCornerShape(8.dp),
ambientColor = Color.Black.copy(alpha = 0.15f),
spotColor = Color.Black.copy(alpha = 0.15f)
),
shape = RoundedCornerShape(8.dp),
color = backgroundColor
) {
Row(
modifier = Modifier.padding(start = 18.dp, top = 16.dp, bottom = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = null,
tint = LocalFeelinColors.current.brandPrimary,
modifier = Modifier.size(24.dp)
)
Text(
text = message,
style = FeelinTypography.body2,
color = textColor
)
}
}
}

@Preview(showBackground = true)
@Composable
private fun FeelinSnackbarLightPreview() {
FeelinTheme(darkTheme = false) {
FeelinSnackbar(
message = "차단 해제되었습니다",
modifier = Modifier.padding(20.dp)
)
}
}

@Preview(showBackground = true, backgroundColor = 0xFF000000)
@Composable
private fun FeelinSnackbarDarkPreview() {
FeelinTheme(darkTheme = true) {
FeelinSnackbar(
message = "차단 해제되었습니다",
modifier = Modifier.padding(20.dp)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ sealed class FeelinDestination(
object MyPage : FeelinDestination(route = "my_page")
object Setting : FeelinDestination(route = "setting")
object UserInfo : FeelinDestination(route = "user_info")
object BlockedUsers : FeelinDestination(route = "blocked_users")

object InternalWebView : FeelinDestination(
route = "internal_webview?$WEB_VIEW_URL_ARGUMENT={$WEB_VIEW_URL_ARGUMENT}"
Expand Down
20 changes: 20 additions & 0 deletions app/src/main/java/com/lyrics/feelin/navigation/FeelinNavHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import com.lyrics.feelin.presentation.view.login.LoginScreen
import com.lyrics.feelin.presentation.view.mypage.MyPageLogoutStatus
import com.lyrics.feelin.presentation.view.mypage.MyPageScreen
import com.lyrics.feelin.presentation.view.mypage.MyPageViewModel
import com.lyrics.feelin.presentation.view.mypage.blockedusers.BlockedUsersScreen
import com.lyrics.feelin.presentation.view.mypage.blockedusers.BlockedUsersViewModel
import com.lyrics.feelin.presentation.view.mypage.setting.SettingScreen
import com.lyrics.feelin.presentation.view.mypage.userinfo.UserInfoScreen
import com.lyrics.feelin.presentation.view.note.detail.NoteDetailScreen
Expand Down Expand Up @@ -345,6 +347,7 @@ private fun NavGraphBuilder.mainNavGraph(navController: NavHostController) {
}
}

@Suppress("LongMethod")
private fun NavGraphBuilder.myPageNavGraph(navController: NavHostController) {
navigation(startDestination = FeelinDestination.MyPage.route, route = FeelinDestination.MyPageGraph.route) {
composable(FeelinDestination.MyPage.route) { backStackEntry ->
Expand Down Expand Up @@ -389,6 +392,7 @@ private fun NavGraphBuilder.myPageNavGraph(navController: NavHostController) {
SettingScreen(
onBackClick = { navController.popBackStack() },
onUserInfoClick = { navController.navigate(FeelinDestination.UserInfo.route) },
onBlockedUsersClick = { navController.navigate(FeelinDestination.BlockedUsers.route) },
onLogoutClick = viewModel::logout,
onInternalWebViewClick = { url ->
navController.navigate(FeelinDestination.InternalWebView.createRoute(url))
Expand All @@ -405,6 +409,22 @@ private fun NavGraphBuilder.myPageNavGraph(navController: NavHostController) {
UserInfoScreen(onBackClick = { navController.popBackStack() })
}
}

composable(FeelinDestination.BlockedUsers.route) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry(FeelinDestination.MyPageGraph.route)
}
val viewModel: BlockedUsersViewModel = hiltViewModel(parentEntry)
val uiState by viewModel.uiState.collectAsState()

MainScaffold(navController = navController, selectedIndex = 2) {
BlockedUsersScreen(
blockedUsers = uiState.blockedUsers,
onUnblockClick = viewModel::unblockUser,
onBackClick = { navController.popBackStack() }
)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.lyrics.feelin.presentation.view.mypage.blockedusers

data class BlockedUserListItemData(
val userId: Long,
val nickname: String,
val profileImageUrl: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package com.lyrics.feelin.presentation.view.mypage.blockedusers

import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.lyrics.feelin.core.designsystem.component.FeelinSnackbarHost
import com.lyrics.feelin.core.designsystem.component.FeelinTopAppBarWithBack
import com.lyrics.feelin.presentation.designsystem.theme.FeelinTheme
import com.lyrics.feelin.presentation.designsystem.theme.FeelinTypography
import com.lyrics.feelin.presentation.designsystem.theme.LocalFeelinColors
import com.lyrics.feelin.presentation.view.mypage.blockedusers.component.BlockedUserListItem
import kotlinx.coroutines.launch

private val previewBlockedUsers = listOf(
BlockedUserListItemData(1L, "차단된 사용자 닉네임", null),
BlockedUserListItemData(2L, "차단된 사용자 닉네임 2", null)
)

@Composable
fun BlockedUsersScreen(
blockedUsers: List<BlockedUserListItemData>,
modifier: Modifier = Modifier,
onUnblockClick: (Long) -> Unit = {},
onBackClick: () -> Unit = {},
) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
val colors = LocalFeelinColors.current
// TODO(@이대근): 3버튼 표시시 하단바와 스낵바가 겹침, 추후 전역 스낵바 도입 및 이를 제거 2026.07.07.
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

Scaffold(
modifier = modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top))
.background(colors.backgroundPrimary),
topBar = {
FeelinTopAppBarWithBack(
title = "차단된 유저 관리",
onBackClick = onBackClick
)
},
snackbarHost = {
FeelinSnackbarHost(hostState = snackbarHostState)
},
containerColor = colors.backgroundPrimary
) { paddingValues ->
if (blockedUsers.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Text(
text = "차단된 유저가 없어요",
style = FeelinTypography.body2,
color = colors.gray04
)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.background(colors.backgroundPrimary)
) {
items(
items = blockedUsers,
key = { it.userId }
) { user ->
BlockedUserListItem(
data = user,
onUnblockClick = {
onUnblockClick(user.userId)
coroutineScope.launch {
snackbarHostState.showSnackbar(message = "차단 해제되었습니다")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
)
}
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun BlockedUsersScreenPreviewListLight() {
FeelinTheme {
BlockedUsersScreen(
blockedUsers = previewBlockedUsers,
onBackClick = {}
)
}
}

@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BlockedUsersScreenPreviewListDark() {
FeelinTheme {
BlockedUsersScreen(
blockedUsers = previewBlockedUsers,
onBackClick = {}
)
}
}

@Preview(showBackground = true)
@Composable
private fun BlockedUsersScreenPreviewEmptyLight() {
FeelinTheme {
BlockedUsersScreen(
blockedUsers = emptyList(),
onBackClick = {}
)
}
}

@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun BlockedUsersScreenPreviewEmptyDark() {
FeelinTheme {
BlockedUsersScreen(
blockedUsers = emptyList(),
onBackClick = {}
)
}
}

@Preview(showBackground = true)
@Composable
private fun BlockedUsersScreenPreviewSnackbarInteractive() {
// Note: To see the snackbar in Preview, run it in Interactive Mode and click '차단 해제'
FeelinTheme {
BlockedUsersScreen(
blockedUsers = listOf(
BlockedUserListItemData(1L, "인터랙티브 모드에서 차단 해제 클릭", null)
),
onBackClick = {}
)
}
}
Loading
Loading