diff --git a/app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt b/app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt new file mode 100644 index 00000000..7b1c9bda --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/request/FollowRequest.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.users.request + +import kotlinx.serialization.Serializable + +@Serializable +data class FollowRequest( + val type: Boolean +) diff --git a/app/src/main/java/com/texthip/thip/data/model/users/request/NicknameRequest.kt b/app/src/main/java/com/texthip/thip/data/model/users/request/NicknameRequest.kt new file mode 100644 index 00000000..b0ad1b79 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/request/NicknameRequest.kt @@ -0,0 +1,8 @@ +package com.texthip.thip.data.model.users.request + +import kotlinx.serialization.Serializable + +@Serializable +data class NicknameRequest( + val nickname: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt new file mode 100644 index 00000000..b4753a6f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/AliasChoiceResponse.kt @@ -0,0 +1,17 @@ +package com.texthip.thip.data.model.users.response + +import com.google.gson.annotations.SerializedName +import kotlinx.serialization.Serializable + +@Serializable +data class AliasChoiceResponse( + @SerializedName("aliasChoices") val aliasChoices: List +) + +@Serializable +data class AliasChoice( + @SerializedName("aliasName") val aliasName: String, + @SerializedName("categoryName") val categoryName: String, + @SerializedName("imageUrl") val imageUrl: String, + @SerializedName("aliasColor") val aliasColor: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/FollowResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/FollowResponse.kt new file mode 100644 index 00000000..51dfea5c --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/FollowResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.users.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FollowResponse( + @SerialName("isFollowing") val isFollowing: Boolean +) diff --git a/app/src/main/java/com/texthip/thip/data/model/users/MyFollowingsResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt similarity index 62% rename from app/src/main/java/com/texthip/thip/data/model/users/MyFollowingsResponse.kt rename to app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt index 91807af3..21b395df 100644 --- a/app/src/main/java/com/texthip/thip/data/model/users/MyFollowingsResponse.kt +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/MyFollowingsResponse.kt @@ -1,4 +1,4 @@ -package com.texthip.thip.data.model.users +package com.texthip.thip.data.model.users.response import com.google.gson.annotations.SerializedName import kotlinx.serialization.Serializable @@ -13,10 +13,22 @@ data class MyFollowingsResponse( @Serializable data class FollowingList( - @SerializedName("userId") val userId: Int, + @SerializedName("userId") val userId: Long, @SerializedName("nickname") val nickname: String, @SerializedName("profileImageUrl") val profileImageUrl: String?, @SerializedName("aliasName") val aliasName: String, @SerializedName("aliasColor") val aliasColor: String, @SerializedName("isFollowing") val isFollowing: Boolean ) + +@Serializable +data class MyRecentFollowingsResponse( + @SerializedName("recentWriters") val recentWriters: List +) + +@Serializable +data class RecentWriterList( + @SerializedName("userId") val userId: Long, + @SerializedName("nickname") val nickname: String, + @SerializedName("profileImageUrl") val profileImageUrl: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/MyPageInfoResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/MyPageInfoResponse.kt new file mode 100644 index 00000000..51acc39f --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/MyPageInfoResponse.kt @@ -0,0 +1,12 @@ +package com.texthip.thip.data.model.users.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class MyPageInfoResponse( + @SerialName("profileImageUrl") val profileImageUrl: String?, + @SerialName("nickname") val nickname: String, + @SerialName("aliasName") val aliasName: String, + @SerialName("aliasColor") val aliasColor: String +) \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/model/users/response/NicknameResponse.kt b/app/src/main/java/com/texthip/thip/data/model/users/response/NicknameResponse.kt new file mode 100644 index 00000000..3dfab011 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/data/model/users/response/NicknameResponse.kt @@ -0,0 +1,9 @@ +package com.texthip.thip.data.model.users.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class NicknameResponse( + @SerialName("isVerified") val isVerified: Boolean +) diff --git a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt index e048f8cb..5cfd6244 100644 --- a/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt +++ b/app/src/main/java/com/texthip/thip/data/repository/UserRepository.kt @@ -1,15 +1,24 @@ package com.texthip.thip.data.repository import com.texthip.thip.data.model.base.handleBaseResponse -import com.texthip.thip.data.model.users.MyFollowingsResponse +import com.texthip.thip.data.model.users.request.FollowRequest +import com.texthip.thip.data.model.users.response.MyFollowingsResponse +import com.texthip.thip.data.model.users.response.MyPageInfoResponse +import com.texthip.thip.data.model.users.request.NicknameRequest +import com.texthip.thip.data.model.users.response.AliasChoiceResponse +import com.texthip.thip.data.model.users.response.FollowResponse +import com.texthip.thip.data.model.users.response.MyRecentFollowingsResponse +import com.texthip.thip.data.model.users.response.NicknameResponse +import com.texthip.thip.data.model.users.response.OthersFollowersResponse import com.texthip.thip.data.service.UserService import javax.inject.Inject import javax.inject.Singleton @Singleton -class UserRepository@Inject constructor( +class UserRepository @Inject constructor( private val userService: UserService ) { + //내 팔로잉 목록 조회 suspend fun getMyFollowings( cursor: String?, size: Int = 10 @@ -18,4 +27,50 @@ class UserRepository@Inject constructor( .handleBaseResponse() .getOrThrow() } + + suspend fun getRecentWriters(): Result = runCatching { + userService.getRecentWriters() + .handleBaseResponse() + .getOrThrow() + } + + //다른 유저 팔로워 목록 조회 + suspend fun getOthersFollowers( + userId: Long, + cursor: String?, + size: Int = 10 + ): Result = runCatching { + userService.getUserFollowers(userId = userId, cursor = cursor, size = size) + .handleBaseResponse() + .getOrThrow() + } + + //마이페이지 정보 조회 + suspend fun getMyPageInfo(): Result = runCatching { + userService.getMyPage() + .handleBaseResponse() + .getOrThrow() + } + + suspend fun checkNickname(nickname: String): Result = runCatching { + userService.checkNickname(NicknameRequest(nickname)) + .handleBaseResponse() + .getOrThrow() + } + + suspend fun getAliasChoices(): Result = runCatching { + userService.getAliasChoices() + .handleBaseResponse() + .getOrThrow() + } + + suspend fun toggleFollow( + followingUserId: Long, + isFollowing: Boolean + ): Result = runCatching { + val request = FollowRequest(type = isFollowing) + userService.toggleFollow(followingUserId, request) + .handleBaseResponse() + .getOrThrow() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/data/service/UserService.kt b/app/src/main/java/com/texthip/thip/data/service/UserService.kt index b2c1f429..6b2549f7 100644 --- a/app/src/main/java/com/texthip/thip/data/service/UserService.kt +++ b/app/src/main/java/com/texthip/thip/data/service/UserService.kt @@ -1,9 +1,18 @@ package com.texthip.thip.data.service import com.texthip.thip.data.model.base.BaseResponse -import com.texthip.thip.data.model.rooms.response.RoomsUsersResponse -import com.texthip.thip.data.model.users.MyFollowingsResponse +import com.texthip.thip.data.model.users.request.FollowRequest +import com.texthip.thip.data.model.users.response.MyFollowingsResponse +import com.texthip.thip.data.model.users.response.MyPageInfoResponse +import com.texthip.thip.data.model.users.request.NicknameRequest +import com.texthip.thip.data.model.users.response.AliasChoiceResponse +import com.texthip.thip.data.model.users.response.FollowResponse +import com.texthip.thip.data.model.users.response.MyRecentFollowingsResponse +import com.texthip.thip.data.model.users.response.NicknameResponse +import com.texthip.thip.data.model.users.response.OthersFollowersResponse +import retrofit2.http.Body import retrofit2.http.GET +import retrofit2.http.POST import retrofit2.http.Path import retrofit2.http.Query @@ -14,4 +23,30 @@ interface UserService { @Query("cursor") cursor: String? = null ): BaseResponse + @GET("users/my-followings/recent-feeds") + suspend fun getRecentWriters(): BaseResponse + + @GET("users/{userId}/followers") + suspend fun getUserFollowers( + @Path("userId") userId: Long, + @Query("size") size: Int = 10, + @Query("cursor") cursor: String? = null + ): BaseResponse + + @GET("users/my-page") + suspend fun getMyPage(): BaseResponse + + @POST("users/nickname") + suspend fun checkNickname( + @Body request: NicknameRequest + ): BaseResponse + + @GET("users/alias") + suspend fun getAliasChoices(): BaseResponse + + @POST("users/following/{followingUserId}") + suspend fun toggleFollow( + @Path("followingUserId") followingUserId: Long, + @Body request: FollowRequest + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt index 717dd18f..8bb88cc5 100644 --- a/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt +++ b/app/src/main/java/com/texthip/thip/ui/common/cards/CardBookList.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -18,7 +20,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -34,6 +38,7 @@ fun CardBookList( author: String, publisher: String, imageUrl: String? = null, // API에서 받은 이미지 URL + showBookmark: Boolean = false, isBookmarked: Boolean = false, onBookmarkClick: () -> Unit = {} ) { @@ -74,17 +79,20 @@ fun CardBookList( Spacer(modifier = Modifier.width(12.dp)) - // 북마크 아이콘 제거(쓰는 화면이 안보임) - /*IconButton( - onClick = onBookmarkClick, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = if (isBookmarked) ImageVector.vectorResource(R.drawable.ic_save_filled) else ImageVector.vectorResource(R.drawable.ic_save), - contentDescription = "북마크", - tint = if (isBookmarked) colors.Purple else colors.Grey01 - ) - }*/ + if(showBookmark) { + IconButton( + onClick = onBookmarkClick, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = if (isBookmarked) ImageVector.vectorResource(R.drawable.ic_save_filled) else ImageVector.vectorResource( + R.drawable.ic_save + ), + contentDescription = "북마크", + tint = if (isBookmarked) colors.Purple else colors.Grey01 + ) + } + } } } @@ -93,6 +101,7 @@ fun CardBookList( @Composable fun PreviewBookTitleCard() { var isBookmarked by remember { mutableStateOf(false) } + var showBookmark by remember { mutableStateOf(true) } Column( modifier = Modifier.padding(16.dp), @@ -102,9 +111,9 @@ fun PreviewBookTitleCard() { title = "책제목입니다.책제목입니다.책제목입니다.책제목입니다.책제목입니다.책제목입니다.", author = "리처드 도킨스", publisher = "을유문화사", + showBookmark = showBookmark, isBookmarked = isBookmarked, onBookmarkClick = { isBookmarked = !isBookmarked } ) } - } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt index af4e3ed8..41fbc4b5 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/FeedScreen.kt @@ -47,7 +47,7 @@ import com.texthip.thip.ui.feed.component.FeedSubscribeBarlist import com.texthip.thip.ui.feed.component.MyFeedCard import com.texthip.thip.ui.feed.component.MySubscribeBarlist import com.texthip.thip.ui.feed.mock.MySubscriptionData -import com.texthip.thip.ui.feed.viewmodel.MySubscriptionViewModel +import com.texthip.thip.ui.feed.viewmodel.FeedViewModel import com.texthip.thip.ui.mypage.component.SavedFeedCard import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.theme.ThipTheme @@ -67,10 +67,12 @@ fun FeedScreen( totalFeedCount: Int = 0, selectedTabIndex: Int = 0, followerProfileImageUrls: List = emptyList(), + feedViewModel: FeedViewModel = hiltViewModel() resultFeedId: Int? = null, onResultConsumed: () -> Unit = {}, - viewModel: MySubscriptionViewModel = hiltViewModel() + mySubscriptionViewModel: MySubscriptionViewModel = hiltViewModel() ) { + val feedUiState by feedViewModel.uiState.collectAsState() val selectedIndex = rememberSaveable { mutableIntStateOf(selectedTabIndex) } val feedStateList = remember { mutableStateListOf().apply { @@ -288,14 +290,14 @@ fun FeedScreen( //피드 item { Spacer(modifier = Modifier.height(20.dp)) - val subscriptionsForBar = subscriptionUiState.followings.map { user -> + val subscriptionsForBar = feedUiState.recentWriters.map { user -> MySubscriptionData( profileImageUrl = user.profileImageUrl, nickname = user.nickname, - role = user.aliasName, + role = "", roleColor = colors.White, subscriberCount = 0, - isSubscribed = user.isFollowing + isSubscribed = true ) } MySubscribeBarlist( @@ -309,7 +311,6 @@ fun FeedScreen( SavedFeedCard( feedItem = feed, - profileImage = profileImage, onBookmarkClick = { val updated = feed.copy(isSaved = !feed.isSaved) feedStateList[index] = updated diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt index e3404c90..96e75127 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/MySubscriptionListScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.texthip.thip.R -import com.texthip.thip.data.model.users.FollowingList +import com.texthip.thip.data.model.users.response.FollowingList import com.texthip.thip.ui.common.header.AuthorHeader import com.texthip.thip.ui.common.modal.ToastWithDate import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar @@ -48,7 +48,7 @@ import kotlinx.coroutines.delay @Composable fun MySubscriptionScreen( - navController: NavController, + navController: NavController?= null, viewModel: MySubscriptionViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsState() @@ -73,7 +73,7 @@ fun MySubscriptionScreen( MySubscriptionContent( uiState = uiState, lazyListState = lazyListState, - onNavigateBack = { navController.popBackStack() }, + onNavigateBack = { navController?.popBackStack() }, onToggleFollow = { userId, nickname -> val followedMessage = context.getString(R.string.toast_thip, nickname) val unfollowedMessage = context.getString(R.string.toast_thip_cancel, nickname) @@ -87,7 +87,7 @@ fun MySubscriptionContent( uiState: MySubscriptionUiState, lazyListState: LazyListState, onNavigateBack: () -> Unit, - onToggleFollow: (userId: Int, nickname: String) -> Unit, + onToggleFollow: (userId: Long, nickname: String) -> Unit, onHideToast: () -> Unit ) { LaunchedEffect(uiState.showToast) { @@ -194,7 +194,7 @@ fun MySubscriptionContent( private fun MySubscriptionListScreenPrev() { val mockUsers = (1..10).map { FollowingList( - userId = it, + userId = it.toLong(), profileImageUrl = null, nickname = "문학소년 $it", aliasName = if (it % 3 == 0) "공식 인플루언서" else "글쓰는 탐험가", diff --git a/app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt b/app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt new file mode 100644 index 00000000..d34d19af --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/screen/OthersSubscriptionListScreen.kt @@ -0,0 +1,181 @@ +package com.texthip.thip.ui.feed.screen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import com.texthip.thip.R +import com.texthip.thip.data.model.users.response.FollowerList +import com.texthip.thip.ui.common.header.AuthorHeader +import com.texthip.thip.ui.common.topappbar.DefaultTopAppBar +import com.texthip.thip.ui.feed.viewmodel.OthersSubscriptionUiState +import com.texthip.thip.ui.feed.viewmodel.OthersSubscriptionViewModel +import com.texthip.thip.ui.theme.ThipTheme +import com.texthip.thip.ui.theme.ThipTheme.colors +import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.color.hexToColor + +@Composable +fun OthersSubsciptionListScreen( + navController: NavController, + viewModel: OthersSubscriptionViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsState() + val lazyListState = rememberLazyListState() + + val isScrolledToEnd by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + if (layoutInfo.totalItemsCount == 0) return@derivedStateOf false + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 + lastVisibleItemIndex >= layoutInfo.totalItemsCount - 1 + } + } + + LaunchedEffect(isScrolledToEnd) { + if (isScrolledToEnd && !uiState.isLoading && !uiState.isLastPage) { + viewModel.fetchOthersFollowers() + } + } + + OthersSubsciptionContent( + uiState = uiState, + lazyListState = lazyListState, + onNavigateBack = { navController.popBackStack() } + ) +} +@Composable +fun OthersSubsciptionContent( + uiState: OthersSubscriptionUiState, + lazyListState: LazyListState, + onNavigateBack: () -> Unit +) { + Column( + Modifier + .background(colors.Black) + .fillMaxSize() + ) { + DefaultTopAppBar( + onLeftClick = onNavigateBack, + title = stringResource(R.string.thip_list) + ) + Column(modifier = Modifier.fillMaxWidth()) { + Spacer(modifier = Modifier.height(40.dp)) + Text( + text = stringResource(R.string.whole_num, uiState.totalCount), + style = typography.menu_m500_s14_h24, + color = colors.Grey, + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, bottom = 4.dp) + ) + HorizontalDivider( + modifier = Modifier.padding(horizontal = 20.dp), + color = colors.DarkGrey02, + thickness = 1.dp + ) + + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 20.dp) + ) { + itemsIndexed( + items = uiState.followers, + key = { _, user -> user.userId } + ) { index, user -> + Column(modifier = Modifier.padding(horizontal = 20.dp)) { + AuthorHeader( + profileImage = user.profileImageUrl, + nickname = user.nickname, + badgeText = user.aliasName, + badgeTextColor = hexToColor(user.aliasColor), + showButton = false, + showThipNum = true, + profileImageSize = 36.dp, + thipNum = user.followerCount, + onThipNumClick = {} + ) + + if (index < uiState.followers.lastIndex) { + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 1.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + + if (uiState.isLoading && !uiState.isLastPage) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.weight(1f)) + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + Spacer(modifier = Modifier.weight(1f)) + } + } + } + } + } + } +} +@Preview +@Composable +private fun OthersSubsciptionListScreenPrev() { + val mockUsers = (1..10).map { + FollowerList( + userId = it.toLong(), + profileImageUrl = null, + nickname = "문학소년 $it", + aliasName = if (it % 3 == 0) "공식 인플루언서" else "글쓰는 탐험가", + aliasColor = if (it % 3 == 0) "#00C7B2" else "#FFD600", + followerCount = it * 10 + ) + } + + ThipTheme { + OthersSubsciptionContent( + uiState = OthersSubscriptionUiState( + isLoading = false, + followers = mockUsers, + totalCount = mockUsers.size, + isLastPage = false + ), + lazyListState = rememberLazyListState(), + onNavigateBack = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt new file mode 100644 index 00000000..04b3bdd5 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/FeedViewModel.kt @@ -0,0 +1,50 @@ +package com.texthip.thip.ui.feed.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.users.response.RecentWriterList +import com.texthip.thip.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class FeedUiState( + val isLoading: Boolean = true, + val recentWriters: List = emptyList(), + val errorMessage: String? = null + //TODO 추후 피드 목록 등 다른 상태들 추가될 예정 +) + +@HiltViewModel +class FeedViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(FeedUiState()) + val uiState = _uiState.asStateFlow() + + init { + fetchRecentWriters() + } + + private fun fetchRecentWriters() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + userRepository.getRecentWriters() + .onSuccess { data -> + _uiState.update { + it.copy( + isLoading = false, + recentWriters = data?.recentWriters ?: emptyList() + ) + } + } + .onFailure { exception -> + _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt index 7f4f03c2..3fe21831 100644 --- a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/MySubscriptionViewModel.kt @@ -2,7 +2,7 @@ package com.texthip.thip.ui.feed.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.texthip.thip.data.model.users.FollowingList +import com.texthip.thip.data.model.users.response.FollowingList import com.texthip.thip.data.repository.UserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -71,24 +71,54 @@ class MySubscriptionViewModel @Inject constructor( } - fun toggleFollow(userId: Int, followedMessage: String, unfollowedMessage: String) { - var toastMsg = "" - _uiState.update { currentState -> - val updatedList = currentState.followings.map { user -> - if (user.userId == userId) { - val isNowFollowing = !user.isFollowing - toastMsg = if (isNowFollowing) followedMessage else unfollowedMessage - user.copy(isFollowing = isNowFollowing) - } else { - user - } + fun toggleFollow(userId: Long, followedMessage: String, unfollowedMessage: String) { + val currentState = _uiState.value + val userToUpdate = currentState.followings.find { it.userId == userId } ?: return + val currentIsFollowing = userToUpdate.isFollowing + //낙관적 업데이트 -> ui 먼저 변경 + val newOptimisticList = currentState.followings.map { user -> + if (user.userId == userId) { + user.copy(isFollowing = !currentIsFollowing) + } else { + user } - currentState.copy( - followings = updatedList, + } + + _uiState.update { + it.copy( + followings = newOptimisticList, showToast = true, - toastMessage = toastMsg + toastMessage = if (!currentIsFollowing) followedMessage else unfollowedMessage ) } + viewModelScope.launch { + val requestType = !currentIsFollowing + + userRepository.toggleFollow(followingUserId = userId, isFollowing = requestType) + .onSuccess { response -> + val serverState = response?.isFollowing ?: requestType + _uiState.update { state -> + state.copy(followings = state.followings.map { user -> + if (user.userId == userId) { + user.copy(isFollowing = serverState) + } else { + user + } + }) + } + } + .onFailure { + _uiState.update { state -> + state.copy(followings = state.followings.map { user -> + if (user.userId == userId) { + user.copy(isFollowing = currentIsFollowing) // 원래 상태로 복원 + } else { + user + } + }) + } + } + } } fun hideToast() { _uiState.update { it.copy(showToast = false) } diff --git a/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt new file mode 100644 index 00000000..ae6bc2f7 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/feed/viewmodel/OthersSubscriptionViewModel.kt @@ -0,0 +1,68 @@ +package com.texthip.thip.ui.feed.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.model.users.response.FollowerList +import com.texthip.thip.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class OthersSubscriptionUiState( + val isLoading: Boolean = false, + val followers: List = emptyList(), + val totalCount: Int = 0, + val isLastPage: Boolean = false, + val errorMessage: String? = null +) + +@HiltViewModel +class OthersSubscriptionViewModel @Inject constructor( + private val userRepository: UserRepository, + savedStateHandle: SavedStateHandle +) : ViewModel() { + private val userId: Long = requireNotNull(savedStateHandle["userId"]) + private val _uiState = MutableStateFlow(OthersSubscriptionUiState()) + val uiState = _uiState.asStateFlow() + + private var nextCursor: String? = null + + init { + fetchOthersFollowers(isInitial = true) + } + + fun fetchOthersFollowers(isInitial: Boolean = false) { + if (_uiState.value.isLoading || (!isInitial && _uiState.value.isLastPage)) { + return + } + + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + val cursorToFetch = if (isInitial) null else nextCursor + + val result = userRepository.getOthersFollowers(userId = userId, cursor = cursorToFetch) + + result.onSuccess { data -> + data?.let { + _uiState.update { currentState -> + currentState.copy( + isLoading = false, + followers = if (isInitial) it.followers else currentState.followers + currentState.followers, + totalCount = it.totalFollowerCount, + isLastPage = it.isLast + ) + } + nextCursor = it.nextCursor + } + }.onFailure { exception -> + _uiState.update { + it.copy(isLoading = false, errorMessage = exception.message) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt index 6c5ff75d..17b63d74 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/BookContent.kt @@ -2,10 +2,15 @@ package com.texthip.thip.ui.mypage.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -28,16 +33,34 @@ fun BookContent( if (bookList.isEmpty()) { EmptyBookContent() } else { - LazyColumn { - items(bookList, key = { it.id }) { book -> + LazyColumn ( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + ){ + itemsIndexed(bookList, key = { _, book -> book.id }) { index, book -> + if (index == 0) { + Spacer(Modifier.height(32.dp)) + } + CardBookList( title = book.title, author = book.author, imageUrl = null, publisher = book.publisher, + showBookmark = true, isBookmarked = book.isSaved, onBookmarkClick = { viewModel.toggleBookmark(book.id) } ) + + if (index != bookList.lastIndex) { + Spacer(Modifier.height(20.dp)) + HorizontalDivider( + color = colors.DarkGrey02, + thickness = 1.dp + ) + Spacer(Modifier.height(20.dp)) + } } } } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt index 078eba10..3670ba8b 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/FeedContent.kt @@ -2,21 +2,19 @@ package com.texthip.thip.ui.mypage.component import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.texthip.thip.R import com.texthip.thip.ui.mypage.mock.FeedItem import com.texthip.thip.ui.mypage.viewmodel.SavedFeedViewModel @@ -31,16 +29,29 @@ fun FeedContent( if (feedList.isEmpty()) { EmptyFeedContent() } else { - LazyColumn { - items(feedList, key = { it.id }) { feed -> - val profileImagePainter = feed.userProfileImage?.let { painterResource(it) } + LazyColumn ( + modifier = Modifier + .fillMaxSize() + ){ + itemsIndexed(feedList, key = { _,feed -> feed.id }) { index,feed -> + if (index == 0) { + Spacer(Modifier.height(32.dp)) + } SavedFeedCard( feedItem = feed, - profileImage = profileImagePainter, onBookmarkClick = { viewModel.toggleBookmark(feed.id) }, onLikeClick = { viewModel.toggleLike(feed.id) } ) + + if (index != feedList.lastIndex) { + Spacer(Modifier.height(40.dp)) + HorizontalDivider( + color = colors.DarkGrey03, + thickness = 6.dp + ) + Spacer(Modifier.height(40.dp)) + } } } } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt index 5e1ea662..58772100 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/RoleCard.kt @@ -1,6 +1,5 @@ package com.texthip.thip.ui.mypage.component -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -10,7 +9,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -22,30 +20,24 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.texthip.thip.R +import coil.compose.AsyncImage import com.texthip.thip.ui.theme.DarkGrey -import com.texthip.thip.ui.theme.NeonGreen -import com.texthip.thip.ui.theme.Art import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography import com.texthip.thip.ui.theme.White +import com.texthip.thip.utils.color.hexToColor @Composable fun RoleCard( modifier: Modifier = Modifier, genre: String, role: String, - imageResId: Int, - genreColor: Color, - roleColor: Color, + imageUrl: String, + roleColor: String, selected: Boolean, onClick: () -> Unit ) { @@ -56,7 +48,7 @@ fun RoleCard( } else { colors.Black700 } - val actualGenreColor = if (selected) genreColor else colors.Grey01 + val actualGenreColor = if (selected) colors.White else colors.Grey01 Box( modifier = modifier .width(162.dp) @@ -70,12 +62,11 @@ fun RoleCard( ) .clickable { onClick() } ) { - Image( - painter = painterResource(id = imageResId), + AsyncImage( + model = imageUrl, contentDescription = null, modifier = Modifier - .align(Alignment.BottomStart) - .size(80.dp), + .align(Alignment.BottomStart), contentScale = ContentScale.Fit, alpha = bgAlpha ) @@ -93,7 +84,7 @@ fun RoleCard( Text( text = role, style = typography.info_r400_s12, - color = roleColor + color = hexToColor(roleColor) ) } } @@ -113,9 +104,8 @@ fun RoleCardPreview() { RoleCard( genre = "문학", role = "문학가", - imageResId = R.drawable.character_literature, - genreColor = White, - roleColor = NeonGreen, + imageUrl = "https://picsum.photos/200", + roleColor = "#A0E931", selected = selected1, onClick = { selected1 = !selected1 } ) @@ -123,9 +113,8 @@ fun RoleCardPreview() { RoleCard( genre = "예술", role = "예술가", - imageResId = R.drawable.character_art, - genreColor = White, - roleColor = Art, + imageUrl = "https://picsum.photos/200", + roleColor = "#A00000", selected = selected2, onClick = { selected2 = !selected2 } ) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt index d0d6198a..53255ff0 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/component/SavedFeedCard.kt @@ -34,7 +34,6 @@ import com.texthip.thip.ui.theme.ThipTheme.typography fun SavedFeedCard( modifier: Modifier = Modifier, feedItem: FeedItem, - profileImage: Painter? = null, onBookmarkClick: () -> Unit = {}, onLikeClick: () -> Unit = {}, onContentClick: () -> Unit = {} @@ -47,10 +46,10 @@ fun SavedFeedCard( Column( modifier = modifier .fillMaxWidth() - .padding(20.dp) + .padding(horizontal = 20.dp) ) { ProfileBar( - profileImage = "https://example.com/image1.jpg", + profileImage = feedItem.userProfileImage.toString(), topText = feedItem.userName, bottomText = feedItem.userRole, showSubscriberInfo = false, @@ -166,7 +165,11 @@ private fun SavedFeedCardPrev() { commentCount = 5, isLiked = false, isSaved = true, - imageUrls = listOf(R.drawable.img_book_cover_sample,R.drawable.img_book_cover_sample,R.drawable.img_book_cover_sample) + imageUrls = listOf( + R.drawable.img_book_cover_sample, + R.drawable.img_book_cover_sample, + R.drawable.img_book_cover_sample + ) ) val scrollState = rememberScrollState() @@ -177,12 +180,10 @@ private fun SavedFeedCardPrev() { .verticalScroll(scrollState) ) { SavedFeedCard( - feedItem = feed1, - profileImage = painterResource(feed1.userProfileImage!!) + feedItem = feed1 ) SavedFeedCard( - feedItem = feed2, - profileImage = painterResource(feed2.userProfileImage!!) + feedItem = feed2 ) } } diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt b/app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt index ce193f5f..3801187b 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/mock/RoleItem.kt @@ -1,10 +1,8 @@ package com.texthip.thip.ui.mypage.mock -import androidx.compose.ui.graphics.Color - data class RoleItem( val genre: String, val role: String, - val imageResId: Int, - val roleColor: Color + val imageUrl: String, + val roleColor: String ) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt index e4d5b5d6..97e661a4 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageEditScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -37,32 +38,32 @@ fun EditProfileScreen() { RoleItem( stringResource(R.string.literature), stringResource(R.string.literary_person), - R.drawable.character_literature, - colors.Literature + "https://photos/1111", + "#FF6B6B" ), RoleItem( stringResource(R.string.science_it), stringResource(R.string.scientist), - R.drawable.character_science, - colors.ScienceIt + "https://photos/1111", + "#FF6B6B" ), RoleItem( stringResource(R.string.social_science), stringResource(R.string.sociologist), - R.drawable.character_sociology, - colors.SocialScience + "https://photos/1111", + "#FF6B6B" ), RoleItem( stringResource(R.string.art), stringResource(R.string.artist), - R.drawable.character_art, - colors.Art + "https://photos/1111", + "#FF6B6B" ), RoleItem( stringResource(R.string.humanities), stringResource(R.string.philosopher), - R.drawable.character_humanities, - colors.Humanities + "https://photos/1111", + "#FF6B6B" ) ) Column( @@ -134,13 +135,12 @@ fun EditProfileScreen() { verticalArrangement = Arrangement.spacedBy(16.dp), userScrollEnabled = false, ) { - items(roleCards.size) { index -> + itemsIndexed(roleCards) { index, roleItem -> RoleCard( - genre = roleCards[index].genre, - role = roleCards[index].role, - imageResId = roleCards[index].imageResId, - genreColor = colors.White, - roleColor = roleCards[index].roleColor, + genre = roleItem.genre, + role = roleItem.role, + imageUrl = roleItem.imageUrl, + roleColor = roleItem.roleColor, selected = selectedIndex == index, onClick = { selectedIndex = index } ) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt index 25e498f2..8885b79d 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageSaveScreen.kt @@ -108,7 +108,7 @@ fun SavedScreen( } } } - Box(modifier = Modifier.fillMaxWidth()) { + Box(modifier = Modifier .weight(1f) .fillMaxWidth()) { when (selectedTabIndex) { 0 -> FeedContent(feedList = feedList, viewModel = feedViewModel) 1 -> BookContent(bookList = bookList, viewModel = bookViewModel) diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt index 4797ea41..b91bef0f 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/screen/MypageScreen.kt @@ -11,14 +11,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -26,178 +22,208 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.texthip.thip.R import com.texthip.thip.ui.common.buttons.MenuItemButton import com.texthip.thip.ui.common.header.AuthorHeader import com.texthip.thip.ui.common.modal.DialogPopup import com.texthip.thip.ui.common.topappbar.LeftNameTopAppBar +import com.texthip.thip.ui.mypage.viewmodel.MyPageUiState +import com.texthip.thip.ui.mypage.viewmodel.MyPageViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography +import com.texthip.thip.utils.color.hexToColor @Composable fun MyPageScreen( - navController: NavController? = null, - nickname: String, - badgeText: String + navController: NavController, + viewModel: MyPageViewModel = hiltViewModel(), + onNavigateToEditProfile: () -> Unit, + onNavigateToSavedFeeds: () -> Unit, + onNavigateToNotificationSettings: () -> Unit, + onDeleteAccount: () -> Unit ) { - var showLogoutDialog by remember { mutableStateOf(false) } + val uiState by viewModel.uiState.collectAsState() - Column( + MyPageContent( + uiState = uiState, + onEditProfileClick = onNavigateToEditProfile, + onSavedFeedsClick = onNavigateToSavedFeeds, + onNotificationSettingsClick = onNavigateToNotificationSettings, + onLogoutClick = { viewModel.onLogoutClick() }, + onDismissLogoutDialog = { viewModel.onDismissLogoutDialog() }, + onConfirmLogout = { viewModel.confirmLogout() }, + onDeleteAccount = onDeleteAccount + ) +} +@Composable +fun MyPageContent( + uiState: MyPageUiState, + onEditProfileClick: () -> Unit, + onSavedFeedsClick: () -> Unit, + onNotificationSettingsClick: () -> Unit, + onLogoutClick: () -> Unit, + onDismissLogoutDialog: () -> Unit, + onConfirmLogout: () -> Unit, + onDeleteAccount: () -> Unit +) { + Box( Modifier .background(colors.Black) .fillMaxSize() ) { - LeftNameTopAppBar( - title = stringResource(R.string.my_page), - leftIcon = painterResource(R.drawable.ic_search), - rightIcon = painterResource(R.drawable.ic_plus) - ) - LazyColumn( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(0.dp) + Column( + Modifier + .background(colors.Black) + .fillMaxSize() ) { - item { - Spacer(modifier = Modifier.height(20.dp)) - AuthorHeader( - profileImage = null, - nickname = nickname, - badgeText = badgeText, - buttonText = stringResource(R.string.edit) - ) - Spacer(modifier = Modifier.height(40.dp)) - } - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - ) { - Text( - text = stringResource(R.string.my_activity), - style = typography.smalltitle_sb600_s18_h24, - color = colors.White, + LeftNameTopAppBar( + title = stringResource(R.string.my_page), + leftIcon = painterResource(R.drawable.ic_search), + rightIcon = painterResource(R.drawable.ic_plus) + ) + LazyColumn( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { + item { + Spacer(modifier = Modifier.height(20.dp)) + AuthorHeader( + profileImage = uiState.profileImageUrl, + nickname = uiState.nickname, + badgeText = uiState.aliasName, + badgeTextColor = hexToColor(uiState.aliasColor), + buttonText = stringResource(R.string.edit), + onButtonClick = onEditProfileClick + ) + Spacer(modifier = Modifier.height(40.dp)) + } + item { + Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp) - ) - MenuItemButton( - text = stringResource(R.string.saved), - icon = painterResource(R.drawable.ic_save), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) + .padding(horizontal = 20.dp) + ) { + Text( + text = stringResource(R.string.my_activity), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) + ) + MenuItemButton( + text = stringResource(R.string.saved), + icon = painterResource(R.drawable.ic_save), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = onSavedFeedsClick + ) + } + Spacer(modifier = Modifier.height(40.dp)) } - Spacer(modifier = Modifier.height(40.dp)) - } - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp) - ) { - Text( - text = stringResource(R.string.etc), - style = typography.smalltitle_sb600_s18_h24, - color = colors.White, + item { + Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp) - ) - MenuItemButton( - text = stringResource(R.string.notification_settings), - icon = painterResource(R.drawable.ic_notice), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) - Spacer(modifier = Modifier.height(16.dp)) - MenuItemButton( - text = stringResource(R.string.customer_service), - icon = painterResource(R.drawable.ic_center), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) - Spacer(modifier = Modifier.height(16.dp)) - MenuItemButton( - text = stringResource(R.string.terms_of_use), - icon = painterResource(R.drawable.ic_doc), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) - Spacer(modifier = Modifier.height(16.dp)) - MenuItemButton( - text = stringResource(R.string.guide), - icon = painterResource(R.drawable.ic_guide), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) - Spacer(modifier = Modifier.height(16.dp)) - MenuItemButton( - text = stringResource(R.string.version_1_0), - icon = painterResource(R.drawable.ic_version), - contentColor = colors.White, - backgroundColor = colors.DarkGrey02, - hasRightIcon = true, - modifier = Modifier.fillMaxWidth(), - onClick = {} - ) + .padding(horizontal = 20.dp) + ) { + Text( + text = stringResource(R.string.etc), + style = typography.smalltitle_sb600_s18_h24, + color = colors.White, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp) + ) + MenuItemButton( + text = stringResource(R.string.notification_settings), + icon = painterResource(R.drawable.ic_notice), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = onNotificationSettingsClick + ) + Spacer(modifier = Modifier.height(16.dp)) + MenuItemButton( + text = stringResource(R.string.customer_service), + icon = painterResource(R.drawable.ic_center), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) + Spacer(modifier = Modifier.height(16.dp)) + MenuItemButton( + text = stringResource(R.string.terms_of_use), + icon = painterResource(R.drawable.ic_doc), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) + Spacer(modifier = Modifier.height(16.dp)) + MenuItemButton( + text = stringResource(R.string.guide), + icon = painterResource(R.drawable.ic_guide), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) + Spacer(modifier = Modifier.height(16.dp)) + MenuItemButton( + text = stringResource(R.string.version_1_0), + icon = painterResource(R.drawable.ic_version), + contentColor = colors.White, + backgroundColor = colors.DarkGrey02, + hasRightIcon = true, + modifier = Modifier.fillMaxWidth(), + onClick = {} + ) + } + Spacer(modifier = Modifier.height(184.dp)) } - Spacer(modifier = Modifier.height(184.dp)) - } - item { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 40.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(R.string.log_out), - style = typography.feedcopy_r400_s14_h20, - color = colors.Grey01, - modifier = Modifier.clickable { showLogoutDialog = true } - ) - Spacer(modifier = Modifier.height(24.dp)) - Text( - text = stringResource(R.string.delete_account), - style = typography.feedcopy_r400_s14_h20, - color = colors.Grey01, - modifier = Modifier.clickable { - // TODO: 탈퇴 로직 - } - ) + item { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 40.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.log_out), + style = typography.feedcopy_r400_s14_h20, + color = colors.Grey01, + modifier = Modifier.clickable { onLogoutClick() } + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(R.string.delete_account), + style = typography.feedcopy_r400_s14_h20, + color = colors.Grey01, + modifier = Modifier.clickable { onDeleteAccount() } + ) + } } } } - - if (showLogoutDialog) { - Dialog(onDismissRequest = { showLogoutDialog = false }) { + if (uiState.showLogoutDialog) { + Dialog(onDismissRequest = { onDismissLogoutDialog() }) { DialogPopup( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), title = stringResource(R.string.log_out), description = stringResource(R.string.logout_description), - onCancel = { showLogoutDialog = false }, - onConfirm = { - showLogoutDialog = false - // TODO: 로그아웃 로직 - } + onCancel = { onDismissLogoutDialog() }, + onConfirm = { onConfirmLogout() } ) } } @@ -209,9 +235,20 @@ fun MyPageScreen( @Composable private fun MyPagePrev() { ThipTheme { - MyPageScreen( - nickname = "ThipUser01", - badgeText = "문학가" + MyPageContent( + uiState = MyPageUiState( + isLoading = false, + nickname = "ThipUser01", + aliasName = "문학가", + aliasColor = "#FFFFFF" + ), + onLogoutClick = {}, + onEditProfileClick = {}, + onSavedFeedsClick = {}, + onNotificationSettingsClick = {}, + onDismissLogoutDialog = {}, + onConfirmLogout = {}, + onDeleteAccount = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt new file mode 100644 index 00000000..bf569aec --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/MyPageViewModel.kt @@ -0,0 +1,69 @@ +package com.texthip.thip.ui.mypage.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class MyPageUiState( + val isLoading: Boolean = true, + val profileImageUrl: String? = null, + val nickname: String = "", + val aliasName: String = "", + val aliasColor: String = "#0XFFFFFF", + val errorMessage: String? = null, + val showLogoutDialog: Boolean = false +) +@HiltViewModel +class MyPageViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(MyPageUiState()) + val uiState = _uiState.asStateFlow() + + init { + fetchMyPageInfo() + } + + fun fetchMyPageInfo() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + userRepository.getMyPageInfo() + .onSuccess { data -> + data?.let { + _uiState.update { + it.copy( + isLoading = false, + profileImageUrl = data.profileImageUrl, + nickname = data.nickname, + aliasName = data.aliasName, + aliasColor = data.aliasColor + ) + } + } + } + .onFailure { exception -> + _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } + } + } + } + + fun onLogoutClick() { + _uiState.update { it.copy(showLogoutDialog = true) } + } + + fun onDismissLogoutDialog() { + _uiState.update { it.copy(showLogoutDialog = false) } + } + + fun confirmLogout() { + _uiState.update { it.copy(showLogoutDialog = false) } + // TODO: 실제 로그아웃 로직 구현 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt index fec20fc2..5e2fca6a 100644 --- a/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt +++ b/app/src/main/java/com/texthip/thip/ui/mypage/viewmodel/SavedBookViewModel.kt @@ -2,7 +2,6 @@ package com.texthip.thip.ui.mypage.viewmodel import androidx.lifecycle.ViewModel import com.texthip.thip.ui.mypage.mock.BookItem -import com.texthip.thip.ui.mypage.mock.FeedItem import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt index 4ca4453a..153d958e 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/extensions/MyPageNavigationExtensions.kt @@ -2,6 +2,7 @@ package com.texthip.thip.ui.navigator.extensions import androidx.navigation.NavHostController import com.texthip.thip.ui.navigator.routes.MainTabRoutes +import com.texthip.thip.ui.navigator.routes.MyPageRoutes // MyPage 관련 네비게이션 확장 함수들 @@ -9,3 +10,18 @@ import com.texthip.thip.ui.navigator.routes.MainTabRoutes fun NavHostController.navigateToMyPage() { navigate(MainTabRoutes.MyPage) } +fun NavHostController.navigateToEditProfile() { + navigate(MyPageRoutes.Edit) +} + +fun NavHostController.navigateToSavedFeeds() { + navigate(MyPageRoutes.Save) +} + +fun NavHostController.navigateToNotificationSettings() { + navigate(MyPageRoutes.NotificationEdit) +} + +fun NavHostController.navigateToLeaveThipScreen() { + navigate(MyPageRoutes.LeaveThip) +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt index 92883f8f..f318dfb6 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/navigations/MyPageNavigation.kt @@ -1,19 +1,43 @@ package com.texthip.thip.ui.navigator.navigations +import androidx.compose.material3.Text import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.compose.composable +import com.texthip.thip.ui.mypage.screen.DeleteAccountScreen +import com.texthip.thip.ui.mypage.screen.EditProfileScreen import com.texthip.thip.ui.mypage.screen.MyPageScreen +import com.texthip.thip.ui.mypage.screen.NotificationScreen +import com.texthip.thip.ui.mypage.screen.SavedScreen +import com.texthip.thip.ui.navigator.extensions.navigateToEditProfile +import com.texthip.thip.ui.navigator.extensions.navigateToLeaveThipScreen +import com.texthip.thip.ui.navigator.extensions.navigateToNotificationSettings +import com.texthip.thip.ui.navigator.extensions.navigateToSavedFeeds import com.texthip.thip.ui.navigator.routes.MainTabRoutes +import com.texthip.thip.ui.navigator.routes.MyPageRoutes // MyPage fun NavGraphBuilder.myPageNavigation(navController: NavHostController) { composable { MyPageScreen( - navController, - nickname = "ThipUser01", - badgeText = "문학가" + navController = navController, + onNavigateToEditProfile = { navController.navigateToEditProfile() }, + onNavigateToSavedFeeds = { navController.navigateToSavedFeeds() }, + onNavigateToNotificationSettings = { navController.navigateToNotificationSettings() }, + onDeleteAccount = { navController.navigateToLeaveThipScreen() } ) } + composable { + EditProfileScreen() + } + composable { + SavedScreen() + } + composable { + NotificationScreen() + } + composable { + DeleteAccountScreen() + } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt b/app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt index 85ab317f..3c0beed9 100644 --- a/app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt +++ b/app/src/main/java/com/texthip/thip/ui/navigator/routes/MyPageRoutes.kt @@ -4,9 +4,9 @@ import kotlinx.serialization.Serializable @Serializable sealed class MyPageRoutes : Routes() { - // 향후 추가될 MyPage 관련 화면들 - // @Serializable data object Edit : MyPageRoutes - // @Serializable data object Save : MyPageRoutes - // @Serializable data object Reaction : MyPageRoutes - // @Serializable data object NotificationEdit : MyPageRoutes + @Serializable data object Edit : MyPageRoutes() + @Serializable data object Save : MyPageRoutes() + @Serializable data object Reaction : MyPageRoutes() + @Serializable data object NotificationEdit : MyPageRoutes() + @Serializable data object LeaveThip : MyPageRoutes() } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt b/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt index 26efed99..30f06578 100644 --- a/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupGenreScreen.kt @@ -9,64 +9,51 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.ui.common.topappbar.InputTopAppBar import com.texthip.thip.ui.mypage.component.RoleCard import com.texthip.thip.ui.mypage.mock.RoleItem +import com.texthip.thip.ui.signin.viewmodel.SignupAliasUiState +import com.texthip.thip.ui.signin.viewmodel.SignupAliasViewModel import com.texthip.thip.ui.theme.ThipTheme import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography - @Composable -fun SignupGenreScreen() { - var selectedIndex by rememberSaveable { mutableStateOf(-1) } - val isRightButtonEnabled by remember { derivedStateOf { selectedIndex != -1 } } +fun SignupGenreScreen( + onNavigateToNext: (RoleItem) -> Unit, // 선택된 아이템 정보를 다음 화면으로 넘겨주기 + viewModel: SignupAliasViewModel = hiltViewModel() +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val roleCards = listOf( - RoleItem( - stringResource(R.string.literature), - stringResource(R.string.literary_person), - R.drawable.character_literature, - colors.Literature - ), - RoleItem( - stringResource(R.string.science_it), - stringResource(R.string.scientist), - R.drawable.character_science, - colors.ScienceIt - ), - RoleItem( - stringResource(R.string.social_science), - stringResource(R.string.sociologist), - R.drawable.character_sociology, - colors.SocialScience - ), - RoleItem( - stringResource(R.string.art), - stringResource(R.string.artist), - R.drawable.character_art, - colors.Art - ), - RoleItem( - stringResource(R.string.humanities), - stringResource(R.string.philosopher), - R.drawable.character_humanities, - colors.Humanities - ) + SignupGenreContent( + uiState = uiState, + onCardSelected = { index -> viewModel.selectCard(index) }, + onNextClick = { + // 선택된 아이템이 있을 경우에만 다음 화면으로 이동 + uiState.roleCards.getOrNull(uiState.selectedIndex)?.let { selectedRoleItem -> + onNavigateToNext(selectedRoleItem) + } + } ) +} +@Composable +fun SignupGenreContent( + uiState: SignupAliasUiState, + onCardSelected: (Int) -> Unit, + onNextClick: () -> Unit +) { + val isRightButtonEnabled = uiState.selectedIndex != -1 && !uiState.isLoading Column( Modifier @@ -78,9 +65,7 @@ fun SignupGenreScreen() { rightButtonName = stringResource(R.string.next), isLeftIconVisible = false, onLeftClick = {}, - onRightClick = { - // TODO 다음 화면으로 이동 - } + onRightClick = onNextClick ) Spacer(modifier = Modifier.height(40.dp)) @@ -114,15 +99,14 @@ fun SignupGenreScreen() { verticalArrangement = Arrangement.spacedBy(16.dp), userScrollEnabled = false, ) { - items(roleCards.size) { index -> + itemsIndexed(uiState.roleCards) { index, roleItem -> RoleCard( - genre = roleCards[index].genre, - role = roleCards[index].role, - imageResId = roleCards[index].imageResId, - genreColor = colors.White, - roleColor = roleCards[index].roleColor, - selected = selectedIndex == index, - onClick = { selectedIndex = index } + genre = roleItem.genre, + role = roleItem.role, + imageUrl = roleItem.imageUrl, + roleColor = roleItem.roleColor, + selected = uiState.selectedIndex == index, + onClick = { onCardSelected(index) } ) } } @@ -134,7 +118,23 @@ fun SignupGenreScreen() { @Preview @Composable private fun SignupGenreScreenPrev() { + val previewRoleCards = listOf( + RoleItem("문학", "문학가", "", "#FFFFFF"), + RoleItem("과학/IT", "과학자", "", "#FFFFFF"), + RoleItem("사회", "사회학자", "", "#FFFFFF"), + RoleItem("예술", "예술가", "", "#FFFFFF"), + RoleItem("인문", "철학자", "", "#FFFFFF") + ) + val previewUiState = SignupAliasUiState( + roleCards = previewRoleCards, + selectedIndex = 1 // 1번 아이템이 선택된 상태로 프리뷰 + ) + ThipTheme { - SignupGenreScreen() + SignupGenreContent( + uiState = previewUiState, + onCardSelected = {}, + onNextClick = {} + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt b/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt index 7c41dfd3..a844de6f 100644 --- a/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt +++ b/app/src/main/java/com/texthip/thip/ui/signin/screen/SignupNicknameScreen.kt @@ -1,5 +1,6 @@ package com.texthip.thip.ui.signin.screen +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -9,32 +10,61 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.texthip.thip.R import com.texthip.thip.ui.common.forms.WarningTextField import com.texthip.thip.ui.common.topappbar.InputTopAppBar +import com.texthip.thip.ui.signin.viewmodel.NicknameViewModel import com.texthip.thip.ui.theme.ThipTheme.colors import com.texthip.thip.ui.theme.ThipTheme.typography -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @Composable -fun SigninNicknameScreen() { - var nickname by rememberSaveable { mutableStateOf("") } - var showWarning by remember { mutableStateOf(false) } - var warningMessageResId by remember { mutableStateOf(null) } - val isRightButtonEnabled by remember {derivedStateOf {nickname.isNotBlank()}} // 닉네임 공백 아닐때 버튼 활성화 - val coroutineScope = rememberCoroutineScope() +fun SignupNicknameScreen( + viewModel: NicknameViewModel = hiltViewModel(), + onNavigateToNext: () -> Unit +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + + LaunchedEffect(uiState.navigateToNext, uiState.errorMessage) { + if (uiState.navigateToNext) { + onNavigateToNext() + viewModel.onNavigated() + } + uiState.errorMessage?.let { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + viewModel.onNavigated() + } + } + + SignupNicknameContent( + nickname = uiState.nickname, + onNicknameChange = viewModel::onNicknameChange, + onNextClick = viewModel::checkNickname, + isLoading = uiState.isLoading, + warningMessageResId = uiState.warningMessageResId + ) +} +@Composable +fun SignupNicknameContent( + nickname: String, + onNicknameChange: (String) -> Unit, + onNextClick: () -> Unit, + isLoading: Boolean, + warningMessageResId: Int? +) { + val isRightButtonEnabled = nickname.isNotBlank() && !isLoading // 닉네임 공백 아닐때 버튼 활성화 Column( Modifier @@ -47,20 +77,7 @@ fun SigninNicknameScreen() { rightButtonName = stringResource(R.string.next), isLeftIconVisible = false, onLeftClick = {}, - onRightClick = { - //TODO 서버 연동시 로직 변경 필요 - coroutineScope.launch { - delay(500) // 서버 응답 시뮬레이션 - if (nickname == "test") { - showWarning = true - warningMessageResId = R.string.nickname_warning - } else { - showWarning = false - warningMessageResId = null - // 다음 페이지로 이동 - } - } - } + onRightClick = onNextClick ) Spacer(modifier = Modifier.height(40.dp)) Column( @@ -79,12 +96,9 @@ fun SigninNicknameScreen() { WarningTextField( containerColor = colors.DarkGrey02, value = nickname, - onValueChange = { - nickname = it - showWarning = false // 입력 중에는 경고 숨기기 - }, + onValueChange = onNicknameChange, hint = stringResource(R.string.nickname_condition), - showWarning = showWarning, + showWarning = warningMessageResId != null, showIcon = false, showLimit = true, maxLength = 10, @@ -96,6 +110,33 @@ fun SigninNicknameScreen() { @Preview @Composable -private fun SigninNicknameScreenPrev() { - SigninNicknameScreen() -} \ No newline at end of file +private fun SignupNicknameContentPrev() { + SignupNicknameContent( + nickname = "중복된닉네임", + onNicknameChange = {}, + onNextClick = {}, + isLoading = false, + warningMessageResId = R.string.nickname_warning + ) +} +@Preview(name = "일반 상태 (비어있음)", showBackground = true) +@Composable +private fun SignupNicknameContentPreview_Normal() { + var nickname by remember { mutableStateOf("") } + var warningMessageResId by remember { mutableStateOf(null) } + + SignupNicknameContent( + nickname = nickname, + onNicknameChange = { + nickname = it + warningMessageResId = null + }, + onNextClick = { + if (nickname == "test") { + warningMessageResId = R.string.nickname_warning + } + }, + isLoading = false, + warningMessageResId = warningMessageResId + ) +} diff --git a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt new file mode 100644 index 00000000..fd9b1a90 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/NicknameViewModel.kt @@ -0,0 +1,81 @@ +package com.texthip.thip.ui.signin.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.R +import com.texthip.thip.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + + +data class NicknameUiState( + val isLoading: Boolean = false, + val nickname: String = "", + val isVerified: Boolean? = null, + val warningMessageResId: Int? = null, + val errorMessage: String? = null, + val navigateToNext: Boolean = false +) + +@HiltViewModel +class NicknameViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + private val _uiState = MutableStateFlow(NicknameUiState()) + val uiState = _uiState.asStateFlow() + + fun onNicknameChange(nickname: String) { + // 닉네임 입력 시, 경고 메시지 초기화 + _uiState.update { + it.copy( + nickname = nickname, + warningMessageResId = null + ) + } + } + + fun checkNickname() { + if (_uiState.value.isLoading) return + if (_uiState.value.nickname.isBlank()) return + + viewModelScope.launch { + _uiState.update { + it.copy( + isLoading = true, + warningMessageResId = null, + errorMessage = null + ) + } + + userRepository.checkNickname(_uiState.value.nickname) + .onSuccess { response -> + if (response?.isVerified == true) { + _uiState.update { it.copy(isLoading = false, navigateToNext = true) } + } else { + // 중복된 닉네임 + _uiState.update { + it.copy( + isLoading = false, + warningMessageResId = R.string.nickname_warning + ) + } + } + } + .onFailure { exception -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = exception.message ?: "알 수 없는 오류가 발생했습니다." + ) + } + } + } + } + fun onNavigated() { + _uiState.update { it.copy(navigateToNext = false, errorMessage = null) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt new file mode 100644 index 00000000..0f0b7ab5 --- /dev/null +++ b/app/src/main/java/com/texthip/thip/ui/signin/viewmodel/SignupAliasViewModel.kt @@ -0,0 +1,59 @@ +package com.texthip.thip.ui.signin.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.texthip.thip.data.repository.UserRepository +import com.texthip.thip.ui.mypage.mock.RoleItem +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.onSuccess + +data class SignupAliasUiState( + val isLoading: Boolean = false, + val roleCards: List = emptyList(), + val selectedIndex: Int = -1, + val errorMessage: String? = null +) + +@HiltViewModel +class SignupAliasViewModel @Inject constructor( + private val userRepository: UserRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SignupAliasUiState()) + val uiState = _uiState.asStateFlow() + + init { + fetchAliasChoices() + } + + fun fetchAliasChoices() { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + userRepository.getAliasChoices() + .onSuccess { response -> + val roleCards = response?.aliasChoices?.map { aliasChoice -> + RoleItem( + genre = aliasChoice.aliasName, + role = aliasChoice.categoryName, + imageUrl = aliasChoice.imageUrl, + roleColor = aliasChoice.aliasColor + ) + } ?: emptyList() + _uiState.update { it.copy(isLoading = false, roleCards = roleCards) } + } + .onFailure { exception -> + _uiState.update { it.copy(isLoading = false, errorMessage = exception.message) } + } + } + } + + fun selectCard(index: Int) { + _uiState.update { it.copy(selectedIndex = index) } + } +} +